前面的ok了, 绑定与角色

This commit is contained in:
yml
2026-05-03 20:58:18 +08:00
parent dd2192b6ae
commit 0f0253bffe
30 changed files with 1900 additions and 409 deletions
@@ -5,6 +5,7 @@ import { showSuccess } from '@/lib/feedback'
import {
fetchAdminFulfillmentBindingConfigs,
fetchAdminKhhaoSourceConfig,
fetchAdminKuaishouEticketSourceConfig,
queryAdminKhhaoOrders,
lookupAdminFulfillmentBindingOrder,
saveAdminFulfillmentBindingConfigs,
@@ -14,6 +15,7 @@ import type {
AdminFulfillmentLookupItem,
AdminFulfillmentLookupResult,
AdminKhhaoOrderPreview,
AdminKuaishouEticketShopConfigItem,
AdminObservedProductItem,
} from '@/types/admin'
import { hasAdminRole } from '@/utils/admin-auth'
@@ -24,6 +26,8 @@ type EditableBinding = {
provider: string
platform: string
shopId: string
shopName: string
khhaoShopId: string
skuCode: string
skuName: string
profileKey: string
@@ -39,6 +43,8 @@ type SaveBindingPayload = {
provider: string
platform: string
shopId: string
shopName: string
khhaoShopId: string
skuCode: string
skuName: string
profileKey: string
@@ -54,7 +60,7 @@ type SaveBindingPayload = {
}
}
type ValidationField = 'skuCode' | 'match' | 'profileKey'
type ValidationField = 'skuCode' | 'match' | 'profileKey' | 'shopId' | 'khhaoShopId'
type ValidationState = {
bindingId: string
@@ -75,6 +81,7 @@ const errorMessage = ref('')
const filePath = ref('')
const bindings = ref<EditableBinding[]>([])
const observedProducts = ref<AdminObservedProductItem[]>([])
const kuaishouShopOptions = ref<AdminKuaishouEticketShopConfigItem[]>([])
const lookupLoading = ref(false)
const lookupErrorMessage = ref('')
const lookupResult = ref<AdminFulfillmentLookupResult | null>(null)
@@ -119,8 +126,10 @@ const pendingObservedCount = computed(() => observedProducts.value.filter((item)
type ImportableProductCandidate = Pick<
AdminObservedProductItem,
'provider' | 'platform' | 'shopId' | 'externalSkuCode' | 'externalItemId' | 'externalSkuName'
>
'provider' | 'platform' | 'shopId' | 'shopName' | 'externalSkuCode' | 'externalItemId' | 'externalSkuName'
> & {
khhaoShopId?: string
}
function createEmptyBinding(): EditableBinding {
return {
@@ -128,6 +137,8 @@ function createEmptyBinding(): EditableBinding {
provider: 'agiso',
platform: '',
shopId: '',
shopName: '',
khhaoShopId: '',
skuCode: '',
skuName: '',
profileKey: 'manual_review',
@@ -141,11 +152,14 @@ function createEmptyBinding(): EditableBinding {
}
function mapEditableBinding(item: AdminFulfillmentBindingConfigItem): EditableBinding {
return {
const matchedShop = resolveKuaishouShopById(item.shopId) || resolveKuaishouShopByName(item.shopName || '')
const binding = {
id: crypto.randomUUID(),
provider: item.provider || 'agiso',
platform: item.platform,
shopId: item.shopId,
shopId: matchedShop?.shopId || item.shopId,
shopName: item.shopName || matchedShop?.kshopName || '',
khhaoShopId: item.khhaoShopId || '',
skuCode: item.skuCode,
skuName: item.skuName,
profileKey: item.profileKey || 'manual_review',
@@ -156,6 +170,9 @@ function mapEditableBinding(item: AdminFulfillmentBindingConfigItem): EditableBi
externalSkuName: item.match.externalSkuName,
resolvedSkuName: String(item.match.config?.resolvedSkuName || ''),
}
syncBindingShopSelection(binding)
return binding
}
function hasMatchCondition(item: Pick<EditableBinding, 'externalSkuCode' | 'externalItemId' | 'externalSkuName'>) {
@@ -203,16 +220,64 @@ function getBindingStatusLabel(binding: EditableBinding) {
}
function getBindingSummary(binding: EditableBinding) {
const shopLabel = isKhhaoKuaishouBinding(binding)
? [
binding.shopName.trim() || binding.shopId.trim() || '未选快手店铺',
binding.khhaoShopId.trim() ? `khhao#${binding.khhaoShopId.trim()}` : '',
].filter(Boolean).join(' / ')
: (binding.shopName.trim() || binding.shopId.trim() || '跨店铺')
const parts = [
binding.provider.trim() || 'agiso',
binding.platform.trim() || '未选平台',
binding.shopId.trim() || '跨店铺',
shopLabel,
binding.profileKey.trim() || 'manual_review',
]
return parts.join(' / ')
}
function isKhhaoKuaishouBinding(binding: Pick<EditableBinding, 'provider' | 'platform'>) {
return binding.provider.trim() === 'khhao' && binding.platform.trim() === 'kuaishou'
}
function resolveKuaishouShopById(shopId: string) {
const normalizedShopId = shopId.trim()
return kuaishouShopOptions.value.find((item) => item.shopId === normalizedShopId) || null
}
function resolveKuaishouShopByName(shopName: string) {
const normalizedShopName = shopName.trim()
return kuaishouShopOptions.value.find((item) => item.kshopName === normalizedShopName) || null
}
function getKuaishouShopOptionLabel(item: AdminKuaishouEticketShopConfigItem) {
return item.kshopName.trim()
? `${item.kshopName} (${item.shopId})`
: item.shopId
}
function syncBindingShopSelection(binding: EditableBinding) {
if (!isKhhaoKuaishouBinding(binding)) {
return
}
const matchedById = resolveKuaishouShopById(binding.shopId)
if (matchedById) {
binding.shopId = matchedById.shopId
binding.shopName = matchedById.kshopName
return
}
const matchedByName = resolveKuaishouShopByName(binding.shopName)
if (matchedByName) {
binding.shopId = matchedByName.shopId
binding.shopName = matchedByName.kshopName
return
}
binding.shopId = ''
}
async function loadConfigs() {
if (!hasAdminRole('admin')) {
loading.value = false
@@ -223,11 +288,13 @@ async function loadConfigs() {
errorMessage.value = ''
try {
const [response, khhaoSourceResponse] = await Promise.all([
const [response, khhaoSourceResponse, kuaishouSourceResponse] = await Promise.all([
fetchAdminFulfillmentBindingConfigs(),
fetchAdminKhhaoSourceConfig(),
fetchAdminKuaishouEticketSourceConfig(),
])
filePath.value = response.data.filePath
kuaishouShopOptions.value = kuaishouSourceResponse.data.source.shops.filter((item) => item.enabled !== false)
const nextBindings = response.data.bindings.map(mapEditableBinding)
bindings.value = nextBindings
collapsedBindingIds.value = buildCollapsedIds(nextBindings)
@@ -259,11 +326,13 @@ function removeBinding(id: string) {
}
function createBindingFromProductCandidate(item: ImportableProductCandidate): EditableBinding {
return {
const binding = {
id: crypto.randomUUID(),
provider: item.provider || 'agiso',
platform: item.platform,
shopId: item.shopId,
shopName: item.shopName,
khhaoShopId: item.khhaoShopId || '',
skuCode: '',
skuName: item.externalSkuName,
profileKey: 'manual_review',
@@ -274,6 +343,9 @@ function createBindingFromProductCandidate(item: ImportableProductCandidate): Ed
externalSkuName: item.externalSkuName,
resolvedSkuName: item.externalSkuName,
}
syncBindingShopSelection(binding)
return binding
}
function importObservedProduct(item: ImportableProductCandidate) {
@@ -298,7 +370,11 @@ function importKhhaoProduct(item: AdminKhhaoOrderPreview) {
importObservedProduct({
provider: item.provider || 'khhao',
platform: item.platform || 'unknown',
shopId: item.shopId,
shopId: isKhhaoKuaishouBinding({ provider: item.provider || 'khhao', platform: item.platform || 'unknown' })
? (item.kuaishouShopId || resolveKuaishouShopByName(item.shopName)?.shopId || '')
: item.shopId,
shopName: item.shopName,
khhaoShopId: item.khhaoShopId || item.shopId,
externalSkuCode: item.skuCode,
externalItemId: item.itemId,
externalSkuName: item.itemTitle,
@@ -314,10 +390,14 @@ function isKhhaoProductImported(platformOrderId: string) {
}
function normalizeBindingForSave(item: EditableBinding): SaveBindingPayload {
syncBindingShopSelection(item)
return {
provider: item.provider.trim() || 'agiso',
platform: item.platform.trim(),
shopId: item.shopId.trim(),
shopName: item.shopName.trim(),
khhaoShopId: item.khhaoShopId.trim(),
skuCode: item.skuCode.trim(),
skuName: item.skuName.trim(),
profileKey: item.profileKey.trim() || 'manual_review',
@@ -338,6 +418,8 @@ function hasBindingContent(item: SaveBindingPayload) {
return Boolean(
item.platform
|| item.shopId
|| item.shopName
|| item.khhaoShopId
|| item.skuCode
|| item.skuName
|| item.match.externalSkuCode
@@ -353,6 +435,24 @@ function hasBindingContent(item: SaveBindingPayload) {
function resolveBindingValidationState(item: SaveBindingPayload, index: number, bindingId: string): ValidationState {
const label = `第 ${index + 1} 条规则`
const provider = item.provider.trim() || 'agiso'
const platform = item.platform.trim()
if (provider === 'khhao' && platform === 'kuaishou' && (!item.shopId || !item.shopName)) {
return {
bindingId,
field: 'shopId',
message: `${label} 需要选择已配置的快手官方店铺`,
}
}
if (provider === 'khhao' && platform === 'kuaishou' && !item.khhaoShopId) {
return {
bindingId,
field: 'khhaoShopId',
message: `${label} 需要填写 khhao 店铺 ID`,
}
}
if (!item.skuCode) {
return {
@@ -397,6 +497,8 @@ async function focusValidationTarget(state: ValidationState) {
card.scrollIntoView({ behavior: 'smooth', block: 'center' })
const selectors: Record<ValidationField, string> = {
khhaoShopId: '[data-field="khhaoShopId"]',
shopId: '[data-field="shopId"]',
skuCode: '[data-field="skuCode"]',
match: '[data-field="externalSkuCode"]',
profileKey: '[data-field="profileKey"]',
@@ -426,6 +528,16 @@ function isFieldInvalid(bindingId: string, field: ValidationField | 'externalSku
return validationState.value.field === field
}
function handleBindingProviderPlatformChange(binding: EditableBinding) {
syncBindingShopSelection(binding)
clearValidationState()
}
function handleKuaishouShopChange(binding: EditableBinding) {
syncBindingShopSelection(binding)
clearValidationState()
}
async function saveConfigs() {
const normalizedBindings = bindings.value.map(normalizeBindingForSave)
const nonEmptyBindings = normalizedBindings.filter(hasBindingContent)
@@ -649,18 +761,90 @@ onMounted(loadConfigs)
<div v-else class="binding-grid">
<label class="field-block">
<span>渠道</span>
<input v-model="binding.provider" class="text-input" placeholder="agiso" @input="clearValidationState" />
<input
v-model="binding.provider"
class="text-input"
placeholder="agiso"
@input="handleBindingProviderPlatformChange(binding)"
/>
</label>
<label class="field-block">
<span>平台</span>
<input v-model="binding.platform" class="text-input" placeholder="xianyu / taobao / pdd" @input="clearValidationState" />
<input
v-model="binding.platform"
class="text-input"
placeholder="xianyu / taobao / pdd"
@input="handleBindingProviderPlatformChange(binding)"
/>
</label>
<label class="field-block">
<span>店铺 ID</span>
<input v-model="binding.shopId" class="text-input" placeholder="留空表示跨店铺通用" @input="clearValidationState" />
</label>
<template v-if="isKhhaoKuaishouBinding(binding)">
<label class="field-block">
<span>khhao 店铺 ID</span>
<input
v-model="binding.khhaoShopId"
:class="['text-input', { 'text-input--invalid': isFieldInvalid(binding.id, 'khhaoShopId') }]"
data-field="khhaoShopId"
placeholder="例如 12 / 13 / 14"
@input="clearValidationState"
/>
<small class="field-help">这里填 khhao 系统里的店铺编号,专门用于同步订单命中规则。</small>
</label>
<label class="field-block">
<span>快手店铺</span>
<select
v-model="binding.shopId"
:class="['text-input', { 'text-input--invalid': isFieldInvalid(binding.id, 'shopId') }]"
data-field="shopId"
@change="handleKuaishouShopChange(binding)"
>
<option value="">请选择已配置的快手官方店铺</option>
<option
v-for="shop in kuaishouShopOptions"
:key="shop.shopId"
:value="shop.shopId"
>
{{ getKuaishouShopOptionLabel(shop) }}
</option>
</select>
<small class="field-help">这里会直接写入快手官方店铺 ID,不再使用 khhao 内部店铺编号。</small>
</label>
<label class="field-block">
<span>快手店铺名</span>
<input
:value="binding.shopName || ''"
class="text-input text-input--readonly"
placeholder="选择店铺后自动带出"
readonly
/>
</label>
</template>
<template v-else>
<label class="field-block">
<span>店铺 ID</span>
<input
v-model="binding.shopId"
:class="['text-input', { 'text-input--invalid': isFieldInvalid(binding.id, 'shopId') }]"
data-field="shopId"
placeholder="留空表示跨店铺通用"
@input="clearValidationState"
/>
</label>
<label class="field-block">
<span>店铺名称</span>
<input
v-model="binding.shopName"
class="text-input"
placeholder="用于保存和识别店铺"
@input="clearValidationState"
/>
</label>
</template>
<label class="field-block">
<span>优先级</span>
@@ -825,7 +1009,8 @@ onMounted(loadConfigs)
<td>
<div class="cell-stack">
<strong>{{ item.shopName || item.shopId || '-' }}</strong>
<span class="cell-subtle">ID: {{ item.shopId || '-' }}</span>
<span class="cell-subtle">khhao ID: {{ item.khhaoShopId || item.shopId || '-' }}</span>
<span v-if="item.kuaishouShopId" class="cell-subtle">快手 ID: {{ item.kuaishouShopId }}</span>
</div>
</td>
<td>
@@ -1379,6 +1564,11 @@ onMounted(loadConfigs)
box-shadow: 0 0 0 3px rgba(254, 226, 226, 0.95);
}
.text-input--readonly {
color: #475467;
background: rgba(148, 163, 184, 0.08);
}
.binding-inline-error {
margin: 10px 0 0;
color: #b42318;