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 kuaishouConsumeShopId: string
kuaishouConsumeShopName: string kuaishouConsumeShopName: string
notes: string notes: string
cloudSourceKeyFallbacks?: string[]
} }
export interface AdminKuaishouCloudFulfillmentConfig { export interface AdminKuaishouCloudFulfillmentConfig {
@@ -45,6 +45,7 @@ onMounted(config.loadConfigs)
<AdminKuaishouCloudRuleSection <AdminKuaishouCloudRuleSection
:enabled="config.enabled.value" :enabled="config.enabled.value"
:saving="config.saving.value" :saving="config.saving.value"
:cloudtentacles-source-options="config.cloudtentaclesSourceOptions.value"
:cloud-sku-catalog-loading="cloudSku.cloudSkuCatalogLoading.value" :cloud-sku-catalog-loading="cloudSku.cloudSkuCatalogLoading.value"
:validation-state="config.validationState.value" :validation-state="config.validationState.value"
:items="config.items.value" :items="config.items.value"
@@ -7,6 +7,7 @@ defineProps<{
item: EditableItem item: EditableItem
index: number index: number
validationState: ValidationState validationState: ValidationState
cloudtentaclesSourceOptions: { key: string; label: string }[]
cloudSkuCatalogErrorMessage: string cloudSkuCatalogErrorMessage: string
cloudSkuCatalogLoading: boolean cloudSkuCatalogLoading: boolean
isCollapsed: boolean isCollapsed: boolean
@@ -177,13 +178,37 @@ const emit = defineEmits<{
/> />
</label> </label>
<label class="field-block"> <label class="field-block">
<span>cloudSourceKey</span> <span>cloud 主账号</span>
<el-input <el-select
v-model="item.cloudSourceKey" v-model="item.cloudSourceKey"
class="text-input" class="text-input"
maxlength="60" placeholder="选择 cloud 账号"
placeholder="默认 default" >
<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>
<label class="field-block"> <label class="field-block">
<span>cloud SKU ID</span> <span>cloud SKU ID</span>
@@ -8,6 +8,7 @@ import AdminKuaishouCloudRuleCard from './AdminKuaishouCloudRuleCard.vue'
defineProps<{ defineProps<{
enabled: boolean enabled: boolean
saving: boolean saving: boolean
cloudtentaclesSourceOptions: { key: string; label: string }[]
cloudSkuCatalogLoading: boolean cloudSkuCatalogLoading: boolean
validationState: ValidationState validationState: ValidationState
items: EditableItem[] items: EditableItem[]
@@ -94,6 +95,7 @@ const emit = defineEmits<{
:item="item" :item="item"
:index="index" :index="index"
:validation-state="validationState" :validation-state="validationState"
:cloudtentacles-source-options="cloudtentaclesSourceOptions"
:cloud-sku-catalog-error-message="cloudSkuCatalogErrorMessage" :cloud-sku-catalog-error-message="cloudSkuCatalogErrorMessage"
:cloud-sku-catalog-loading="cloudSkuCatalogLoading" :cloud-sku-catalog-loading="cloudSkuCatalogLoading"
:is-collapsed="isCollapsed(item.localId)" :is-collapsed="isCollapsed(item.localId)"
@@ -4,6 +4,7 @@ import { showError, showSuccess } from '@/lib/feedback'
import { import {
fetchAdminKuaishouCloudFulfillmentConfig, fetchAdminKuaishouCloudFulfillmentConfig,
fetchAdminKuaishouEticketSourceConfig, fetchAdminKuaishouEticketSourceConfig,
fetchAdminCloudtentaclesSourceConfig,
saveAdminKuaishouCloudFulfillmentConfig, saveAdminKuaishouCloudFulfillmentConfig,
} from '@/services/admin' } from '@/services/admin'
import type { import type {
@@ -26,6 +27,7 @@ export function useKuaishouCloudConfig() {
const collapsedIds = ref<string[]>([]) const collapsedIds = ref<string[]>([])
const ruleFilter = ref<RuleFilter>('all') const ruleFilter = ref<RuleFilter>('all')
const kuaishouConsumeShops = ref<AdminKuaishouEticketShopConfigItem[]>([]) const kuaishouConsumeShops = ref<AdminKuaishouEticketShopConfigItem[]>([])
const cloudtentaclesSourceOptions = ref<{ key: string; label: string }[]>([])
// ── computed ────────────────────────────────────────── // ── computed ──────────────────────────────────────────
@@ -84,6 +86,7 @@ export function useKuaishouCloudConfig() {
kuaishouConsumeShopId: defaultShop?.shopId || '', kuaishouConsumeShopId: defaultShop?.shopId || '',
kuaishouConsumeShopName: defaultShop?.kshopName || '', kuaishouConsumeShopName: defaultShop?.kshopName || '',
notes: '', notes: '',
cloudSourceKeyFallbacks: [],
} }
} }
@@ -117,6 +120,9 @@ export function useKuaishouCloudConfig() {
kuaishouConsumeShopId: matchedShop?.shopId || item.kuaishouConsumeShopId || '', kuaishouConsumeShopId: matchedShop?.shopId || item.kuaishouConsumeShopId || '',
kuaishouConsumeShopName: matchedShop?.kshopName || item.kuaishouConsumeShopName || '', kuaishouConsumeShopName: matchedShop?.kshopName || item.kuaishouConsumeShopName || '',
notes: item.notes || '', notes: item.notes || '',
cloudSourceKeyFallbacks: Array.isArray(item.cloudSourceKeyFallbacks)
? item.cloudSourceKeyFallbacks
: [],
} }
} }
@@ -147,8 +153,13 @@ export function useKuaishouCloudConfig() {
autoReturnNumberAfterDispatch: item.autoReturnNumberAfterDispatch, autoReturnNumberAfterDispatch: item.autoReturnNumberAfterDispatch,
autoConsumeAfterDispatch: item.autoConsumeAfterDispatch, autoConsumeAfterDispatch: item.autoConsumeAfterDispatch,
kuaishouConsumeShopId: String(matchedShop?.shopId || item.kuaishouConsumeShopId).trim(), 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(), notes: item.notes.trim(),
cloudSourceKeyFallbacks: Array.isArray(item.cloudSourceKeyFallbacks)
? item.cloudSourceKeyFallbacks.filter(Boolean)
: [],
} }
} }
@@ -387,13 +398,21 @@ export function useKuaishouCloudConfig() {
errorMessage.value = '' errorMessage.value = ''
try { try {
const [response, eticketResponse] = await Promise.all([ const [response, eticketResponse, cloudtentaclesSourceResponse] = await Promise.all([
fetchAdminKuaishouCloudFulfillmentConfig(), fetchAdminKuaishouCloudFulfillmentConfig(),
fetchAdminKuaishouEticketSourceConfig(), fetchAdminKuaishouEticketSourceConfig(),
fetchAdminCloudtentaclesSourceConfig(),
]) ])
kuaishouConsumeShops.value = Array.isArray(eticketResponse.data.source.shops) kuaishouConsumeShops.value = Array.isArray(eticketResponse.data.source.shops)
? 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 filePath.value = response.data.filePath
enabled.value = response.data.source.enabled !== false enabled.value = response.data.source.enabled !== false
items.value = (response.data.source.items || []).map(mapEditableItem) items.value = (response.data.source.items || []).map(mapEditableItem)
@@ -523,5 +542,7 @@ export function useKuaishouCloudConfig() {
removeItem, removeItem,
saveConfigs, saveConfigs,
focusValidationTarget, focusValidationTarget,
// cloudtentacles sources
cloudtentaclesSourceOptions,
} }
} }
@@ -7,29 +7,34 @@ import type { AdminCloudtentaclesSkuItem } from '@/types/admin'
import type { EditableItem } from './types' import type { EditableItem } from './types'
export function useKuaishouCloudSku() { export function useKuaishouCloudSku() {
const cloudSkuCatalogLoading = ref(false) const cloudSkuCatalogLoadingBySourceKey = ref<Record<string, boolean>>({})
const cloudSkuCatalogErrorMessage = ref('') const cloudSkuCatalogErrorMessage = ref('')
const cloudSkuCatalog = ref<AdminCloudtentaclesSkuItem[]>([]) const cloudSkuCatalogBySourceKey = ref<Record<string, AdminCloudtentaclesSkuItem[]>>({})
async function ensureCloudSkuCatalogLoaded(force = false) { function getSourceKey(item: EditableItem) {
if (!force && cloudSkuCatalog.value.length > 0) { return String(item.cloudSourceKey || 'default').trim()
return cloudSkuCatalog.value
} }
cloudSkuCatalogLoading.value = true async function ensureCloudSkuCatalogLoaded(item: EditableItem, force = false) {
const sourceKey = getSourceKey(item)
if (!force && (cloudSkuCatalogBySourceKey.value[sourceKey]?.length ?? 0) > 0) {
return cloudSkuCatalogBySourceKey.value[sourceKey]
}
cloudSkuCatalogLoadingBySourceKey.value[sourceKey] = true
cloudSkuCatalogErrorMessage.value = '' cloudSkuCatalogErrorMessage.value = ''
try { try {
const response = await fetchAdminCloudtentaclesSkuList({}) const response = await fetchAdminCloudtentaclesSkuList({ sourceKey })
cloudSkuCatalog.value = Array.isArray(response.data.items) ? response.data.items : [] cloudSkuCatalogBySourceKey.value[sourceKey] = Array.isArray(response.data.items) ? response.data.items : []
return cloudSkuCatalog.value return cloudSkuCatalogBySourceKey.value[sourceKey]
} catch (error) { } catch (error) {
cloudSkuCatalog.value = [] cloudSkuCatalogBySourceKey.value[sourceKey] = []
cloudSkuCatalogErrorMessage.value = cloudSkuCatalogErrorMessage.value =
error instanceof Error ? error.message : 'cloud SKU 列表查询失败' error instanceof Error ? error.message : 'cloud SKU 列表查询失败'
throw error throw error
} finally { } finally {
cloudSkuCatalogLoading.value = false cloudSkuCatalogLoadingBySourceKey.value[sourceKey] = false
} }
} }
@@ -76,8 +81,10 @@ export function useKuaishouCloudSku() {
} }
function getCloudSkuOptions(item: EditableItem) { function getCloudSkuOptions(item: EditableItem) {
const sourceKey = getSourceKey(item)
const catalog = cloudSkuCatalogBySourceKey.value[sourceKey] || []
const keyword = getCloudSkuKeyword(item) const keyword = getCloudSkuKeyword(item)
const scored = cloudSkuCatalog.value const scored = catalog
.map((sku) => ({ sku, score: scoreCloudSkuMatch(sku, keyword) })) .map((sku) => ({ sku, score: scoreCloudSkuMatch(sku, keyword) }))
.filter((entry) => entry.score > 0 || entry.sku.id === Number(item.cloudSkuId || 0)) .filter((entry) => entry.score > 0 || entry.sku.id === Number(item.cloudSkuId || 0))
.sort((left, right) => { .sort((left, right) => {
@@ -93,7 +100,7 @@ export function useKuaishouCloudSku() {
return scored.slice(0, 80) return scored.slice(0, 80)
} }
return cloudSkuCatalog.value.slice(0, 80) return catalog.slice(0, 80)
} }
function formatCloudSkuOptionLabel(item: AdminCloudtentaclesSkuItem) { function formatCloudSkuOptionLabel(item: AdminCloudtentaclesSkuItem) {
@@ -111,7 +118,9 @@ export function useKuaishouCloudSku() {
return 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 || '' item.cloudSkuName = matched?.name || item.cloudSkuName || ''
} }
@@ -121,7 +130,7 @@ export function useKuaishouCloudSku() {
} }
try { try {
await ensureCloudSkuCatalogLoaded() await ensureCloudSkuCatalogLoaded(item)
if (item.cloudSkuId && !item.cloudSkuName) { if (item.cloudSkuId && !item.cloudSkuName) {
handleCloudSkuSelected(item, item.cloudSkuId) handleCloudSkuSelected(item, item.cloudSkuId)
} }
@@ -130,23 +139,44 @@ export function useKuaishouCloudSku() {
} }
} }
async function refreshCloudSkuCatalog() { async function refreshCloudSkuCatalog(item?: EditableItem) {
try { try {
await ensureCloudSkuCatalogLoaded(true) if (item) {
showSuccess(`cloud SKU 列表已刷新,共 ${cloudSkuCatalog.value.length}`) 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) { } catch (error) {
showError(error instanceof Error ? error.message : 'cloud SKU 列表查询失败') 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 { return {
cloudSkuCatalogLoading, cloudSkuCatalogLoading,
cloudSkuCatalogLoadingBySourceKey,
cloudSkuCatalogErrorMessage, cloudSkuCatalogErrorMessage,
cloudSkuCatalog, cloudSkuCatalogBySourceKey,
getCloudSkuOptions, getCloudSkuOptions,
formatCloudSkuOptionLabel, formatCloudSkuOptionLabel,
handleCloudSkuSelected, handleCloudSkuSelected,
handleCloudSkuDropdownVisible, handleCloudSkuDropdownVisible,
refreshCloudSkuCatalog, refreshCloudSkuCatalog,
syncCloudSkuCatalogLoading,
} }
} }