完整流程-调试
This commit is contained in:
@@ -65,6 +65,7 @@ type ValidationState = {
|
||||
const PROFILE_OPTIONS = [
|
||||
{ label: '腾讯领取兑换', value: 'tencent_claim_redeem' },
|
||||
{ label: '腾讯领取兑换(半自动+人工)', value: 'tencent_claim_assisted' },
|
||||
{ label: '快手 Cloud 履约', value: 'kuaishou_ct_assisted' },
|
||||
{ label: '人工发货', value: 'manual_review' },
|
||||
]
|
||||
|
||||
@@ -102,6 +103,7 @@ const ruleMetrics = computed(() => {
|
||||
const completedCount = bindings.value.filter(isEditableBindingComplete).length
|
||||
const manualCount = bindings.value.filter((item) => item.profileKey.trim() === 'manual_review').length
|
||||
const assistedCount = bindings.value.filter((item) => item.profileKey.trim() === 'tencent_claim_assisted').length
|
||||
const kuaishouCloudCount = bindings.value.filter((item) => item.profileKey.trim() === 'kuaishou_ct_assisted').length
|
||||
|
||||
return {
|
||||
totalCount: bindings.value.length,
|
||||
@@ -109,6 +111,7 @@ const ruleMetrics = computed(() => {
|
||||
draftCount: Math.max(bindings.value.length - completedCount, 0),
|
||||
manualCount,
|
||||
assistedCount,
|
||||
kuaishouCloudCount,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -536,7 +539,7 @@ onMounted(loadConfigs)
|
||||
<header class="panel-header">
|
||||
<div class="panel-header-copy">
|
||||
<h1>履约配置</h1>
|
||||
<p>把外部商品统一映射到内部履约 SKU,并指定最终走自动、半自动还是人工处理。</p>
|
||||
<p>把外部商品统一映射到内部履约 SKU,并指定最终走腾讯领取、快手 Cloud 或人工处理。</p>
|
||||
</div>
|
||||
<div class="header-tags">
|
||||
<span class="header-tag">外部条件命中 1 个即可</span>
|
||||
@@ -603,6 +606,7 @@ onMounted(loadConfigs)
|
||||
<span class="mini-stat-chip">待完善 {{ ruleMetrics.draftCount }}</span>
|
||||
<span class="mini-stat-chip">人工 {{ ruleMetrics.manualCount }}</span>
|
||||
<span class="mini-stat-chip">半自动 {{ ruleMetrics.assistedCount }}</span>
|
||||
<span class="mini-stat-chip">快手 Cloud {{ ruleMetrics.kuaishouCloudCount }}</span>
|
||||
</div>
|
||||
<div class="section-action-group">
|
||||
<el-button round @click="addBinding">新增规则</el-button>
|
||||
|
||||
@@ -0,0 +1,802 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
|
||||
import { showError, showSuccess } from '@/lib/feedback'
|
||||
import {
|
||||
fetchAdminKuaishouCloudFulfillmentConfig,
|
||||
saveAdminKuaishouCloudFulfillmentConfig,
|
||||
} from '@/services/admin'
|
||||
import type {
|
||||
AdminKuaishouCloudFulfillmentConfig,
|
||||
AdminKuaishouCloudFulfillmentItem,
|
||||
} from '@/types/admin'
|
||||
import { hasAdminRole } from '@/utils/admin-auth'
|
||||
|
||||
type EditableItem = AdminKuaishouCloudFulfillmentItem & {
|
||||
localId: string
|
||||
}
|
||||
|
||||
type ValidationState = {
|
||||
localId: string
|
||||
message: string
|
||||
} | null
|
||||
|
||||
const loading = ref(true)
|
||||
const saving = ref(false)
|
||||
const errorMessage = ref('')
|
||||
const validationState = ref<ValidationState>(null)
|
||||
const filePath = ref('')
|
||||
const enabled = ref(true)
|
||||
const items = ref<EditableItem[]>([])
|
||||
const collapsedIds = ref<string[]>([])
|
||||
|
||||
const metrics = computed(() => {
|
||||
const readyCount = items.value.filter(isItemComplete).length
|
||||
const enabledCount = items.value.filter((item) => item.enabled).length
|
||||
return {
|
||||
total: items.value.length,
|
||||
readyCount,
|
||||
enabledCount,
|
||||
draftCount: Math.max(items.value.length - readyCount, 0),
|
||||
}
|
||||
})
|
||||
|
||||
function createEmptyItem(): EditableItem {
|
||||
return {
|
||||
localId: crypto.randomUUID(),
|
||||
id: '',
|
||||
enabled: true,
|
||||
priority: 100,
|
||||
provider: 'khhao',
|
||||
platform: 'kuaishou',
|
||||
shopId: '',
|
||||
internalSkuCode: '',
|
||||
internalSkuName: '',
|
||||
externalSkuCode: '',
|
||||
externalItemId: '',
|
||||
externalSkuName: '',
|
||||
resolvedSkuName: '',
|
||||
cloudSourceKey: 'default',
|
||||
cloudSkuId: 0,
|
||||
cloudSkuName: '',
|
||||
vnKey: '',
|
||||
autoBuyEnabled: true,
|
||||
minAssetReserve: 0,
|
||||
autoReturnNumberAfterDispatch: false,
|
||||
autoConsumeAfterDispatch: false,
|
||||
kuaishouConsumeShopId: '',
|
||||
notes: '',
|
||||
}
|
||||
}
|
||||
|
||||
function mapEditableItem(item: AdminKuaishouCloudFulfillmentItem): EditableItem {
|
||||
return {
|
||||
localId: crypto.randomUUID(),
|
||||
id: item.id || crypto.randomUUID(),
|
||||
enabled: item.enabled !== false,
|
||||
priority: item.priority || 100,
|
||||
provider: item.provider || 'khhao',
|
||||
platform: item.platform || 'kuaishou',
|
||||
shopId: item.shopId || '',
|
||||
internalSkuCode: item.internalSkuCode || '',
|
||||
internalSkuName: item.internalSkuName || '',
|
||||
externalSkuCode: item.externalSkuCode || '',
|
||||
externalItemId: item.externalItemId || '',
|
||||
externalSkuName: item.externalSkuName || '',
|
||||
resolvedSkuName: item.resolvedSkuName || '',
|
||||
cloudSourceKey: item.cloudSourceKey || 'default',
|
||||
cloudSkuId: Number(item.cloudSkuId || 0) || 0,
|
||||
cloudSkuName: item.cloudSkuName || '',
|
||||
vnKey: item.vnKey || '',
|
||||
autoBuyEnabled: item.autoBuyEnabled !== false,
|
||||
minAssetReserve: Number(item.minAssetReserve || 0) || 0,
|
||||
autoReturnNumberAfterDispatch: item.autoReturnNumberAfterDispatch === true,
|
||||
autoConsumeAfterDispatch: item.autoConsumeAfterDispatch === true,
|
||||
kuaishouConsumeShopId: item.kuaishouConsumeShopId || '',
|
||||
notes: item.notes || '',
|
||||
}
|
||||
}
|
||||
|
||||
function mapSaveItem(item: EditableItem): AdminKuaishouCloudFulfillmentItem {
|
||||
return {
|
||||
id: item.id.trim() || item.internalSkuCode.trim() || item.localId,
|
||||
enabled: item.enabled,
|
||||
priority: Number(item.priority || 100) || 100,
|
||||
provider: item.provider.trim() || 'khhao',
|
||||
platform: item.platform.trim() || 'kuaishou',
|
||||
shopId: item.shopId.trim(),
|
||||
internalSkuCode: item.internalSkuCode.trim(),
|
||||
internalSkuName: item.internalSkuName.trim(),
|
||||
externalSkuCode: item.externalSkuCode.trim(),
|
||||
externalItemId: item.externalItemId.trim(),
|
||||
externalSkuName: item.externalSkuName.trim(),
|
||||
resolvedSkuName: item.resolvedSkuName.trim(),
|
||||
cloudSourceKey: item.cloudSourceKey.trim() || 'default',
|
||||
cloudSkuId: Number(item.cloudSkuId || 0) || 0,
|
||||
cloudSkuName: item.cloudSkuName.trim(),
|
||||
vnKey: item.vnKey.trim(),
|
||||
autoBuyEnabled: item.autoBuyEnabled,
|
||||
minAssetReserve: Math.max(0, Number(item.minAssetReserve || 0) || 0),
|
||||
autoReturnNumberAfterDispatch: item.autoReturnNumberAfterDispatch,
|
||||
autoConsumeAfterDispatch: item.autoConsumeAfterDispatch,
|
||||
kuaishouConsumeShopId: item.kuaishouConsumeShopId.trim(),
|
||||
notes: item.notes.trim(),
|
||||
}
|
||||
}
|
||||
|
||||
function hasExternalMatch(item: Pick<EditableItem, 'externalSkuCode' | 'externalItemId' | 'externalSkuName'>) {
|
||||
return Boolean(item.externalSkuCode.trim() || item.externalItemId.trim() || item.externalSkuName.trim())
|
||||
}
|
||||
|
||||
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() !== 'khhao'
|
||||
|| item.platform.trim() !== 'kuaishou'
|
||||
|| item.priority !== 100
|
||||
|| item.autoBuyEnabled !== true
|
||||
|| item.minAssetReserve !== 0
|
||||
|| item.autoReturnNumberAfterDispatch
|
||||
|| item.autoConsumeAfterDispatch
|
||||
|| item.enabled !== true,
|
||||
)
|
||||
}
|
||||
|
||||
function isItemComplete(item: EditableItem) {
|
||||
return Boolean(
|
||||
item.internalSkuCode.trim()
|
||||
&& Number(item.cloudSkuId || 0) > 0
|
||||
&& item.vnKey.trim()
|
||||
&& hasExternalMatch(item),
|
||||
)
|
||||
}
|
||||
|
||||
function resolveValidation(item: EditableItem, index: number): ValidationState {
|
||||
if (!hasMeaningfulContent(item)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const title = `第 ${index + 1} 条规则`
|
||||
|
||||
if (!item.internalSkuCode.trim()) {
|
||||
return { localId: item.localId, message: `${title} 缺少内部 SKU 编码` }
|
||||
}
|
||||
|
||||
if (Number(item.cloudSkuId || 0) <= 0) {
|
||||
return { localId: item.localId, message: `${title} 需要填写 cloud SKU ID` }
|
||||
}
|
||||
|
||||
if (!item.vnKey.trim()) {
|
||||
return { localId: item.localId, message: `${title} 需要填写虚拟号 VN Key` }
|
||||
}
|
||||
|
||||
if (!hasExternalMatch(item)) {
|
||||
return { localId: item.localId, message: `${title} 至少填写一种外部匹配条件` }
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function getCardTitle(item: EditableItem, index: number) {
|
||||
return item.internalSkuName.trim() || item.externalSkuName.trim() || item.internalSkuCode.trim() || `规则 ${index + 1}`
|
||||
}
|
||||
|
||||
function getCardSummary(item: EditableItem) {
|
||||
const parts = [
|
||||
item.provider.trim() || 'khhao',
|
||||
item.platform.trim() || 'kuaishou',
|
||||
item.shopId.trim() || '跨店铺',
|
||||
item.cloudSkuId > 0 ? `cloud#${item.cloudSkuId}` : '待填 cloud SKU',
|
||||
]
|
||||
return parts.join(' / ')
|
||||
}
|
||||
|
||||
function isCollapsed(localId: string) {
|
||||
return collapsedIds.value.includes(localId)
|
||||
}
|
||||
|
||||
function setCollapsed(localId: string, collapsed: boolean) {
|
||||
const next = new Set(collapsedIds.value)
|
||||
if (collapsed) {
|
||||
next.add(localId)
|
||||
} else {
|
||||
next.delete(localId)
|
||||
}
|
||||
collapsedIds.value = Array.from(next)
|
||||
}
|
||||
|
||||
function toggleCollapsed(localId: string) {
|
||||
setCollapsed(localId, !isCollapsed(localId))
|
||||
}
|
||||
|
||||
function rebuildCollapsedState() {
|
||||
collapsedIds.value = items.value.filter(isItemComplete).map((item) => item.localId)
|
||||
}
|
||||
|
||||
async function loadConfigs() {
|
||||
if (!hasAdminRole('admin')) {
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
errorMessage.value = ''
|
||||
|
||||
try {
|
||||
const response = await fetchAdminKuaishouCloudFulfillmentConfig()
|
||||
filePath.value = response.data.filePath
|
||||
enabled.value = response.data.source.enabled !== false
|
||||
items.value = (response.data.source.items || []).map(mapEditableItem)
|
||||
rebuildCollapsedState()
|
||||
} catch (error) {
|
||||
errorMessage.value = error instanceof Error ? error.message : '读取新履约配置失败'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function addItem() {
|
||||
validationState.value = null
|
||||
const next = createEmptyItem()
|
||||
items.value.unshift(next)
|
||||
setCollapsed(next.localId, false)
|
||||
}
|
||||
|
||||
function removeItem(localId: string) {
|
||||
if (validationState.value?.localId === localId) {
|
||||
validationState.value = null
|
||||
}
|
||||
items.value = items.value.filter((item) => item.localId !== localId)
|
||||
setCollapsed(localId, false)
|
||||
}
|
||||
|
||||
async function saveConfigs() {
|
||||
const invalid = items.value.reduce<ValidationState>((state, item, index) => state || resolveValidation(item, index), null)
|
||||
if (invalid) {
|
||||
validationState.value = invalid
|
||||
setCollapsed(invalid.localId, false)
|
||||
return
|
||||
}
|
||||
|
||||
const payloadItems = items.value.filter(hasMeaningfulContent).map(mapSaveItem)
|
||||
const payload: AdminKuaishouCloudFulfillmentConfig = {
|
||||
enabled: enabled.value,
|
||||
items: payloadItems,
|
||||
}
|
||||
|
||||
saving.value = true
|
||||
validationState.value = null
|
||||
|
||||
try {
|
||||
const response = await saveAdminKuaishouCloudFulfillmentConfig(payload)
|
||||
filePath.value = response.data.filePath
|
||||
enabled.value = response.data.source.enabled !== false
|
||||
items.value = (response.data.source.items || []).map(mapEditableItem)
|
||||
rebuildCollapsedState()
|
||||
showSuccess('新履约配置已保存')
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : '保存新履约配置失败'
|
||||
validationState.value = { localId: '', message }
|
||||
showError(message)
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadConfigs)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="admin-panel">
|
||||
<header class="panel-header hero-card">
|
||||
<div class="hero-copy">
|
||||
<h1>新履约配置</h1>
|
||||
<p>独立维护快手下单后经 cloudtentacles 完成绑定、发货与退号的新链路,不影响旧履约规则。</p>
|
||||
</div>
|
||||
<div class="hero-chips">
|
||||
<span>独立新流程</span>
|
||||
<span>保存后同步到任务匹配</span>
|
||||
<span>支持折叠维护</span>
|
||||
</div>
|
||||
<div class="hero-actions">
|
||||
<el-switch v-model="enabled" active-text="启用整条配置" />
|
||||
<el-button round @click="loadConfigs">刷新</el-button>
|
||||
<el-button round type="primary" :loading="saving" @click="saveConfigs">保存配置</el-button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div v-if="!hasAdminRole('admin')" class="empty-block">仅管理员可以维护新履约配置。</div>
|
||||
|
||||
<template v-else>
|
||||
<section class="overview-card">
|
||||
<div class="overview-file">
|
||||
<span>配置文件</span>
|
||||
<code>{{ filePath || '-' }}</code>
|
||||
</div>
|
||||
<div class="overview-stats">
|
||||
<article>
|
||||
<span>规则总数</span>
|
||||
<strong>{{ metrics.total }}</strong>
|
||||
</article>
|
||||
<article>
|
||||
<span>已完成</span>
|
||||
<strong>{{ metrics.readyCount }}</strong>
|
||||
</article>
|
||||
<article>
|
||||
<span>草稿中</span>
|
||||
<strong>{{ metrics.draftCount }}</strong>
|
||||
</article>
|
||||
<article>
|
||||
<span>已启用</span>
|
||||
<strong>{{ metrics.enabledCount }}</strong>
|
||||
</article>
|
||||
</div>
|
||||
<div class="overview-notes">
|
||||
<span>外部商品命中后会映射到 `kuaishou_ct_assisted`</span>
|
||||
<span>客服只保留“确认绑定完成并发货”一个动作</span>
|
||||
<span>退还号码作为最终收口动作</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<p v-if="errorMessage" class="error-copy">{{ errorMessage }}</p>
|
||||
<p v-if="validationState?.message" class="validation-copy">{{ validationState.message }}</p>
|
||||
<div v-if="loading" class="empty-block">新履约配置加载中</div>
|
||||
|
||||
<template v-else>
|
||||
<section class="toolbar-card">
|
||||
<div>
|
||||
<h3>规则列表</h3>
|
||||
<p>每一条规则描述“快手外部商品”如何映射到“内部 SKU + cloud 资源”。</p>
|
||||
</div>
|
||||
<div class="toolbar-actions">
|
||||
<el-button round @click="addItem">新增规则</el-button>
|
||||
<el-button round type="primary" :loading="saving" @click="saveConfigs">保存全部</el-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div v-if="items.length === 0" class="empty-block">当前还没有新履约规则,先新增一条。</div>
|
||||
|
||||
<article
|
||||
v-for="(item, index) in items"
|
||||
:key="item.localId"
|
||||
class="rule-card"
|
||||
:data-invalid="validationState?.localId === item.localId"
|
||||
>
|
||||
<button class="rule-head" type="button" @click="toggleCollapsed(item.localId)">
|
||||
<div>
|
||||
<strong>{{ getCardTitle(item, index) }}</strong>
|
||||
<p>{{ getCardSummary(item) }}</p>
|
||||
</div>
|
||||
<div class="rule-head-meta">
|
||||
<span class="status-pill" :data-tone="isItemComplete(item) ? 'success' : 'warning'">
|
||||
{{ isItemComplete(item) ? '可用' : '待完善' }}
|
||||
</span>
|
||||
<span class="collapse-indicator">{{ isCollapsed(item.localId) ? '展开' : '收起' }}</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div v-if="!isCollapsed(item.localId)" class="rule-body">
|
||||
<div class="grid cols-4">
|
||||
<label class="field-block">
|
||||
<span>内部 SKU</span>
|
||||
<el-input v-model="item.internalSkuCode" maxlength="80" placeholder="必填,用于生成任务" />
|
||||
</label>
|
||||
<label class="field-block">
|
||||
<span>内部商品名</span>
|
||||
<el-input v-model="item.internalSkuName" maxlength="120" placeholder="展示用,可选但建议填" />
|
||||
</label>
|
||||
<label class="field-block">
|
||||
<span>优先级</span>
|
||||
<el-input-number v-model="item.priority" :min="1" :step="1" controls-position="right" />
|
||||
</label>
|
||||
<label class="field-block field-block--switch">
|
||||
<span>规则开关</span>
|
||||
<el-switch v-model="item.enabled" active-text="启用" inactive-text="停用" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="grid cols-4">
|
||||
<label class="field-block">
|
||||
<span>来源 provider</span>
|
||||
<el-input v-model="item.provider" maxlength="32" placeholder="默认 khhao" />
|
||||
</label>
|
||||
<label class="field-block">
|
||||
<span>来源 platform</span>
|
||||
<el-input v-model="item.platform" maxlength="32" placeholder="默认 kuaishou" />
|
||||
</label>
|
||||
<label class="field-block">
|
||||
<span>店铺 ID</span>
|
||||
<el-input v-model="item.shopId" maxlength="80" placeholder="留空表示跨店铺共用" />
|
||||
</label>
|
||||
<label class="field-block">
|
||||
<span>解析商品名</span>
|
||||
<el-input v-model="item.resolvedSkuName" maxlength="120" placeholder="可作为商品名兜底" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="section-split">
|
||||
<div>
|
||||
<h4>外部匹配条件</h4>
|
||||
<p>外部 SKU、商品 ID、商品名三选一即可命中,建议尽量多填提高稳定性。</p>
|
||||
</div>
|
||||
<div class="grid cols-3">
|
||||
<label class="field-block">
|
||||
<span>外部 SKU</span>
|
||||
<el-input v-model="item.externalSkuCode" maxlength="120" placeholder="例如 khhao 的 skuCode" />
|
||||
</label>
|
||||
<label class="field-block">
|
||||
<span>外部商品 ID</span>
|
||||
<el-input v-model="item.externalItemId" maxlength="120" placeholder="平台商品 ID" />
|
||||
</label>
|
||||
<label class="field-block">
|
||||
<span>外部商品名</span>
|
||||
<el-input v-model="item.externalSkuName" maxlength="200" placeholder="用于名称匹配" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-split">
|
||||
<div>
|
||||
<h4>cloud 资源</h4>
|
||||
<p>这里决定是否走背包、自动购买、申请虚拟号,以及后续发货动作会使用哪一个 cloud SKU。</p>
|
||||
</div>
|
||||
<div class="grid cols-4">
|
||||
<label class="field-block">
|
||||
<span>cloudSourceKey</span>
|
||||
<el-input v-model="item.cloudSourceKey" maxlength="60" placeholder="默认 default" />
|
||||
</label>
|
||||
<label class="field-block">
|
||||
<span>cloud SKU ID</span>
|
||||
<el-input-number v-model="item.cloudSkuId" :min="0" :step="1" controls-position="right" />
|
||||
</label>
|
||||
<label class="field-block">
|
||||
<span>cloud SKU 名称</span>
|
||||
<el-input v-model="item.cloudSkuName" maxlength="120" placeholder="可选,方便辨认" />
|
||||
</label>
|
||||
<label class="field-block">
|
||||
<span>虚拟号 VN Key</span>
|
||||
<el-input v-model="item.vnKey" maxlength="40" placeholder="必填,例如 1" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="grid cols-4">
|
||||
<label class="field-block field-block--switch">
|
||||
<span>自动购买</span>
|
||||
<el-switch v-model="item.autoBuyEnabled" active-text="开启" inactive-text="关闭" />
|
||||
</label>
|
||||
<label class="field-block">
|
||||
<span>最低保留余额</span>
|
||||
<el-input-number v-model="item.minAssetReserve" :min="0" :step="1" controls-position="right" />
|
||||
</label>
|
||||
<label class="field-block field-block--switch">
|
||||
<span>发货后自动退号</span>
|
||||
<el-switch v-model="item.autoReturnNumberAfterDispatch" active-text="开启" inactive-text="关闭" />
|
||||
</label>
|
||||
<label class="field-block field-block--switch">
|
||||
<span>发货后自动核销</span>
|
||||
<el-switch v-model="item.autoConsumeAfterDispatch" active-text="开启" inactive-text="关闭" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid cols-2">
|
||||
<label class="field-block">
|
||||
<span>快手核销店铺 ID</span>
|
||||
<el-input v-model="item.kuaishouConsumeShopId" maxlength="80" placeholder="预留给后续自动核销" />
|
||||
</label>
|
||||
<label class="field-block">
|
||||
<span>备注</span>
|
||||
<el-input
|
||||
v-model="item.notes"
|
||||
maxlength="400"
|
||||
placeholder="补充说明,例如绑定场景、客服注意事项"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="rule-footer">
|
||||
<span>配置 ID:{{ item.id || '保存后自动生成' }}</span>
|
||||
<el-button round text type="danger" @click="removeItem(item.localId)">删除规则</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</template>
|
||||
</template>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.admin-panel {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.hero-card,
|
||||
.overview-card,
|
||||
.toolbar-card,
|
||||
.rule-card,
|
||||
.empty-block,
|
||||
.error-copy,
|
||||
.validation-copy {
|
||||
border-radius: 24px;
|
||||
border: 1px solid rgba(86, 108, 138, 0.12);
|
||||
background: rgba(255, 255, 255, 0.94);
|
||||
box-shadow: 0 18px 48px rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
|
||||
.hero-card {
|
||||
padding: 24px;
|
||||
background:
|
||||
radial-gradient(circle at top right, rgba(26, 127, 255, 0.12), transparent 24%),
|
||||
radial-gradient(circle at bottom left, rgba(16, 185, 129, 0.12), transparent 28%),
|
||||
rgba(255, 255, 255, 0.96);
|
||||
}
|
||||
|
||||
.hero-copy h1,
|
||||
.toolbar-card h3,
|
||||
.section-split h4 {
|
||||
margin: 0;
|
||||
color: #17324d;
|
||||
}
|
||||
|
||||
.hero-copy p,
|
||||
.toolbar-card p,
|
||||
.section-split p {
|
||||
margin: 8px 0 0;
|
||||
color: #64748b;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.hero-chips,
|
||||
.hero-actions,
|
||||
.overview-notes,
|
||||
.toolbar-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.hero-chips {
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.hero-chips span,
|
||||
.overview-notes span {
|
||||
padding: 8px 12px;
|
||||
border-radius: 999px;
|
||||
background: rgba(17, 72, 153, 0.08);
|
||||
color: #175cd3;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
margin-top: 18px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.overview-card,
|
||||
.toolbar-card,
|
||||
.empty-block,
|
||||
.error-copy,
|
||||
.validation-copy {
|
||||
padding: 18px 20px;
|
||||
}
|
||||
|
||||
.overview-card {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.overview-file {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.overview-file span {
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.overview-file code {
|
||||
color: #17324d;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.overview-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.overview-stats article {
|
||||
padding: 14px 16px;
|
||||
border-radius: 18px;
|
||||
background: #f8fbff;
|
||||
border: 1px solid rgba(86, 108, 138, 0.08);
|
||||
}
|
||||
|
||||
.overview-stats span {
|
||||
display: block;
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.overview-stats strong {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
color: #17324d;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.error-copy,
|
||||
.validation-copy {
|
||||
color: #b42318;
|
||||
}
|
||||
|
||||
.toolbar-card {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.rule-card {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.rule-card[data-invalid='true'] {
|
||||
border-color: rgba(180, 35, 24, 0.25);
|
||||
box-shadow: 0 18px 48px rgba(180, 35, 24, 0.08);
|
||||
}
|
||||
|
||||
.rule-head {
|
||||
width: 100%;
|
||||
padding: 18px 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
border: 0;
|
||||
background: linear-gradient(180deg, rgba(248, 250, 252, 0.98), rgba(243, 247, 252, 0.98));
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.rule-head strong {
|
||||
display: block;
|
||||
color: #17324d;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.rule-head p {
|
||||
margin: 6px 0 0;
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.rule-head-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.status-pill {
|
||||
padding: 6px 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.status-pill[data-tone='success'] {
|
||||
color: #0f766e;
|
||||
background: #ecfdf3;
|
||||
}
|
||||
|
||||
.status-pill[data-tone='warning'] {
|
||||
color: #b54708;
|
||||
background: #fffaeb;
|
||||
}
|
||||
|
||||
.collapse-indicator {
|
||||
color: #175cd3;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.rule-body {
|
||||
padding: 0 20px 20px;
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.section-split {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
padding: 16px;
|
||||
border-radius: 18px;
|
||||
background: rgba(248, 250, 252, 0.9);
|
||||
border: 1px solid rgba(86, 108, 138, 0.08);
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.cols-2 {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.cols-3 {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.cols-4 {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.field-block {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.field-block span {
|
||||
color: #17324d;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.field-block--switch {
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
.rule-footer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.cols-4,
|
||||
.cols-3,
|
||||
.cols-2 {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.hero-actions,
|
||||
.toolbar-card,
|
||||
.rule-head,
|
||||
.rule-footer {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.cols-4,
|
||||
.cols-3,
|
||||
.cols-2 {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -40,6 +40,7 @@ const navItems = computed(() => {
|
||||
baseItems.splice(1, 0, { to: '/admin/users', label: '用户' })
|
||||
baseItems.push({ to: '/admin/platform-shops', label: '平台配置' })
|
||||
baseItems.push({ to: '/admin/platform-fulfillment', label: '履约配置' })
|
||||
baseItems.push({ to: '/admin/platform-kuaishou-cloud-fulfillment', label: '新履约配置' })
|
||||
baseItems.push({ to: '/admin/audit-logs', label: '审计' })
|
||||
}
|
||||
|
||||
@@ -133,6 +134,7 @@ async function submitLogout() {
|
||||
margin-top: 28px;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
.admin-meta {
|
||||
|
||||
@@ -8,12 +8,15 @@ import {
|
||||
closeAdminTask,
|
||||
confirmAdminTaskAssistedRole,
|
||||
completeAdminTaskManualDispatch,
|
||||
dispatchAdminTaskKuaishouCloudFulfillment,
|
||||
redeemAdminTaskAssisted,
|
||||
fetchAdminTaskScreenshot,
|
||||
fetchAdminTaskDetail,
|
||||
markAdminTaskManualReview,
|
||||
prepareAdminTaskKuaishouCloudFulfillment,
|
||||
regenerateAdminTaskClaimLink,
|
||||
releaseAdminTaskInventoryBinding,
|
||||
returnNumberAdminTaskKuaishouCloudFulfillment,
|
||||
retryAdminTask,
|
||||
} from '@/services/admin'
|
||||
import type { AdminTaskActionResponse, AdminTaskDetail } from '@/types/admin'
|
||||
@@ -33,9 +36,14 @@ const manualDispatchForm = reactive({
|
||||
deliveredCredential: '',
|
||||
resultMessage: '',
|
||||
})
|
||||
const kuaishouCloudForm = reactive({
|
||||
ticketCode: '',
|
||||
})
|
||||
const canManageTaskLifecycle = computed(() => hasAdminRole('operator'))
|
||||
const canCloseTasks = computed(() => hasAdminRole('support'))
|
||||
const canViewSensitiveTaskData = computed(() => Boolean(detail.value?.operations.canViewSensitiveTaskData))
|
||||
const kuaishouCloudFlow = computed(() => detail.value?.kuaishouCloudFulfillment || null)
|
||||
const isKuaishouCloudTask = computed(() => detail.value?.task.executorKey === 'kuaishou_ct_assisted')
|
||||
const claimUrl = computed(() => {
|
||||
const tokenStatus = String(detail.value?.claimToken?.status || '').trim()
|
||||
const taskStatus = String(detail.value?.task.status || '').trim()
|
||||
@@ -66,6 +74,7 @@ async function loadDetail() {
|
||||
const response = await fetchAdminTaskDetail(String(route.params.taskId || ''))
|
||||
detail.value = response.data
|
||||
syncManualDispatchForm(response.data)
|
||||
syncKuaishouCloudForm(response.data)
|
||||
await loadScreenshotPreview(response.data)
|
||||
} catch (error) {
|
||||
errorMessage.value = error instanceof Error ? error.message : '读取任务详情失败'
|
||||
@@ -133,6 +142,10 @@ function syncManualDispatchForm(taskDetail: AdminTaskDetail) {
|
||||
manualDispatchForm.resultMessage = taskDetail.manualDispatch?.resultMessage || taskDetail.task.resultMessage || ''
|
||||
}
|
||||
|
||||
function syncKuaishouCloudForm(taskDetail: AdminTaskDetail) {
|
||||
kuaishouCloudForm.ticketCode = taskDetail.kuaishouCloudFulfillment?.ticket.code || ''
|
||||
}
|
||||
|
||||
async function submitManualDispatch(outcome: 'delivered' | 'failed') {
|
||||
if (!detail.value) {
|
||||
return
|
||||
@@ -155,6 +168,68 @@ async function submitManualDispatch(outcome: 'delivered' | 'failed') {
|
||||
)
|
||||
}
|
||||
|
||||
async function submitKuaishouCloudPrepare() {
|
||||
if (!detail.value) {
|
||||
return
|
||||
}
|
||||
|
||||
await runAction(
|
||||
() => prepareAdminTaskKuaishouCloudFulfillment(detail.value!.task.taskId),
|
||||
'绑定资源已准备完成',
|
||||
`确认开始为任务 ${detail.value.task.taskNo} 准备绑定资源吗?系统会自动检查背包、余额并申请虚拟号。`,
|
||||
)
|
||||
}
|
||||
|
||||
async function submitKuaishouCloudDispatch() {
|
||||
if (!detail.value) {
|
||||
return
|
||||
}
|
||||
|
||||
await runAction(
|
||||
() => dispatchAdminTaskKuaishouCloudFulfillment(detail.value!.task.taskId, {
|
||||
ticketCode: kuaishouCloudForm.ticketCode,
|
||||
}),
|
||||
'已完成绑定确认并发货',
|
||||
`确认客户已经完成绑定,并立即为任务 ${detail.value.task.taskNo} 执行发货吗?这个动作会把“确认绑定完成”和“发货”合并为一步。`,
|
||||
)
|
||||
}
|
||||
|
||||
async function submitKuaishouCloudReturnNumber() {
|
||||
if (!detail.value) {
|
||||
return
|
||||
}
|
||||
|
||||
await runAction(
|
||||
() => returnNumberAdminTaskKuaishouCloudFulfillment(detail.value!.task.taskId),
|
||||
'号码已退还',
|
||||
`确认退还任务 ${detail.value.task.taskNo} 当前使用的虚拟号吗?退号后该流程会正式收口。`,
|
||||
)
|
||||
}
|
||||
|
||||
async function copyText(value: string, successMessage: string) {
|
||||
const text = String(value || '').trim()
|
||||
if (!text) {
|
||||
showError('当前没有可复制的内容')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
showSuccess(successMessage)
|
||||
} catch {
|
||||
showError('复制失败,请手动复制')
|
||||
}
|
||||
}
|
||||
|
||||
function openExternalLink(value: string) {
|
||||
const text = String(value || '').trim()
|
||||
if (!text) {
|
||||
return
|
||||
}
|
||||
|
||||
window.open(text, '_blank', 'noopener,noreferrer')
|
||||
}
|
||||
|
||||
function formatTaskEventPayload(payload: Record<string, unknown>) {
|
||||
const parts = [
|
||||
payload.outcome ? `结果 ${payload.outcome}` : '',
|
||||
@@ -166,11 +241,19 @@ function formatTaskEventPayload(payload: Record<string, unknown>) {
|
||||
payload.inventoryItemId ? `库存项 #${payload.inventoryItemId}` : '',
|
||||
payload.previousInventoryItemId ? `旧库存 #${payload.previousInventoryItemId}` : '',
|
||||
payload.nextInventoryItemId ? `新库存 #${payload.nextInventoryItemId}` : '',
|
||||
payload.ticketCodeMasked ? `卡券号 ${payload.ticketCodeMasked}` : '',
|
||||
payload.vnId ? `虚拟号 #${payload.vnId}` : '',
|
||||
payload.vnPhoneMasked ? `号码 ${payload.vnPhoneMasked}` : '',
|
||||
payload.bindUrl ? `绑定链接 ${payload.bindUrl}` : '',
|
||||
payload.sendType ? `发送类型 ${payload.sendType}` : '',
|
||||
payload.note ? `备注 ${payload.note}` : '',
|
||||
payload.previousOutcome ? `切换原因 ${formatRedeemOutcomeLabel(String(payload.previousOutcome || ''))}` : '',
|
||||
payload.deliveryReference ? `单号 ${payload.deliveryReference}` : '',
|
||||
payload.platformOrderId ? `平台单 ${payload.platformOrderId}` : '',
|
||||
payload.trigger ? `触发 ${formatAutoDeliveryTrigger(String(payload.trigger || ''))}` : '',
|
||||
payload.reason ? `原因 ${payload.reason}` : '',
|
||||
payload.purchaseTriggered === true ? '已触发购买' : '',
|
||||
payload.usedKnapsack === true ? '使用背包库存' : '',
|
||||
payload.responseStatus ? `HTTP ${payload.responseStatus}` : '',
|
||||
payload.requestId ? `请求 ${payload.requestId}` : '',
|
||||
payload.errorMessage ? `错误 ${payload.errorMessage}` : '',
|
||||
@@ -349,6 +432,33 @@ onBeforeUnmount(clearScreenshotPreview)
|
||||
>
|
||||
客服开始兑换
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="canManageTaskLifecycle && detail.operations.canPrepareKuaishouCloudFulfillment"
|
||||
:loading="actionLoading"
|
||||
round
|
||||
type="primary"
|
||||
@click="submitKuaishouCloudPrepare"
|
||||
>
|
||||
准备绑定资源
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="canManageTaskLifecycle && detail.operations.canDispatchKuaishouCloudFulfillment"
|
||||
:loading="actionLoading"
|
||||
round
|
||||
type="success"
|
||||
@click="submitKuaishouCloudDispatch"
|
||||
>
|
||||
确认绑定完成并发货
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="canManageTaskLifecycle && detail.operations.canReturnKuaishouCloudFulfillment"
|
||||
:loading="actionLoading"
|
||||
round
|
||||
type="warning"
|
||||
@click="submitKuaishouCloudReturnNumber"
|
||||
>
|
||||
退还号码
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="canManageTaskLifecycle"
|
||||
:disabled="!detail.operations.canMarkManualReview"
|
||||
@@ -423,6 +533,122 @@ onBeforeUnmount(clearScreenshotPreview)
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section v-if="isKuaishouCloudTask && kuaishouCloudFlow" class="table-card flow-card">
|
||||
<div class="flow-card-head">
|
||||
<div>
|
||||
<h3>快手 Cloud 履约面板</h3>
|
||||
<p class="section-copy">这里集中处理绑定链接、客户卡券号、发货动作与退号收口。</p>
|
||||
</div>
|
||||
<div class="flow-head-actions">
|
||||
<el-button
|
||||
v-if="kuaishouCloudFlow.binding.bindUrl"
|
||||
round
|
||||
@click="copyText(kuaishouCloudFlow.binding.bindUrl, '绑定链接已复制')"
|
||||
>
|
||||
复制绑定链接
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="kuaishouCloudFlow.binding.bindUrl"
|
||||
round
|
||||
type="primary"
|
||||
plain
|
||||
@click="openExternalLink(kuaishouCloudFlow.binding.bindUrl)"
|
||||
>
|
||||
打开绑定页
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="redeem-summary-grid">
|
||||
<article class="redeem-summary-card">
|
||||
<span>准备状态</span>
|
||||
<div class="auto-delivery-tag-line">
|
||||
<AdminStatusTag :status="kuaishouCloudFlow.binding.prepareStatus" />
|
||||
</div>
|
||||
</article>
|
||||
<article class="redeem-summary-card">
|
||||
<span>发货状态</span>
|
||||
<div class="auto-delivery-tag-line">
|
||||
<AdminStatusTag :status="kuaishouCloudFlow.dispatch.status" />
|
||||
</div>
|
||||
</article>
|
||||
<article class="redeem-summary-card">
|
||||
<span>退号状态</span>
|
||||
<div class="auto-delivery-tag-line">
|
||||
<AdminStatusTag :status="kuaishouCloudFlow.returnNumber.status" />
|
||||
</div>
|
||||
</article>
|
||||
<article class="redeem-summary-card">
|
||||
<span>当前任务状态</span>
|
||||
<div class="auto-delivery-tag-line">
|
||||
<AdminStatusTag :status="detail.task.status" />
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div class="flow-grid">
|
||||
<article class="flow-block">
|
||||
<h4>客户绑定</h4>
|
||||
<p>客户下单后需要先完成外部绑定,客服只需要把链接发给客户,并在完成后点一次发货按钮。</p>
|
||||
<div class="flow-meta-list">
|
||||
<div><span>绑定链接</span><strong>{{ kuaishouCloudFlow.binding.bindUrl || '-' }}</strong></div>
|
||||
<div><span>准备时间</span><strong>{{ formatAdminDateTime(kuaishouCloudFlow.binding.bindPreparedAt) }}</strong></div>
|
||||
<div><span>虚拟号 ID</span><strong>{{ kuaishouCloudFlow.binding.vnId || '-' }}</strong></div>
|
||||
<div><span>虚拟号码</span><strong>{{ kuaishouCloudFlow.binding.vnPhone || '-' }}</strong></div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="flow-block">
|
||||
<h4>cloud 资源</h4>
|
||||
<div class="flow-meta-list">
|
||||
<div><span>cloudSourceKey</span><strong>{{ kuaishouCloudFlow.binding.cloudSourceKey || '-' }}</strong></div>
|
||||
<div><span>cloud SKU</span><strong>{{ kuaishouCloudFlow.binding.skuId ? `${kuaishouCloudFlow.binding.skuId} / ${kuaishouCloudFlow.binding.skuName || '-'}` : '-' }}</strong></div>
|
||||
<div><span>VN Key</span><strong>{{ kuaishouCloudFlow.binding.vnKey || '-' }}</strong></div>
|
||||
<div><span>自动购买</span><strong>{{ kuaishouCloudFlow.purchase.autoBuyEnabled ? '开启' : '关闭' }}</strong></div>
|
||||
<div><span>背包命中</span><strong>{{ kuaishouCloudFlow.purchase.usedKnapsack ? '是' : '否' }}</strong></div>
|
||||
<div><span>触发购买</span><strong>{{ kuaishouCloudFlow.purchase.purchaseTriggered ? '是' : '否' }}</strong></div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div class="flow-form-card">
|
||||
<label class="field-block">
|
||||
<span>客户提供的卡券号</span>
|
||||
<el-input
|
||||
v-model="kuaishouCloudForm.ticketCode"
|
||||
:disabled="!canManageTaskLifecycle"
|
||||
maxlength="120"
|
||||
placeholder="客户下单后提供的卡券号,可在发货时一并记录"
|
||||
/>
|
||||
</label>
|
||||
<p class="manual-hint">“确认绑定完成并发货”已经合并为一个按钮,不再拆成两步。</p>
|
||||
</div>
|
||||
|
||||
<div class="flow-grid">
|
||||
<article class="flow-block">
|
||||
<h4>发货结果</h4>
|
||||
<div class="flow-meta-list">
|
||||
<div><span>最近卡券号</span><strong>{{ kuaishouCloudFlow.ticket.code || '-' }}</strong></div>
|
||||
<div><span>记录时间</span><strong>{{ formatAdminDateTime(kuaishouCloudFlow.ticket.capturedAt) }}</strong></div>
|
||||
<div><span>发货时间</span><strong>{{ formatAdminDateTime(kuaishouCloudFlow.dispatch.dispatchAt) }}</strong></div>
|
||||
<div><span>发送类型</span><strong>{{ kuaishouCloudFlow.dispatch.sendType || '-' }}</strong></div>
|
||||
<div><span>结果说明</span><strong>{{ kuaishouCloudFlow.dispatch.note || detail.task.resultMessage || '-' }}</strong></div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="flow-block">
|
||||
<h4>流程收口</h4>
|
||||
<div class="flow-meta-list">
|
||||
<div><span>退号时间</span><strong>{{ formatAdminDateTime(kuaishouCloudFlow.returnNumber.returnedAt) }}</strong></div>
|
||||
<div><span>自动退号</span><strong>{{ kuaishouCloudFlow.returnNumber.autoReturnEnabled ? '开启' : '关闭' }}</strong></div>
|
||||
<div><span>自动核销</span><strong>{{ kuaishouCloudFlow.consume.autoConsumeEnabled ? '开启' : '关闭' }}</strong></div>
|
||||
<div><span>核销店铺</span><strong>{{ kuaishouCloudFlow.consume.shopId || '-' }}</strong></div>
|
||||
<div><span>备注</span><strong>{{ kuaishouCloudFlow.notes || '-' }}</strong></div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section
|
||||
v-if="detail.order?.provider === 'agiso' && detail.order?.platform === 'xianyu'"
|
||||
class="table-card"
|
||||
@@ -740,6 +966,63 @@ onBeforeUnmount(clearScreenshotPreview)
|
||||
.manual-summary { margin-top: 16px; padding: 14px; border-radius: 16px; background: rgba(248,250,252,.9); }
|
||||
.manual-summary p { margin: 0 0 8px; color: #334155; }
|
||||
.manual-summary p:last-child { margin-bottom: 0; }
|
||||
.flow-card {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
background:
|
||||
radial-gradient(circle at top right, rgba(37, 99, 235, 0.08), transparent 28%),
|
||||
linear-gradient(180deg, rgba(255,255,255,.96), rgba(248,250,252,.96));
|
||||
}
|
||||
.flow-card-head {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 14px;
|
||||
}
|
||||
.flow-head-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
.flow-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
.flow-block,
|
||||
.flow-form-card {
|
||||
padding: 16px;
|
||||
border-radius: 18px;
|
||||
border: 1px solid rgba(86,108,138,.1);
|
||||
background: rgba(248,250,252,.9);
|
||||
}
|
||||
.flow-block h4 {
|
||||
margin: 0;
|
||||
color: #1d3555;
|
||||
}
|
||||
.flow-block p {
|
||||
margin: 10px 0 0;
|
||||
color: #64748b;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.flow-meta-list {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
.flow-meta-list div {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
}
|
||||
.flow-meta-list span {
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
}
|
||||
.flow-meta-list strong {
|
||||
color: #1d3555;
|
||||
word-break: break-all;
|
||||
}
|
||||
.event-copy { color: #334155; }
|
||||
.event-time { margin-top: 6px; color: #64748b; font-size: 12px; }
|
||||
.screenshot-link { display: inline-block; margin-bottom: 12px; color: #175cd3; text-decoration: none; }
|
||||
@@ -751,4 +1034,11 @@ onBeforeUnmount(clearScreenshotPreview)
|
||||
border: 1px solid rgba(86,108,138,.12);
|
||||
box-shadow: 0 20px 48px rgba(15,23,42,.08);
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.flow-card-head,
|
||||
.flow-head-actions {
|
||||
display: grid;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user