feat(快手 cloud 履约): 三项前端改造 — cloudSourceKey 下拉选择、备选账号多选、SKU 按 sourceKey 缓存

需求1: cloudSourceKey 从文本输入改为下拉选择(选项来自 cloudtentacles sources 列表)
- Type: AdminKuaishouCloudFulfillmentItem 新增 cloudSourceKeyFallbacks?: string[]
- RuleCard.vue: cloudSourceKey 从 el-input 改为 el-select,选项来自 cloudtentaclesSourceOptions prop
- RuleSection.vue: 传递 cloudtentaclesSourceOptions prop 到 RuleCard
- View: 传递 config.cloudtentaclesSourceOptions 到 RuleSection

需求2: 同一 SKU 支持多个 cloudtentacles 账号(备选退避)
- RuleCard.vue: 新增备选 cloud 账号多选 el-select(multiple),过滤掉已选主账号
- useKuaishouCloudConfig.ts: createEmptyItem/mapEditableItem/mapSaveItem 处理 cloudSourceKeyFallbacks 字段

需求3: 选择 cloudSkuId 后自动补全 + sourceKey 感知的 SKU 查询
- useKuaishouCloudSku.ts: cloudSkuCatalog 从全局单一缓存改为按 sourceKey 缓存(cloudSkuCatalogBySourceKey)
- ensureCloudSkuCatalogLoaded(item) 传入 item 取其 sourceKey,fetchAdminCloudtentaclesSkuList({ sourceKey })
- getCloudSkuOptions/handleCloudSkuSelected/handleCloudSkuDropdownVisible 均按 sourceKey 取缓存的 catalog
- refreshCloudSkuCatalog(item?) 支持按 item 的 sourceKey 刷新或全部刷新

