优化feifei商品名映射

This commit is contained in:
yml2213
2026-07-07 15:12:48 +08:00
parent 6af2fe2e23
commit 177f72dd2c
11 changed files with 235 additions and 144 deletions
@@ -5,6 +5,7 @@ import type {
AdminKuaishouFeifeiMatchResult,
AdminKuaishouFeifeiOrderResult,
AdminKuaishouFeifeiProductListResult,
AdminKuaishouFeifeiProductSyncResult,
} from '@/types/admin'
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: {
platformOrderNo: string
productCode: string
+1
View File
@@ -54,6 +54,7 @@ export type {
AdminKuaishouFeifeiConfig,
AdminKuaishouFeifeiEffectiveConfig,
AdminKuaishouFeifeiConfigResponse,
AdminKuaishouFeifeiProductSyncResult,
AdminKuaishouFeifeiProductMatch,
AdminKuaishouFeifeiMatchResult,
AdminKuaishouFeifeiProductItem,
@@ -35,6 +35,7 @@ export type {
AdminKuaishouFeifeiConfig,
AdminKuaishouFeifeiEffectiveConfig,
AdminKuaishouFeifeiConfigResponse,
AdminKuaishouFeifeiProductSyncResult,
AdminKuaishouFeifeiProductMatch,
AdminKuaishouFeifeiMatchResult,
AdminKuaishouFeifeiProductItem,
@@ -34,6 +34,15 @@ export interface AdminKuaishouFeifeiConfigResponse {
effective: AdminKuaishouFeifeiEffectiveConfig
}
export interface AdminKuaishouFeifeiProductSyncResult extends AdminKuaishouFeifeiConfigResponse {
sync: {
productCount: number
ruleCount: number
status: string
supplyProductName: string
}
}
export interface AdminKuaishouFeifeiProductMatch {
matchMode: string
productName: string
@@ -1,56 +1,44 @@
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import { Check, Delete, Plus, RefreshRight, Search } from '@element-plus/icons-vue'
import { computed, onMounted, ref } from 'vue'
import { RefreshRight, Search } from '@element-plus/icons-vue'
import { showError, showSuccess } from '@/lib/feedback'
import {
fetchAdminKuaishouFeifeiConfig,
matchAdminKuaishouFeifeiProduct,
saveAdminKuaishouFeifeiConfig,
syncAdminKuaishouFeifeiProducts,
} from '@/services/admin'
import type {
AdminKuaishouFeifeiConfig,
AdminKuaishouFeifeiConfigResponse,
AdminKuaishouFeifeiMatchResult,
AdminKuaishouFeifeiProductRule,
} from '@/types/admin'
import { hasAdminRole } from '@/utils/admin-auth'
type EditableRule = AdminKuaishouFeifeiProductRule
const loading = ref(true)
const saving = ref(false)
const syncing = ref(false)
const matching = ref(false)
const errorMessage = ref('')
const filePath = ref('')
const matchInput = ref('')
const matchResult = ref<AdminKuaishouFeifeiMatchResult | null>(null)
const sourceConfig = reactive<AdminKuaishouFeifeiConfig>({
enabled: true,
baseUrl: '',
appKey: '',
appSecret: '',
timeoutMs: 10000,
notifyUrl: '',
productRules: [],
})
const rules = ref<EditableRule[]>([])
const rules = ref<AdminKuaishouFeifeiProductRule[]>([])
const lastSyncText = ref('')
const enabledRuleCount = computed(() => rules.value.filter((rule) => rule.enabled !== false).length)
const configuredRuleCount = computed(
() => rules.value.filter((rule) => rule.productName.trim() && rule.productCode.trim()).length,
const mappingCount = computed(() =>
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(() => [
{
label: '启用规则',
value: String(enabledRuleCount.value),
detail: ` ${rules.value.length}`,
label: '名字映射',
value: String(mappingCount.value),
detail: `启用 ${enabledRuleCount.value}`,
},
{
label: '已配置映射',
value: String(configuredRuleCount.value),
detail: incompleteRuleCount.value > 0 ? `待补全 ${incompleteRuleCount.value}` : '无缺失',
label: '匹配方式',
value: '同名',
detail: '使用 91 商品名规范化匹配',
},
{
label: '配置文件',
@@ -74,25 +62,28 @@ async function loadConfig() {
const response = await fetchAdminKuaishouFeifeiConfig()
hydrateConfig(response.data)
} catch (error) {
errorMessage.value = error instanceof Error ? error.message : '读取 kuaishou-feifei 履约配置失败'
errorMessage.value = error instanceof Error ? error.message : '读取 kuaishou-feifei 履约映射失败'
} finally {
loading.value = false
}
}
async function saveRules() {
saving.value = true
async function syncProducts() {
syncing.value = true
errorMessage.value = ''
try {
const response = await saveAdminKuaishouFeifeiConfig(buildPayload())
const response = await syncAdminKuaishouFeifeiProducts({
status: 'on_sale',
})
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) {
errorMessage.value = error instanceof Error ? error.message : '保存 kuaishou-feifei 履约配置失败'
errorMessage.value = error instanceof Error ? error.message : '同步 kuaishou-feifei 商品映射失败'
showError(errorMessage.value)
} finally {
saving.value = false
syncing.value = false
}
}
@@ -118,52 +109,17 @@ async function previewMatch() {
function hydrateConfig(data: AdminKuaishouFeifeiConfigResponse) {
filePath.value = data.filePath || ''
sourceConfig.enabled = data.source.enabled !== false
sourceConfig.baseUrl = data.source.baseUrl || ''
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))
rules.value = Array.isArray(data.source.productRules)
? data.source.productRules.map((rule) => normalizeRule(rule))
: []
}
function buildPayload(): AdminKuaishouFeifeiConfig {
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 {
function normalizeRule(rule: Partial<AdminKuaishouFeifeiProductRule>): AdminKuaishouFeifeiProductRule {
const productName = String(rule.productName || '').trim()
const productCode = String(rule.productCode || '').trim()
return {
id: String(rule.id || createRuleId()).trim(),
id: String(rule.id || productName || productCode).trim(),
enabled: rule.enabled !== false,
productName,
normalizedProductName: String(rule.normalizedProductName || '').trim(),
@@ -172,10 +128,6 @@ function normalizeEditableRule(rule: Partial<AdminKuaishouFeifeiProductRule>): E
notes: String(rule.notes || '').trim(),
}
}
function createRuleId() {
return `feifei-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
}
</script>
<template>
@@ -199,8 +151,8 @@ function createRuleId() {
</div>
<div class="overview-actions">
<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>
</div>
</section>
@@ -208,10 +160,9 @@ function createRuleId() {
<section class="rule-panel">
<div class="section-head rule-head-title">
<div>
<h3>商品履约规则</h3>
<p>91 商品名命中后会创建 kuaishou-feifei 履约任务</p>
<h3>商品名字映射</h3>
<p>{{ lastSyncText || 'feifei 商品名会按规范化名字匹配 91 商品名。' }}</p>
</div>
<el-button type="primary" plain :icon="Plus" @click="addRule">新增规则</el-button>
</div>
<div class="match-checker">
@@ -232,38 +183,35 @@ function createRuleId() {
<span v-else>{{ matchResult.normalizedProductName || matchResult.productName }}</span>
</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">
<article v-for="(rule, index) in rules" :key="rule.id" class="rule-item">
<div class="rule-head">
<el-switch
v-model="rule.enabled"
inline-prompt
active-text="启用"
inactive-text="停用"
/>
<el-button type="danger" plain :icon="Delete" @click="removeRule(index)">
删除
</el-button>
</div>
<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>
<el-table
v-else
:data="rules"
border
stripe
height="520"
class="rule-table"
>
<el-table-column label="feifei 商品名" min-width="260">
<template #default="{ row }">
<div class="product-cell">
<strong>{{ row.productName || row.skuName || '-' }}</strong>
<small>{{ row.normalizedProductName || '-' }}</small>
</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>
</template>
</section>
@@ -278,14 +226,13 @@ function createRuleId() {
.rule-overview {
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);
align-items: stretch;
}
.metric-card,
.rule-panel,
.rule-item {
.rule-panel {
min-width: 0;
border: 1px solid var(--border-default);
border-radius: var(--radius-md);
@@ -300,7 +247,8 @@ function createRuleId() {
.metric-card span,
.metric-card small,
.section-head p {
.section-head p,
.product-cell small {
color: var(--text-secondary);
}
@@ -312,8 +260,7 @@ function createRuleId() {
.overview-actions,
.rule-head-title,
.match-checker,
.rule-head {
.match-checker {
display: flex;
align-items: center;
gap: var(--space-2);
@@ -323,8 +270,7 @@ function createRuleId() {
justify-content: flex-end;
}
.rule-panel,
.rule-item {
.rule-panel {
display: grid;
gap: var(--space-4);
padding: var(--space-4);
@@ -371,29 +317,25 @@ function createRuleId() {
background: var(--el-color-success-light-9);
}
.rule-list {
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) {
.rule-table {
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) {
.rule-overview,
.rule-form {
.rule-overview {
grid-template-columns: 1fr;
}