优化feifei商品名映射
This commit is contained in:
@@ -6,6 +6,7 @@ import {
|
|||||||
listAdminKuaishouFeifeiProducts,
|
listAdminKuaishouFeifeiProducts,
|
||||||
matchAdminKuaishouFeifeiProduct,
|
matchAdminKuaishouFeifeiProduct,
|
||||||
queryAdminKuaishouFeifeiTestOrder,
|
queryAdminKuaishouFeifeiTestOrder,
|
||||||
|
syncAdminKuaishouFeifeiProductRules,
|
||||||
updateAdminKuaishouFeifeiConfig,
|
updateAdminKuaishouFeifeiConfig,
|
||||||
} from "../../../services/admin/platform-config/kuaishou-feifei-service.js";
|
} from "../../../services/admin/platform-config/kuaishou-feifei-service.js";
|
||||||
import { createJsonHandler } from "../session.js";
|
import { createJsonHandler } from "../session.js";
|
||||||
@@ -74,6 +75,33 @@ router.post(
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
"/kuaishou-feifei/sync-products",
|
||||||
|
createJsonHandler(
|
||||||
|
(req) => syncAdminKuaishouFeifeiProductRules(req.body as JsonRecord),
|
||||||
|
{
|
||||||
|
successMessage: "kuaishou-feifei 商品映射已同步",
|
||||||
|
errorMessage: "同步 kuaishou-feifei 商品映射失败",
|
||||||
|
scope: "[admin/platform-config/kuaishou-feifei/sync-products]",
|
||||||
|
audit: (_req, data) => {
|
||||||
|
const result = data as JsonRecord;
|
||||||
|
const sync = result.sync as JsonRecord | undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
action: "platform_kuaishou_feifei_products_synced",
|
||||||
|
targetType: "platform_config",
|
||||||
|
targetId: "kuaishou_feifei",
|
||||||
|
data: {
|
||||||
|
productCount: Number(sync?.productCount || 0) || 0,
|
||||||
|
ruleCount: Number(sync?.ruleCount || 0) || 0,
|
||||||
|
status: String(sync?.status || "").trim(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
"/kuaishou-feifei/test-order",
|
"/kuaishou-feifei/test-order",
|
||||||
createJsonHandler(
|
createJsonHandler(
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
createKuaishouFeifeiOrder,
|
createKuaishouFeifeiOrder,
|
||||||
listKuaishouFeifeiProducts,
|
listKuaishouFeifeiProducts,
|
||||||
queryKuaishouFeifeiOrder,
|
queryKuaishouFeifeiOrder,
|
||||||
|
type KuaishouFeifeiProductListResult,
|
||||||
} from '../../platforms/kuaishou-feifei/order-service.js'
|
} from '../../platforms/kuaishou-feifei/order-service.js'
|
||||||
import { resolveKuaishouFeifeiProductByName } from '../../platforms/kuaishou-feifei/product-rule-service.js'
|
import { resolveKuaishouFeifeiProductByName } from '../../platforms/kuaishou-feifei/product-rule-service.js'
|
||||||
import {
|
import {
|
||||||
@@ -64,6 +65,52 @@ export async function listAdminKuaishouFeifeiProducts(payload: JsonObject = {})
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function syncAdminKuaishouFeifeiProductRules(payload: JsonObject = {}) {
|
||||||
|
const source = getAdminEditableKuaishouFeifeiConfig()
|
||||||
|
const status = String(payload.status || 'on_sale').trim() || 'on_sale'
|
||||||
|
const supplyProductName = String(payload.supplyProductName || payload.supply_product_name || '').trim()
|
||||||
|
const products = await listAllKuaishouFeifeiProducts({
|
||||||
|
status,
|
||||||
|
supplyProductName,
|
||||||
|
})
|
||||||
|
const productRules = products
|
||||||
|
.map((product) => {
|
||||||
|
const productName = String(product.name || '').trim()
|
||||||
|
const productCode = String(product.productCode || '').trim()
|
||||||
|
if (!productName || !productCode) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: normalizeCloudtentaclesMatchName(productName) || productCode,
|
||||||
|
enabled: product.status !== 'off_sale' && product.supplyStatus !== 'off_sale',
|
||||||
|
productName,
|
||||||
|
normalizedProductName: normalizeCloudtentaclesMatchName(productName),
|
||||||
|
productCode,
|
||||||
|
skuName: productName,
|
||||||
|
notes: `feifei 商品目录同步:${product.channel || 'unknown'}`,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((rule): rule is NonNullable<typeof rule> => Boolean(rule))
|
||||||
|
const saved = saveKuaishouFeifeiSourceConfig({
|
||||||
|
...source,
|
||||||
|
productRules,
|
||||||
|
})
|
||||||
|
const effective = getKuaishouFeifeiConfig()
|
||||||
|
|
||||||
|
return {
|
||||||
|
filePath: getKuaishouFeifeiConfigFilePath(),
|
||||||
|
source: saved,
|
||||||
|
effective: mapEffectiveKuaishouFeifeiConfig(effective),
|
||||||
|
sync: {
|
||||||
|
productCount: products.length,
|
||||||
|
ruleCount: saved.productRules.length,
|
||||||
|
status,
|
||||||
|
supplyProductName,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function createAdminKuaishouFeifeiTestOrder(payload: JsonObject = {}) {
|
export async function createAdminKuaishouFeifeiTestOrder(payload: JsonObject = {}) {
|
||||||
const platformOrderNo = String(payload.platformOrderNo || payload.platform_order_no || '').trim()
|
const platformOrderNo = String(payload.platformOrderNo || payload.platform_order_no || '').trim()
|
||||||
const productCode = String(payload.productCode || payload.product_code || '').trim()
|
const productCode = String(payload.productCode || payload.product_code || '').trim()
|
||||||
@@ -139,6 +186,34 @@ function mapEffectiveKuaishouFeifeiConfig(config: ReturnType<typeof getKuaishouF
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function listAllKuaishouFeifeiProducts(input: {
|
||||||
|
status: string
|
||||||
|
supplyProductName: string
|
||||||
|
}) {
|
||||||
|
const perPage = 100
|
||||||
|
const firstPage = await listKuaishouFeifeiProducts({
|
||||||
|
page: 1,
|
||||||
|
perPage,
|
||||||
|
status: input.status,
|
||||||
|
supplyProductName: input.supplyProductName,
|
||||||
|
})
|
||||||
|
const products = [...firstPage.items]
|
||||||
|
const total = Number(firstPage.total || products.length) || products.length
|
||||||
|
const pageCount = Math.max(1, Math.ceil(total / perPage))
|
||||||
|
|
||||||
|
for (let page = 2; page <= pageCount; page += 1) {
|
||||||
|
const result: KuaishouFeifeiProductListResult = await listKuaishouFeifeiProducts({
|
||||||
|
page,
|
||||||
|
perPage,
|
||||||
|
status: input.status,
|
||||||
|
supplyProductName: input.supplyProductName,
|
||||||
|
})
|
||||||
|
products.push(...result.items)
|
||||||
|
}
|
||||||
|
|
||||||
|
return products
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeOptionalNumber(value: unknown) {
|
function normalizeOptionalNumber(value: unknown) {
|
||||||
if (value == null || String(value).trim() === '') {
|
if (value == null || String(value).trim() === '') {
|
||||||
return undefined
|
return undefined
|
||||||
|
|||||||
@@ -545,9 +545,9 @@ async function resolveDynamicKuaishouFeifeiProfile(
|
|||||||
kuaishouFeifei: {
|
kuaishouFeifei: {
|
||||||
productCode,
|
productCode,
|
||||||
productName: String(feifei.skuName || feifei.productName || item.sku_name || '').trim(),
|
productName: String(feifei.skuName || feifei.productName || item.sku_name || '').trim(),
|
||||||
matchMode: String(feifei.matchMode || 'kuaishou_feifei_rule').trim(),
|
matchMode: String(feifei.matchMode || 'kuaishou_feifei_name').trim(),
|
||||||
},
|
},
|
||||||
notes: '91卡券商品名命中 kuaishou-feifei 商品规则',
|
notes: '91卡券商品名自动匹配 kuaishou-feifei 商品映射',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { kuaishouFeifeiRequest } from './http-client.js'
|
|||||||
|
|
||||||
type JsonObject = Record<string, any>
|
type JsonObject = Record<string, any>
|
||||||
|
|
||||||
|
export type KuaishouFeifeiProductListResult = ReturnType<typeof mapKuaishouFeifeiProductList>
|
||||||
|
|
||||||
export async function listKuaishouFeifeiProducts(input: {
|
export async function listKuaishouFeifeiProducts(input: {
|
||||||
page?: number
|
page?: number
|
||||||
perPage?: number
|
perPage?: number
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import test from 'node:test'
|
||||||
|
import assert from 'node:assert/strict'
|
||||||
|
|
||||||
|
import { resolveKuaishouFeifeiProductByName } from './product-rule-service.js'
|
||||||
|
|
||||||
|
test('resolveKuaishouFeifeiProductByName matches generated name-code mapping by normalized name', () => {
|
||||||
|
const match = resolveKuaishouFeifeiProductByName('【自动发货】套装-Alan Walker', {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
productName: '套装-Alan Walker',
|
||||||
|
normalizedProductName: '套装 alan walker',
|
||||||
|
productCode: '10000013',
|
||||||
|
skuName: '套装-Alan Walker',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.equal(match?.productCode, '10000013')
|
||||||
|
assert.equal(match?.matchMode, 'kuaishou_feifei_name')
|
||||||
|
assert.equal(match?.skuName, '套装-Alan Walker')
|
||||||
|
assert.equal(match?.normalizedProductName, '套装 alan walker')
|
||||||
|
})
|
||||||
@@ -3,7 +3,7 @@ import { getKuaishouFeifeiConfig } from './config.js'
|
|||||||
import type { KuaishouFeifeiProductRule } from '../../../types/runtime-config.js'
|
import type { KuaishouFeifeiProductRule } from '../../../types/runtime-config.js'
|
||||||
|
|
||||||
export type KuaishouFeifeiProductMatch = {
|
export type KuaishouFeifeiProductMatch = {
|
||||||
matchMode: 'kuaishou_feifei_rule'
|
matchMode: 'kuaishou_feifei_name'
|
||||||
productName: string
|
productName: string
|
||||||
normalizedProductName: string
|
normalizedProductName: string
|
||||||
productCode: string
|
productCode: string
|
||||||
@@ -37,7 +37,7 @@ export function resolveKuaishouFeifeiProductByName(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
matchMode: 'kuaishou_feifei_rule',
|
matchMode: 'kuaishou_feifei_name',
|
||||||
productName: String(matched.productName || productName || '').trim(),
|
productName: String(matched.productName || productName || '').trim(),
|
||||||
normalizedProductName,
|
normalizedProductName,
|
||||||
productCode,
|
productCode,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type {
|
|||||||
AdminKuaishouFeifeiMatchResult,
|
AdminKuaishouFeifeiMatchResult,
|
||||||
AdminKuaishouFeifeiOrderResult,
|
AdminKuaishouFeifeiOrderResult,
|
||||||
AdminKuaishouFeifeiProductListResult,
|
AdminKuaishouFeifeiProductListResult,
|
||||||
|
AdminKuaishouFeifeiProductSyncResult,
|
||||||
} from '@/types/admin'
|
} from '@/types/admin'
|
||||||
|
|
||||||
export function fetchAdminKuaishouFeifeiConfig() {
|
export function fetchAdminKuaishouFeifeiConfig() {
|
||||||
@@ -39,6 +40,16 @@ export function fetchAdminKuaishouFeifeiProducts(payload: {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function syncAdminKuaishouFeifeiProducts(payload: {
|
||||||
|
status?: string
|
||||||
|
supplyProductName?: string
|
||||||
|
} = {}) {
|
||||||
|
return apiPost<AdminKuaishouFeifeiProductSyncResult>(
|
||||||
|
'/api/v1/admin/platform-config/kuaishou-feifei/sync-products',
|
||||||
|
payload,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function createAdminKuaishouFeifeiTestOrder(payload: {
|
export function createAdminKuaishouFeifeiTestOrder(payload: {
|
||||||
platformOrderNo: string
|
platformOrderNo: string
|
||||||
productCode: string
|
productCode: string
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ export type {
|
|||||||
AdminKuaishouFeifeiConfig,
|
AdminKuaishouFeifeiConfig,
|
||||||
AdminKuaishouFeifeiEffectiveConfig,
|
AdminKuaishouFeifeiEffectiveConfig,
|
||||||
AdminKuaishouFeifeiConfigResponse,
|
AdminKuaishouFeifeiConfigResponse,
|
||||||
|
AdminKuaishouFeifeiProductSyncResult,
|
||||||
AdminKuaishouFeifeiProductMatch,
|
AdminKuaishouFeifeiProductMatch,
|
||||||
AdminKuaishouFeifeiMatchResult,
|
AdminKuaishouFeifeiMatchResult,
|
||||||
AdminKuaishouFeifeiProductItem,
|
AdminKuaishouFeifeiProductItem,
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ export type {
|
|||||||
AdminKuaishouFeifeiConfig,
|
AdminKuaishouFeifeiConfig,
|
||||||
AdminKuaishouFeifeiEffectiveConfig,
|
AdminKuaishouFeifeiEffectiveConfig,
|
||||||
AdminKuaishouFeifeiConfigResponse,
|
AdminKuaishouFeifeiConfigResponse,
|
||||||
|
AdminKuaishouFeifeiProductSyncResult,
|
||||||
AdminKuaishouFeifeiProductMatch,
|
AdminKuaishouFeifeiProductMatch,
|
||||||
AdminKuaishouFeifeiMatchResult,
|
AdminKuaishouFeifeiMatchResult,
|
||||||
AdminKuaishouFeifeiProductItem,
|
AdminKuaishouFeifeiProductItem,
|
||||||
|
|||||||
@@ -34,6 +34,15 @@ export interface AdminKuaishouFeifeiConfigResponse {
|
|||||||
effective: AdminKuaishouFeifeiEffectiveConfig
|
effective: AdminKuaishouFeifeiEffectiveConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AdminKuaishouFeifeiProductSyncResult extends AdminKuaishouFeifeiConfigResponse {
|
||||||
|
sync: {
|
||||||
|
productCount: number
|
||||||
|
ruleCount: number
|
||||||
|
status: string
|
||||||
|
supplyProductName: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export interface AdminKuaishouFeifeiProductMatch {
|
export interface AdminKuaishouFeifeiProductMatch {
|
||||||
matchMode: string
|
matchMode: string
|
||||||
productName: string
|
productName: string
|
||||||
|
|||||||
+81
-139
@@ -1,56 +1,44 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, reactive, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
import { Check, Delete, Plus, RefreshRight, Search } from '@element-plus/icons-vue'
|
import { RefreshRight, Search } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
import { showError, showSuccess } from '@/lib/feedback'
|
import { showError, showSuccess } from '@/lib/feedback'
|
||||||
import {
|
import {
|
||||||
fetchAdminKuaishouFeifeiConfig,
|
fetchAdminKuaishouFeifeiConfig,
|
||||||
matchAdminKuaishouFeifeiProduct,
|
matchAdminKuaishouFeifeiProduct,
|
||||||
saveAdminKuaishouFeifeiConfig,
|
syncAdminKuaishouFeifeiProducts,
|
||||||
} from '@/services/admin'
|
} from '@/services/admin'
|
||||||
import type {
|
import type {
|
||||||
AdminKuaishouFeifeiConfig,
|
|
||||||
AdminKuaishouFeifeiConfigResponse,
|
AdminKuaishouFeifeiConfigResponse,
|
||||||
AdminKuaishouFeifeiMatchResult,
|
AdminKuaishouFeifeiMatchResult,
|
||||||
AdminKuaishouFeifeiProductRule,
|
AdminKuaishouFeifeiProductRule,
|
||||||
} from '@/types/admin'
|
} from '@/types/admin'
|
||||||
import { hasAdminRole } from '@/utils/admin-auth'
|
import { hasAdminRole } from '@/utils/admin-auth'
|
||||||
|
|
||||||
type EditableRule = AdminKuaishouFeifeiProductRule
|
|
||||||
|
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const saving = ref(false)
|
const syncing = ref(false)
|
||||||
const matching = ref(false)
|
const matching = ref(false)
|
||||||
const errorMessage = ref('')
|
const errorMessage = ref('')
|
||||||
const filePath = ref('')
|
const filePath = ref('')
|
||||||
const matchInput = ref('')
|
const matchInput = ref('')
|
||||||
const matchResult = ref<AdminKuaishouFeifeiMatchResult | null>(null)
|
const matchResult = ref<AdminKuaishouFeifeiMatchResult | null>(null)
|
||||||
const sourceConfig = reactive<AdminKuaishouFeifeiConfig>({
|
const rules = ref<AdminKuaishouFeifeiProductRule[]>([])
|
||||||
enabled: true,
|
const lastSyncText = ref('')
|
||||||
baseUrl: '',
|
|
||||||
appKey: '',
|
|
||||||
appSecret: '',
|
|
||||||
timeoutMs: 10000,
|
|
||||||
notifyUrl: '',
|
|
||||||
productRules: [],
|
|
||||||
})
|
|
||||||
const rules = ref<EditableRule[]>([])
|
|
||||||
|
|
||||||
const enabledRuleCount = computed(() => rules.value.filter((rule) => rule.enabled !== false).length)
|
const enabledRuleCount = computed(() => rules.value.filter((rule) => rule.enabled !== false).length)
|
||||||
const configuredRuleCount = computed(
|
const mappingCount = computed(() =>
|
||||||
() => rules.value.filter((rule) => rule.productName.trim() && rule.productCode.trim()).length,
|
rules.value.filter((rule) => rule.productName.trim() && rule.productCode.trim()).length,
|
||||||
)
|
)
|
||||||
const incompleteRuleCount = computed(() => Math.max(0, rules.value.length - configuredRuleCount.value))
|
|
||||||
const metrics = computed(() => [
|
const metrics = computed(() => [
|
||||||
{
|
{
|
||||||
label: '启用规则',
|
label: '名字映射',
|
||||||
value: String(enabledRuleCount.value),
|
value: String(mappingCount.value),
|
||||||
detail: `共 ${rules.value.length} 条`,
|
detail: `启用 ${enabledRuleCount.value} 条`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '已配置映射',
|
label: '匹配方式',
|
||||||
value: String(configuredRuleCount.value),
|
value: '同名',
|
||||||
detail: incompleteRuleCount.value > 0 ? `待补全 ${incompleteRuleCount.value} 条` : '无缺失',
|
detail: '使用 91 商品名规范化匹配',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '配置文件',
|
label: '配置文件',
|
||||||
@@ -74,25 +62,28 @@ async function loadConfig() {
|
|||||||
const response = await fetchAdminKuaishouFeifeiConfig()
|
const response = await fetchAdminKuaishouFeifeiConfig()
|
||||||
hydrateConfig(response.data)
|
hydrateConfig(response.data)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
errorMessage.value = error instanceof Error ? error.message : '读取 kuaishou-feifei 履约配置失败'
|
errorMessage.value = error instanceof Error ? error.message : '读取 kuaishou-feifei 履约映射失败'
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveRules() {
|
async function syncProducts() {
|
||||||
saving.value = true
|
syncing.value = true
|
||||||
errorMessage.value = ''
|
errorMessage.value = ''
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await saveAdminKuaishouFeifeiConfig(buildPayload())
|
const response = await syncAdminKuaishouFeifeiProducts({
|
||||||
|
status: 'on_sale',
|
||||||
|
})
|
||||||
hydrateConfig(response.data)
|
hydrateConfig(response.data)
|
||||||
showSuccess(`kuaishou-feifei 履约规则已保存,启用 ${enabledRuleCount.value} 条`)
|
lastSyncText.value = `商品 ${response.data.sync.productCount} 个,映射 ${response.data.sync.ruleCount} 条`
|
||||||
|
showSuccess(`kuaishou-feifei 商品映射已同步,${lastSyncText.value}`)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
errorMessage.value = error instanceof Error ? error.message : '保存 kuaishou-feifei 履约配置失败'
|
errorMessage.value = error instanceof Error ? error.message : '同步 kuaishou-feifei 商品映射失败'
|
||||||
showError(errorMessage.value)
|
showError(errorMessage.value)
|
||||||
} finally {
|
} finally {
|
||||||
saving.value = false
|
syncing.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,52 +109,17 @@ async function previewMatch() {
|
|||||||
|
|
||||||
function hydrateConfig(data: AdminKuaishouFeifeiConfigResponse) {
|
function hydrateConfig(data: AdminKuaishouFeifeiConfigResponse) {
|
||||||
filePath.value = data.filePath || ''
|
filePath.value = data.filePath || ''
|
||||||
sourceConfig.enabled = data.source.enabled !== false
|
rules.value = Array.isArray(data.source.productRules)
|
||||||
sourceConfig.baseUrl = data.source.baseUrl || ''
|
? data.source.productRules.map((rule) => normalizeRule(rule))
|
||||||
sourceConfig.appKey = data.source.appKey || ''
|
: []
|
||||||
sourceConfig.appSecret = data.source.appSecret || ''
|
|
||||||
sourceConfig.timeoutMs = Number(data.source.timeoutMs || 10000) || 10000
|
|
||||||
sourceConfig.notifyUrl = data.source.notifyUrl || ''
|
|
||||||
sourceConfig.productRules = Array.isArray(data.source.productRules) ? data.source.productRules : []
|
|
||||||
rules.value = sourceConfig.productRules.map((rule) => normalizeEditableRule(rule))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildPayload(): AdminKuaishouFeifeiConfig {
|
function normalizeRule(rule: Partial<AdminKuaishouFeifeiProductRule>): AdminKuaishouFeifeiProductRule {
|
||||||
return {
|
|
||||||
enabled: sourceConfig.enabled,
|
|
||||||
baseUrl: sourceConfig.baseUrl,
|
|
||||||
appKey: sourceConfig.appKey,
|
|
||||||
appSecret: sourceConfig.appSecret,
|
|
||||||
timeoutMs: Number(sourceConfig.timeoutMs || 10000) || 10000,
|
|
||||||
notifyUrl: sourceConfig.notifyUrl,
|
|
||||||
productRules: rules.value.map((rule) => normalizeEditableRule(rule)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function addRule() {
|
|
||||||
rules.value.unshift(
|
|
||||||
normalizeEditableRule({
|
|
||||||
id: createRuleId(),
|
|
||||||
enabled: true,
|
|
||||||
productName: matchInput.value.trim(),
|
|
||||||
normalizedProductName: '',
|
|
||||||
productCode: '',
|
|
||||||
skuName: matchInput.value.trim(),
|
|
||||||
notes: '',
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeRule(index: number) {
|
|
||||||
rules.value.splice(index, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeEditableRule(rule: Partial<AdminKuaishouFeifeiProductRule>): EditableRule {
|
|
||||||
const productName = String(rule.productName || '').trim()
|
const productName = String(rule.productName || '').trim()
|
||||||
const productCode = String(rule.productCode || '').trim()
|
const productCode = String(rule.productCode || '').trim()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: String(rule.id || createRuleId()).trim(),
|
id: String(rule.id || productName || productCode).trim(),
|
||||||
enabled: rule.enabled !== false,
|
enabled: rule.enabled !== false,
|
||||||
productName,
|
productName,
|
||||||
normalizedProductName: String(rule.normalizedProductName || '').trim(),
|
normalizedProductName: String(rule.normalizedProductName || '').trim(),
|
||||||
@@ -172,10 +128,6 @@ function normalizeEditableRule(rule: Partial<AdminKuaishouFeifeiProductRule>): E
|
|||||||
notes: String(rule.notes || '').trim(),
|
notes: String(rule.notes || '').trim(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createRuleId() {
|
|
||||||
return `feifei-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -199,8 +151,8 @@ function createRuleId() {
|
|||||||
</div>
|
</div>
|
||||||
<div class="overview-actions">
|
<div class="overview-actions">
|
||||||
<el-button :icon="RefreshRight" :loading="loading" @click="loadConfig">刷新</el-button>
|
<el-button :icon="RefreshRight" :loading="loading" @click="loadConfig">刷新</el-button>
|
||||||
<el-button type="primary" :icon="Check" :loading="saving" @click="saveRules">
|
<el-button type="primary" :icon="RefreshRight" :loading="syncing" @click="syncProducts">
|
||||||
保存规则
|
同步商品映射
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -208,10 +160,9 @@ function createRuleId() {
|
|||||||
<section class="rule-panel">
|
<section class="rule-panel">
|
||||||
<div class="section-head rule-head-title">
|
<div class="section-head rule-head-title">
|
||||||
<div>
|
<div>
|
||||||
<h3>商品履约规则</h3>
|
<h3>商品名字映射</h3>
|
||||||
<p>91 商品名命中后会创建 kuaishou-feifei 履约任务。</p>
|
<p>{{ lastSyncText || 'feifei 商品名会按规范化名字匹配 91 商品名。' }}</p>
|
||||||
</div>
|
</div>
|
||||||
<el-button type="primary" plain :icon="Plus" @click="addRule">新增规则</el-button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="match-checker">
|
<div class="match-checker">
|
||||||
@@ -232,38 +183,35 @@ function createRuleId() {
|
|||||||
<span v-else>{{ matchResult.normalizedProductName || matchResult.productName }}</span>
|
<span v-else>{{ matchResult.normalizedProductName || matchResult.productName }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-empty v-if="rules.length === 0" description="暂无 kuaishou-feifei 商品规则" />
|
<el-empty v-if="rules.length === 0" description="暂无 kuaishou-feifei 商品映射" />
|
||||||
|
|
||||||
<div v-else class="rule-list">
|
<el-table
|
||||||
<article v-for="(rule, index) in rules" :key="rule.id" class="rule-item">
|
v-else
|
||||||
<div class="rule-head">
|
:data="rules"
|
||||||
<el-switch
|
border
|
||||||
v-model="rule.enabled"
|
stripe
|
||||||
inline-prompt
|
height="520"
|
||||||
active-text="启用"
|
class="rule-table"
|
||||||
inactive-text="停用"
|
>
|
||||||
/>
|
<el-table-column label="feifei 商品名" min-width="260">
|
||||||
<el-button type="danger" plain :icon="Delete" @click="removeRule(index)">
|
<template #default="{ row }">
|
||||||
删除
|
<div class="product-cell">
|
||||||
</el-button>
|
<strong>{{ row.productName || row.skuName || '-' }}</strong>
|
||||||
</div>
|
<small>{{ row.normalizedProductName || '-' }}</small>
|
||||||
|
|
||||||
<el-form label-position="top" class="rule-form">
|
|
||||||
<el-form-item label="91 商品名">
|
|
||||||
<el-input v-model="rule.productName" placeholder="套装-Alan Walker" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="feifei 商品编码">
|
|
||||||
<el-input v-model="rule.productCode" placeholder="product_code" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="展示名称">
|
|
||||||
<el-input v-model="rule.skuName" placeholder="后台展示名称" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="备注">
|
|
||||||
<el-input v-model="rule.notes" placeholder="可选" />
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
</article>
|
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="product_code" prop="productCode" min-width="150" />
|
||||||
|
<el-table-column label="展示名称" prop="skuName" min-width="220" />
|
||||||
|
<el-table-column label="状态" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.enabled !== false ? 'success' : 'info'" effect="light">
|
||||||
|
{{ row.enabled !== false ? '启用' : '停用' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="来源" prop="notes" min-width="180" />
|
||||||
|
</el-table>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
</section>
|
</section>
|
||||||
@@ -278,14 +226,13 @@ function createRuleId() {
|
|||||||
|
|
||||||
.rule-overview {
|
.rule-overview {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr)) minmax(150px, auto);
|
grid-template-columns: repeat(3, minmax(0, 1fr)) minmax(190px, auto);
|
||||||
gap: var(--space-3);
|
gap: var(--space-3);
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.metric-card,
|
.metric-card,
|
||||||
.rule-panel,
|
.rule-panel {
|
||||||
.rule-item {
|
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
border: 1px solid var(--border-default);
|
border: 1px solid var(--border-default);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
@@ -300,7 +247,8 @@ function createRuleId() {
|
|||||||
|
|
||||||
.metric-card span,
|
.metric-card span,
|
||||||
.metric-card small,
|
.metric-card small,
|
||||||
.section-head p {
|
.section-head p,
|
||||||
|
.product-cell small {
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,8 +260,7 @@ function createRuleId() {
|
|||||||
|
|
||||||
.overview-actions,
|
.overview-actions,
|
||||||
.rule-head-title,
|
.rule-head-title,
|
||||||
.match-checker,
|
.match-checker {
|
||||||
.rule-head {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--space-2);
|
gap: var(--space-2);
|
||||||
@@ -323,8 +270,7 @@ function createRuleId() {
|
|||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rule-panel,
|
.rule-panel {
|
||||||
.rule-item {
|
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: var(--space-4);
|
gap: var(--space-4);
|
||||||
padding: var(--space-4);
|
padding: var(--space-4);
|
||||||
@@ -371,29 +317,25 @@ function createRuleId() {
|
|||||||
background: var(--el-color-success-light-9);
|
background: var(--el-color-success-light-9);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rule-list {
|
.rule-table {
|
||||||
display: grid;
|
|
||||||
gap: var(--space-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.rule-head {
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rule-form {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
gap: var(--space-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.rule-form :deep(.el-input),
|
|
||||||
.rule-form :deep(.el-select) {
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.product-cell {
|
||||||
|
display: grid;
|
||||||
|
gap: 4px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-cell strong,
|
||||||
|
.product-cell small {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
.rule-overview,
|
.rule-overview {
|
||||||
.rule-form {
|
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user