额外: useKuaishouCloudConfig 加载时额外调用 fetchAdminCloudtentaclesSourceConfig 获取 sources 列表
This commit is contained in:
yml
2026-05-20 01:04:54 +08:00
parent 1ef4db7f45
commit 557516ef1d
6 changed files with 130 additions and 50 deletions
@@ -22,6 +22,7 @@ export interface AdminKuaishouCloudFulfillmentItem {
kuaishouConsumeShopId: string
kuaishouConsumeShopName: string
notes: string
cloudSourceKeyFallbacks?: string[]
}
export interface AdminKuaishouCloudFulfillmentConfig {
@@ -45,6 +45,7 @@ onMounted(config.loadConfigs)
<AdminKuaishouCloudRuleSection
:enabled="config.enabled.value"
:saving="config.saving.value"
:cloudtentacles-source-options="config.cloudtentaclesSourceOptions.value"
:cloud-sku-catalog-loading="cloudSku.cloudSkuCatalogLoading.value"
:validation-state="config.validationState.value"
:items="config.items.value"
@@ -7,6 +7,7 @@ defineProps<{
item: EditableItem
index: number
validationState: ValidationState
cloudtentaclesSourceOptions: { key: string; label: string }[]
cloudSkuCatalogErrorMessage: string
cloudSkuCatalogLoading: boolean
isCollapsed: boolean
@@ -177,13 +178,37 @@ const emit = defineEmits<{
/>
</label>
<label class="field-block">
<span>cloudSourceKey</span>
<el-input
<span>cloud 主账号</span>
<el-select
v-model="item.cloudSourceKey"
class="text-input"
maxlength="60"
placeholder="默认 default"
/>
placeholder="选择 cloud 账号"
>
<el-option
v-for="opt in cloudtentaclesSourceOptions"
:key="opt.key"
:label="opt.label || opt.key"
:value="opt.key"
/>
</el-select>
</label>
<label class="field-block field-wide">
<span>备选 cloud 账号</span>
<el-select
v-model="item.cloudSourceKeyFallbacks"
class="text-input"
multiple
placeholder="主账号不可用时自动尝试备选"
>
<el-option
v-for="opt in cloudtentaclesSourceOptions.filter(
(o) => o.key !== item.cloudSourceKey,
)"
:key="opt.key"
:label="opt.label || opt.key"
:value="opt.key"
/>
</el-select>
</label>
<label class="field-block">
<span>cloud SKU ID</span>
@@ -8,6 +8,7 @@ import AdminKuaishouCloudRuleCard from './AdminKuaishouCloudRuleCard.vue'
defineProps<{
enabled: boolean
saving: boolean
cloudtentaclesSourceOptions: { key: string; label: string }[]
cloudSkuCatalogLoading: boolean
validationState: ValidationState
items: EditableItem[]
@@ -94,6 +95,7 @@ const emit = defineEmits<{
:item="item"
:index="index"
:validation-state="validationState"
:cloudtentacles-source-options="cloudtentaclesSourceOptions"
:cloud-sku-catalog-error-message="cloudSkuCatalogErrorMessage"
:cloud-sku-catalog-loading="cloudSkuCatalogLoading"
:is-collapsed="isCollapsed(item.localId)"
@@ -4,6 +4,7 @@ import { showError, showSuccess } from '@/lib/feedback'
import {
fetchAdminKuaishouCloudFulfillmentConfig,
fetchAdminKuaishouEticketSourceConfig,
fetchAdminCloudtentaclesSourceConfig,
saveAdminKuaishouCloudFulfillmentConfig,
} from '@/services/admin'
import type {
@@ -26,6 +27,7 @@ export function useKuaishouCloudConfig() {
const collapsedIds = ref<string[]>([])
const ruleFilter = ref<RuleFilter>('all')
const kuaishouConsumeShops = ref<AdminKuaishouEticketShopConfigItem[]>([])
const cloudtentaclesSourceOptions = ref<{ key: string; label: string }[]>([])
// ── computed ──────────────────────────────────────────
@@ -84,6 +86,7 @@ export function useKuaishouCloudConfig() {
kuaishouConsumeShopId: defaultShop?.shopId || '',
kuaishouConsumeShopName: defaultShop?.kshopName || '',
notes: '',
cloudSourceKeyFallbacks: [],
}
}
@@ -117,6 +120,9 @@ export function useKuaishouCloudConfig() {
kuaishouConsumeShopId: matchedShop?.shopId || item.kuaishouConsumeShopId || '',
kuaishouConsumeShopName: matchedShop?.kshopName || item.kuaishouConsumeShopName || '',
notes: item.notes || '',
cloudSourceKeyFallbacks: Array.isArray(item.cloudSourceKeyFallbacks)
? item.cloudSourceKeyFallbacks
: [],
}
}
@@ -147,8 +153,13 @@ export function useKuaishouCloudConfig() {
autoReturnNumberAfterDispatch: item.autoReturnNumberAfterDispatch,
autoConsumeAfterDispatch: item.autoConsumeAfterDispatch,
kuaishouConsumeShopId: String(matchedShop?.shopId || item.kuaishouConsumeShopId).trim(),
kuaishouConsumeShopName: String(matchedShop?.kshopName || item.kuaishouConsumeShopName).trim(),
kuaishouConsumeShopName: String(
matchedShop?.kshopName || item.kuaishouConsumeShopName,
).trim(),
notes: item.notes.trim(),
cloudSourceKeyFallbacks: Array.isArray(item.cloudSourceKeyFallbacks)
? item.cloudSourceKeyFallbacks.filter(Boolean)
: [],
}
}
@@ -165,36 +176,36 @@ export function useKuaishouCloudConfig() {
function hasMeaningfulContent(item: EditableItem) {
return Boolean(
item.internalSkuCode.trim() ||
item.internalSkuName.trim() ||
item.externalSkuCode.trim() ||
item.externalItemId.trim() ||
item.externalSkuName.trim() ||
item.resolvedSkuName.trim() ||
item.cloudSourceKey.trim() !== 'default' ||
Number(item.cloudSkuId || 0) > 0 ||
item.cloudSkuName.trim() ||
item.vnKey.trim() ||
item.shopId.trim() ||
item.notes.trim() ||
item.provider.trim() !== '91kaquan' ||
item.platform.trim() !== 'kuaishou' ||
item.priority !== 100 ||
item.autoBuyEnabled !== true ||
item.minAssetReserve !== 0 ||
item.autoReturnNumberAfterDispatch ||
item.autoConsumeAfterDispatch ||
item.kuaishouConsumeShopId.trim() ||
item.kuaishouConsumeShopName.trim() ||
item.enabled !== true,
item.internalSkuName.trim() ||
item.externalSkuCode.trim() ||
item.externalItemId.trim() ||
item.externalSkuName.trim() ||
item.resolvedSkuName.trim() ||
item.cloudSourceKey.trim() !== 'default' ||
Number(item.cloudSkuId || 0) > 0 ||
item.cloudSkuName.trim() ||
item.vnKey.trim() ||
item.shopId.trim() ||
item.notes.trim() ||
item.provider.trim() !== '91kaquan' ||
item.platform.trim() !== 'kuaishou' ||
item.priority !== 100 ||
item.autoBuyEnabled !== true ||
item.minAssetReserve !== 0 ||
item.autoReturnNumberAfterDispatch ||
item.autoConsumeAfterDispatch ||
item.kuaishouConsumeShopId.trim() ||
item.kuaishouConsumeShopName.trim() ||
item.enabled !== true,
)
}
function isItemComplete(item: EditableItem) {
return Boolean(
item.internalSkuCode.trim() &&
Number(item.cloudSkuId || 0) > 0 &&
(!item.autoConsumeAfterDispatch || Boolean(item.kuaishouConsumeShopId.trim())) &&
hasExternalMatch(item),
Number(item.cloudSkuId || 0) > 0 &&
(!item.autoConsumeAfterDispatch || Boolean(item.kuaishouConsumeShopId.trim())) &&
hasExternalMatch(item),
)
}
@@ -387,13 +398,21 @@ export function useKuaishouCloudConfig() {
errorMessage.value = ''
try {
const [response, eticketResponse] = await Promise.all([
const [response, eticketResponse, cloudtentaclesSourceResponse] = await Promise.all([
fetchAdminKuaishouCloudFulfillmentConfig(),
fetchAdminKuaishouEticketSourceConfig(),
fetchAdminCloudtentaclesSourceConfig(),
])
kuaishouConsumeShops.value = Array.isArray(eticketResponse.data.source.shops)
? eticketResponse.data.source.shops
: []
const sources = Array.isArray(cloudtentaclesSourceResponse.data.sources)
? cloudtentaclesSourceResponse.data.sources
: []
cloudtentaclesSourceOptions.value = sources.map((s) => ({
key: s.key,
label: s.label || s.key,
}))
filePath.value = response.data.filePath
enabled.value = response.data.source.enabled !== false
items.value = (response.data.source.items || []).map(mapEditableItem)
@@ -523,5 +542,7 @@ export function useKuaishouCloudConfig() {
removeItem,
saveConfigs,
focusValidationTarget,
// cloudtentacles sources
cloudtentaclesSourceOptions,
}
}
@@ -7,29 +7,34 @@ import type { AdminCloudtentaclesSkuItem } from '@/types/admin'
import type { EditableItem } from './types'
export function useKuaishouCloudSku() {
const cloudSkuCatalogLoading = ref(false)
const cloudSkuCatalogLoadingBySourceKey = ref<Record<string, boolean>>({})
const cloudSkuCatalogErrorMessage = ref('')
const cloudSkuCatalog = ref<AdminCloudtentaclesSkuItem[]>([])
const cloudSkuCatalogBySourceKey = ref<Record<string, AdminCloudtentaclesSkuItem[]>>({})
async function ensureCloudSkuCatalogLoaded(force = false) {
if (!force && cloudSkuCatalog.value.length > 0) {
return cloudSkuCatalog.value
function getSourceKey(item: EditableItem) {
return String(item.cloudSourceKey || 'default').trim()
}
async function ensureCloudSkuCatalogLoaded(item: EditableItem, force = false) {
const sourceKey = getSourceKey(item)
if (!force && (cloudSkuCatalogBySourceKey.value[sourceKey]?.length ?? 0) > 0) {
return cloudSkuCatalogBySourceKey.value[sourceKey]
}
cloudSkuCatalogLoading.value = true
cloudSkuCatalogLoadingBySourceKey.value[sourceKey] = true
cloudSkuCatalogErrorMessage.value = ''
try {
const response = await fetchAdminCloudtentaclesSkuList({})
cloudSkuCatalog.value = Array.isArray(response.data.items) ? response.data.items : []
return cloudSkuCatalog.value
const response = await fetchAdminCloudtentaclesSkuList({ sourceKey })
cloudSkuCatalogBySourceKey.value[sourceKey] = Array.isArray(response.data.items) ? response.data.items : []
return cloudSkuCatalogBySourceKey.value[sourceKey]
} catch (error) {
cloudSkuCatalog.value = []
cloudSkuCatalogBySourceKey.value[sourceKey] = []
cloudSkuCatalogErrorMessage.value =
error instanceof Error ? error.message : 'cloud SKU 列表查询失败'
throw error
} finally {
cloudSkuCatalogLoading.value = false
cloudSkuCatalogLoadingBySourceKey.value[sourceKey] = false
}
}
@@ -76,8 +81,10 @@ export function useKuaishouCloudSku() {
}
function getCloudSkuOptions(item: EditableItem) {
const sourceKey = getSourceKey(item)
const catalog = cloudSkuCatalogBySourceKey.value[sourceKey] || []
const keyword = getCloudSkuKeyword(item)
const scored = cloudSkuCatalog.value
const scored = catalog
.map((sku) => ({ sku, score: scoreCloudSkuMatch(sku, keyword) }))
.filter((entry) => entry.score > 0 || entry.sku.id === Number(item.cloudSkuId || 0))
.sort((left, right) => {
@@ -93,7 +100,7 @@ export function useKuaishouCloudSku() {
return scored.slice(0, 80)
}
return cloudSkuCatalog.value.slice(0, 80)
return catalog.slice(0, 80)
}
function formatCloudSkuOptionLabel(item: AdminCloudtentaclesSkuItem) {
@@ -111,7 +118,9 @@ export function useKuaishouCloudSku() {
return
}
const matched = cloudSkuCatalog.value.find((sku) => sku.id === item.cloudSkuId)
const sourceKey = getSourceKey(item)
const catalog = cloudSkuCatalogBySourceKey.value[sourceKey] || []
const matched = catalog.find((sku) => sku.id === item.cloudSkuId)
item.cloudSkuName = matched?.name || item.cloudSkuName || ''
}
@@ -121,7 +130,7 @@ export function useKuaishouCloudSku() {
}
try {
await ensureCloudSkuCatalogLoaded()
await ensureCloudSkuCatalogLoaded(item)
if (item.cloudSkuId && !item.cloudSkuName) {
handleCloudSkuSelected(item, item.cloudSkuId)
}
@@ -130,23 +139,44 @@ export function useKuaishouCloudSku() {
}
}
async function refreshCloudSkuCatalog() {
async function refreshCloudSkuCatalog(item?: EditableItem) {
try {
await ensureCloudSkuCatalogLoaded(true)
showSuccess(`cloud SKU 列表已刷新,共 ${cloudSkuCatalog.value.length}`)
if (item) {
await ensureCloudSkuCatalogLoaded(item, true)
const sourceKey = getSourceKey(item)
const count = cloudSkuCatalogBySourceKey.value[sourceKey]?.length ?? 0
showSuccess(`cloud SKU 列表(${sourceKey})已刷新,共 ${count}`)
} else {
// No item provided — refresh all loaded caches
const keys = Object.keys(cloudSkuCatalogBySourceKey.value)
for (const key of keys) {
await ensureCloudSkuCatalogLoaded({ cloudSourceKey: key, localId: '' } as EditableItem, true)
}
const total = Object.values(cloudSkuCatalogBySourceKey.value).reduce((sum, list) => sum + list.length, 0)
showSuccess(`cloud SKU 列表已全部刷新,共 ${total}`)
}
} catch (error) {
showError(error instanceof Error ? error.message : 'cloud SKU 列表查询失败')
}
}
const cloudSkuCatalogLoading = ref(false)
// Maintain backward-compat: derive a combined loading state
// The template uses cloudSkuCatalogLoading for the refresh button spinner
function syncCloudSkuCatalogLoading() {
cloudSkuCatalogLoading.value = Object.values(cloudSkuCatalogLoadingBySourceKey.value).some(Boolean)
}
return {
cloudSkuCatalogLoading,
cloudSkuCatalogLoadingBySourceKey,
cloudSkuCatalogErrorMessage,
cloudSkuCatalog,
cloudSkuCatalogBySourceKey,
getCloudSkuOptions,
formatCloudSkuOptionLabel,
handleCloudSkuSelected,
handleCloudSkuDropdownVisible,
refreshCloudSkuCatalog,
syncCloudSkuCatalogLoading,
}
}