履约配置优化

This commit is contained in:
yml
2026-05-03 23:03:21 +08:00
parent 0f0253bffe
commit f07a4f8dcb
27 changed files with 2767 additions and 633 deletions
@@ -4,9 +4,7 @@ import { computed, nextTick, onMounted, reactive, ref } from 'vue'
import { showSuccess } from '@/lib/feedback'
import {
fetchAdminFulfillmentBindingConfigs,
fetchAdminKhhaoSourceConfig,
fetchAdminKuaishouEticketSourceConfig,
queryAdminKhhaoOrders,
lookupAdminFulfillmentBindingOrder,
saveAdminFulfillmentBindingConfigs,
} from '@/services/admin'
@@ -14,7 +12,6 @@ import type {
AdminFulfillmentBindingConfigItem,
AdminFulfillmentLookupItem,
AdminFulfillmentLookupResult,
AdminKhhaoOrderPreview,
AdminKuaishouEticketShopConfigItem,
AdminObservedProductItem,
} from '@/types/admin'
@@ -93,18 +90,6 @@ const lookupForm = reactive({
shopId: '',
platformOrderId: '',
})
const khhaoLookupLoading = ref(false)
const khhaoLookupErrorMessage = ref('')
const khhaoLookupResults = ref<AdminKhhaoOrderPreview[]>([])
const importedKhhaoOrderIds = ref<string[]>([])
const khhaoLookupForm = reactive({
baseUrl: 'https://admin.khhao.com',
username: '',
password: '',
page: 1,
limit: 10,
maxCaptchaAttempts: 3,
})
const ruleMetrics = computed(() => {
const completedCount = bindings.value.filter(isEditableBindingComplete).length
@@ -288,9 +273,8 @@ async function loadConfigs() {
errorMessage.value = ''
try {
const [response, khhaoSourceResponse, kuaishouSourceResponse] = await Promise.all([
const [response, kuaishouSourceResponse] = await Promise.all([
fetchAdminFulfillmentBindingConfigs(),
fetchAdminKhhaoSourceConfig(),
fetchAdminKuaishouEticketSourceConfig(),
])
filePath.value = response.data.filePath
@@ -299,10 +283,6 @@ async function loadConfigs() {
bindings.value = nextBindings
collapsedBindingIds.value = buildCollapsedIds(nextBindings)
observedProducts.value = response.data.observedProducts
khhaoLookupForm.baseUrl = khhaoSourceResponse.data.source.baseUrl || 'https://admin.khhao.com'
khhaoLookupForm.username = khhaoSourceResponse.data.source.username || ''
khhaoLookupForm.password = khhaoSourceResponse.data.source.password || ''
khhaoLookupForm.maxCaptchaAttempts = khhaoSourceResponse.data.source.maxCaptchaAttempts || 3
} catch (error) {
errorMessage.value = error instanceof Error ? error.message : '读取履约配置失败'
} finally {
@@ -366,29 +346,6 @@ function isLookupProductImported(lineId: string) {
return importedLookupLineIds.value.includes(lineId)
}
function importKhhaoProduct(item: AdminKhhaoOrderPreview) {
importObservedProduct({
provider: item.provider || 'khhao',
platform: item.platform || 'unknown',
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,
})
if (!importedKhhaoOrderIds.value.includes(item.platformOrderId)) {
importedKhhaoOrderIds.value = [...importedKhhaoOrderIds.value, item.platformOrderId]
}
}
function isKhhaoProductImported(platformOrderId: string) {
return importedKhhaoOrderIds.value.includes(platformOrderId)
}
function normalizeBindingForSave(item: EditableBinding): SaveBindingPayload {
syncBindingShopSelection(item)
@@ -615,34 +572,6 @@ async function lookupOrderProducts() {
}
}
async function lookupKhhaoProducts() {
if (!khhaoLookupForm.username.trim() || !khhaoLookupForm.password.trim()) {
khhaoLookupErrorMessage.value = '请先填写 khhao 账号和密码'
return
}
khhaoLookupLoading.value = true
khhaoLookupErrorMessage.value = ''
try {
const response = await queryAdminKhhaoOrders({
baseUrl: khhaoLookupForm.baseUrl.trim() || 'https://admin.khhao.com',
username: khhaoLookupForm.username.trim(),
password: khhaoLookupForm.password.trim(),
page: Number(khhaoLookupForm.page || 1),
limit: Number(khhaoLookupForm.limit || 10),
maxCaptchaAttempts: Number(khhaoLookupForm.maxCaptchaAttempts || 3),
})
khhaoLookupResults.value = response.data.items
importedKhhaoOrderIds.value = []
} catch (error) {
khhaoLookupResults.value = []
khhaoLookupErrorMessage.value = error instanceof Error ? error.message : 'khhao 订单查询失败'
} finally {
khhaoLookupLoading.value = false
}
}
onMounted(loadConfigs)
</script>
@@ -938,113 +867,6 @@ onMounted(loadConfigs)
</div>
</section>
<section class="table-card table-card--dense">
<div class="section-title-row section-title-row--tight">
<div>
<h3>khhao 订单取样导入</h3>
<p>先用 khhao 账号查订单样本再直接把商品导入成规则草稿适合首批建规则</p>
</div>
<span class="section-note-chip">khhao / kuaishou</span>
</div>
<div class="khhao-toolbar">
<label class="field-block field-wide">
<span>Base URL</span>
<input v-model="khhaoLookupForm.baseUrl" class="text-input" placeholder="https://admin.khhao.com" />
</label>
<label class="field-block">
<span>账号</span>
<input v-model="khhaoLookupForm.username" class="text-input" placeholder="khhao 登录账号" />
</label>
<label class="field-block">
<span>密码</span>
<input v-model="khhaoLookupForm.password" class="text-input" type="password" placeholder="khhao 登录密码" />
</label>
<label class="field-block">
<span>页码</span>
<input v-model.number="khhaoLookupForm.page" class="text-input" type="number" min="1" />
</label>
<label class="field-block">
<span>每页条数</span>
<input v-model.number="khhaoLookupForm.limit" class="text-input" type="number" min="1" max="50" />
</label>
<label class="field-block">
<span>验证码重试</span>
<input v-model.number="khhaoLookupForm.maxCaptchaAttempts" class="text-input" type="number" min="1" max="5" />
</label>
<div class="field-block field-block--action">
<span>操作</span>
<el-button :loading="khhaoLookupLoading" round type="primary" @click="lookupKhhaoProducts">查询 khhao 订单</el-button>
</div>
</div>
<p class="toolbar-hint">这里直接读取 khhao 订单列表不写库只用于导入规则草稿</p>
<p v-if="khhaoLookupErrorMessage" class="error-copy lookup-error">{{ khhaoLookupErrorMessage }}</p>
<div v-else-if="khhaoLookupLoading" class="empty-inline">正在查询 khhao 订单</div>
<table v-else class="data-table">
<thead>
<tr>
<th>订单</th>
<th>店铺</th>
<th>商品</th>
<th>金额 / 状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="item in khhaoLookupResults" :key="`${item.platformOrderId}:${item.skuCode}:${item.itemId}`">
<td>
<div class="cell-stack">
<strong>{{ item.platformOrderId }}</strong>
<span class="cell-subtle">{{ item.platformLabel || item.platform || '-' }}</span>
</div>
</td>
<td>
<div class="cell-stack">
<strong>{{ item.shopName || item.shopId || '-' }}</strong>
<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>
<div class="cell-stack">
<strong>{{ item.itemTitle || '-' }}</strong>
<span class="cell-subtle">SKU: {{ item.skuCode || '-' }}</span>
<span class="cell-subtle">ItemId: {{ item.itemId || '-' }}</span>
</div>
</td>
<td>
<div class="cell-stack">
<strong>{{ (item.totalAmountFen / 100).toFixed(2) }}</strong>
<span class="cell-subtle">{{ item.statusLabel || item.status || '-' }}</span>
</div>
</td>
<td>
<el-button
v-if="!isKhhaoProductImported(item.platformOrderId)"
link
type="primary"
@click="importKhhaoProduct(item)"
>
导入到规则
</el-button>
<span v-else class="cell-subtle">已导入草稿</span>
</td>
</tr>
<tr v-if="khhaoLookupResults.length === 0">
<td colspan="5" class="empty-inline">还没有 khhao 查询结果</td>
</tr>
</tbody>
</table>
</section>
<section class="table-card table-card--dense">
<div class="section-title-row section-title-row--tight">
<div>
@@ -0,0 +1,135 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import AdminFulfillmentBindingsView from '@/views/admin/AdminFulfillmentBindingsView.vue'
import AdminKuaishouCloudFulfillmentView from '@/views/admin/AdminKuaishouCloudFulfillmentView.vue'
type FulfillmentTabKey = 'legacy' | 'kuaishou-cloud'
const route = useRoute()
const router = useRouter()
const TAB_OPTIONS: Array<{ key: FulfillmentTabKey; label: string; desc: string }> = [
{
key: 'legacy',
label: '旧履约规则',
desc: '这里维护原有统一履约绑定,支持腾讯领取、人工发货,以及旧任务匹配逻辑。',
},
{
key: 'kuaishou-cloud',
label: '快手 Cloud 新履约',
desc: '这里维护快手下单后走 cloudtentacles 绑定、发货、退号的新流程,不会覆盖旧规则。',
},
]
const activeTab = computed<FulfillmentTabKey>({
get() {
const value = String(route.query.tab || '').trim()
return value === 'kuaishou-cloud' ? 'kuaishou-cloud' : 'legacy'
},
set(value) {
const next = value === 'kuaishou-cloud' ? 'kuaishou-cloud' : 'legacy'
void router.replace({
path: '/admin/platform-fulfillment',
query: next === 'legacy' ? {} : { tab: next },
})
},
})
</script>
<template>
<section class="hub-panel">
<header class="hub-hero">
<div>
<h1>履约配置中心</h1>
<p>旧规则和快手 Cloud 新流程都放在这里维护不再拆成两个侧边栏入口</p>
</div>
<div class="hero-tags">
<span>一个入口</span>
<span>两套配置并存</span>
<span>互不覆盖</span>
</div>
</header>
<section class="hub-card">
<div class="tab-copy">
<h3>{{ TAB_OPTIONS.find((item) => item.key === activeTab)?.label }}</h3>
<p>{{ TAB_OPTIONS.find((item) => item.key === activeTab)?.desc }}</p>
</div>
<el-tabs v-model="activeTab" class="hub-tabs">
<el-tab-pane label="旧履约规则" name="legacy">
<AdminFulfillmentBindingsView />
</el-tab-pane>
<el-tab-pane label="快手 Cloud 新履约" name="kuaishou-cloud">
<AdminKuaishouCloudFulfillmentView />
</el-tab-pane>
</el-tabs>
</section>
</section>
</template>
<style scoped>
.hub-panel {
display: grid;
gap: 16px;
}
.hub-hero,
.hub-card {
padding: 20px;
border-radius: 24px;
border: 1px solid rgba(86, 108, 138, 0.12);
background: rgba(255, 255, 255, 0.94);
}
.hub-hero {
display: grid;
gap: 14px;
background:
radial-gradient(circle at top right, rgba(26, 127, 255, 0.1), transparent 26%),
linear-gradient(135deg, rgba(255,255,255,.98), rgba(240,247,255,.96));
}
.hub-hero h1,
.tab-copy h3 {
margin: 0;
color: #17324d;
}
.hub-hero p,
.tab-copy p {
margin: 8px 0 0;
color: #64748b;
line-height: 1.6;
}
.hero-tags {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.hero-tags span {
padding: 8px 12px;
border-radius: 999px;
background: rgba(23, 92, 211, 0.08);
color: #175cd3;
font-size: 12px;
font-weight: 700;
}
.hub-card {
display: grid;
gap: 16px;
}
.tab-copy {
display: grid;
gap: 4px;
}
:deep(.hub-tabs .el-tabs__content) {
padding-top: 12px;
}
</style>
File diff suppressed because it is too large Load Diff
@@ -40,7 +40,6 @@ 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: '审计' })
}
@@ -1533,6 +1533,10 @@ onMounted(loadConfigs)
<span class="meta-label">自动同步状态</span>
<span>{{ khhaoAutoSyncState.lastRunStatus || 'idle' }}</span>
</div>
<div class="meta-line">
<span class="meta-label">当前节奏</span>
<span>{{ khhaoAutoSyncState.watchMode === 'boosted' ? '提频观察中(3-5 秒)' : '常规同步' }}</span>
</div>
<div class="meta-line">
<span class="meta-label">同步基线</span>
<span>{{ formatAdminDateTime(khhaoAutoSyncState.syncFromCreatedAt) }}</span>
@@ -1549,6 +1553,10 @@ onMounted(loadConfigs)
<span class="meta-label">最近结果</span>
<span>拉取 {{ khhaoAutoSyncState.fetchedCount }} 成功 {{ khhaoAutoSyncState.syncedCount }} 忽略 {{ khhaoAutoSyncState.ignoredCount }} </span>
</div>
<div v-if="khhaoAutoSyncState.watchOrders.length > 0" class="meta-line">
<span class="meta-label">观察订单</span>
<span>{{ khhaoAutoSyncState.watchOrders.map((item) => `${item.shopName || item.shopId || '-'} / ${item.platformOrderId}`).join('') }}</span>
</div>
<div v-if="khhaoAutoSyncState.lastErrorMessage" class="meta-line">
<span class="meta-label">最近错误</span>
<span class="error-copy">{{ khhaoAutoSyncState.lastErrorMessage }}</span>
@@ -14,6 +14,7 @@ import {
fetchAdminTaskDetail,
markAdminTaskManualReview,
prepareAdminTaskKuaishouCloudFulfillment,
refreshAdminTaskKuaishouCloudRoleInfo,
regenerateAdminTaskClaimLink,
releaseAdminTaskInventoryBinding,
returnNumberAdminTaskKuaishouCloudFulfillment,
@@ -40,7 +41,109 @@ 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 kuaishouCloudRole = computed(() => kuaishouCloudFlow.value?.role || null)
const kuaishouCloudCollapsePanels = ref(['claim', 'ticket', 'cloud'])
const isKuaishouCloudTask = computed(() => detail.value?.task.executorKey === 'kuaishou_ct_assisted')
const summaryCards = computed(() => {
if (!detail.value) {
return []
}
return [
{
label: '任务状态',
value: detail.value.task.status || '-',
tone: 'blue',
},
{
label: '履约状态',
value: detail.value.task.deliveryStatus || '-',
tone: 'green',
},
{
label: '当前角色',
value: kuaishouCloudRole.value?.name || detail.value.task.roleName || '-',
tone: 'amber',
},
{
label: '角色 ID',
value: kuaishouCloudRole.value?.rid || detail.value.task.roleId || '-',
tone: 'slate',
},
]
})
const kuaishouRoleInfoEntries = computed(() => {
const rawInfo = kuaishouCloudRole.value?.rawInfo
if (!rawInfo) {
return []
}
return Object.entries(rawInfo)
.filter(([, value]) => value !== null && value !== undefined && String(value).trim() !== '')
.map(([key, value]) => ({
key,
label: formatKuaishouRoleInfoLabel(key),
value: typeof value === 'object' ? JSON.stringify(value) : String(value),
}))
})
const kuaishouCloudChecklist = computed(() => {
const flow = kuaishouCloudFlow.value
const role = kuaishouCloudRole.value
if (!flow) {
return []
}
return [
{
key: 'ticket',
label: '核销码校验',
status: flow.ticket.status || 'pending',
detail: flow.ticket.verifiedAt
? `已于 ${formatAdminDateTime(flow.ticket.verifiedAt)} 校验`
: '等待客户提交并校验核销码',
},
{
key: 'bind',
label: '绑定资源',
status: flow.binding.prepareStatus || 'pending',
detail: flow.binding.bindPreparedAt
? `绑定资源已准备,VN ${flow.binding.vnId || '-'}`
: '等待准备 Cloud 绑定资源',
},
{
key: 'role',
label: '角色识别',
status: role?.status || 'pending',
detail: role?.name || role?.rid
? `${role?.name || '-'} / ${role?.rid || '-'}`
: '客户绑定后刷新角色信息',
},
{
key: 'dispatch',
label: '发货执行',
status: flow.dispatch.status || 'pending',
detail: flow.dispatch.dispatchAt
? `已于 ${formatAdminDateTime(flow.dispatch.dispatchAt)} 发货`
: '等待客服确认绑定并发货',
},
{
key: 'return',
label: '退还号码',
status: flow.returnNumber.status || 'pending',
detail: flow.returnNumber.returnedAt
? `已于 ${formatAdminDateTime(flow.returnNumber.returnedAt)} 退号`
: '发货后执行退还号码',
},
{
key: 'consume',
label: '快手核销',
status: flow.consume.status || 'pending',
detail: flow.consume.consumedAt
? `已于 ${formatAdminDateTime(flow.consume.consumedAt)} 完成核销`
: (flow.consume.errorMessage || '等待退号后核销收口'),
},
]
})
const claimUrl = computed(() => {
const tokenStatus = String(detail.value?.claimToken?.status || '').trim()
const taskStatus = String(detail.value?.task.status || '').trim()
@@ -184,6 +287,18 @@ async function submitKuaishouCloudDispatch() {
)
}
async function submitKuaishouCloudRefreshRoleInfo() {
if (!detail.value) {
return
}
await runAction(
() => refreshAdminTaskKuaishouCloudRoleInfo(detail.value!.task.taskId),
'角色信息已刷新',
`确认刷新任务 ${detail.value.task.taskNo} 当前云绑定角色信息吗?系统会重新查询 cloudtentacles 的绑定结果。`,
)
}
async function submitKuaishouCloudReturnNumber() {
if (!detail.value) {
return
@@ -321,6 +436,19 @@ function formatAutoDeliveryReason(value: string) {
}
}
function formatKuaishouRoleInfoLabel(value: string) {
const normalized = String(value || '').trim()
switch (normalized) {
case 'name':
return '角色名称'
case 'rid':
return '角色 ID'
default:
return normalized || '-'
}
}
function resolveTaskAutoDeliveryStatus(taskDetail: AdminTaskDetail) {
if (taskDetail.task.agisoAutoDelivery?.status) {
return taskDetail.task.agisoAutoDelivery.status
@@ -362,7 +490,42 @@ onBeforeUnmount(clearScreenshotPreview)
<div v-if="loading" class="empty-block">任务详情加载中</div>
<template v-else-if="detail">
<section class="info-card">
<section v-if="isKuaishouCloudTask && kuaishouCloudFlow" class="hero-card">
<div class="hero-copy">
<span class="hero-eyebrow">{{ detail.task.executorKey || 'task' }}</span>
<h2>{{ detail.task.taskNo }}</h2>
<p>
订单 {{ detail.order?.platformOrderId || '-' }} ·
商品 {{ detail.orderItem?.skuName || detail.orderItem?.skuCode || '-' }} ·
店铺 {{ detail.order?.shopName || detail.order?.shopId || '-' }}
</p>
</div>
<div class="hero-meta-grid">
<article
v-for="card in summaryCards"
:key="card.label"
:class="['hero-metric', `hero-metric--${card.tone}`]"
>
<span>{{ card.label }}</span>
<strong>{{ card.value }}</strong>
</article>
</div>
<div class="hero-inline">
<span>系统绑定 <AdminStatusTag :status="detail.task.systemBindingStatus" /></span>
<span>完整绑定 <AdminStatusTag :status="detail.task.userBindingStatus" /></span>
<span v-if="detail.task.lastError">最近异常 {{ detail.task.lastError }}</span>
</div>
<div v-if="claimUrl" class="hero-link-row">
<span>客户领取页</span>
<a class="claim-link" :href="claimUrl" target="_blank" rel="noreferrer">{{ claimUrl }}</a>
</div>
<div v-else-if="claimLinkInvalid" class="hero-link-row">
<span>客户领取页</span>
<strong>已失效</strong>
</div>
</section>
<section v-else class="info-card">
<h2>{{ detail.task.taskNo }}</h2>
<p>状态<AdminStatusTag :status="detail.task.status" /> · 订单{{ detail.order?.platformOrderId || '-' }}</p>
<p>执行器{{ detail.task.executorKey || '-' }} · 履约<AdminStatusTag :status="detail.task.deliveryStatus" /></p>
@@ -489,8 +652,8 @@ onBeforeUnmount(clearScreenshotPreview)
</el-button>
</section>
<section class="table-card">
<h3>任务信息</h3>
<section v-if="!isKuaishouCloudTask" class="table-card">
<h3>任务信息</h3>
<table class="data-table">
<tbody>
<tr><th>商品</th><td>{{ detail.orderItem?.skuName || '-' }}</td></tr>
@@ -521,15 +684,23 @@ onBeforeUnmount(clearScreenshotPreview)
<tr><th>截图路径</th><td>{{ detail.task.screenshotPath || '-' }}</td></tr>
</tbody>
</table>
</section>
</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>
<p class="section-copy">是一套独立的快手 Cloud 任务面板优先展示角色信息与当前履约推进点次级明细可按需展开</p>
</div>
<div class="flow-head-actions">
<el-button
v-if="detail.operations.canRefreshKuaishouCloudRoleInfo"
:loading="actionLoading"
round
@click="submitKuaishouCloudRefreshRoleInfo"
>
刷新角色信息
</el-button>
<el-button
v-if="kuaishouCloudFlow.binding.bindUrl"
round
@@ -576,80 +747,126 @@ onBeforeUnmount(clearScreenshotPreview)
</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 class="kuaishou-focus-grid">
<article class="flow-block flow-block--spotlight">
<div class="spotlight-head">
<div>
<h4>角色信息</h4>
<p class="flow-copy">角色信息来自 cloudtentacles `vn/bind_info`客服可在客户绑定后手动刷新</p>
</div>
<div class="spotlight-actions">
<AdminStatusTag :status="kuaishouCloudRole?.status || 'pending'" />
<el-button
v-if="detail.operations.canRefreshKuaishouCloudRoleInfo"
:loading="actionLoading"
round
size="small"
@click="submitKuaishouCloudRefreshRoleInfo"
>
刷新
</el-button>
</div>
</div>
<div class="role-hero">
<div>
<span>角色名</span>
<strong>{{ kuaishouCloudRole?.name || detail.task.roleName || '等待识别' }}</strong>
</div>
<div>
<span>角色 ID</span>
<strong>{{ kuaishouCloudRole?.rid || detail.task.roleId || '等待识别' }}</strong>
</div>
<div>
<span>最近刷新</span>
<strong>{{ formatAdminDateTime(kuaishouCloudRole?.refreshedAt) }}</strong>
</div>
</div>
<p v-if="kuaishouCloudRole?.errorMessage" class="role-warning">{{ kuaishouCloudRole.errorMessage }}</p>
<div v-if="kuaishouRoleInfoEntries.length > 0" class="flow-meta-list flow-meta-list--dense">
<div v-for="entry in kuaishouRoleInfoEntries" :key="entry.key">
<span>{{ entry.label }}</span>
<strong>{{ entry.value }}</strong>
</div>
</div>
</article>
<article class="flow-block">
<h4>cloud 资源</h4>
<div class="flow-meta-list">
<article class="flow-block flow-block--checklist">
<h4>流程检查</h4>
<p class="flow-copy">客服可以先看这里快速判断卡在哪一步再决定是刷新角色准备资源发货还是退号</p>
<div class="checklist-grid">
<div v-for="item in kuaishouCloudChecklist" :key="item.key" class="checklist-item">
<div class="checklist-item-head">
<span>{{ item.label }}</span>
<AdminStatusTag :status="item.status" />
</div>
<strong>{{ item.detail }}</strong>
</div>
</div>
</article>
</div>
<el-collapse v-model="kuaishouCloudCollapsePanels" class="flow-collapse">
<el-collapse-item name="claim" title="客户领取与绑定">
<div class="flow-meta-list flow-meta-list--two-column">
<div><span>客户链接</span><strong>{{ claimUrl || '-' }}</strong></div>
<div><span>绑定链接</span><strong>{{ kuaishouCloudFlow.binding.bindUrl || '-' }}</strong></div>
<div><span>Token 状态</span><strong>{{ detail.claimToken?.status || '-' }}</strong></div>
<div><span>准备时间</span><strong>{{ formatAdminDateTime(kuaishouCloudFlow.binding.bindPreparedAt) }}</strong></div>
<div><span>首次提交</span><strong>{{ formatAdminDateTime(kuaishouCloudFlow.ticket.capturedAt) }}</strong></div>
<div><span>校验时间</span><strong>{{ formatAdminDateTime(kuaishouCloudFlow.ticket.verifiedAt) }}</strong></div>
<div><span>虚拟号 ID</span><strong>{{ kuaishouCloudFlow.binding.vnId || '-' }}</strong></div>
<div><span>虚拟号码</span><strong>{{ kuaishouCloudFlow.binding.vnPhone || '-' }}</strong></div>
</div>
<p class="manual-hint">客户会先在领取页查看图文指引并提交核销码只有校验通过后才会自动跳转绑定链接</p>
</el-collapse-item>
<el-collapse-item name="ticket" title="核销与商品信息">
<div class="flow-meta-list flow-meta-list--two-column">
<div><span>核销码</span><strong>{{ kuaishouCloudFlow.ticket.code || '-' }}</strong></div>
<div><span>校验状态</span><strong>{{ kuaishouCloudFlow.ticket.status || '-' }}</strong></div>
<div><span>商品标题</span><strong>{{ kuaishouCloudFlow.ticket.goodsTitle || '-' }}</strong></div>
<div><span>剩余次数</span><strong>{{ kuaishouCloudFlow.ticket.leftCount || 0 }}</strong></div>
</div>
<p class="manual-hint">确认绑定完成并发货已经合并为一个按钮只有核销码通过校验后才能继续</p>
</el-collapse-item>
<el-collapse-item name="cloud" title="Cloud 资源与发货">
<div class="flow-meta-list flow-meta-list--two-column">
<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-grid">
<article class="flow-block">
<h4>客户领取页</h4>
<div class="flow-meta-list">
<div><span>客户链接</span><strong>{{ claimUrl || '-' }}</strong></div>
<div><span>Token 状态</span><strong>{{ detail.claimToken?.status || '-' }}</strong></div>
<div><span>首次提交</span><strong>{{ formatAdminDateTime(kuaishouCloudFlow.ticket.capturedAt) }}</strong></div>
<div><span>校验时间</span><strong>{{ formatAdminDateTime(kuaishouCloudFlow.ticket.verifiedAt) }}</strong></div>
</div>
<p class="manual-hint">客户会先在这个页面查看图文指引并提交核销码校验通过后才会自动跳转绑定链接</p>
</article>
<article class="flow-block">
<h4>核销码状态</h4>
<div class="flow-meta-list">
<div><span>核销码</span><strong>{{ kuaishouCloudFlow.ticket.code || '-' }}</strong></div>
<div><span>校验状态</span><strong>{{ kuaishouCloudFlow.ticket.status || '-' }}</strong></div>
<div><span>商品标题</span><strong>{{ kuaishouCloudFlow.ticket.goodsTitle || '-' }}</strong></div>
<div><span>剩余次数</span><strong>{{ kuaishouCloudFlow.ticket.leftCount || 0 }}</strong></div>
</div>
<p class="manual-hint">确认绑定完成并发货已经合并为一个按钮只有客户提交并校验过核销码后才能继续</p>
</article>
</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.purchase.purchaseAt) }}</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>
</el-collapse-item>
<article class="flow-block">
<h4>流程收口</h4>
<div class="flow-meta-list">
<el-collapse-item name="closure" title="流程收口">
<div class="flow-meta-list flow-meta-list--two-column">
<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.consume.shopName || '-' }}</strong></div>
<div><span>核销时间</span><strong>{{ formatAdminDateTime(kuaishouCloudFlow.consume.consumedAt) }}</strong></div>
<div><span>核销异常</span><strong>{{ kuaishouCloudFlow.consume.errorMessage || '-' }}</strong></div>
<div><span>备注</span><strong>{{ kuaishouCloudFlow.notes || '-' }}</strong></div>
</div>
</article>
</div>
</el-collapse-item>
<el-collapse-item v-if="kuaishouRoleInfoEntries.length > 0" name="raw" title="角色原始字段">
<div class="flow-meta-list flow-meta-list--two-column">
<div v-for="entry in kuaishouRoleInfoEntries" :key="`raw-${entry.key}`">
<span>{{ entry.label }}</span>
<strong>{{ entry.value }}</strong>
</div>
</div>
</el-collapse-item>
</el-collapse>
</section>
<section
@@ -746,7 +963,7 @@ onBeforeUnmount(clearScreenshotPreview)
</div>
</section>
<section class="table-card">
<section v-if="!isKuaishouCloudTask" class="table-card">
<h3>库存绑定</h3>
<p class="section-copy">这里展示任务当前和历史绑定过的库存项便于核对多库存任务的真实履约轨迹</p>
<table class="data-table">
@@ -889,12 +1106,81 @@ onBeforeUnmount(clearScreenshotPreview)
.panel-header h1 { margin: 0; color: #1d3555; }
.panel-header p { margin: 8px 0 0; color: #64748b; }
.claim-link { color: #175cd3; text-decoration: none; word-break: break-all; }
.info-card,.table-card,.empty-block,.error-copy {
.info-card,.hero-card,.table-card,.empty-block,.error-copy {
padding: 18px; border-radius: 20px; background: rgba(255,255,255,.94); border: 1px solid rgba(86,108,138,.1);
}
.info-card p {
margin: 10px 0 0;
color: #334155;
}
.hero-card {
display: grid;
gap: 16px;
background:
radial-gradient(circle at top right, rgba(14, 165, 233, 0.16), transparent 26%),
linear-gradient(135deg, rgba(255,255,255,.98), rgba(239,246,255,.96));
}
.hero-copy {
display: grid;
gap: 6px;
}
.hero-copy h2 {
margin: 0;
color: #0f172a;
font-size: 30px;
line-height: 1.1;
}
.hero-copy p,
.hero-inline span,
.hero-link-row span {
color: #475467;
}
.hero-eyebrow {
color: #175cd3;
font-size: 12px;
font-weight: 700;
letter-spacing: .08em;
text-transform: uppercase;
}
.hero-meta-grid,
.overview-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 12px;
}
.hero-metric {
padding: 14px 16px;
border-radius: 18px;
border: 1px solid rgba(86,108,138,.1);
background: rgba(255,255,255,.82);
}
.hero-metric span {
display: block;
margin-bottom: 8px;
color: #64748b;
font-size: 12px;
}
.hero-metric strong {
color: #0f172a;
font-size: 18px;
line-height: 1.3;
word-break: break-word;
}
.hero-metric--blue { box-shadow: inset 0 0 0 1px rgba(59,130,246,.08); }
.hero-metric--green { box-shadow: inset 0 0 0 1px rgba(16,185,129,.08); }
.hero-metric--amber { box-shadow: inset 0 0 0 1px rgba(245,158,11,.08); }
.hero-metric--slate { box-shadow: inset 0 0 0 1px rgba(100,116,139,.08); }
.hero-inline,
.hero-link-row {
display: flex;
flex-wrap: wrap;
gap: 12px;
align-items: center;
}
.error-copy { color: #b42318; }
.action-row { display: flex; flex-wrap: wrap; gap: 12px; }
.table-card h3 { margin: 0 0 12px; color: #1d3555; }
.compact-card { padding: 18px; }
.section-copy { margin: 0 0 12px; color: #64748b; }
.auto-delivery-tag-line { display: flex; align-items: center; }
.auto-delivery-note {
@@ -988,6 +1274,7 @@ onBeforeUnmount(clearScreenshotPreview)
flex-wrap: wrap;
gap: 10px;
}
.kuaishou-focus-grid,
.flow-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
@@ -1000,6 +1287,11 @@ onBeforeUnmount(clearScreenshotPreview)
border: 1px solid rgba(86,108,138,.1);
background: rgba(248,250,252,.9);
}
.flow-block--spotlight {
background:
linear-gradient(160deg, rgba(255,255,255,.98), rgba(239,246,255,.95)),
rgba(248,250,252,.9);
}
.flow-block h4 {
margin: 0;
color: #1d3555;
@@ -1026,6 +1318,89 @@ onBeforeUnmount(clearScreenshotPreview)
color: #1d3555;
word-break: break-all;
}
.flow-meta-list--dense {
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
}
.flow-meta-list--two-column {
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
}
.spotlight-head {
display: flex;
justify-content: space-between;
gap: 12px;
align-items: flex-start;
}
.spotlight-actions {
display: flex;
align-items: center;
gap: 10px;
}
.flow-copy {
margin: 8px 0 0;
color: #64748b;
line-height: 1.6;
}
.flow-collapse {
display: grid;
gap: 12px;
}
.checklist-grid {
display: grid;
gap: 12px;
margin-top: 16px;
}
.checklist-item {
padding: 14px 16px;
border-radius: 16px;
background: rgba(255,255,255,.84);
border: 1px solid rgba(86,108,138,.08);
}
.checklist-item-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 8px;
}
.checklist-item-head span {
color: #64748b;
font-size: 12px;
}
.checklist-item strong {
color: #0f172a;
line-height: 1.5;
}
.role-hero {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 12px;
margin-top: 16px;
}
.role-hero div {
padding: 14px 16px;
border-radius: 16px;
background: rgba(255,255,255,.88);
border: 1px solid rgba(86,108,138,.08);
}
.role-hero span {
display: block;
margin-bottom: 6px;
color: #64748b;
font-size: 12px;
}
.role-hero strong {
color: #0f172a;
font-size: 18px;
line-height: 1.35;
word-break: break-word;
}
.role-warning {
margin: 14px 0 0;
padding: 12px 14px;
border-radius: 14px;
background: rgba(255, 244, 229, 0.9);
color: #b54708;
}
.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; }
@@ -1039,8 +1414,13 @@ onBeforeUnmount(clearScreenshotPreview)
}
@media (max-width: 720px) {
.hero-inline,
.hero-link-row,
.flow-card-head,
.flow-head-actions {
.flow-head-actions,
.spotlight-head,
.spotlight-actions,
.checklist-item-head {
display: grid;
}
}