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