使用element ui

This commit is contained in:
yml
2026-05-21 20:16:27 +08:00
parent be7b057034
commit a39d43c4d6
39 changed files with 1380 additions and 1644 deletions
@@ -15,13 +15,14 @@
"externalSkuName": "183999074511936",
"resolvedSkuName": "183999074511936",
"cloudSourceKey": "default",
"cloudSourceKeyFallbacks": [],
"cloudSkuId": 28,
"cloudSkuName": "",
"cloudSkuName": "套装-浪漫天命",
"vnKey": "1",
"autoBuyEnabled": true,
"minAssetReserve": 0,
"autoReturnNumberAfterDispatch": false,
"autoConsumeAfterDispatch": false,
"autoReturnNumberAfterDispatch": true,
"autoConsumeAfterDispatch": true,
"kuaishouConsumeShopId": "3676797936",
"kuaishouConsumeShopName": "小鱼儿游戏服务店",
"notes": "从 91卡券订单 2613900255300561 导入"
@@ -40,6 +41,7 @@
"externalSkuName": "183999074510936",
"resolvedSkuName": "183999074510936",
"cloudSourceKey": "default",
"cloudSourceKeyFallbacks": [],
"cloudSkuId": 28,
"cloudSkuName": "套装-浪漫天命",
"vnKey": "1",
+4
View File
@@ -15,6 +15,7 @@ declare module 'vue' {
AdminResultCard: typeof import('./components/admin/AdminResultCard.vue')['default']
AdminStatusTag: typeof import('./components/admin/AdminStatusTag.vue')['default']
ElAlert: typeof import('element-plus/es')['ElAlert']
ElAutocomplete: typeof import('element-plus/es')['ElAutocomplete']
ElButton: typeof import('element-plus/es')['ElButton']
ElCard: typeof import('element-plus/es')['ElCard']
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
@@ -32,6 +33,7 @@ declare module 'vue' {
ElForm: typeof import('element-plus/es')['ElForm']
ElFormItem: typeof import('element-plus/es')['ElFormItem']
ElIcon: typeof import('element-plus/es')['ElIcon']
ElImage: typeof import('element-plus/es')['ElImage']
ElInput: typeof import('element-plus/es')['ElInput']
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
ElMenu: typeof import('element-plus/es')['ElMenu']
@@ -44,6 +46,8 @@ declare module 'vue' {
ElRow: typeof import('element-plus/es')['ElRow']
ElSelect: typeof import('element-plus/es')['ElSelect']
ElSkeleton: typeof import('element-plus/es')['ElSkeleton']
ElSpace: typeof import('element-plus/es')['ElSpace']
ElStatistic: typeof import('element-plus/es')['ElStatistic']
ElSwitch: typeof import('element-plus/es')['ElSwitch']
ElTable: typeof import('element-plus/es')['ElTable']
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
@@ -6,11 +6,11 @@ defineProps<{
</script>
<template>
<article class="result-card">
<el-card shadow="never" class="result-card">
<span class="result-label">{{ label }}</span>
<strong>{{ value }}</strong>
<slot />
</article>
</el-card>
</template>
<style scoped>
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import { computed, onMounted, ref } from 'vue'
import { useAdminListPage } from '@/composables/useAdminListPage'
import AdminPageHeader from '@/components/admin/AdminPageHeader.vue'
@@ -66,6 +66,10 @@ function resetFilters() {
dateTo.value = ''
void loadAuditLogs(1)
}
onMounted(() => {
void loadAuditLogs(1)
})
</script>
<template>
@@ -1,81 +1,85 @@
@import '../../../../styles/admin-config-pages.css';
/* ============================================================
AdminFulfillmentBindings — 绿色主题覆盖 & 页面特有样式
============================================================ */
.admin-panel {
display: grid;
gap: 12px;
gap: var(--space-4);
}
.section-note-chip,
.mini-stat-chip,
.summary-label,
.overview-label {
display: inline-flex;
align-items: center;
min-height: 28px;
padding: 0 10px;
border-radius: var(--radius-full);
background: rgba(37, 89, 167, 0.08);
border: 1px solid rgba(37, 89, 167, 0.12);
color: var(--color-primary-dark);
font-size: var(--text-xs);
font-weight: 700;
letter-spacing: 0.04em;
}
/* ---------- 主题:概览标签 ---------- */
.overview-note {
display: inline-flex;
align-items: center;
min-height: 32px;
padding: 0 12px;
border-radius: var(--radius-full);
background: rgba(15, 23, 42, 0.04);
color: var(--text-secondary);
font-size: var(--text-xs);
font-weight: 600;
}
/* ---------- 主题覆写:header-actions 间距 ---------- */
.header-actions {
gap: 10px;
}
/* ---------- 主题覆写:卡片圆角 ---------- */
.overview-card,
.table-card,
.empty-block,
.error-copy {
border-radius: var(--radius-xl);
}
.overview-file code {
background: rgba(15, 23, 42, 0.05);
color: var(--text-primary);
}
/* ---------- 主题覆写:概览统计 ---------- */
.overview-stat {
.section-card {
border-radius: var(--radius-lg);
}
.overview-stat span {
.card-header {
display: grid;
gap: var(--space-1);
}
.card-header--split {
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: var(--space-4);
}
.card-title {
display: block;
font-size: var(--text-base);
font-weight: 700;
color: var(--text-primary);
}
.card-desc {
display: block;
margin-top: 4px;
font-size: var(--text-sm);
color: var(--text-secondary);
}
.overview-stat strong {
.overview-card {
display: grid;
gap: var(--space-4);
}
.overview-file code {
color: var(--text-primary);
font-size: 22px;
word-break: break-all;
}
.overview-stats {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: var(--space-3);
}
.overview-stat {
min-width: 0;
padding: var(--space-3);
border: 1px solid var(--border-default);
border-radius: var(--radius-md);
background: var(--bg-subtle);
}
.overview-stat :deep(.el-statistic__head) {
margin-bottom: 6px;
color: var(--text-secondary);
font-size: var(--text-xs);
font-weight: 700;
}
.overview-stat :deep(.el-statistic__content) {
color: var(--text-primary);
font-size: var(--text-2xl);
font-weight: 700;
}
.overview-notes,
.section-mini-stats {
display: flex;
flex-wrap: wrap;
gap: var(--space-2);
}
/* ---------- 查询工具栏 (本页特有) ---------- */
.lookup-toolbar,
.lookup-summary {
display: grid;
gap: 12px;
gap: var(--space-3);
}
.lookup-toolbar {
@@ -84,132 +88,88 @@
}
.lookup-summary {
margin-top: 14px;
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.lookup-summary-item {
display: grid;
gap: 6px;
padding: 12px 14px;
border-radius: var(--radius-lg);
background: #f8fafc;
border: 1px solid rgba(148, 163, 184, 0.14);
}
.lookup-summary-item strong {
color: var(--text-primary);
}
/* ---------- 主题覆写:字段块 ---------- */
.field-block span {
color: var(--text-secondary);
margin-top: var(--space-4);
}
.field-help {
display: block;
margin-top: 6px;
color: var(--text-secondary);
font-size: var(--text-xs);
line-height: 1.5;
}
.field-wide {
grid-column: 1 / -1;
}
/* ---------- 主题覆写:文本输入 ---------- */
.text-input {
min-height: 40px;
}
/* ---------- Element Plus 输入框绿色主题覆盖 ---------- */
.admin-panel :deep(.el-input__wrapper),
.admin-panel :deep(.el-textarea__inner),
.admin-panel :deep(.el-select__wrapper) {
min-height: 40px;
border: 0;
border-radius: 14px;
background: rgba(255, 255, 255, 0.96);
box-shadow: 0 0 0 1px rgba(86, 108, 138, 0.18) inset;
}
.admin-panel :deep(.el-input__wrapper:hover),
.admin-panel :deep(.el-select__wrapper:hover) {
box-shadow: 0 0 0 1px rgba(86, 108, 138, 0.32) inset;
}
.admin-panel :deep(.el-input__wrapper.is-focus),
.admin-panel :deep(.el-select__wrapper.is-focused) {
box-shadow:
0 0 0 1px rgba(37, 89, 167, 0.38) inset,
0 0 0 3px rgba(37, 89, 167, 0.1);
width: 100%;
}
.admin-panel :deep(.text-input--invalid .el-input__wrapper),
.admin-panel :deep(.text-input--invalid .el-select__wrapper) {
box-shadow: 0 0 0 1px rgba(220, 38, 38, 0.45) inset;
background: #fff8f8;
border-color: var(--color-danger);
box-shadow: 0 0 0 1px var(--color-danger) inset;
}
.admin-panel :deep(.text-input--invalid .el-input__wrapper.is-focus),
.admin-panel :deep(.text-input--invalid .el-select__wrapper.is-focused) {
box-shadow:
0 0 0 1px rgba(220, 38, 38, 0.6) inset,
0 0 0 3px rgba(254, 226, 226, 0.95);
.admin-panel :deep(.el-form-item) {
margin-bottom: 0;
}
.admin-panel :deep(.is-disabled .el-input__wrapper),
.admin-panel :deep(.el-input__wrapper.is-disabled),
.admin-panel :deep(.el-select__wrapper.is-disabled) {
background: rgba(148, 163, 184, 0.08);
.admin-panel :deep(.el-form-item__label) {
color: var(--text-secondary);
}
.admin-panel :deep(.el-input-number .el-input__wrapper) {
padding-left: 8px;
}
.admin-panel :deep(.el-input-number__decrease),
.admin-panel :deep(.el-input-number__increase) {
border-color: rgba(86, 108, 138, 0.18);
background: #f8fafc;
}
/* ---------- 主题覆写:校验/错误/空状态 ---------- */
.validation-banner {
background: #fff4f2;
border: 1px solid rgba(244, 63, 94, 0.18);
color: var(--color-danger);
}
.binding-inline-error {
color: var(--color-danger);
}
.empty-block,
.empty-inline {
color: var(--text-secondary);
}
/* ---------- 主题覆写:表格 ---------- */
.data-table th,
.data-table td {
padding: 12px 10px;
color: var(--text-primary);
}
.data-table th {
color: var(--text-secondary);
font-size: var(--text-sm);
font-size: var(--text-xs);
font-weight: 700;
line-height: 1.4;
margin-bottom: 6px;
}
.cell-subtle {
color: var(--text-secondary);
display: block;
font-size: var(--text-xs);
line-height: 1.5;
}
/* ---------- 主题覆写:绑定卡片 ---------- */
.binding-card {
border-radius: var(--radius-lg);
margin-top: var(--space-3);
background: var(--bg-surface);
}
.binding-card :deep(.el-card__header) {
padding: var(--space-3) var(--space-4);
background: var(--bg-subtle);
}
.binding-card :deep(.el-card__body) {
display: grid;
gap: var(--space-3);
padding: var(--space-4);
}
.binding-card--invalid {
border-color: var(--color-danger);
box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.08);
}
.binding-header {
display: flex;
justify-content: space-between;
gap: var(--space-3);
align-items: flex-start;
}
.binding-summary {
display: grid;
gap: 4px;
}
.binding-title-row {
display: flex;
gap: var(--space-2);
align-items: center;
flex-wrap: wrap;
color: var(--text-primary);
}
@@ -220,43 +180,37 @@
.binding-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px 12px;
gap: var(--space-4);
}
/* ---------- 主题覆写:折叠/操作/复选框 ---------- */
.binding-collapsed-preview {
color: var(--text-secondary);
font-size: var(--text-sm);
}
.collapse-button {
color: var(--text-primary);
.binding-actions {
display: flex;
justify-content: space-between;
gap: var(--space-3);
align-items: center;
}
.checkbox-line {
color: var(--text-secondary);
}
.toolbar-hint {
color: var(--text-secondary);
}
/* ==============================================
响应式
============================================== */
@media (max-width: 900px) {
.section-title-row,
.section-header-tools,
.card-header--split,
.binding-actions,
.binding-header {
flex-direction: column;
display: grid;
grid-template-columns: 1fr;
align-items: stretch;
}
.overview-stats,
.lookup-toolbar,
.lookup-summary,
.binding-grid {
grid-template-columns: 1fr;
}
.binding-collapsed-preview :deep(.el-descriptions__body .el-descriptions__table) {
display: block;
}
}
@@ -18,7 +18,7 @@ onMounted(bindings.loadConfigs)
<template>
<section class="admin-panel">
<div v-if="!hasAdminRole('admin')" class="empty-block">仅管理员可以维护履约配置</div>
<el-result v-if="!hasAdminRole('admin')" icon="warning" title="仅管理员可以维护履约配置" />
<template v-else>
<AdminBindingsOverviewSection
@@ -28,8 +28,14 @@ onMounted(bindings.loadConfigs)
:observed-products-length="bindings.observedProducts.value.length"
/>
<p v-if="bindings.errorMessage.value" class="error-copy">{{ bindings.errorMessage.value }}</p>
<div v-if="bindings.loading.value" class="empty-block">履约配置加载中</div>
<el-alert
v-if="bindings.errorMessage.value"
:title="bindings.errorMessage.value"
type="error"
show-icon
:closable="false"
/>
<el-skeleton v-if="bindings.loading.value" :rows="12" animated />
<template v-else>
<AdminBindingsRuleSection
@@ -25,63 +25,63 @@ const emit = defineEmits<{
</script>
<template>
<div
<el-card
:data-binding-id="binding.id"
:class="['binding-card', { 'binding-card--invalid': isInvalid }]"
shadow="never"
>
<div class="binding-header">
<div class="binding-summary">
<div class="binding-title-row">
<strong>{{ title }}</strong>
<span :class="['binding-status-chip', { 'binding-status-chip--done': isComplete }]">
{{ statusLabel }}
</span>
<template #header>
<div class="binding-header">
<div class="binding-summary">
<div class="binding-title-row">
<strong>{{ title }}</strong>
<el-tag :type="isComplete ? 'success' : 'warning'" effect="plain">
{{ statusLabel }}
</el-tag>
</div>
<span class="binding-subtle">{{ summary }}</span>
</div>
<span class="binding-subtle">{{ summary }}</span>
<el-button link type="primary" @click="emit('toggleCollapsed')">
{{ isBindingCollapsed ? '展开' : '折叠' }}
</el-button>
</div>
<el-button link type="primary" @click="emit('toggleCollapsed')">
{{ isBindingCollapsed ? '展开' : '折叠' }}
</el-button>
</div>
</template>
<div v-if="isBindingCollapsed" class="binding-collapsed-preview">
<span
>外部商品{{
binding.externalSkuName || binding.externalSkuCode || binding.externalItemId || '-'
}}</span
>
<span>内部履约 SKU{{ binding.skuCode || '-' }}</span>
<span
>履约方式{{
<el-descriptions v-if="isBindingCollapsed" :column="3" border class="binding-collapsed-preview">
<el-descriptions-item label="外部商品">
{{ binding.externalSkuName || binding.externalSkuCode || binding.externalItemId || '-' }}
</el-descriptions-item>
<el-descriptions-item label="内部履约 SKU">
{{ binding.skuCode || '-' }}
</el-descriptions-item>
<el-descriptions-item label="履约方式">
{{
PROFILE_OPTIONS.find((item) => item.value === binding.profileKey)?.label ||
binding.profileKey
}}</span
>
</div>
}}
</el-descriptions-item>
</el-descriptions>
<div v-else class="binding-grid">
<label class="field-block">
<span>渠道</span>
<el-form v-else label-position="top" class="binding-grid">
<el-form-item label="渠道">
<el-input
v-model="binding.provider"
class="text-input"
placeholder="agiso"
@input="emit('clearValidation')"
/>
</label>
</el-form-item>
<label class="field-block">
<span>平台</span>
<el-form-item label="平台">
<el-input
v-model="binding.platform"
class="text-input"
placeholder="xianyu / taobao / pdd"
@input="emit('clearValidation')"
/>
</label>
</el-form-item>
<label class="field-block">
<span>店铺 ID</span>
<el-form-item label="店铺 ID">
<el-input
v-model="binding.shopId"
:class="[{ 'text-input--invalid': isFieldInvalid('shopId') }]"
@@ -90,20 +90,18 @@ const emit = defineEmits<{
placeholder="留空表示跨店铺通用"
@input="emit('clearValidation')"
/>
</label>
</el-form-item>
<label class="field-block">
<span>店铺名称</span>
<el-form-item label="店铺名称">
<el-input
v-model="binding.shopName"
class="text-input"
placeholder="用于保存和识别店铺"
@input="emit('clearValidation')"
/>
</label>
</el-form-item>
<label class="field-block">
<span>优先级</span>
<el-form-item label="优先级">
<el-input-number
v-model="binding.priority"
class="text-input"
@@ -111,10 +109,9 @@ const emit = defineEmits<{
controls-position="right"
@change="emit('clearValidation')"
/>
</label>
</el-form-item>
<label class="field-block">
<span>外部 SKU 编码</span>
<el-form-item label="外部 SKU 编码">
<el-input
v-model="binding.externalSkuCode"
:class="[{ 'text-input--invalid': isFieldInvalid('externalSkuCode') }]"
@@ -123,10 +120,9 @@ const emit = defineEmits<{
placeholder="例如 6046726460016"
@input="emit('clearValidation')"
/>
</label>
</el-form-item>
<label class="field-block">
<span>外部商品 ID</span>
<el-form-item label="外部商品 ID">
<el-input
v-model="binding.externalItemId"
:class="[{ 'text-input--invalid': isFieldInvalid('externalItemId') }]"
@@ -135,10 +131,9 @@ const emit = defineEmits<{
placeholder="例如 1033324289962"
@input="emit('clearValidation')"
/>
</label>
</el-form-item>
<label class="field-block field-wide">
<span>外部商品名</span>
<el-form-item label="外部商品名" class="field-wide">
<el-input
v-model="binding.externalSkuName"
:class="[{ 'text-input--invalid': isFieldInvalid('externalSkuName') }]"
@@ -147,10 +142,9 @@ const emit = defineEmits<{
placeholder="例如 奥利奥动作"
@input="emit('clearValidation')"
/>
</label>
</el-form-item>
<label class="field-block">
<span>内部履约 SKU</span>
<el-form-item label="内部履约 SKU" class="field-with-help">
<el-input
v-model="binding.skuCode"
:class="[{ 'text-input--invalid': isFieldInvalid('skuCode') }]"
@@ -162,20 +156,18 @@ const emit = defineEmits<{
<small class="field-help"
>填你们系统内部统一 SKU相同履约商品尽量跨平台共用同一个内部 SKU</small
>
</label>
</el-form-item>
<label class="field-block">
<span>内部商品名</span>
<el-form-item label="内部商品名">
<el-input
v-model="binding.skuName"
class="text-input"
placeholder="用于后台展示"
@input="emit('clearValidation')"
/>
</label>
</el-form-item>
<label class="field-block">
<span>履约方式</span>
<el-form-item label="履约方式">
<el-select
v-model="binding.profileKey"
:class="[{ 'text-input--invalid': isFieldInvalid('profileKey') }]"
@@ -190,28 +182,32 @@ const emit = defineEmits<{
:value="item.value"
/>
</el-select>
</label>
</el-form-item>
<label class="field-block">
<span>匹配后展示名</span>
<el-form-item label="匹配后展示名">
<el-input
v-model="binding.resolvedSkuName"
class="text-input"
placeholder="例如 三角洲行动-自动领取"
@input="emit('clearValidation')"
/>
</label>
</div>
</el-form-item>
</el-form>
<p v-if="isInvalid" class="binding-inline-error">
{{ validationState?.message }}
</p>
<el-alert
v-if="isInvalid"
:title="validationState?.message"
type="error"
show-icon
:closable="false"
class="mt-4"
/>
<div class="binding-actions">
<el-checkbox v-model="binding.enabled">启用规则</el-checkbox>
<el-button link type="danger" @click="emit('remove')">删除</el-button>
</div>
</div>
</el-card>
</template>
<style scoped src="../AdminFulfillmentBindings.css"></style>
@@ -20,83 +20,94 @@ const emit = defineEmits<{
</script>
<template>
<section class="table-card table-card--dense">
<div class="section-title-row section-title-row--tight">
<div>
<h3>手动补查订单商品</h3>
<p>新商品还没配规则时可以按店铺 ID 和平台订单号临时补查查完直接导入规则草稿</p>
<el-card shadow="never" class="section-card">
<template #header>
<div class="card-header card-header--split">
<div>
<span class="card-title">手动补查订单商品</span>
<span class="card-desc"
>新商品还没配规则时可以按店铺 ID 和平台订单号临时补查查完直接导入规则草稿</span
>
</div>
<el-tag effect="plain">仅支持 Agiso 咸鱼订单</el-tag>
</div>
<span class="section-note-chip">仅支持 Agiso 咸鱼订单</span>
</div>
</template>
<div class="lookup-toolbar">
<label class="field-block">
<span>渠道</span>
<el-form label-position="top" class="lookup-toolbar">
<el-form-item label="渠道">
<el-input class="text-input" model-value="agiso" disabled />
</label>
</el-form-item>
<label class="field-block">
<span>平台</span>
<el-form-item label="平台">
<el-input class="text-input" model-value="xianyu" disabled />
</label>
</el-form-item>
<label class="field-block">
<span>店铺 ID</span>
<el-form-item label="店铺 ID">
<el-input
v-model="lookupForm.shopId"
class="text-input"
placeholder="例如 252609"
@keydown.enter.prevent="emit('lookup')"
/>
</label>
</el-form-item>
<label class="field-block">
<span>平台订单号</span>
<el-form-item label="平台订单号">
<el-input
v-model="lookupForm.platformOrderId"
class="text-input"
placeholder="输入需要补查的订单号"
@keydown.enter.prevent="emit('lookup')"
/>
</label>
</el-form-item>
<div class="field-block field-block--action">
<span>操作</span>
<el-button :loading="lookupLoading" round type="primary" @click="emit('lookup')"
>查询订单商品</el-button
>
</div>
</div>
<el-form-item label="操作">
<el-button :loading="lookupLoading" type="primary" class="w-full" @click="emit('lookup')">
查询订单商品
</el-button>
</el-form-item>
</el-form>
<p class="toolbar-hint">查询结果只用于补配置不会写入订单库也不会影响现有订单数据</p>
<el-alert
title="查询结果只用于补配置,不会写入订单库,也不会影响现有订单数据。"
type="info"
show-icon
:closable="false"
class="mt-4"
/>
<p v-if="lookupErrorMessage" class="error-copy lookup-error">{{ lookupErrorMessage }}</p>
<div v-else-if="lookupLoading" class="empty-inline">正在查询订单详情</div>
<el-alert
v-if="lookupErrorMessage"
:title="lookupErrorMessage"
type="error"
show-icon
:closable="false"
class="mt-4"
/>
<el-skeleton v-else-if="lookupLoading" :rows="4" animated class="mt-4" />
<template v-else-if="lookupResult">
<div class="lookup-summary">
<div class="lookup-summary-item">
<span class="summary-label">订单</span>
<strong>{{ lookupResult.order.platformOrderId }}</strong>
<span class="cell-subtle">{{
lookupResult.order.shopName || lookupResult.order.shopId || '-'
}}</span>
</div>
<div class="lookup-summary-item">
<span class="summary-label">买家</span>
<strong>{{ lookupResult.order.buyerName || '-' }}</strong>
<span class="cell-subtle"
>支付时间{{ formatAdminDateTime(lookupResult.order.paidAt) }}</span
>
</div>
<div class="lookup-summary-item">
<span class="summary-label">金额</span>
<strong>{{ lookupResult.order.totalAmount || '0.00' }}</strong>
<span class="cell-subtle"
>补查结果{{
lookupResult.order.enriched ? '已补全' : lookupResult.order.enrichReason || '已返回'
}}</span
>
</div>
<el-descriptions :column="3" border>
<el-descriptions-item label="订单">
<strong>{{ lookupResult.order.platformOrderId }}</strong>
<span class="cell-subtle">{{
lookupResult.order.shopName || lookupResult.order.shopId || '-'
}}</span>
</el-descriptions-item>
<el-descriptions-item label="买家">
<strong>{{ lookupResult.order.buyerName || '-' }}</strong>
<span class="cell-subtle"
>支付时间{{ formatAdminDateTime(lookupResult.order.paidAt) }}</span
>
</el-descriptions-item>
<el-descriptions-item label="金额">
<strong>{{ lookupResult.order.totalAmount || '0.00' }}</strong>
<span class="cell-subtle">
补查结果{{
lookupResult.order.enriched ? '已补全' : lookupResult.order.enrichReason || '已返回'
}}
</span>
</el-descriptions-item>
</el-descriptions>
</div>
<el-table
@@ -148,7 +159,7 @@ const emit = defineEmits<{
</el-table-column>
</el-table>
</template>
</section>
</el-card>
</template>
<style scoped src="../AdminFulfillmentBindings.css"></style>
@@ -15,17 +15,21 @@ const emit = defineEmits<{
</script>
<template>
<section class="table-card table-card--dense">
<div class="section-title-row section-title-row--tight">
<div>
<h3>近期识别到的外部商品</h3>
<p>从已入库订单里提取的外部商品信息优先处理未配置商品导入后再补齐内部 SKU</p>
<el-card shadow="never" class="section-card">
<template #header>
<div class="card-header card-header--split">
<div>
<span class="card-title">近期识别到的外部商品</span>
<span class="card-desc"
>从已入库订单里提取的外部商品信息优先处理未配置商品导入后再补齐内部 SKU</span
>
</div>
<el-space wrap>
<el-tag type="warning" effect="plain">待补 {{ pendingObservedCount }}</el-tag>
<el-tag effect="plain">总计 {{ observedProducts.length }}</el-tag>
</el-space>
</div>
<div class="section-mini-stats">
<span class="mini-stat-chip">待补 {{ pendingObservedCount }}</span>
<span class="mini-stat-chip">总计 {{ observedProducts.length }}</span>
</div>
</div>
</template>
<el-table
:data="observedProducts"
@@ -78,7 +82,7 @@ const emit = defineEmits<{
</template>
</el-table-column>
</el-table>
</section>
</el-card>
</template>
<style scoped src="../AdminFulfillmentBindings.css"></style>
@@ -15,41 +15,34 @@ defineProps<{
</script>
<template>
<section class="overview-card">
<div class="overview-file">
<span class="overview-label">配置文件</span>
<code>{{ filePath || '-' }}</code>
</div>
<el-card shadow="never" class="section-card overview-card">
<template #header>
<div class="card-header">
<span class="card-title">旧履约规则概览</span>
<span class="card-desc">按外部商品信息匹配内部履约 SKU 和交付方式</span>
</div>
</template>
<el-descriptions :column="1" border class="overview-file">
<el-descriptions-item label="配置文件">
<code>{{ filePath || '-' }}</code>
</el-descriptions-item>
</el-descriptions>
<div class="overview-stats">
<article class="overview-stat">
<span>规则总数</span>
<strong>{{ ruleMetrics.totalCount }}</strong>
</article>
<article class="overview-stat">
<span>完整规则</span>
<strong>{{ ruleMetrics.completedCount }}</strong>
</article>
<article class="overview-stat">
<span>待完善</span>
<strong>{{ ruleMetrics.draftCount }}</strong>
</article>
<article class="overview-stat">
<span>待补商品</span>
<strong>{{ pendingObservedCount }}</strong>
</article>
<article class="overview-stat">
<span>已识别商品</span>
<strong>{{ observedProductsLength }}</strong>
</article>
<el-statistic class="overview-stat" title="规则总数" :value="ruleMetrics.totalCount" />
<el-statistic class="overview-stat" title="完整规则" :value="ruleMetrics.completedCount" />
<el-statistic class="overview-stat" title="待完善" :value="ruleMetrics.draftCount" />
<el-statistic class="overview-stat" title="待补商品" :value="pendingObservedCount" />
<el-statistic class="overview-stat" title="已识别商品" :value="observedProductsLength" />
</div>
<div class="overview-notes">
<span class="overview-note">外部 SKU / 商品 ID / 商品名命中任一即可</span>
<span class="overview-note">内部 SKU 决定库存绑定与履约链路</span>
<span class="overview-note">同类商品跨平台尽量共用同一内部 SKU</span>
<el-tag effect="plain">外部 SKU / 商品 ID / 商品名命中任一即可</el-tag>
<el-tag effect="plain">内部 SKU 决定库存绑定与履约链路</el-tag>
<el-tag effect="plain">同类商品跨平台尽量共用同一内部 SKU</el-tag>
</div>
</section>
</el-card>
</template>
<style scoped src="../AdminFulfillmentBindings.css"></style>
@@ -37,35 +37,47 @@ const emit = defineEmits<{
</script>
<template>
<section class="table-card table-card--dense">
<div class="section-title-row section-title-row--tight">
<div>
<h3>已配置规则</h3>
<p>正式规则集中维护在这里新补查到的商品也会导入到这个区域继续补全</p>
</div>
<div class="section-header-tools">
<div class="section-mini-stats">
<span class="mini-stat-chip">完整 {{ ruleMetrics.completedCount }}</span>
<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="emit('addBinding')">新增规则</el-button>
<el-button round @click="emit('loadConfigs')">刷新</el-button>
<el-button :loading="saving" round type="primary" @click="emit('saveConfigs')"
>保存配置</el-button
<el-card shadow="never" class="section-card">
<template #header>
<div class="card-header card-header--split">
<div>
<span class="card-title">已配置规则</span>
<span class="card-desc"
>正式规则集中维护在这里新补查到的商品也会导入到这个区域继续补全</span
>
</div>
<el-space wrap>
<el-button @click="emit('addBinding')">新增规则</el-button>
<el-button @click="emit('loadConfigs')">刷新</el-button>
<el-button :loading="saving" type="primary" @click="emit('saveConfigs')">
保存配置
</el-button>
</el-space>
</div>
</template>
<div class="section-mini-stats">
<el-tag type="success" effect="plain">完整 {{ ruleMetrics.completedCount }}</el-tag>
<el-tag type="warning" effect="plain">待完善 {{ ruleMetrics.draftCount }}</el-tag>
<el-tag effect="plain">人工 {{ ruleMetrics.manualCount }}</el-tag>
<el-tag effect="plain">半自动 {{ ruleMetrics.assistedCount }}</el-tag>
<el-tag effect="plain">快手 Cloud {{ ruleMetrics.kuaishouCloudCount }}</el-tag>
</div>
<p v-if="validationState?.message" class="validation-banner">
{{ validationState.message }}
</p>
<el-alert
v-if="validationState?.message"
:title="validationState.message"
type="error"
show-icon
:closable="false"
class="mt-4"
/>
<div v-if="bindings.length === 0" class="empty-inline">当前还没有履约规则先新增一条</div>
<el-empty
v-if="bindings.length === 0"
description="当前还没有履约规则,先新增一条。"
:image-size="60"
/>
<AdminBindingRuleCard
v-for="(binding, index) in bindings"
@@ -84,7 +96,7 @@ const emit = defineEmits<{
@clear-validation="emit('clearValidation')"
@remove="emit('removeBinding', binding.id)"
/>
</section>
</el-card>
</template>
<style scoped src="../AdminFulfillmentBindings.css"></style>
@@ -1,229 +1,199 @@
@import '../../../../styles/admin-config-pages.css';
/* ============================================================
AdminKuaishouCloudFulfillment — 蓝色主题覆盖 & 双栏映射布局
模板已统一使用 admin-config-pages.css 共享类名
============================================================ */
/* ---------- 页面容器 & CSS 自定义属性 ---------- */
.admin-panel {
--panel-ink: var(--text-primary);
--panel-muted: var(--text-secondary);
--panel-border: var(--border-default);
--panel-soft: #f6f8fb;
--panel-line: #dce3ec;
--accent: var(--color-primary);
--accent-strong: var(--color-primary-dark);
--warn: var(--color-warning);
--danger: var(--color-danger);
display: grid;
gap: var(--space-4);
}
/* ---------- 标题颜色覆盖 ---------- */
.section-title-row h3 {
color: #1d3555;
.section-card {
border-radius: var(--radius-lg);
}
.section-title-row p {
color: #64748b;
line-height: 1.55;
.card-header {
display: grid;
gap: var(--space-1);
}
/* ---------- chip 标签 (蓝色) ---------- */
.mini-stat-chip,
.overview-note {
color: #175cd3;
.card-header--split {
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: var(--space-4);
}
/* ---------- 卡片圆角覆盖 ---------- */
.overview-card,
.table-card,
.empty-block,
.error-copy {
border-radius: 20px;
box-shadow: none;
.card-title {
display: block;
color: var(--text-primary);
font-size: var(--text-base);
font-weight: 700;
}
.card-desc {
display: block;
margin-top: 4px;
color: var(--text-secondary);
font-size: var(--text-sm);
}
.overview-card {
display: grid;
gap: var(--space-4);
}
.overview-file code {
background: rgba(15, 23, 42, 0.05);
color: #1d3555;
color: var(--text-primary);
word-break: break-all;
}
.overview-stats {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: var(--space-3);
}
/* ---------- 概览统计覆盖 ---------- */
.overview-stat {
border-radius: 16px;
min-width: 0;
padding: var(--space-3);
border: 1px solid var(--border-default);
border-radius: var(--radius-md);
background: var(--bg-subtle);
}
.overview-stat span {
color: #64748b;
.overview-stat :deep(.el-statistic__head) {
margin-bottom: 6px;
color: var(--text-secondary);
font-size: var(--text-xs);
font-weight: 700;
}
.overview-stat strong {
color: #1d3555;
.overview-stat :deep(.el-statistic__content) {
color: var(--text-primary);
font-size: var(--text-2xl);
font-weight: 700;
}
.overview-notes {
display: flex;
flex-wrap: wrap;
gap: var(--space-2);
}
/* ---------- 91卡券查询工具栏 ---------- */
.lookup-toolbar {
display: grid;
grid-template-columns: minmax(180px, 1fr) 110px 130px 180px;
gap: 12px;
gap: var(--space-3);
align-items: end;
}
/* ---------- 字段块 ---------- */
.field-block span {
color: #1d3555;
.rule-action-row {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: var(--space-3);
align-items: center;
}
.field-help,
.toolbar-hint,
.cell-subtle {
color: #64748b;
.rule-enabled-control {
display: flex;
justify-content: flex-end;
}
.field-wide {
grid-column: span 2;
}
.field-block :deep(.el-input-number),
.field-block :deep(.el-select) {
width: 100%;
}
/* ---------- 文本输入 (蓝色聚焦环) ---------- */
.text-input {
min-height: 36px;
color: #1d3555;
}
.textarea-input {
min-height: 72px;
resize: vertical;
}
/* ---------- 校验/错误覆盖 ---------- */
.validation-banner,
.binding-inline-error {
background: #fff5f4;
border: 1px solid rgba(180, 35, 24, 0.14);
color: #b42318;
font-size: var(--text-sm);
}
.error-copy {
color: #b42318;
}
/* ---------- 表格覆盖 ---------- */
.data-table th,
.data-table td {
padding: 11px 10px;
}
.data-table th {
color: #64748b;
font-size: var(--text-xs);
}
.cell-stack strong {
color: #1d3555;
}
/* ---------- 绑定卡片覆盖 ---------- */
.binding-card {
border-radius: 16px;
box-shadow: none;
margin-top: var(--space-3);
background: var(--bg-surface);
}
.binding-card :deep(.el-card__header) {
padding: var(--space-3) var(--space-4);
background: var(--bg-subtle);
}
.binding-card :deep(.el-card__body) {
display: grid;
gap: var(--space-3);
padding: var(--space-4);
}
.binding-card--invalid {
border-color: var(--color-danger);
box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.08);
}
.binding-header {
display: flex;
justify-content: space-between;
gap: var(--space-3);
align-items: flex-start;
}
.binding-summary {
display: grid;
gap: 4px;
}
.binding-title-row {
color: #1d3555;
display: flex;
gap: var(--space-2);
align-items: center;
flex-wrap: wrap;
color: var(--text-primary);
}
.binding-subtle {
color: #64748b;
.binding-subtle,
.cell-subtle {
color: var(--text-secondary);
}
.binding-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
}
.collapse-button {
color: #1d3555;
cursor: pointer;
.cell-subtle {
display: block;
font-size: var(--text-xs);
line-height: 1.5;
}
.binding-collapsed-preview {
padding: 10px 12px;
border-radius: 14px;
background: rgba(255, 255, 255, 0.78);
color: #64748b;
font-size: var(--text-xs);
}
.checkbox-line {
color: #1d3555;
color: var(--text-secondary);
font-size: var(--text-sm);
}
.binding-actions {
margin-top: 12px;
}
.toolbar-hint {
margin: 10px 0 0;
}
.empty-inline {
padding: 14px;
color: #64748b;
text-align: center;
}
/* ---------- 双栏映射编辑器 (快手 Cloud 独有) ---------- */
.mapping-editor {
display: grid;
grid-template-columns: minmax(0, 1fr) 74px minmax(0, 1fr);
gap: 12px;
gap: var(--space-3);
align-items: stretch;
}
.mapping-panel {
display: grid;
align-content: start;
gap: 12px;
padding: 14px;
border-radius: 16px;
border: 1px solid rgba(148, 163, 184, 0.18);
min-width: 0;
background: var(--bg-surface);
}
.mapping-panel--external {
background: rgba(239, 246, 255, 0.72);
background: #f8fbff;
}
.mapping-panel--internal {
background: rgba(255, 255, 255, 0.86);
border-color: rgba(15, 93, 216, 0.16);
background: #fbfcff;
}
.mapping-panel :deep(.el-card__header) {
padding: var(--space-3) var(--space-4);
background: var(--bg-subtle);
}
.mapping-panel :deep(.el-card__body) {
padding: var(--space-4);
}
.mapping-panel-head {
display: flex;
justify-content: space-between;
gap: 10px;
gap: var(--space-3);
align-items: baseline;
}
.mapping-panel-head strong {
color: #1d3555;
font-size: 15px;
color: var(--text-primary);
}
.mapping-panel-head span {
color: #64748b;
color: var(--text-secondary);
font-size: var(--text-xs);
}
@@ -231,13 +201,18 @@
.mapping-extra {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px 12px;
gap: var(--space-4);
}
.mapping-extra {
padding-top: var(--space-3);
border-top: 1px dashed var(--border-default);
}
.mapping-arrow {
display: grid;
place-items: center;
color: #175cd3;
color: var(--color-primary);
font-size: var(--text-xs);
font-weight: 700;
}
@@ -250,126 +225,72 @@
min-width: 54px;
padding: 0 10px;
border-radius: var(--radius-full);
background: rgba(15, 93, 216, 0.08);
border: 1px solid rgba(15, 93, 216, 0.14);
background: var(--color-primary-light);
border: 1px solid var(--border-default);
}
.mapping-extra {
margin-top: 12px;
padding-top: 12px;
border-top: 1px dashed rgba(86, 108, 138, 0.18);
.field-wide {
grid-column: 1 / -1;
}
.overview-label {
color: #64748b;
.field-help {
display: block;
margin-top: 6px;
color: var(--text-secondary);
font-size: var(--text-xs);
line-height: 1.5;
}
/* ---------- Element Plus 蓝色主题覆盖 ---------- */
.admin-panel :deep(.el-button) {
min-height: 34px;
border-radius: var(--radius-full);
.text-input {
width: 100%;
}
.admin-panel :deep(.el-button--primary) {
--el-button-bg-color: #3a91f7;
--el-button-border-color: #3a91f7;
--el-button-hover-bg-color: #0f5dd8;
--el-button-hover-border-color: #0f5dd8;
.textarea-input {
min-height: 72px;
}
.admin-panel :deep(.el-input__wrapper),
.admin-panel :deep(.el-textarea__inner),
.admin-panel :deep(.el-select__wrapper) {
min-height: 36px;
border: 0;
border-radius: var(--radius-sm);
background: #fff;
box-shadow: 0 0 0 1px var(--panel-line) inset;
transition:
border-color 0.16s ease,
box-shadow 0.16s ease;
.admin-panel :deep(.el-form-item) {
margin-bottom: 0;
}
.admin-panel :deep(.el-input__wrapper:hover),
.admin-panel :deep(.el-textarea__inner:hover),
.admin-panel :deep(.el-select__wrapper:hover) {
box-shadow: 0 0 0 1px rgba(15, 107, 95, 0.38) inset;
.admin-panel :deep(.el-form-item__label) {
color: var(--text-secondary);
font-size: var(--text-xs);
font-weight: 700;
line-height: 1.4;
margin-bottom: 6px;
}
.admin-panel :deep(.el-input__wrapper.is-focus),
.admin-panel :deep(.el-textarea__inner:focus),
.admin-panel :deep(.el-select__wrapper.is-focused) {
box-shadow:
0 0 0 1px var(--accent) inset,
0 0 0 3px rgba(15, 107, 95, 0.12);
.binding-actions {
display: flex;
flex-wrap: wrap;
gap: var(--space-3);
align-items: center;
}
.admin-panel :deep(.is-disabled .el-input__wrapper),
.admin-panel :deep(.el-input__wrapper.is-disabled),
.admin-panel :deep(.el-select__wrapper.is-disabled) {
background: #f8fafc;
color: #64748b;
.data-table {
margin-top: var(--space-4);
}
.admin-panel :deep(.el-textarea__inner[readonly]),
.admin-panel :deep(.el-input__wrapper.is-readonly) {
background: #f8fafc;
color: #64748b;
}
.admin-panel :deep(.el-input-number .el-input__wrapper) {
padding-left: 8px;
}
.admin-panel :deep(.el-input-number__decrease),
.admin-panel :deep(.el-input-number__increase) {
border-color: var(--panel-line);
background: #f8fafc;
}
.admin-panel :deep(.el-switch.is-checked .el-switch__core) {
border-color: var(--accent);
background-color: var(--accent);
}
/* ---------- 响应式 ---------- */
@media (max-width: 900px) {
.overview-stats,
.lookup-toolbar,
.binding-grid,
.mapping-editor {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.mapping-arrow {
display: none;
}
.field-wide {
grid-column: span 2;
}
}
@media (max-width: 640px) {
.section-title-row,
.section-header-tools,
.binding-actions,
.binding-header {
flex-direction: column;
.card-header--split,
.rule-action-row,
.binding-header,
.mapping-panel-head {
display: grid;
grid-template-columns: 1fr;
align-items: stretch;
}
.overview-stats,
.lookup-toolbar,
.binding-grid,
.mapping-editor,
.mapping-grid,
.mapping-extra {
grid-template-columns: 1fr;
}
.field-wide {
grid-column: auto;
.mapping-arrow {
display: none;
}
}
@@ -19,7 +19,7 @@ onMounted(config.loadConfigs)
<template>
<section class="admin-panel">
<div v-if="!hasAdminRole('admin')" class="empty-block">仅管理员可以维护新履约配置</div>
<el-result v-if="!hasAdminRole('admin')" icon="warning" title="仅管理员可以维护新履约配置" />
<template v-else>
<AdminKuaishouCloudOverviewSection
@@ -27,8 +27,14 @@ onMounted(config.loadConfigs)
:metrics="config.metrics.value"
/>
<p v-if="config.errorMessage.value" class="error-copy">{{ config.errorMessage.value }}</p>
<div v-if="config.loading.value" class="empty-block">新履约配置加载中</div>
<el-alert
v-if="config.errorMessage.value"
:title="config.errorMessage.value"
type="error"
show-icon
:closable="false"
/>
<el-skeleton v-if="config.loading.value" :rows="12" animated />
<template v-else>
<AdminKuaishouCloudNinetyoneSection
@@ -26,39 +26,45 @@ const emit = defineEmits<{
</script>
<template>
<section class="table-card table-card--dense">
<div class="section-title-row section-title-row--tight">
<div>
<h3>91卡券订单取样导入</h3>
<p>查询已接收的 91卡券订单 productNo 导入为规则草稿再补齐内部 SKU cloud 资源</p>
<el-card shadow="never" class="section-card">
<template #header>
<div class="card-header card-header--split">
<div>
<span class="card-title">91卡券订单取样导入</span>
<span class="card-desc"
>查询已接收的 91卡券订单 productNo 导入为规则草稿再补齐内部 SKU cloud
资源</span
>
</div>
<el-space wrap>
<el-tag effect="plain">结果 {{ ninetyoneLookupMetrics.total }}</el-tag>
<el-tag type="warning" effect="plain"
>待补 {{ ninetyoneLookupMetrics.pendingCount }}</el-tag
>
<el-tag type="success" effect="plain"
>待导入 {{ ninetyoneLookupMetrics.availableCount }}</el-tag
>
</el-space>
</div>
<div class="section-mini-stats">
<span class="mini-stat-chip">结果 {{ ninetyoneLookupMetrics.total }}</span>
<span class="mini-stat-chip">待补 {{ ninetyoneLookupMetrics.pendingCount }}</span>
<span class="mini-stat-chip">待导入 {{ ninetyoneLookupMetrics.availableCount }}</span>
</div>
</div>
</template>
<div class="lookup-toolbar">
<label class="field-block">
<span>订单状态</span>
<el-form label-position="top" class="lookup-toolbar">
<el-form-item label="订单状态">
<el-select v-model="ninetyoneLookupForm.status" class="text-input">
<el-option label="待补全" value="pending_config" />
<el-option label="已失败" value="manual_failed" />
<el-option label="全部" value="all" />
</el-select>
</label>
<label class="field-block">
<span>页码</span>
</el-form-item>
<el-form-item label="页码">
<el-input-number
v-model="ninetyoneLookupForm.page"
class="text-input"
:min="1"
controls-position="right"
/>
</label>
<label class="field-block">
<span>每页条数</span>
</el-form-item>
<el-form-item label="每页条数">
<el-input-number
v-model="ninetyoneLookupForm.pageSize"
class="text-input"
@@ -66,24 +72,36 @@ const emit = defineEmits<{
:min="1"
controls-position="right"
/>
</label>
<div class="field-block field-block--action">
<span>操作</span>
<el-button :loading="ninetyoneLookupLoading" round type="primary" @click="emit('lookup')"
>查询 91卡券订单</el-button
</el-form-item>
<el-form-item label="操作">
<el-button
:loading="ninetyoneLookupLoading"
type="primary"
class="w-full"
@click="emit('lookup')"
>
</div>
</div>
查询 91卡券订单
</el-button>
</el-form-item>
</el-form>
<p class="toolbar-hint">
导入后会创建 provider=91kaquanplatform=kuaishou外部 SKU=productNo
的规则草稿保存规则后回到"平台配置 -> 91卡券接入"重试订单
</p>
<el-alert
title="导入后会创建 provider=91kaquan、platform=kuaishou、外部 SKU=productNo 的规则草稿;保存规则后,回到“平台配置 -> 91卡券接入”重试订单。"
type="info"
show-icon
:closable="false"
class="mt-4"
/>
<p v-if="ninetyoneLookupErrorMessage" class="error-copy lookup-error">
{{ ninetyoneLookupErrorMessage }}
</p>
<div v-else-if="ninetyoneLookupLoading" class="empty-inline">正在查询 91卡券订单</div>
<el-alert
v-if="ninetyoneLookupErrorMessage"
:title="ninetyoneLookupErrorMessage"
type="error"
show-icon
:closable="false"
class="mt-4"
/>
<el-skeleton v-else-if="ninetyoneLookupLoading" :rows="4" animated class="mt-4" />
<el-table
v-else
@@ -139,7 +157,7 @@ const emit = defineEmits<{
</template>
</el-table-column>
</el-table>
</section>
</el-card>
</template>
<style scoped src="../AdminKuaishouCloudFulfillment.css"></style>
@@ -12,41 +12,34 @@ defineProps<{
</script>
<template>
<section class="overview-card">
<div class="overview-file">
<span class="overview-label">配置文件</span>
<code>{{ filePath || '-' }}</code>
</div>
<el-card shadow="never" class="section-card overview-card">
<template #header>
<div class="card-header">
<span class="card-title">快手 Cloud 新履约概览</span>
<span class="card-desc">维护 91卡券商品到内部 SKUcloud 资源和核销店铺的映射</span>
</div>
</template>
<el-descriptions :column="1" border class="overview-file">
<el-descriptions-item label="配置文件">
<code>{{ filePath || '-' }}</code>
</el-descriptions-item>
</el-descriptions>
<div class="overview-stats">
<article class="overview-stat">
<span>规则总数</span>
<strong>{{ metrics.total }}</strong>
</article>
<article class="overview-stat">
<span>可投产</span>
<strong>{{ metrics.readyCount }}</strong>
</article>
<article class="overview-stat">
<span>待完善</span>
<strong>{{ metrics.draftCount }}</strong>
</article>
<article class="overview-stat">
<span>已启用</span>
<strong>{{ metrics.enabledCount }}</strong>
</article>
<article class="overview-stat">
<span>已停用</span>
<strong>{{ metrics.disabledCount }}</strong>
</article>
<el-statistic class="overview-stat" title="规则总数" :value="metrics.total" />
<el-statistic class="overview-stat" title="可投产" :value="metrics.readyCount" />
<el-statistic class="overview-stat" title="待完善" :value="metrics.draftCount" />
<el-statistic class="overview-stat" title="已启用" :value="metrics.enabledCount" />
<el-statistic class="overview-stat" title="已停用" :value="metrics.disabledCount" />
</div>
<div class="overview-notes">
<span class="overview-note">91卡券订单来自已接收的待补全队列</span>
<span class="overview-note">内部 SKU 决定最终任务与库存绑定</span>
<span class="overview-note">cloud SKU 决定自动购买发货与退号资源</span>
<el-tag effect="plain">91卡券订单来自已接收的待补全队列</el-tag>
<el-tag effect="plain">内部 SKU 决定最终任务与库存绑定</el-tag>
<el-tag effect="plain">cloud SKU 决定自动购买发货与退号资源</el-tag>
</div>
</section>
</el-card>
</template>
<style scoped src="../AdminKuaishouCloudFulfillment.css"></style>
@@ -34,151 +34,151 @@ const emit = defineEmits<{
</script>
<template>
<div
<el-card
:data-local-id="item.localId"
:class="[
'binding-card',
{ 'binding-card--invalid': validationState?.localId === item.localId },
]"
shadow="never"
>
<div class="binding-header">
<div class="binding-summary">
<div class="binding-title-row">
<strong>{{ getCardTitle(item, index) }}</strong>
<span
:class="['binding-status-chip', { 'binding-status-chip--done': isItemComplete(item) }]"
>
{{ getRuleState(item).label }}
</span>
<template #header>
<div class="binding-header">
<div class="binding-summary">
<div class="binding-title-row">
<strong>{{ getCardTitle(item, index) }}</strong>
<el-tag :type="isItemComplete(item) ? 'success' : 'warning'" effect="plain">
{{ getRuleState(item).label }}
</el-tag>
</div>
<span class="binding-subtle">{{ getCardSummary(item) }}</span>
</div>
<span class="binding-subtle">{{ getCardSummary(item) }}</span>
<el-button link type="primary" @click="emit('toggleCollapsed')">
{{ isCollapsed ? '展开' : '折叠' }}
</el-button>
</div>
<el-button link type="primary" @click="emit('toggleCollapsed')">
{{ isCollapsed ? '展开' : '折叠' }}
</el-button>
</div>
</template>
<div v-if="isCollapsed" class="binding-collapsed-preview">
<span>外部商品{{ getExternalMatchSummary(item) }}</span>
<span>内部 SKU{{ item.internalSkuCode || '-' }}</span>
<span>cloud 资源{{ getCloudSkuSummary(item) }}</span>
<span>核销店铺{{ getConsumeShopSummary(item) }}</span>
</div>
<el-descriptions v-if="isCollapsed" :column="4" border class="binding-collapsed-preview">
<el-descriptions-item label="外部商品">{{
getExternalMatchSummary(item)
}}</el-descriptions-item>
<el-descriptions-item label="内部 SKU">{{
item.internalSkuCode || '-'
}}</el-descriptions-item>
<el-descriptions-item label="cloud 资源">{{ getCloudSkuSummary(item) }}</el-descriptions-item>
<el-descriptions-item label="核销店铺">{{
getConsumeShopSummary(item)
}}</el-descriptions-item>
</el-descriptions>
<div v-else class="mapping-editor">
<section class="mapping-panel mapping-panel--external">
<div class="mapping-panel-head">
<strong>外部商品</strong>
<span>来源平台 / 快手侧命中条件</span>
</div>
<el-card shadow="never" class="mapping-panel mapping-panel--external">
<template #header>
<div class="mapping-panel-head">
<strong>外部商品</strong>
<span>来源平台 / 快手侧命中条件</span>
</div>
</template>
<div class="mapping-grid">
<label class="field-block">
<span>来源 provider</span>
<el-form label-position="top" class="mapping-grid">
<el-form-item label="来源 provider">
<el-input
v-model="item.provider"
class="text-input"
maxlength="32"
placeholder="默认 91kaquan"
/>
</label>
<label class="field-block">
<span>来源 platform</span>
</el-form-item>
<el-form-item label="来源 platform">
<el-input
v-model="item.platform"
class="text-input"
maxlength="32"
placeholder="默认 kuaishou"
/>
</label>
<label class="field-block field-wide">
<span>店铺 ID</span>
</el-form-item>
<el-form-item label="店铺 ID" class="field-wide">
<el-input
v-model="item.shopId"
class="text-input"
maxlength="80"
placeholder="留空表示跨店铺共用"
/>
</label>
<label class="field-block">
<span>外部 SKU</span>
</el-form-item>
<el-form-item label="外部 SKU">
<el-input
v-model="item.externalSkuCode"
class="text-input"
maxlength="120"
placeholder="例如 91卡券 productNo"
/>
</label>
<label class="field-block">
<span>外部商品 ID</span>
</el-form-item>
<el-form-item label="外部商品 ID">
<el-input
v-model="item.externalItemId"
class="text-input"
maxlength="120"
placeholder="平台商品 ID"
/>
</label>
<label class="field-block field-wide">
<span>外部商品名</span>
</el-form-item>
<el-form-item label="外部商品名" class="field-wide">
<el-input
v-model="item.externalSkuName"
class="text-input"
maxlength="200"
placeholder="用于名称匹配"
/>
</label>
<label class="field-block field-wide">
<span>解析商品名</span>
</el-form-item>
<el-form-item label="解析商品名" class="field-wide">
<el-input
v-model="item.resolvedSkuName"
class="text-input"
maxlength="120"
placeholder="可作为商品名兜底"
/>
</label>
</div>
</section>
</el-form-item>
</el-form>
</el-card>
<div class="mapping-arrow" aria-hidden="true">
<span>映射到</span>
</div>
<section class="mapping-panel mapping-panel--internal">
<div class="mapping-panel-head">
<strong>内部履约</strong>
<span>内部 SKU + cloud 资源</span>
</div>
<el-card shadow="never" class="mapping-panel mapping-panel--internal">
<template #header>
<div class="mapping-panel-head">
<strong>内部履约</strong>
<span>内部 SKU + cloud 资源</span>
</div>
</template>
<div class="mapping-grid">
<label class="field-block">
<span>内部 SKU</span>
<el-form label-position="top" class="mapping-grid">
<el-form-item label="内部 SKU">
<el-input
v-model="item.internalSkuCode"
class="text-input"
maxlength="80"
placeholder="必填,用于生成任务"
/>
</label>
<label class="field-block">
<span>优先级</span>
</el-form-item>
<el-form-item label="优先级">
<el-input-number
v-model="item.priority"
class="text-input"
:min="1"
controls-position="right"
/>
</label>
<label class="field-block field-wide">
<span>内部商品名</span>
</el-form-item>
<el-form-item label="内部商品名" class="field-wide">
<el-input
v-model="item.internalSkuName"
class="text-input"
maxlength="120"
placeholder="展示用,可选但建议填"
/>
</label>
<label class="field-block">
<span>cloud 主账号</span>
</el-form-item>
<el-form-item label="cloud 主账号">
<el-select
v-model="item.cloudSourceKey"
class="text-input"
@@ -191,9 +191,8 @@ const emit = defineEmits<{
:value="opt.key"
/>
</el-select>
</label>
<label class="field-block field-wide">
<span>备选 cloud 账号</span>
</el-form-item>
<el-form-item label="备选 cloud 账号" class="field-wide">
<el-select
v-model="item.cloudSourceKeyFallbacks"
class="text-input"
@@ -209,9 +208,8 @@ const emit = defineEmits<{
:value="opt.key"
/>
</el-select>
</label>
<label class="field-block">
<span>cloud SKU ID</span>
</el-form-item>
<el-form-item label="cloud SKU ID">
<el-select
v-model="item.cloudSkuId"
class="text-input"
@@ -227,36 +225,32 @@ const emit = defineEmits<{
/>
</el-select>
<small class="field-help">先填内部 SKU / 商品名,再选择会更容易匹配。</small>
</label>
<label class="field-block field-wide">
<span>cloud SKU 名称</span>
</el-form-item>
<el-form-item label="cloud SKU 名称" class="field-wide">
<el-input
:model-value="item.cloudSkuName"
class="text-input text-input--readonly"
class="text-input"
placeholder="选择 cloud SKU 后自动带出"
readonly
/>
</label>
<label class="field-block">
<span>虚拟号 VN Key</span>
<el-input class="text-input text-input--readonly" model-value="1" readonly />
</label>
<label class="field-block">
<span>最低保留余额</span>
</el-form-item>
<el-form-item label="虚拟号 VN Key">
<el-input class="text-input" model-value="1" readonly />
</el-form-item>
<el-form-item label="最低保留余额">
<el-input-number
v-model="item.minAssetReserve"
class="text-input"
:min="0"
controls-position="right"
/>
</label>
</div>
</section>
</el-form-item>
</el-form>
</el-card>
</div>
<div v-if="!isCollapsed" class="mapping-extra">
<label class="field-block field-wide">
<span>快手核销店铺</span>
<el-form v-if="!isCollapsed" label-position="top" class="mapping-extra">
<el-form-item label="快手核销店铺" class="field-wide">
<el-select
v-model="item.kuaishouConsumeShopId"
class="text-input"
@@ -274,25 +268,14 @@ const emit = defineEmits<{
<small class="field-help"
>来自"平台店铺 -> 快手小店核销"配置,保存时会自动带出店铺名。</small
>
</label>
<label class="field-block">
<span>核销店铺 ID</span>
<el-input
:model-value="item.kuaishouConsumeShopId || '-'"
class="text-input text-input--readonly"
readonly
/>
</label>
<label class="field-block">
<span>核销店铺名</span>
<el-input
:model-value="item.kuaishouConsumeShopName || '-'"
class="text-input text-input--readonly"
readonly
/>
</label>
<label class="field-block field-wide">
<span>备注</span>
</el-form-item>
<el-form-item label="核销店铺 ID">
<el-input :model-value="item.kuaishouConsumeShopId || '-'" class="text-input" readonly />
</el-form-item>
<el-form-item label="核销店铺名">
<el-input :model-value="item.kuaishouConsumeShopName || '-'" class="text-input" readonly />
</el-form-item>
<el-form-item label="备注" class="field-wide">
<el-input
v-model="item.notes"
class="text-input textarea-input"
@@ -300,15 +283,23 @@ const emit = defineEmits<{
placeholder="绑定场景、客服注意事项等"
type="textarea"
/>
</label>
</div>
</el-form-item>
</el-form>
<p v-if="validationState?.localId === item.localId" class="binding-inline-error">
{{ validationState?.message }}
</p>
<p v-if="cloudSkuCatalogErrorMessage" class="binding-inline-error">
{{ cloudSkuCatalogErrorMessage }}
</p>
<el-alert
v-if="validationState?.localId === item.localId"
:title="validationState?.message"
type="error"
show-icon
:closable="false"
/>
<el-alert
v-if="cloudSkuCatalogErrorMessage"
:title="cloudSkuCatalogErrorMessage"
type="error"
show-icon
:closable="false"
/>
<div class="binding-actions">
<el-checkbox v-model="item.enabled">启用规则</el-checkbox>
@@ -317,7 +308,7 @@ const emit = defineEmits<{
<el-checkbox v-model="item.autoConsumeAfterDispatch">发货后核销</el-checkbox>
<el-button link type="danger" @click="emit('remove')">删除</el-button>
</div>
</div>
</el-card>
</template>
<style scoped src="../AdminKuaishouCloudFulfillment.css"></style>
@@ -47,50 +47,68 @@ const emit = defineEmits<{
</script>
<template>
<section class="table-card table-card--dense">
<div class="section-title-row section-title-row--tight">
<div>
<h3>规则编辑</h3>
<p>每一条规则描述"快手外部商品"如何映射到"内部 SKU + cloud 资源"</p>
</div>
<div class="section-header-tools">
<div class="section-mini-stats">
<el-card shadow="never" class="section-card">
<template #header>
<div class="card-header card-header--split">
<div>
<span class="card-title">规则编辑</span>
<span class="card-desc"
>每一条规则描述快手外部商品如何映射到内部 SKU + cloud 资源</span
>
</div>
<el-space wrap>
<el-button
v-for="option in ruleFilterOptions"
:key="option.value"
round
size="small"
:type="ruleFilter === option.value ? 'primary' : 'default'"
@click="emit('update:ruleFilter', option.value)"
>
{{ option.label }} {{ option.count }}
</el-button>
</div>
<div class="section-action-group">
<el-button round @click="emit('expandAll')">全部展开</el-button>
<el-checkbox
:model-value="enabled"
@update:model-value="emit('update:enabled', Boolean($event))"
>
启用整条配置
</el-checkbox>
<el-button round @click="emit('collapseReady')">收起已完成</el-button>
<el-button round :loading="cloudSkuCatalogLoading" @click="emit('refreshCloudSku')"
>刷新 cloud SKU</el-button
>
<el-button round @click="emit('addItem')">新增规则</el-button>
<el-button round type="primary" :loading="saving" @click="emit('saveConfigs')"
>保存全部</el-button
>
</div>
</el-space>
</div>
</template>
<div class="rule-action-row">
<el-space wrap>
<el-button @click="emit('expandAll')">全部展开</el-button>
<el-button @click="emit('collapseReady')">收起已完成</el-button>
<el-button :loading="cloudSkuCatalogLoading" @click="emit('refreshCloudSku')">
刷新 cloud SKU
</el-button>
<el-button @click="emit('addItem')">新增规则</el-button>
<el-button type="primary" :loading="saving" @click="emit('saveConfigs')">
保存全部
</el-button>
</el-space>
<div class="rule-enabled-control">
<el-switch
:model-value="enabled"
active-text="启用整条配置"
@update:model-value="emit('update:enabled', Boolean($event))"
/>
</div>
</div>
<p v-if="validationState?.message" class="validation-banner">
{{ validationState.message }}
</p>
<div v-if="items.length === 0" class="empty-inline">当前还没有新履约规则先新增一条</div>
<div v-else-if="filteredItems.length === 0" class="empty-inline">当前筛选下没有规则</div>
<el-alert
v-if="validationState?.message"
:title="validationState.message"
type="error"
show-icon
:closable="false"
class="mt-4"
/>
<el-empty
v-if="items.length === 0"
description="当前还没有新履约规则,先新增一条。"
:image-size="60"
/>
<el-empty
v-else-if="filteredItems.length === 0"
description="当前筛选下没有规则。"
:image-size="60"
/>
<AdminKuaishouCloudRuleCard
v-for="(item, index) in filteredItems"
@@ -119,7 +137,7 @@ const emit = defineEmits<{
@handle-kuaishou-consume-shop-selected="emit('handleKuaishouConsumeShopSelected', item)"
@remove="emit('removeItem', item.localId)"
/>
</section>
</el-card>
</template>
<style scoped src="../AdminKuaishouCloudFulfillment.css"></style>
@@ -1,6 +1,14 @@
<script setup lang="ts">
import { adminInventoryCredentialTypeOptions } from '@/utils/admin-options'
type SkuSuggestion = {
credentialType: string
inventoryGroupCode: string
skuCode: string
availableCount: number
totalCount: number
}
const entrySkuCode = defineModel<string>('entrySkuCode', { required: true })
const entryCredentialType = defineModel<string>('entryCredentialType', { required: true })
const entryBatchNo = defineModel<string>('entryBatchNo', { required: true })
@@ -8,17 +16,11 @@ const entryInventoryGroupCode = defineModel<string>('entryInventoryGroupCode', {
const singleDisplayValue = defineModel<string>('singleDisplayValue', { required: true })
const bulkText = defineModel<string>('bulkText', { required: true })
defineProps<{
const props = defineProps<{
creating: boolean
importing: boolean
skuSuggestionsLoading: boolean
entrySkuSuggestions: Array<{
credentialType: string
inventoryGroupCode: string
skuCode: string
availableCount: number
totalCount: number
}>
entrySkuSuggestions: SkuSuggestion[]
submitCreate: () => Promise<void>
submitImport: () => Promise<void>
applySkuSuggestion: (skuCode: string) => void
@@ -26,6 +28,15 @@ defineProps<{
getCredentialTypeHint: (value: string) => string
getSkuNamingHint: () => string
}>()
function querySkuSuggestions(query: string, callback: (items: SkuSuggestion[]) => void) {
const keyword = query.trim().toLowerCase()
const items = keyword
? props.entrySkuSuggestions.filter((item) => item.skuCode.toLowerCase().includes(keyword))
: props.entrySkuSuggestions
callback(items)
}
</script>
<template>
@@ -40,22 +51,25 @@ defineProps<{
</template>
<div class="form-grid">
<div class="sku-input-wrap">
<input
v-model="entrySkuCode"
list="inventory-sku-suggestions"
class="native-input"
placeholder="内部履约 SKU,可选已有同类型,也可自定义新的中文名"
/>
<datalist id="inventory-sku-suggestions">
<option
v-for="suggestion in entrySkuSuggestions"
:key="`${suggestion.credentialType}-${suggestion.inventoryGroupCode}-${suggestion.skuCode}`"
:value="suggestion.skuCode"
:label="`${suggestion.skuCode} · ${suggestion.inventoryGroupCode || '未分组'} · 可用 ${suggestion.availableCount} / 共 ${suggestion.totalCount}`"
/>
</datalist>
</div>
<el-autocomplete
v-model="entrySkuCode"
class="sku-input-wrap"
value-key="skuCode"
:fetch-suggestions="querySkuSuggestions"
placeholder="内部履约 SKU,可选已有同类型,也可自定义新的中文名"
clearable
@select="(item: SkuSuggestion) => applySkuSuggestion(item.skuCode)"
>
<template #default="{ item }">
<div class="autocomplete-option">
<strong>{{ item.skuCode }}</strong>
<span>
{{ item.inventoryGroupCode || '未分组' }} · 可用 {{ item.availableCount }} /
{{ item.totalCount }}
</span>
</div>
</template>
</el-autocomplete>
<el-select v-model="entryCredentialType" placeholder="凭据类型">
<el-option
v-for="opt in adminInventoryCredentialTypeOptions.filter((item) => item.value)"
@@ -65,11 +79,7 @@ defineProps<{
/>
</el-select>
<el-input v-model="entryBatchNo" placeholder="批次号,可选" clearable />
<el-input
v-model="entryInventoryGroupCode"
placeholder="库存组,例如 A组 / B组"
clearable
/>
<el-input v-model="entryInventoryGroupCode" placeholder="库存组,例如 A组 / B组" clearable />
</div>
<div class="sku-helper-bar">
@@ -78,19 +88,19 @@ defineProps<{
</div>
<div v-if="entrySkuSuggestions.length > 0" class="sku-chip-row">
<button
<el-button
v-for="suggestion in entrySkuSuggestions"
:key="`${suggestion.credentialType}-${suggestion.inventoryGroupCode}-${suggestion.skuCode}-chip`"
type="button"
class="sku-chip"
plain
@click="applySkuSuggestion(suggestion.skuCode)"
>
<span class="sku-chip-title">{{ suggestion.skuCode }}</span>
<span class="sku-chip-meta"
>{{ suggestion.inventoryGroupCode || '未分组' }} · 可用
{{ suggestion.availableCount }} / {{ suggestion.totalCount }}</span
>{{ suggestion.inventoryGroupCode || '未分组' }} · 可用 {{ suggestion.availableCount }} /
{{ suggestion.totalCount }}</span
>
</button>
</el-button>
</div>
<div class="helper-grid">
@@ -114,9 +124,7 @@ defineProps<{
placeholder="单条新增的凭据内容"
@keyup.enter="submitCreate"
/>
<el-button :loading="creating" round type="success" @click="submitCreate"
>单条新增</el-button
>
<el-button :loading="creating" round type="success" @click="submitCreate">单条新增</el-button>
</div>
<el-input
@@ -148,30 +156,18 @@ defineProps<{
}
.sku-input-wrap {
min-width: 0;
}
.autocomplete-option {
display: grid;
min-width: 0;
gap: 2px;
line-height: 1.4;
}
.native-input {
width: 100%;
min-width: 0;
min-height: 40px;
padding: 0 14px;
border-radius: var(--radius-md);
border: 1px solid var(--border-default);
background: rgba(255, 255, 255, 0.74);
color: var(--text-primary);
font-size: var(--text-sm);
outline: none;
transition:
border-color 0.16s ease,
box-shadow 0.16s ease;
}
.native-input:focus {
border-color: rgba(37, 89, 167, 0.38);
background: rgba(255, 255, 255, 0.9);
box-shadow: 0 0 0 3px rgba(37, 89, 167, 0.08);
.autocomplete-option span {
color: var(--text-secondary);
font-size: var(--text-xs);
}
.single-row,
@@ -234,25 +230,12 @@ defineProps<{
}
.sku-chip {
display: inline-grid;
gap: 4px;
display: inline-flex;
flex-direction: column;
align-items: flex-start;
max-width: 100%;
padding: 10px 14px;
border-radius: var(--radius-md);
border: 1px solid rgba(86, 108, 138, 0.16);
background: rgba(248, 250, 252, 0.78);
color: var(--text-primary);
cursor: pointer;
transition:
transform 0.16s ease,
border-color 0.16s ease,
box-shadow 0.16s ease;
}
.sku-chip:hover {
transform: translateY(-1px);
border-color: rgba(37, 89, 167, 0.26);
box-shadow: 0 12px 24px rgba(37, 89, 167, 0.08);
height: auto;
padding: 8px 12px;
}
.sku-chip-title {
min-width: 0;
@@ -4,7 +4,12 @@ import type { InventoryPoolGroup } from '../composables/types'
defineProps<{
inventoryGroups: InventoryPoolGroup[]
pageOverview: { skuCount: number; groupCount: number }
pageSummary: { availableCount: number; reservedCount: number; deliveredCount: number; invalidCount: number }
pageSummary: {
availableCount: number
reservedCount: number
deliveredCount: number
invalidCount: number
}
useGroupAsFilter: (group: InventoryPoolGroup) => void
useGroupAsEntry: (group: InventoryPoolGroup) => void
getCredentialTypeLabel: (value: string) => string
@@ -45,13 +50,13 @@ defineProps<{
>
</div>
<div class="pool-grid">
<article v-for="group in inventoryGroups" :key="group.key" class="pool-card">
<el-card v-for="group in inventoryGroups" :key="group.key" shadow="never" class="pool-card">
<div class="pool-topline">
<div class="pool-title-block">
<h4 class="pool-title">{{ group.skuCode }}</h4>
<span class="pool-subtitle"
>{{ getCredentialTypeLabel(group.credentialType) }} ·
{{ group.credentialType }} · {{ group.inventoryGroupCode || '未分组' }}</span
>{{ getCredentialTypeLabel(group.credentialType) }} · {{ group.credentialType }} ·
{{ group.inventoryGroupCode || '未分组' }}</span
>
</div>
<span class="pool-total"> {{ group.totalCount }} </span>
@@ -73,11 +78,9 @@ defineProps<{
<div class="pool-actions">
<el-button round @click="useGroupAsFilter(group)">只看这个池</el-button>
<el-button round type="primary" @click="useGroupAsEntry(group)"
>作为入库目标</el-button
>
<el-button round type="primary" @click="useGroupAsEntry(group)">作为入库目标</el-button>
</div>
</article>
</el-card>
</div>
</div>
</template>
@@ -123,7 +123,7 @@ const taskDetailLink = computed(() =>
:status-label="statusLabel"
/>
<div v-if="detailLoading && !detail" class="empty-block">人工兑换详情加载中</div>
<el-skeleton v-if="detailLoading && !detail" :rows="8" animated />
<div v-else-if="detail" class="layout-grid">
<TencentAuthCard
@@ -203,10 +203,14 @@ const taskDetailLink = computed(() =>
@open-screenshot-preview="screenshotPreviewVisible = true"
/>
<section v-else-if="!detail && !detailLoading" class="empty-block">
<strong>还没有进行中的人工兑换任务</strong>
<p>先在上方输入唯一凭据和内部 SKU系统会立即校验唯一性并预占库存。</p>
</section>
<el-empty
v-else-if="!detail && !detailLoading"
description="先在上方输入唯一凭据和内部 SKU系统会立即校验唯一性并预占库存"
>
<template #default>
<strong>还没有进行中的人工兑换任务</strong>
</template>
</el-empty>
<el-dialog
v-model="qrPreviewVisible"
@@ -257,28 +261,6 @@ const taskDetailLink = computed(() =>
display: grid;
}
.empty-block {
min-height: 180px;
padding: var(--space-6);
border-radius: var(--radius-xl);
display: grid;
place-items: center;
text-align: center;
background: var(--bg-surface);
border: 1px dashed var(--border-default);
color: var(--text-secondary);
}
.empty-block strong {
color: var(--text-primary);
font-size: var(--text-lg);
}
.empty-block p {
margin: var(--space-2) 0 0;
line-height: 1.7;
}
.preview-image {
display: block;
width: 100%;
@@ -286,7 +268,6 @@ const taskDetailLink = computed(() =>
}
@media (max-width: 900px) {
.summary-card,
.layout-grid {
grid-template-columns: 1fr;
}
@@ -5,7 +5,13 @@ type Props = {
skuCode: string
remark: string
}
suggestions: { credentialType: string; inventoryGroupCode: string; skuCode: string; availableCount: number; totalCount: number }[]
suggestions: {
credentialType: string
inventoryGroupCode: string
skuCode: string
availableCount: number
totalCount: number
}[]
canCreateTask: boolean
createLoading: boolean
createTaskNow: () => void | Promise<void>
@@ -15,25 +21,24 @@ defineProps<Props>()
</script>
<template>
<section class="create-card">
<div class="card-head">
<div>
<h2>1. 创建人工兑换任务</h2>
<p>先录入唯一凭据并锁定库存后续二维码角色确认和兑换都基于这条任务继续</p>
<el-card shadow="never" class="section-card">
<template #header>
<div class="card-head">
<div>
<span class="card-title">1. 创建人工兑换任务</span>
<span class="card-desc"
>先录入唯一凭据并锁定库存后续二维码角色确认和兑换都基于这条任务继续</span
>
</div>
</div>
</div>
</template>
<div class="form-grid">
<div class="field">
<span>唯一凭据</span>
<el-input
v-model="form.proofValue"
placeholder="输入订单号、客户名或其他唯一可追踪信息"
/>
</div>
<el-form label-position="top" class="form-grid">
<el-form-item label="唯一凭据">
<el-input v-model="form.proofValue" placeholder="输入订单号、客户名或其他唯一可追踪信息" />
</el-form-item>
<div class="field">
<span>内部履约 SKU</span>
<el-form-item label="内部履约 SKU">
<el-input
v-model="form.skuCode"
list="manual-redeem-sku-suggestions"
@@ -47,18 +52,17 @@ defineProps<Props>()
:label="`${item.skuCode} · ${item.inventoryGroupCode || '未分组'} · 可用 ${item.availableCount} / 共 ${item.totalCount}`"
/>
</datalist>
</div>
</el-form-item>
<div class="field field-remark">
<span>备注</span>
<el-form-item label="备注" class="field-remark">
<el-input
v-model="form.remark"
type="textarea"
:rows="3"
placeholder="可选:记录客户补充信息、兑换要求或客服备注"
/>
</div>
</div>
</el-form-item>
</el-form>
<div class="action-row">
<el-button
@@ -71,18 +75,10 @@ defineProps<Props>()
创建任务并预占库存
</el-button>
</div>
</section>
</el-card>
</template>
<style scoped>
.create-card {
padding: 22px var(--space-6);
border-radius: var(--radius-xl);
background: var(--bg-surface);
border: 1px solid var(--border-default);
box-shadow: var(--shadow-lg);
}
.card-head {
display: flex;
align-items: center;
@@ -90,14 +86,16 @@ defineProps<Props>()
gap: var(--space-4);
}
.card-head h2 {
margin: 0;
.card-title {
display: block;
font-size: var(--text-lg);
font-weight: 700;
color: var(--text-primary);
}
.card-head p {
margin: 6px 0 0;
.card-desc {
display: block;
margin-top: 6px;
color: var(--text-secondary);
line-height: 1.7;
}
@@ -109,12 +107,7 @@ defineProps<Props>()
gap: var(--space-4);
}
.field {
display: grid;
gap: var(--space-2);
}
.field span {
.section-card :deep(.el-form-item__label) {
color: var(--text-secondary);
font-size: var(--text-sm);
font-weight: 600;
@@ -16,54 +16,55 @@ defineProps<Props>()
</script>
<template>
<section class="review-card">
<div class="card-head">
<div>
<h2>2. {{ reviewCardTitle }}</h2>
<p v-if="reviewReady">
客户登录后系统会把当前角色截图回传到这里客服复制后可直接发给客户做二次确认
</p>
<p v-else-if="resultReady">最终兑换完成后客服可以把结果截图直接发给客户确认</p>
<p v-else>
{{ screenshotEmptyMessage }}
</p>
<el-card shadow="never" class="section-card">
<template #header>
<div class="card-head">
<div>
<span class="card-title">2. {{ reviewCardTitle }}</span>
<span v-if="reviewReady" class="card-desc">
客户登录后系统会把当前角色截图回传到这里客服复制后可直接发给客户做二次确认
</span>
<span v-else-if="resultReady" class="card-desc">
最终兑换完成后客服可以把结果截图直接发给客户确认
</span>
<span v-else class="card-desc">
{{ screenshotEmptyMessage }}
</span>
</div>
<el-space wrap>
<el-button :disabled="!screenshotUrl || screenshotLoading" plain @click="copyScreenshot">
复制图片
</el-button>
<el-button
:disabled="!screenshotUrl || screenshotLoading"
plain
@click="downloadScreenshot"
>
下载图片
</el-button>
</el-space>
</div>
<div class="header-actions">
<el-button :disabled="!screenshotUrl || screenshotLoading" plain @click="copyScreenshot">
复制图片
</el-button>
<el-button
:disabled="!screenshotUrl || screenshotLoading"
plain
@click="downloadScreenshot"
>
下载图片
</el-button>
</div>
</div>
</template>
<div v-if="screenshotUrl" class="review-stage">
<button class="screenshot-button" type="button" @click="openPreview">
<img :src="screenshotUrl" alt="manual redeem screenshot" />
</button>
<el-image
class="screenshot-preview"
:src="screenshotUrl"
alt="manual redeem screenshot"
fit="contain"
@click="openPreview"
/>
</div>
<div v-else class="empty-block">
<strong>{{ screenshotEmptyTitle || '' }}</strong>
<p>{{ screenshotEmptyMessage }}</p>
</div>
</section>
<el-empty v-else :description="screenshotEmptyMessage">
<template #default>
<strong>{{ screenshotEmptyTitle || '' }}</strong>
</template>
</el-empty>
</el-card>
</template>
<style scoped>
.review-card {
padding: 22px var(--space-6);
border-radius: var(--radius-xl);
background: var(--bg-surface);
border: 1px solid var(--border-default);
box-shadow: var(--shadow-lg);
}
.card-head {
display: flex;
align-items: center;
@@ -71,22 +72,20 @@ defineProps<Props>()
gap: var(--space-4);
}
.card-head h2 {
margin: 0;
.card-title {
display: block;
font-size: var(--text-lg);
font-weight: 700;
color: var(--text-primary);
}
.card-head p {
margin: 6px 0 0;
.card-desc {
display: block;
margin-top: 6px;
color: var(--text-secondary);
line-height: 1.7;
}
.header-actions {
flex-wrap: wrap;
}
.review-stage {
margin-top: 18px;
border-radius: var(--radius-lg);
@@ -95,40 +94,8 @@ defineProps<Props>()
background: var(--bg-page);
}
.screenshot-button {
display: block;
.screenshot-preview {
width: 100%;
padding: 0;
border: 0;
background: transparent;
cursor: zoom-in;
}
.screenshot-button img {
display: block;
width: 100%;
height: auto;
}
.empty-block {
min-height: 180px;
padding: var(--space-6);
border-radius: var(--radius-xl);
display: grid;
place-items: center;
text-align: center;
background: var(--bg-surface);
border: 1px dashed var(--border-default);
color: var(--text-secondary);
}
.empty-block strong {
color: var(--text-primary);
font-size: var(--text-lg);
}
.empty-block p {
margin: var(--space-2) 0 0;
line-height: 1.7;
}
</style>
@@ -29,60 +29,28 @@ defineProps<Props>()
</script>
<template>
<section v-if="task" class="summary-card">
<article>
<span>唯一凭据</span>
<strong>{{ manualRequest?.proofValue || order?.platformOrderId || '-' }}</strong>
</article>
<article>
<span>任务号</span>
<strong>{{ task.taskNo || '-' }}</strong>
</article>
<article>
<span>商品</span>
<strong>{{ orderItem?.skuName || '-' }}</strong>
</article>
<article>
<span>当前状态</span>
<strong>{{ statusLabel }}</strong>
</article>
</section>
<el-card v-if="task" shadow="never" class="section-card">
<el-descriptions :column="4" border>
<el-descriptions-item label="唯一凭据">
{{ manualRequest?.proofValue || order?.platformOrderId || '-' }}
</el-descriptions-item>
<el-descriptions-item label="任务号">
{{ task.taskNo || '-' }}
</el-descriptions-item>
<el-descriptions-item label="商品">
{{ orderItem?.skuName || '-' }}
</el-descriptions-item>
<el-descriptions-item label="当前状态">
{{ statusLabel }}
</el-descriptions-item>
</el-descriptions>
</el-card>
</template>
<style scoped>
.summary-card {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: var(--space-3);
padding: 22px var(--space-6);
border-radius: var(--radius-xl);
background: var(--bg-surface);
border: 1px solid var(--border-default);
box-shadow: var(--shadow-lg);
}
.summary-card article {
padding: 14px;
border-radius: var(--radius-md);
background: var(--bg-page);
}
.summary-card span {
display: block;
font-size: var(--text-xs);
color: var(--text-muted);
}
.summary-card strong {
display: block;
margin-top: var(--space-2);
color: var(--text-primary);
word-break: break-word;
}
@media (max-width: 900px) {
.summary-card {
grid-template-columns: 1fr;
.section-card :deep(.el-descriptions__body .el-descriptions__table) {
display: block;
}
}
</style>
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import { computed, onMounted, ref } from 'vue'
import { useAdminListPage } from '@/composables/useAdminListPage'
import AdminPageHeader from '@/components/admin/AdminPageHeader.vue'
@@ -58,6 +58,10 @@ function resetFilters() {
dateTo.value = ''
void loadMessageDeliveries(1)
}
onMounted(() => {
void loadMessageDeliveries(1)
})
</script>
<template>
@@ -1,120 +1,98 @@
<script setup lang="ts">
import { onMounted, ref } from "vue";
import { useRoute } from "vue-router";
import { onMounted, ref } from 'vue'
import { useRoute } from 'vue-router'
import AdminStatusTag from "@/components/admin/AdminStatusTag.vue";
import { fetchAdminOrderDetail } from "@/services/admin";
import type { AdminOrderDetail } from "@/types/admin";
import AdminStatusTag from '@/components/admin/AdminStatusTag.vue'
import { fetchAdminOrderDetail } from '@/services/admin'
import type { AdminOrderDetail } from '@/types/admin'
import {
formatAutoDeliveryReason,
formatBindingRoleSummary,
formatBindingCountSummary,
resolveTaskAutoDeliveryStatus,
formatTaskAutoDeliveryReason,
} from "@/utils/admin-display";
import { formatAdminDateTime } from "@/utils/admin-time";
import { formatAdminWebhookEventType } from "@/utils/admin-webhook";
} from '@/utils/admin-display'
import { formatAdminDateTime } from '@/utils/admin-time'
import { formatAdminWebhookEventType } from '@/utils/admin-webhook'
const route = useRoute();
const loading = ref(true);
const errorMessage = ref("");
const detail = ref<AdminOrderDetail | null>(null);
const route = useRoute()
const loading = ref(true)
const errorMessage = ref('')
const detail = ref<AdminOrderDetail | null>(null)
onMounted(async () => {
try {
const response = await fetchAdminOrderDetail(
String(route.params.orderId || "")
);
detail.value = response.data;
const response = await fetchAdminOrderDetail(String(route.params.orderId || ''))
detail.value = response.data
} catch (error) {
errorMessage.value =
error instanceof Error ? error.message : "读取订单详情失败";
errorMessage.value = error instanceof Error ? error.message : '读取订单详情失败'
} finally {
loading.value = false;
loading.value = false
}
});
})
function formatJsonBlock(value: Record<string, unknown>) {
return JSON.stringify(value || {}, null, 2);
return JSON.stringify(value || {}, null, 2)
}
function formatSpecSummary(spec: Record<string, unknown>) {
const entries = Object.entries(spec || {})
.filter(
([, value]) =>
value !== null && value !== undefined && String(value).trim() !== ""
)
.filter(([, value]) => value !== null && value !== undefined && String(value).trim() !== '')
.slice(0, 4)
.map(([key, value]) => `${key}: ${String(value)}`);
.map(([key, value]) => `${key}: ${String(value)}`)
return entries.length > 0 ? entries.join(" | ") : "-";
return entries.length > 0 ? entries.join(' | ') : '-'
}
</script>
<template>
<section class="admin-panel">
<AdminPageHeader
title="订单详情"
description="查看订单原始信息、商品子项和关联任务。"
/>
<AdminPageHeader title="订单详情" description="查看订单原始信息、商品子项和关联任务。" />
<p v-if="errorMessage" class="error-copy">{{ errorMessage }}</p>
<el-alert v-if="errorMessage" :title="errorMessage" type="error" show-icon :closable="false" />
<el-skeleton v-if="loading" :rows="8" animated />
<template v-else-if="detail">
<section class="info-card">
<div class="info-card-header">
<el-card shadow="never" class="section-card">
<div class="detail-card-header">
<div>
<h2>{{ detail.order.platformOrderId }}</h2>
<p class="subtitle">
{{ detail.order.platform }} /
{{ detail.order.shopName || detail.order.shopId || "未识别店铺" }}
{{ detail.order.shopName || detail.order.shopId || '未识别店铺' }}
</p>
</div>
<div class="status-stack">
<AdminStatusTag :status="detail.order.payStatus" />
<AdminStatusTag :status="detail.order.orderStatus" />
<AdminStatusTag
:status="detail.order.bindingSummary.systemBindingStatus"
/>
<AdminStatusTag
:status="detail.order.bindingSummary.userBindingStatus"
/>
<AdminStatusTag :status="detail.order.bindingSummary.systemBindingStatus" />
<AdminStatusTag :status="detail.order.bindingSummary.userBindingStatus" />
</div>
</div>
<div class="info-grid">
<article class="info-tile">
<el-card shadow="never" class="info-tile">
<span>商品标题</span>
<strong>{{ detail.order.itemSummary || "-" }}</strong>
<strong>{{ detail.order.itemSummary || '-' }}</strong>
<small>来自订单详情 title / 商品标题字段</small>
</article>
<article class="info-tile">
</el-card>
<el-card shadow="never" class="info-tile">
<span>订单金额</span>
<strong
>{{ detail.order.totalAmount }}
{{ detail.order.currency }}</strong
>
<small
>支付时间{{ formatAdminDateTime(detail.order.paidAt) }}</small
>
</article>
<article class="info-tile">
<strong>{{ detail.order.totalAmount }} {{ detail.order.currency }}</strong>
<small>支付时间{{ formatAdminDateTime(detail.order.paidAt) }}</small>
</el-card>
<el-card shadow="never" class="info-tile">
<span>买家信息</span>
<strong>{{ detail.order.buyerName || "-" }}</strong>
<small>ID{{ detail.order.buyerId || "-" }}</small>
</article>
<article class="info-tile">
<strong>{{ detail.order.buyerName || '-' }}</strong>
<small>ID{{ detail.order.buyerId || '-' }}</small>
</el-card>
<el-card shadow="never" class="info-tile">
<span>联系方式</span>
<strong>{{ detail.order.receiverContact || "-" }}</strong>
<small
>店铺{{
detail.order.shopName || detail.order.shopId || "-"
}}</small
>
</article>
<article class="info-tile">
<strong>{{ detail.order.receiverContact || '-' }}</strong>
<small>店铺{{ detail.order.shopName || detail.order.shopId || '-' }}</small>
</el-card>
<el-card shadow="never" class="info-tile">
<span>任务进度</span>
<strong
>{{ detail.order.bindingSummary.completedBindingTaskCount }}/{{
@@ -128,23 +106,17 @@ function formatSpecSummary(spec: Record<string, unknown>) {
}}
系统绑定</small
>
</article>
<article class="info-tile">
</el-card>
<el-card shadow="never" class="info-tile">
<span>库存绑定</span>
<strong
>{{
detail.order.bindingSummary.totalBindingCount
}}
条绑定记录</strong
>
<strong>{{ detail.order.bindingSummary.totalBindingCount }} 条绑定记录</strong>
<small
>预占 {{ detail.order.bindingSummary.reservedBindingCount }} ·
已消费 {{ detail.order.bindingSummary.consumedBindingCount }} ·
已释放
>预占 {{ detail.order.bindingSummary.reservedBindingCount }} · 已消费
{{ detail.order.bindingSummary.consumedBindingCount }} · 已释放
{{ detail.order.bindingSummary.releasedBindingCount }}</small
>
</article>
<article v-if="detail.order.agisoAutoDelivery" class="info-tile">
</el-card>
<el-card v-if="detail.order.agisoAutoDelivery" shadow="never" class="info-tile">
<span>自动发货</span>
<div class="tile-tag-line">
<AdminStatusTag :status="detail.order.agisoAutoDelivery.status" />
@@ -152,10 +124,8 @@ function formatSpecSummary(spec: Record<string, unknown>) {
<strong>
{{
detail.order.agisoAutoDelivery.errorMessage ||
formatAutoDeliveryReason(
detail.order.agisoAutoDelivery.reason
) ||
"全部任务交付后会自动触发"
formatAutoDeliveryReason(detail.order.agisoAutoDelivery.reason) ||
'全部任务交付后会自动触发'
}}
</strong>
<small>
@@ -163,11 +133,9 @@ function formatSpecSummary(spec: Record<string, unknown>) {
detail.order.agisoAutoDelivery.totalTaskCount
}}
· 最近执行
{{
formatAdminDateTime(detail.order.agisoAutoDelivery.updatedAt)
}}
{{ formatAdminDateTime(detail.order.agisoAutoDelivery.updatedAt) }}
</small>
</article>
</el-card>
</div>
<el-table :data="[detail.order]" size="small" :show-header="false">
@@ -181,47 +149,39 @@ function formatSpecSummary(spec: Record<string, unknown>) {
<template #default="{ row }">{{ row.platform }}</template>
</el-table-column>
<el-table-column label="店铺 ID">
<template #default="{ row }">{{ row.shopId || "-" }}</template>
<template #default="{ row }">{{ row.shopId || '-' }}</template>
</el-table-column>
<el-table-column label="创建时间" width="180">
<template #default="{ row }">{{
formatAdminDateTime(row.createdAt)
}}</template>
<template #default="{ row }">{{ formatAdminDateTime(row.createdAt) }}</template>
</el-table-column>
<el-table-column label="更新时间" width="180">
<template #default="{ row }">{{
formatAdminDateTime(row.updatedAt)
}}</template>
<template #default="{ row }">{{ formatAdminDateTime(row.updatedAt) }}</template>
</el-table-column>
</el-table>
</section>
</el-card>
<section class="info-card">
<el-card shadow="never" class="section-card">
<h3>商品子项</h3>
<el-table :data="detail.items" size="small">
<el-table-column prop="skuCode" label="SKU" />
<el-table-column prop="itemTitle" label="标题">
<template #default="{ row }">{{ row.itemTitle || "-" }}</template>
<template #default="{ row }">{{ row.itemTitle || '-' }}</template>
</el-table-column>
<el-table-column prop="skuName" label="商品" />
<el-table-column prop="deliveryMode" label="发放方式" />
<el-table-column prop="quantity" label="数量" width="80" />
<el-table-column label="规格摘要">
<template #default="{ row }">{{
formatSpecSummary(row.spec)
}}</template>
<template #default="{ row }">{{ formatSpecSummary(row.spec) }}</template>
</el-table-column>
</el-table>
</section>
</el-card>
<section class="info-card">
<el-card shadow="never" class="section-card">
<h3>关联任务</h3>
<el-table :data="detail.tasks" size="small">
<el-table-column label="任务号">
<template #default="{ row }">
<RouterLink :to="`/admin/tasks/${row.taskId}`">{{
row.taskNo
}}</RouterLink>
<RouterLink :to="`/admin/tasks/${row.taskId}`">{{ row.taskNo }}</RouterLink>
</template>
</el-table-column>
<el-table-column label="状态">
@@ -258,15 +218,15 @@ function formatSpecSummary(spec: Record<string, unknown>) {
</template>
</el-table-column>
<el-table-column label="登录方式">
<template #default="{ row }">{{ row.loginType || "-" }}</template>
<template #default="{ row }">{{ row.loginType || '-' }}</template>
</el-table-column>
<el-table-column label="错误">
<template #default="{ row }">{{ row.lastError || "-" }}</template>
<template #default="{ row }">{{ row.lastError || '-' }}</template>
</el-table-column>
</el-table>
</section>
</el-card>
<section class="info-card">
<el-card shadow="never" class="section-card">
<h3>Webhook 摘要</h3>
<el-table :data="detail.webhookEvents" size="small">
<el-table-column label="事件 ID">
@@ -277,21 +237,16 @@ function formatSpecSummary(spec: Record<string, unknown>) {
</template>
</el-table-column>
<el-table-column label="类型">
<template #default="{ row }">{{
formatAdminWebhookEventType(row.eventType)
}}</template>
<template #default="{ row }">{{ formatAdminWebhookEventType(row.eventType) }}</template>
</el-table-column>
<el-table-column label="平台 / 店铺">
<template #default="{ row }"
>{{ row.platform }} /
{{ row.shopName || row.shopId || "-" }}</template
>{{ row.platform }} / {{ row.shopName || row.shopId || '-' }}</template
>
</el-table-column>
<el-table-column label="验签">
<template #default="{ row }">
<AdminStatusTag
:status="row.signatureValid ? 'success' : 'failed'"
/>
<AdminStatusTag :status="row.signatureValid ? 'success' : 'failed'" />
</template>
</el-table-column>
<el-table-column label="处理">
@@ -300,23 +255,21 @@ function formatSpecSummary(spec: Record<string, unknown>) {
</template>
</el-table-column>
<el-table-column label="时间" width="180">
<template #default="{ row }">{{
formatAdminDateTime(row.createdAt)
}}</template>
<template #default="{ row }">{{ formatAdminDateTime(row.createdAt) }}</template>
</el-table-column>
<template #empty>
<el-empty description="当前订单还没有关联的 webhook 记录" />
</template>
</el-table>
</section>
</el-card>
<section class="info-card">
<el-card shadow="never" class="section-card">
<h3>原始载荷</h3>
<details class="payload-box">
<summary>展开查看原始订单 JSON</summary>
<pre>{{ formatJsonBlock(detail.order.rawPayload) }}</pre>
</details>
</section>
</el-card>
</template>
<el-empty v-else description="订单不存在或已被删除" />
@@ -329,27 +282,12 @@ function formatSpecSummary(spec: Record<string, unknown>) {
gap: var(--space-4);
}
.info-card {
padding: 18px;
border-radius: var(--radius-lg);
background: var(--bg-surface);
border: 1px solid var(--border-default);
}
.info-card h3 {
.section-card h3 {
margin: 0 0 var(--space-4);
color: var(--text-primary);
}
.error-copy {
padding: 18px;
border-radius: var(--radius-lg);
background: var(--bg-surface);
border: 1px solid var(--border-default);
color: var(--color-danger);
}
.info-card-header {
.detail-card-header {
display: flex;
justify-content: space-between;
gap: var(--space-4);
@@ -357,7 +295,7 @@ function formatSpecSummary(spec: Record<string, unknown>) {
margin-bottom: var(--space-4);
}
.info-card-header h2 {
.detail-card-header h2 {
margin: 0;
color: var(--text-primary);
}
@@ -384,11 +322,7 @@ function formatSpecSummary(spec: Record<string, unknown>) {
.info-tile {
padding: 14px var(--space-4);
border-radius: var(--radius-md);
background: linear-gradient(
180deg,
rgba(247, 250, 255, 0.98),
rgba(241, 245, 249, 0.92)
);
background: linear-gradient(180deg, rgba(247, 250, 255, 0.98), rgba(241, 245, 249, 0.92));
border: 1px solid var(--border-default);
display: grid;
gap: 6px;
@@ -440,7 +374,7 @@ function formatSpecSummary(spec: Record<string, unknown>) {
}
@media (max-width: 900px) {
.info-card-header {
.detail-card-header {
flex-direction: column;
}
@@ -133,51 +133,49 @@ defineProps<Props>()
<div class="job-params">
<div class="param-row">
<span class="param-label">检查间隔</span>
<input
<el-input-number
v-model.number="bindDuration(job, 'intervalSeconds').value"
type="number"
min="1"
:min="1"
class="mini-input"
controls-position="right"
/>
<select
<el-select
v-model.number="bindDurationUnit(job, 'intervalSeconds').value"
class="mini-select"
>
<option v-for="unit in timeUnitOptions" :key="unit.value" :value="unit.value">
<el-option v-for="unit in timeUnitOptions" :key="unit.value" :value="unit.value">
{{ unit.label }}
</option>
</select>
<span class="param-hint"
> {{ formatDuration(job.intervalSeconds) }} 检查一次</span
>
</el-option>
</el-select>
<span class="param-hint"> {{ formatDuration(job.intervalSeconds) }} 检查一次</span>
</div>
<div class="param-row">
<span class="param-label">通知冷却</span>
<input
<el-input-number
v-model.number="bindDuration(job, 'cooldownSeconds').value"
type="number"
min="1"
:min="1"
class="mini-input"
controls-position="right"
/>
<select
<el-select
v-model.number="bindDurationUnit(job, 'cooldownSeconds').value"
class="mini-select"
>
<option v-for="unit in timeUnitOptions" :key="unit.value" :value="unit.value">
<el-option v-for="unit in timeUnitOptions" :key="unit.value" :value="unit.value">
{{ unit.label }}
</option>
</select>
</el-option>
</el-select>
<span class="param-hint"
>同类异常 {{ formatDuration(job.cooldownSeconds) }} 内只通知一次</span
>
</div>
<div class="param-row">
<span class="param-label">余额阈值</span>
<input
<el-input-number
v-model.number="job.assetThreshold"
type="number"
min="0"
:min="0"
class="mini-input"
controls-position="right"
/>
<span class="param-hint">低于该值时通知</span>
</div>
@@ -188,16 +186,12 @@ defineProps<Props>()
<template #default="{ row: job }">
<div class="runtime-info">
<el-tag
:type="
getScheduledJobRuntime(job.id)?.lastStatus === 'success' ? 'success' : 'info'
"
:type="getScheduledJobRuntime(job.id)?.lastStatus === 'success' ? 'success' : 'info'"
size="small"
>
{{ getScheduledJobRuntime(job.id)?.lastStatus || 'pending' }}
</el-tag>
<span class="param-hint">{{
getScheduledJobRuntime(job.id)?.lastMessage || '-'
}}</span>
<span class="param-hint">{{ getScheduledJobRuntime(job.id)?.lastMessage || '-' }}</span>
<span class="param-hint"
>余额{{ getScheduledJobRuntime(job.id)?.lastAsset ?? '-' }}</span
>
@@ -9,21 +9,12 @@
word-break: break-all;
}
.info-card,
.hero-card,
.error-copy {
padding: 18px;
border-radius: var(--radius-lg);
background: var(--bg-surface);
border: 1px solid var(--border-default);
}
.info-card p {
.section-card p {
margin: 10px 0 0;
color: var(--text-primary);
}
.info-card h3 {
.section-card h3 {
margin: 0 0 var(--space-3);
color: var(--text-primary);
}
@@ -110,10 +101,6 @@
align-items: center;
}
.error-copy {
color: var(--color-danger);
}
.action-row {
display: flex;
flex-wrap: wrap;
@@ -147,23 +134,6 @@
margin-bottom: 0;
}
.data-table {
width: 100%;
border-collapse: collapse;
}
.data-table th,
.data-table td {
padding: var(--space-3) 10px;
text-align: left;
border-bottom: 1px solid var(--border-default);
}
.data-table th {
width: 160px;
color: var(--text-secondary);
}
.status-stack {
display: flex;
flex-wrap: wrap;
@@ -252,11 +222,16 @@
gap: 14px;
}
.field-block {
display: grid;
gap: var(--space-2);
color: var(--text-primary);
font-weight: 600;
.admin-panel :deep(.el-form-item) {
margin-bottom: 0;
}
.admin-panel :deep(.el-form-item__label) {
color: var(--text-secondary);
font-size: var(--text-xs);
font-weight: 700;
line-height: 1.4;
margin-bottom: 6px;
}
.manual-hint {
@@ -65,7 +65,7 @@ onMounted(loadDetail)
<section class="admin-panel">
<AdminPageHeader title="任务详情" description="查看任务全量信息并执行人工操作。" />
<p v-if="errorMessage" class="error-copy">{{ errorMessage }}</p>
<el-alert v-if="errorMessage" :title="errorMessage" type="error" show-icon :closable="false" />
<el-skeleton v-if="loading" :rows="6" animated />
@@ -83,16 +83,52 @@ onMounted(loadDetail)
:action-loading="actionLoading"
:can-manage-task-lifecycle="canManageTaskLifecycle"
:can-close-tasks="canCloseTasks"
@retry="runAction(() => retryAdminTask(detail!.task.taskId), '任务已重试', `确认重试任务 ${detail!.task.taskNo} 吗?`)"
@regenerate-claim-link="runAction(() => regenerateAdminTaskClaimLink(detail!.task.taskId), '领取链接已重新生成', `确认重新生成任务 ${detail!.task.taskNo} 的领取链接吗?旧链接会失效。`)"
@confirm-role="runAction(() => confirmAdminTaskAssistedRole(detail!.task.taskId), '角色已确认', `确认以客服身份锁定任务 ${detail!.task.taskNo} 当前识别到的角色信息吗?`)"
@support-redeem="runAction(() => redeemAdminTaskAssisted(detail!.task.taskId), '兑换任务已启动', `确认开始执行任务 ${detail!.task.taskNo} 的自动兑换吗?`)"
@retry="
runAction(
() => retryAdminTask(detail!.task.taskId),
'任务已重试',
`确认重试任务 ${detail!.task.taskNo} 吗?`,
)
"
@regenerate-claim-link="
runAction(
() => regenerateAdminTaskClaimLink(detail!.task.taskId),
'领取链接已重新生成',
`确认重新生成任务 ${detail!.task.taskNo} 的领取链接吗?旧链接会失效。`,
)
"
@confirm-role="
runAction(
() => confirmAdminTaskAssistedRole(detail!.task.taskId),
'角色已确认',
`确认以客服身份锁定任务 ${detail!.task.taskNo} 当前识别到的角色信息吗?`,
)
"
@support-redeem="
runAction(
() => redeemAdminTaskAssisted(detail!.task.taskId),
'兑换任务已启动',
`确认开始执行任务 ${detail!.task.taskNo} 的自动兑换吗?`,
)
"
@prepare-kuaishou-cloud="submitKuaishouCloudPrepare"
@dispatch-kuaishou-cloud="submitKuaishouCloudDispatch"
@return-kuaishou-cloud="submitKuaishouCloudReturnNumber"
@mark-manual-review="runAction(() => markAdminTaskManualReview(detail!.task.taskId), '任务已转人工处理', `确认将任务 ${detail!.task.taskNo} 转为人工处理吗?`)"
@mark-manual-review="
runAction(
() => markAdminTaskManualReview(detail!.task.taskId),
'任务已转人工处理',
`确认将任务 ${detail!.task.taskNo} 转为人工处理吗?`,
)
"
@submit-manual-dispatch="submitManualDispatch"
@close-task="runAction(() => closeAdminTask(detail!.task.taskId), '任务已关闭', `确认关闭任务 ${detail!.task.taskNo} 吗?关闭后不会自动继续推进。`)"
@close-task="
runAction(
() => closeAdminTask(detail!.task.taskId),
'任务已关闭',
`确认关闭任务 ${detail!.task.taskNo} 吗?关闭后不会自动继续推进。`,
)
"
/>
<AdminTaskInfoSection
@@ -123,17 +159,20 @@ onMounted(loadDetail)
:auto-delivery-summary="resolveTaskAutoDeliverySummary(detail)"
/>
<AdminTaskRedeemResolutionSection
v-if="detail.redeemResolution"
:detail="detail"
/>
<AdminTaskRedeemResolutionSection v-if="detail.redeemResolution" :detail="detail" />
<AdminTaskInventoryBindingsSection
v-if="!isKuaishouCloudTask"
:detail="detail"
:can-view-sensitive-task-data="canViewSensitiveTaskData"
:action-loading="actionLoading"
@release-binding="runAction(() => releaseAdminTaskInventoryBinding(detail!.task.taskId, $event), '库存绑定已释放', `确认释放该库存绑定吗?`)"
@release-binding="
runAction(
() => releaseAdminTaskInventoryBinding(detail!.task.taskId, $event),
'库存绑定已释放',
`确认释放该库存绑定吗?`,
)
"
/>
<AdminTaskManualDispatchSection
@@ -146,10 +185,7 @@ onMounted(loadDetail)
@submit="submitManualDispatch"
/>
<AdminTaskEventsSection
v-if="detail.events.length > 0"
:detail="detail"
/>
<AdminTaskEventsSection v-if="detail.events.length > 0" :detail="detail" />
<AdminTaskScreenshotSection
v-if="detail.screenshotUrl"
@@ -159,7 +195,6 @@ onMounted(loadDetail)
</template>
<el-empty v-else description="任务不存在或已被删除" />
</section>
</template>
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref } from "vue";
import { onMounted, ref } from "vue";
import { useAdminAction } from "@/composables/useAdminAction";
import { useAdminListPage } from "@/composables/useAdminListPage";
@@ -130,6 +130,10 @@ function resetFilters() {
dateTo.value = "";
void loadTasks(1);
}
onMounted(() => {
void loadTasks(1);
});
</script>
<template>
@@ -14,30 +14,28 @@ defineProps<Props>()
</script>
<template>
<section class="info-card">
<el-card shadow="never" class="section-card">
<h3>咸鱼自动发货</h3>
<p class="section-copy">
这里展示这条任务最近一次触发咸鱼自动发货的状态原因和接口回执摘要
</p>
<p class="section-copy">这里展示这条任务最近一次触发咸鱼自动发货的状态原因和接口回执摘要</p>
<div class="redeem-summary-grid">
<article class="redeem-summary-card">
<el-card shadow="never" class="redeem-summary-card">
<span>当前状态</span>
<div class="auto-delivery-tag-line">
<AdminStatusTag :status="autoDeliveryStatus" />
</div>
</article>
<article class="redeem-summary-card">
</el-card>
<el-card shadow="never" class="redeem-summary-card">
<span>触发来源</span>
<strong>{{
formatAutoDeliveryTrigger(detail.task.agisoAutoDelivery?.trigger || '')
}}</strong>
</article>
<article class="redeem-summary-card">
</el-card>
<el-card shadow="never" class="redeem-summary-card">
<span>最近执行</span>
<strong>{{ formatAdminDateTime(detail.task.agisoAutoDelivery?.updatedAt) }}</strong>
</article>
<article class="redeem-summary-card">
</el-card>
<el-card shadow="never" class="redeem-summary-card">
<span>接口回执</span>
<strong>
{{
@@ -47,7 +45,7 @@ defineProps<Props>()
: '-')
}}
</strong>
</article>
</el-card>
</div>
<div class="auto-delivery-note">
@@ -62,7 +60,7 @@ defineProps<Props>()
请求 ID{{ detail.task.agisoAutoDelivery.requestId }}
</p>
</div>
</section>
</el-card>
</template>
<style scoped src="../AdminTaskDetail.css"></style>
@@ -11,24 +11,26 @@ defineProps<Props>()
</script>
<template>
<section class="info-card">
<el-card shadow="never" class="section-card">
<h3>任务事件</h3>
<table class="data-table">
<tbody>
<tr v-for="event in detail.events" :key="event.eventId">
<th>{{ formatTaskEventType(event.eventType) }}</th>
<td>
<div class="event-copy">
{{ formatTaskEventPayload(event.payload) }}
</div>
<div class="event-time">
{{ formatAdminDateTime(event.createdAt) }}
</div>
</td>
</tr>
</tbody>
</table>
</section>
<el-table :data="detail.events" size="small">
<el-table-column label="事件" width="180">
<template #default="{ row: event }">
{{ formatTaskEventType(event.eventType) }}
</template>
</el-table-column>
<el-table-column label="内容">
<template #default="{ row: event }">
<div class="event-copy">
{{ formatTaskEventPayload(event.payload) }}
</div>
<div class="event-time">
{{ formatAdminDateTime(event.createdAt) }}
</div>
</template>
</el-table-column>
</el-table>
</el-card>
</template>
<style scoped src="../AdminTaskDetail.css"></style>
@@ -17,7 +17,11 @@ defineProps<Props>()
<template>
<!-- Kuaishou Cloud hero card -->
<section v-if="isKuaishouCloudTask && detail.kuaishouCloudFulfillment" class="hero-card">
<el-card
v-if="isKuaishouCloudTask && detail.kuaishouCloudFulfillment"
shadow="never"
class="section-card hero-card"
>
<div class="hero-copy">
<span class="hero-eyebrow">{{ detail.task.executorKey || 'task' }}</span>
<h2>{{ detail.task.taskNo }}</h2>
@@ -29,14 +33,15 @@ defineProps<Props>()
</p>
</div>
<div class="hero-meta-grid">
<article
<el-card
v-for="card in summaryCards"
:key="card.label"
shadow="never"
:class="['hero-metric', `hero-metric--${card.tone}`]"
>
<span>{{ card.label }}</span>
<strong>{{ card.value }}</strong>
</article>
</el-card>
</div>
<div class="hero-inline">
<span>系统绑定 <AdminStatusTag :status="detail.task.systemBindingStatus" /></span>
@@ -51,10 +56,10 @@ defineProps<Props>()
<span>客户领取页</span>
<strong>已失效</strong>
</div>
</section>
</el-card>
<!-- Simple info card for other task types -->
<section v-else class="info-card">
<el-card v-else shadow="never" class="section-card">
<h2>{{ detail.task.taskNo }}</h2>
<p>
状态<AdminStatusTag :status="detail.task.status" /> · 订单{{
@@ -80,7 +85,7 @@ defineProps<Props>()
<a class="claim-link" :href="claimUrl" target="_blank" rel="noreferrer">{{ claimUrl }}</a>
</p>
<p v-else-if="claimLinkInvalid">领取链接已失效</p>
</section>
</el-card>
</template>
<style scoped src="../AdminTaskDetail.css"></style>
@@ -14,106 +14,74 @@ defineProps<Props>()
</script>
<template>
<section class="info-card">
<el-card shadow="never" class="section-card">
<h3>任务信息</h3>
<table class="data-table">
<tbody>
<tr>
<th>商品</th>
<td>{{ detail.orderItem?.skuName || '-' }}</td>
</tr>
<tr>
<th>SKU</th>
<td>{{ detail.orderItem?.skuCode || '-' }}</td>
</tr>
<tr>
<th>任务执行器</th>
<td>{{ detail.task.executorKey || '-' }}</td>
</tr>
<tr>
<th>履约状态</th>
<td>
<AdminStatusTag :status="detail.task.deliveryStatus || ''" />
</td>
</tr>
<tr>
<th>结果代码</th>
<td>{{ detail.task.resultCode || '-' }}</td>
</tr>
<tr>
<th>结果说明</th>
<td>{{ detail.task.resultMessage || '-' }}</td>
</tr>
<tr v-if="canViewSensitiveTaskData">
<th>库存凭据</th>
<td>{{ detail.inventory?.displayValue || '-' }}</td>
</tr>
<tr v-if="canViewSensitiveTaskData">
<th>凭据类型</th>
<td>{{ detail.inventory?.credentialType || '-' }}</td>
</tr>
<tr v-if="canViewSensitiveTaskData">
<th>库存状态</th>
<td>
<AdminStatusTag :status="detail.inventory?.status || ''" />
</td>
</tr>
<tr v-if="canViewSensitiveTaskData">
<th>Claim Token</th>
<td>{{ detail.claimToken?.token || '-' }}</td>
</tr>
<tr v-else>
<th>库存 / Token</th>
<td>客服账号不可查看库存凭据与原始领取 Token</td>
</tr>
<tr>
<th>Token 状态</th>
<td>
<AdminStatusTag :status="detail.claimToken?.status || ''" />
</td>
</tr>
<tr>
<th>Token 过期</th>
<td>{{ formatAdminDateTime(detail.claimToken?.expiredAt) }}</td>
</tr>
<tr>
<th>领取链接</th>
<td>
<a
v-if="claimUrl"
class="claim-link"
:href="claimUrl"
target="_blank"
rel="noreferrer"
>{{ claimUrl }}</a
>
<span v-else-if="claimLinkInvalid">已失效</span>
<span v-else>-</span>
</td>
</tr>
<tr>
<th>浏览器会话</th>
<td>{{ detail.task.browserSessionId || '-' }}</td>
</tr>
<tr>
<th>用户打开链接</th>
<td>{{ formatAdminDateTime(detail.task.claimedAt) }}</td>
</tr>
<tr>
<th>用户提交绑定</th>
<td>{{ formatAdminDateTime(detail.task.roleConfirmedAt) }}</td>
</tr>
<tr>
<th>完整绑定成功</th>
<td>{{ formatAdminDateTime(detail.task.redeemedAt) }}</td>
</tr>
<tr>
<th>截图路径</th>
<td>{{ detail.task.screenshotPath || '-' }}</td>
</tr>
</tbody>
</table>
</section>
<el-descriptions :column="2" border>
<el-descriptions-item label="商品">{{
detail.orderItem?.skuName || '-'
}}</el-descriptions-item>
<el-descriptions-item label="SKU">{{
detail.orderItem?.skuCode || '-'
}}</el-descriptions-item>
<el-descriptions-item label="任务执行器">
{{ detail.task.executorKey || '-' }}
</el-descriptions-item>
<el-descriptions-item label="履约状态">
<AdminStatusTag :status="detail.task.deliveryStatus || ''" />
</el-descriptions-item>
<el-descriptions-item label="结果代码">{{
detail.task.resultCode || '-'
}}</el-descriptions-item>
<el-descriptions-item label="结果说明">
{{ detail.task.resultMessage || '-' }}
</el-descriptions-item>
<template v-if="canViewSensitiveTaskData">
<el-descriptions-item label="库存凭据">
{{ detail.inventory?.displayValue || '-' }}
</el-descriptions-item>
<el-descriptions-item label="凭据类型">
{{ detail.inventory?.credentialType || '-' }}
</el-descriptions-item>
<el-descriptions-item label="库存状态">
<AdminStatusTag :status="detail.inventory?.status || ''" />
</el-descriptions-item>
<el-descriptions-item label="Claim Token">
{{ detail.claimToken?.token || '-' }}
</el-descriptions-item>
</template>
<el-descriptions-item v-else label="库存 / Token" :span="2">
客服账号不可查看库存凭据与原始领取 Token
</el-descriptions-item>
<el-descriptions-item label="Token 状态">
<AdminStatusTag :status="detail.claimToken?.status || ''" />
</el-descriptions-item>
<el-descriptions-item label="Token 过期">
{{ formatAdminDateTime(detail.claimToken?.expiredAt) }}
</el-descriptions-item>
<el-descriptions-item label="领取链接" :span="2">
<a v-if="claimUrl" class="claim-link" :href="claimUrl" target="_blank" rel="noreferrer">{{
claimUrl
}}</a>
<span v-else-if="claimLinkInvalid">已失效</span>
<span v-else>-</span>
</el-descriptions-item>
<el-descriptions-item label="浏览器会话">
{{ detail.task.browserSessionId || '-' }}
</el-descriptions-item>
<el-descriptions-item label="用户打开链接">
{{ formatAdminDateTime(detail.task.claimedAt) }}
</el-descriptions-item>
<el-descriptions-item label="用户提交绑定">
{{ formatAdminDateTime(detail.task.roleConfirmedAt) }}
</el-descriptions-item>
<el-descriptions-item label="完整绑定成功">
{{ formatAdminDateTime(detail.task.redeemedAt) }}
</el-descriptions-item>
<el-descriptions-item label="截图路径" :span="2">
{{ detail.task.screenshotPath || '-' }}
</el-descriptions-item>
</el-descriptions>
</el-card>
</template>
<style scoped src="../AdminTaskDetail.css"></style>
@@ -18,82 +18,81 @@ const emit = defineEmits<{
</script>
<template>
<section class="info-card">
<el-card shadow="never" class="section-card">
<h3>库存绑定</h3>
<p class="section-copy">
这里展示任务当前和历史绑定过的库存项便于核对多库存任务的真实履约轨迹
</p>
<table class="data-table">
<thead>
<tr>
<th>绑定</th>
<th>角色</th>
<th>库存项</th>
<th>状态</th>
<th>凭据内容</th>
<th>时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-if="detail.inventoryBindings.length === 0">
<td colspan="7" class="table-empty">当前任务还没有库存绑定记录</td>
</tr>
<tr v-for="binding in detail.inventoryBindings" :key="binding.bindingId">
<td>
<div>#{{ binding.bindingId }}</div>
<div v-if="binding.isPrimary" class="binding-primary">主库存项</div>
</td>
<td>
<div>{{ binding.roleKey || '-' }}</div>
<div class="binding-meta">数量 {{ binding.quantity }}</div>
</td>
<td>
<div>ID {{ binding.inventoryItemId }}</div>
<div>{{ binding.skuCode || '-' }}</div>
<div class="binding-meta">
{{ binding.credentialType || '-' }} ·
{{ binding.batchNo || '无批次' }}
</div>
</td>
<td>
<div class="status-stack">
<AdminStatusTag :status="binding.bindingStatus" />
<AdminStatusTag :status="binding.inventoryStatus" />
</div>
<div v-if="binding.invalidReason" class="binding-meta">
{{
binding.inventoryStatus === 'invalid'
? '作废原因'
: binding.inventoryStatus === 'consumed'
? '消耗原因'
: '处理原因'
}}{{ binding.invalidReason }}
</div>
</td>
<td>
{{ canViewSensitiveTaskData ? binding.displayValue || '-' : '客服不可见' }}
</td>
<td>
<div>绑定 {{ formatAdminDateTime(binding.createdAt) }}</div>
<div class="binding-meta">消费 {{ formatAdminDateTime(binding.consumedAt) }}</div>
<div class="binding-meta">释放 {{ formatAdminDateTime(binding.releasedAt) }}</div>
</td>
<td>
<el-button
v-if="hasAdminRole('admin') && binding.canRelease"
:loading="actionLoading"
link
@click="emit('releaseBinding', binding.bindingId)"
>
释放绑定
</el-button>
<span v-else class="binding-meta">-</span>
</td>
</tr>
</tbody>
</table>
</section>
<el-table :data="detail.inventoryBindings" size="small">
<el-table-column label="绑定" width="100">
<template #default="{ row: binding }">
<div>#{{ binding.bindingId }}</div>
<div v-if="binding.isPrimary" class="binding-primary">主库存项</div>
</template>
</el-table-column>
<el-table-column label="角色" min-width="120">
<template #default="{ row: binding }">
<div>{{ binding.roleKey || '-' }}</div>
<div class="binding-meta">数量 {{ binding.quantity }}</div>
</template>
</el-table-column>
<el-table-column label="库存项" min-width="180">
<template #default="{ row: binding }">
<div>ID {{ binding.inventoryItemId }}</div>
<div>{{ binding.skuCode || '-' }}</div>
<div class="binding-meta">
{{ binding.credentialType || '-' }} ·
{{ binding.batchNo || '无批次' }}
</div>
</template>
</el-table-column>
<el-table-column label="状态" min-width="160">
<template #default="{ row: binding }">
<div class="status-stack">
<AdminStatusTag :status="binding.bindingStatus" />
<AdminStatusTag :status="binding.inventoryStatus" />
</div>
<div v-if="binding.invalidReason" class="binding-meta">
{{
binding.inventoryStatus === 'invalid'
? '作废原因'
: binding.inventoryStatus === 'consumed'
? '消耗原因'
: '处理原因'
}}{{ binding.invalidReason }}
</div>
</template>
</el-table-column>
<el-table-column label="凭据内容" min-width="180">
<template #default="{ row: binding }">
{{ canViewSensitiveTaskData ? binding.displayValue || '-' : '客服不可见' }}
</template>
</el-table-column>
<el-table-column label="时间" min-width="180">
<template #default="{ row: binding }">
<div>绑定 {{ formatAdminDateTime(binding.createdAt) }}</div>
<div class="binding-meta">消费 {{ formatAdminDateTime(binding.consumedAt) }}</div>
<div class="binding-meta">释放 {{ formatAdminDateTime(binding.releasedAt) }}</div>
</template>
</el-table-column>
<el-table-column label="操作" width="100">
<template #default="{ row: binding }">
<el-button
v-if="hasAdminRole('admin') && binding.canRelease"
:loading="actionLoading"
link
@click="emit('releaseBinding', binding.bindingId)"
>
释放绑定
</el-button>
<span v-else class="binding-meta">-</span>
</template>
</el-table-column>
<template #empty>
<el-empty description="当前任务还没有库存绑定记录" :image-size="60" />
</template>
</el-table>
</el-card>
</template>
<style scoped src="../AdminTaskDetail.css"></style>
@@ -37,7 +37,7 @@ const emit = defineEmits<{
</script>
<template>
<section class="info-card flow-card">
<el-card shadow="never" class="section-card flow-card">
<div class="flow-card-head">
<div>
<h3>快手 Cloud 履约面板</h3>
@@ -74,34 +74,34 @@ const emit = defineEmits<{
</div>
<div class="redeem-summary-grid">
<article class="redeem-summary-card">
<el-card shadow="never" class="redeem-summary-card">
<span>准备状态</span>
<div class="auto-delivery-tag-line">
<AdminStatusTag :status="flow.binding.prepareStatus" />
</div>
</article>
<article class="redeem-summary-card">
</el-card>
<el-card shadow="never" class="redeem-summary-card">
<span>发货状态</span>
<div class="auto-delivery-tag-line">
<AdminStatusTag :status="flow.dispatch.status" />
</div>
</article>
<article class="redeem-summary-card">
</el-card>
<el-card shadow="never" class="redeem-summary-card">
<span>退号状态</span>
<div class="auto-delivery-tag-line">
<AdminStatusTag :status="flow.returnNumber.status" />
</div>
</article>
<article class="redeem-summary-card">
</el-card>
<el-card shadow="never" class="redeem-summary-card">
<span>当前任务状态</span>
<div class="auto-delivery-tag-line">
<AdminStatusTag :status="detail.task.status" />
</div>
</article>
</el-card>
</div>
<div class="kuaishou-focus-grid">
<article class="flow-block flow-block--spotlight">
<el-card shadow="never" class="flow-block flow-block--spotlight">
<div class="spotlight-head">
<div>
<h4>角色信息</h4>
@@ -145,9 +145,9 @@ const emit = defineEmits<{
<strong>{{ entry.value }}</strong>
</div>
</div>
</article>
</el-card>
<article class="flow-block flow-block--checklist">
<el-card shadow="never" class="flow-block flow-block--checklist">
<h4>流程检查</h4>
<p class="flow-copy">
客服可以先看这里快速判断卡在哪一步再决定是刷新角色准备资源发货还是退号
@@ -161,7 +161,7 @@ const emit = defineEmits<{
<strong>{{ item.detail }}</strong>
</div>
</div>
</article>
</el-card>
</div>
<el-collapse :model-value="collapsePanels" class="flow-collapse">
@@ -302,7 +302,7 @@ const emit = defineEmits<{
</div>
</el-collapse-item>
</el-collapse>
</section>
</el-card>
</template>
<style scoped src="../AdminTaskDetail.css"></style>
@@ -25,11 +25,10 @@ function updateField(field: keyof ManualDispatchForm, value: string) {
</script>
<template>
<section class="info-card">
<el-card shadow="never" class="section-card">
<h3>人工履约回写</h3>
<div class="manual-grid">
<label class="field-block">
<span>外部流水 / 单号</span>
<el-form label-position="top" class="manual-grid">
<el-form-item label="外部流水 / 单号">
<el-input
:model-value="form.deliveryReference"
:disabled="!canManageTaskLifecycle"
@@ -37,9 +36,8 @@ function updateField(field: keyof ManualDispatchForm, value: string) {
placeholder="例如快递单号、平台消息回执号"
@update:model-value="updateField('deliveryReference', $event)"
/>
</label>
<label class="field-block">
<span>已交付内容</span>
</el-form-item>
<el-form-item label="已交付内容">
<el-input
:model-value="form.deliveredCredential"
:disabled="!canManageTaskLifecycle"
@@ -49,9 +47,8 @@ function updateField(field: keyof ManualDispatchForm, value: string) {
:rows="3"
@update:model-value="updateField('deliveredCredential', $event)"
/>
</label>
<label class="field-block">
<span>处理备注</span>
</el-form-item>
<el-form-item label="处理备注">
<el-input
:model-value="form.resultMessage"
:disabled="!canManageTaskLifecycle"
@@ -61,25 +58,35 @@ function updateField(field: keyof ManualDispatchForm, value: string) {
:rows="4"
@update:model-value="updateField('resultMessage', $event)"
/>
</label>
</div>
<p class="manual-hint">回写后会直接更新任务终态不再走旧式领取链接流程</p>
<div v-if="detail.manualDispatch" class="manual-summary">
<p>
最近回写<AdminStatusTag
:status="detail.manualDispatch.outcome || detail.task.deliveryStatus"
/>
</el-form-item>
</el-form>
<el-alert
title="回写后会直接更新任务终态,不再走旧式领取链接流程。"
type="info"
show-icon
:closable="false"
class="mt-4"
/>
<el-descriptions v-if="detail.manualDispatch" :column="2" border class="manual-summary">
<el-descriptions-item label="最近回写">
<AdminStatusTag :status="detail.manualDispatch.outcome || detail.task.deliveryStatus" />
· {{ formatAdminDateTime(detail.manualDispatch.completedAt) }}
</p>
<p>
处理人{{ detail.manualDispatch.completedBy?.username || '-' }} /
</el-descriptions-item>
<el-descriptions-item label="处理人">
{{ detail.manualDispatch.completedBy?.username || '-' }} /
{{ detail.manualDispatch.completedBy?.role || '-' }}
</p>
<p>流水号{{ detail.manualDispatch.deliveryReference || '-' }}</p>
<p>交付内容{{ detail.manualDispatch.deliveredCredential || '-' }}</p>
<p>备注{{ detail.manualDispatch.resultMessage || '-' }}</p>
</div>
</section>
</el-descriptions-item>
<el-descriptions-item label="流水号">
{{ detail.manualDispatch.deliveryReference || '-' }}
</el-descriptions-item>
<el-descriptions-item label="交付内容">
{{ detail.manualDispatch.deliveredCredential || '-' }}
</el-descriptions-item>
<el-descriptions-item label="备注" :span="2">
{{ detail.manualDispatch.resultMessage || '-' }}
</el-descriptions-item>
</el-descriptions>
</el-card>
</template>
<style scoped src="../AdminTaskDetail.css"></style>
@@ -11,33 +11,34 @@ defineProps<Props>()
</script>
<template>
<section class="info-card">
<el-card shadow="never" class="section-card">
<h3>兑换重试链路</h3>
<p class="section-copy">
这里记录自动兑换时每次使用过的 CDK判定结果以及是否切换到了新的同类型凭据
</p>
<div class="redeem-summary-grid">
<article class="redeem-summary-card">
<el-card shadow="never" class="redeem-summary-card">
<span>处理结果</span>
<strong>{{ formatRedeemResolutionStatus(detail.redeemResolution!.status) }}</strong>
</article>
<article class="redeem-summary-card">
</el-card>
<el-card shadow="never" class="redeem-summary-card">
<span>任务收口状态</span>
<strong>{{ detail.redeemResolution!.taskStatus || detail.task.status || '-' }}</strong>
</article>
<article class="redeem-summary-card">
</el-card>
<el-card shadow="never" class="redeem-summary-card">
<span>更换新码次数</span>
<strong>{{ detail.redeemResolution!.replacementCount }}</strong>
</article>
<article class="redeem-summary-card">
</el-card>
<el-card shadow="never" class="redeem-summary-card">
<span>完成时间</span>
<strong>{{ formatAdminDateTime(detail.redeemResolution!.finishedAt) }}</strong>
</article>
</el-card>
</div>
<div v-if="detail.redeemResolution!.attempts.length > 0" class="redeem-attempts">
<article
<el-card
shadow="never"
v-for="attempt in detail.redeemResolution!.attempts"
:key="`${attempt.attempt}-${attempt.inventoryItemId || 'na'}`"
class="redeem-attempt-card"
@@ -67,9 +68,9 @@ defineProps<Props>()
<p class="redeem-attempt-copy">
{{ attempt.resultMessage || '-' }}
</p>
</article>
</el-card>
</div>
</section>
</el-card>
</template>
<style scoped src="../AdminTaskDetail.css"></style>
@@ -8,15 +8,10 @@ defineProps<Props>()
</script>
<template>
<section class="info-card">
<el-card shadow="never" class="section-card">
<h3>{{ sectionTitle }}</h3>
<p v-if="!screenshotPreviewUrl" class="screenshot-empty">截图加载中或当前不可读取</p>
<a
v-else
class="screenshot-link"
:href="screenshotPreviewUrl"
target="_blank"
rel="noreferrer"
<a v-else class="screenshot-link" :href="screenshotPreviewUrl" target="_blank" rel="noreferrer"
>打开原图</a
>
<img
@@ -25,7 +20,7 @@ defineProps<Props>()
:src="screenshotPreviewUrl"
:alt="sectionTitle"
/>
</section>
</el-card>
</template>
<style scoped src="../AdminTaskDetail.css"></style>
@@ -100,11 +100,11 @@ onMounted(loadDetail)
>
</AdminPageHeader>
<p v-if="errorMessage" class="error-copy">{{ errorMessage }}</p>
<div v-if="loading" class="empty-block">Webhook 详情加载中</div>
<el-alert v-if="errorMessage" :title="errorMessage" type="error" show-icon :closable="false" />
<el-skeleton v-if="loading" :rows="8" animated />
<template v-else-if="detail">
<section class="info-card">
<el-card shadow="never" class="section-card">
<div class="overview-top">
<div>
<h3>{{ formatAdminWebhookEventType(detail.eventType) }}</h3>
@@ -175,9 +175,9 @@ onMounted(loadDetail)
</div>
<div v-if="detail.processError" class="inline-error">{{ detail.processError }}</div>
</section>
</el-card>
<section v-if="lastReplayResult" class="info-card">
<el-card v-if="lastReplayResult" shadow="never" class="section-card">
<h3>最近一次重放结果</h3>
<p class="result-copy">
{{ lastReplayResult.provider }} / {{ lastReplayResult.platform }}
@@ -202,27 +202,27 @@ onMounted(loadDetail)
</template>
</el-table-column>
</el-table>
</section>
</el-card>
<section class="info-card">
<el-card shadow="never" class="section-card">
<h3>解析后的 Payload</h3>
<pre class="json-block">{{ prettyJson(detail.payload) }}</pre>
</section>
</el-card>
<section class="info-card">
<el-card shadow="never" class="section-card">
<h3>Headers</h3>
<pre class="json-block">{{ prettyJson(detail.headers) }}</pre>
</section>
</el-card>
<section class="info-card">
<el-card shadow="never" class="section-card">
<h3>Query</h3>
<pre class="json-block">{{ prettyJson(detail.query) }}</pre>
</section>
</el-card>
<section class="info-card">
<el-card shadow="never" class="section-card">
<h3>Body</h3>
<pre class="json-block">{{ prettyJson(detail.body) }}</pre>
</section>
</el-card>
</template>
</section>
</template>
@@ -233,15 +233,6 @@ onMounted(loadDetail)
gap: var(--space-4);
}
.info-card,
.empty-block,
.error-copy {
padding: 18px;
border-radius: var(--radius-lg);
background: var(--bg-surface);
border: 1px solid var(--border-default);
}
.overview-top {
display: flex;
justify-content: space-between;
@@ -250,7 +241,7 @@ onMounted(loadDetail)
}
.overview-top h3,
.info-card h3 {
.section-card h3 {
margin: 0;
color: var(--text-primary);
}
@@ -319,10 +310,6 @@ onMounted(loadDetail)
white-space: pre-wrap;
}
.error-copy {
color: var(--color-danger);
}
.inline-error {
margin-top: var(--space-4);
padding: var(--space-3) 14px;