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:
@@ -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 {
|
||||||
|
|||||||
+1
@@ -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"
|
||||||
|
|||||||
+29
-4
@@ -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>
|
||||||
|
|||||||
+2
@@ -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)"
|
||||||
|
|||||||
+23
-2
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+49
-19
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user