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