后端可视化调试

This commit is contained in:
yml2213
2026-04-14 00:21:32 +08:00
parent 0c2f2e0a12
commit fa36be6539
14 changed files with 617 additions and 34 deletions
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { computed, onMounted, ref, watch } from 'vue'
import AdminPaginationBar from '@/components/admin/AdminPaginationBar.vue'
import AdminStatusTag from '@/components/admin/AdminStatusTag.vue'
@@ -7,11 +7,12 @@ import { showConfirm, showError, showSuccess } from '@/lib/feedback'
import {
createAdminInventoryItem,
fetchAdminInventoryItems,
fetchAdminInventorySkuSuggestions,
importAdminInventoryItems,
invalidateAdminInventoryItem,
releaseAdminInventoryItem,
} from '@/services/admin'
import type { AdminInventoryItemListItem, AdminPagination } from '@/types/admin'
import type { AdminInventoryItemListItem, AdminInventorySkuSuggestion, AdminPagination } from '@/types/admin'
import { hasAdminRole } from '@/utils/admin-auth'
import { formatAdminDateTime } from '@/utils/admin-time'
import {
@@ -55,6 +56,8 @@ const entryCredentialType = ref('tencent_code')
const entryBatchNo = ref('')
const singleDisplayValue = ref('')
const bulkText = ref('')
const skuSuggestionsLoading = ref(false)
const inventorySkuSuggestions = ref<AdminInventorySkuSuggestion[]>([])
const items = ref<AdminInventoryItemListItem[]>([])
const pagination = ref<AdminPagination>({
@@ -158,6 +161,19 @@ const pageOverview = computed(() => ({
groupCount: inventoryGroups.value.length,
}))
const entrySkuSuggestions = computed(() => {
const keyword = entrySkuCode.value.trim().toLowerCase()
const suggestions = inventorySkuSuggestions.value.filter((item) => item.credentialType === entryCredentialType.value)
if (!keyword) {
return suggestions.slice(0, 12)
}
return suggestions
.filter((item) => item.skuCode.toLowerCase().includes(keyword))
.slice(0, 12)
})
function getCredentialTypeLabel(value: string) {
const normalized = String(value || '').trim()
const option = adminInventoryCredentialTypeOptions.find((item) => item.value === normalized)
@@ -168,7 +184,7 @@ function getCredentialTypeHint(value: string) {
const normalized = String(value || '').trim()
if (normalized === 'tencent_code') {
return '当前腾讯自动领取链路会优先预占这一类兑换码,三角洲行动这类场景就使用这个类型。'
return '当前腾讯自动领取链路会优先预占这一类兑换码,三角洲行动这类场景就使用这个类型。内部 SKU 支持中文、英文和数字混用。'
}
if (normalized === 'card_password') {
@@ -190,6 +206,14 @@ function getCredentialTypeHint(value: string) {
return '库存预占时会按凭据类型与内部履约 SKU 一起匹配。'
}
function getSkuNamingHint() {
if (entrySkuSuggestions.value.length > 0) {
return `当前凭据类型下已有 ${entrySkuSuggestions.value.length} 个可复用的内部 SKU,也可以直接输入新的中文或英文名称。`
}
return '内部 SKU 支持中文、英文、数字和常见连接符。建议同一类商品长期使用同一个命名,方便共用库存池。'
}
function formatInventoryStatus(item: AdminInventoryItemListItem) {
if (item.status === 'consumed') {
return '已完成发放'
@@ -241,6 +265,10 @@ function useGroupAsEntry(group: InventoryPoolGroup) {
showSuccess(`已把 ${group.skuCode} / ${group.credentialType} 填入入库表单`)
}
function applySkuSuggestion(skuCode: string) {
entrySkuCode.value = String(skuCode || '').trim()
}
function buildImportRows() {
const lines = bulkText.value
.split('\n')
@@ -312,6 +340,22 @@ async function loadInventoryItems(page = pagination.value.page) {
}
}
async function loadInventorySkuSuggestions() {
skuSuggestionsLoading.value = true
try {
const response = await fetchAdminInventorySkuSuggestions({
credentialType: entryCredentialType.value.trim() || 'tencent_code',
limit: 50,
})
inventorySkuSuggestions.value = response.data.items
} catch (error) {
showError(error instanceof Error ? error.message : '读取内部 SKU 建议失败')
} finally {
skuSuggestionsLoading.value = false
}
}
function resetFilters() {
filterSkuCode.value = ''
filterCredentialType.value = ''
@@ -402,7 +446,16 @@ async function runRowAction(
}
}
onMounted(loadInventoryItems)
watch(entryCredentialType, () => {
void loadInventorySkuSuggestions()
})
onMounted(async () => {
await Promise.all([
loadInventoryItems(),
loadInventorySkuSuggestions(),
])
})
</script>
<template>
@@ -463,12 +516,27 @@ onMounted(loadInventoryItems)
<div class="card-head">
<div>
<h2>入库与导入</h2>
<p>这里填写的是内部履约 SKU不是平台侧 `sku_code`例如三角洲行动自动领取统一使用 `sjz_hdl_test01`这样同类订单才能共用一个库存池</p>
<p>这里填写的是内部履约 SKU不是平台侧 `sku_code`内部 SKU 支持中文命名同一类商品建议长期复用同一个名字这样订单才能稳定共用库存池</p>
</div>
</div>
<div class="form-grid">
<input v-model="entrySkuCode" class="text-input" placeholder="内部履约 SKU,例如 sjz_hdl_test01" />
<div class="sku-input-wrap">
<input
v-model="entrySkuCode"
list="inventory-sku-suggestions"
class="text-input"
placeholder="内部履约 SKU,可选已有同类型,也可自定义新的中文名"
/>
<datalist id="inventory-sku-suggestions">
<option
v-for="suggestion in entrySkuSuggestions"
:key="`${suggestion.credentialType}-${suggestion.skuCode}`"
:value="suggestion.skuCode"
:label="`${suggestion.skuCode} · 可用 ${suggestion.availableCount} / 共 ${suggestion.totalCount}`"
/>
</datalist>
</div>
<select v-model="entryCredentialType" class="text-input select-input">
<option
v-for="option in adminInventoryCredentialTypeOptions.filter((item) => item.value)"
@@ -481,6 +549,24 @@ onMounted(loadInventoryItems)
<input v-model="entryBatchNo" class="text-input" placeholder="批次号,可选" />
</div>
<div class="sku-helper-bar">
<span class="section-tip sku-helper-copy">{{ getSkuNamingHint() }}</span>
<span v-if="skuSuggestionsLoading" class="section-tip">正在加载同类型内部 SKU</span>
</div>
<div v-if="entrySkuSuggestions.length > 0" class="sku-chip-row">
<button
v-for="suggestion in entrySkuSuggestions"
:key="`${suggestion.credentialType}-${suggestion.skuCode}-chip`"
type="button"
class="sku-chip"
@click="applySkuSuggestion(suggestion.skuCode)"
>
<span class="sku-chip-title">{{ suggestion.skuCode }}</span>
<span class="sku-chip-meta">可用 {{ suggestion.availableCount }} / {{ suggestion.totalCount }}</span>
</button>
</div>
<div class="helper-grid">
<div class="helper-item">
<span class="helper-title">当前默认匹配</span>
@@ -756,6 +842,10 @@ onMounted(loadInventoryItems)
gap: 12px;
}
.sku-input-wrap {
display: grid;
}
.single-row,
.action-row {
display: flex;
@@ -771,6 +861,52 @@ onMounted(loadInventoryItems)
margin-top: 14px;
}
.sku-helper-bar {
display: flex;
justify-content: space-between;
gap: 12px;
align-items: center;
margin-top: 12px;
}
.sku-helper-copy {
margin: 0;
}
.sku-chip-row {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 12px;
}
.sku-chip {
display: inline-grid;
gap: 4px;
padding: 10px 14px;
border-radius: 14px;
border: 1px solid rgba(86, 108, 138, 0.16);
background: linear-gradient(180deg, rgba(244, 248, 255, 0.92), rgba(255, 255, 255, 0.98));
color: #1d3555;
cursor: pointer;
transition: transform 0.16s ease, border-color 0.16s ease, box-shadow 0.16s ease;
}
.sku-chip:hover {
transform: translateY(-1px);
border-color: rgba(37, 89, 167, 0.26);
box-shadow: 0 12px 24px rgba(37, 89, 167, 0.08);
}
.sku-chip-title {
font-weight: 700;
}
.sku-chip-meta {
color: #64748b;
font-size: 12px;
}
.helper-item {
display: grid;
gap: 6px;
@@ -1057,7 +1193,8 @@ onMounted(loadInventoryItems)
}
.single-row,
.action-row {
.action-row,
.sku-helper-bar {
flex-direction: column;
align-items: stretch;
}
@@ -45,6 +45,49 @@ function formatTaskBindingCountSummary(task: AdminOrderDetail['tasks'][number])
const summary = task.bindingSummary
return `${summary.totalBindingCount} 条 · 预占 ${summary.reservedBindingCount} · 已消费 ${summary.consumedBindingCount} · 已释放 ${summary.releasedBindingCount}`
}
function formatAutoDeliveryReason(value: string) {
const normalized = String(value || '').trim()
switch (normalized) {
case 'waiting_other_tasks':
return '等待订单下其他任务全部交付后再统一触发'
case 'already_sent':
return '该订单已经执行过自动发货'
case 'auto_delivery_disabled':
return '自动发货开关已关闭'
case 'missing_config':
return '缺少 Agiso 自动发货配置'
case 'request_failed':
return 'Agiso 接口返回失败'
case 'request_error':
return '请求 Agiso 接口时发生异常'
default:
return normalized || '-'
}
}
function resolveTaskAutoDeliveryStatus(task: AdminOrderDetail['tasks'][number]) {
if (task.agisoAutoDelivery?.status) {
return task.agisoAutoDelivery.status
}
return task.deliveryStatus === 'delivered' ? 'pending' : 'waiting'
}
function formatTaskAutoDeliveryReason(task: AdminOrderDetail['tasks'][number]) {
if (task.agisoAutoDelivery?.errorMessage) {
return task.agisoAutoDelivery.errorMessage
}
if (task.agisoAutoDelivery?.reason) {
return formatAutoDeliveryReason(task.agisoAutoDelivery.reason)
}
return task.deliveryStatus === 'delivered'
? '当前任务已交付,等待订单统一自动发货'
: '当前任务尚未完成交付'
}
</script>
<template>
@@ -105,6 +148,19 @@ function formatTaskBindingCountSummary(task: AdminOrderDetail['tasks'][number])
<strong>{{ detail.order.bindingSummary.totalBindingCount }} 条绑定记录</strong>
<small>预占 {{ detail.order.bindingSummary.reservedBindingCount }} · 已消费 {{ detail.order.bindingSummary.consumedBindingCount }} · 已释放 {{ detail.order.bindingSummary.releasedBindingCount }}</small>
</article>
<article v-if="detail.order.agisoAutoDelivery" class="info-tile">
<span>自动发货</span>
<div class="tile-tag-line">
<AdminStatusTag :status="detail.order.agisoAutoDelivery.status" />
</div>
<strong>
{{ detail.order.agisoAutoDelivery.errorMessage || formatAutoDeliveryReason(detail.order.agisoAutoDelivery.reason) || '全部任务交付后会自动触发' }}
</strong>
<small>
已交付 {{ detail.order.agisoAutoDelivery.deliveredTaskCount }}/{{ detail.order.agisoAutoDelivery.totalTaskCount }}
· 最近执行 {{ formatAdminDateTime(detail.order.agisoAutoDelivery.updatedAt) }}
</small>
</article>
</div>
<table class="meta-table">
@@ -151,6 +207,7 @@ function formatTaskBindingCountSummary(task: AdminOrderDetail['tasks'][number])
<th>状态</th>
<th>系统绑定</th>
<th>完整绑定</th>
<th>自动发货</th>
<th>库存绑定</th>
<th>登录方式</th>
<th>错误</th>
@@ -164,6 +221,12 @@ function formatTaskBindingCountSummary(task: AdminOrderDetail['tasks'][number])
<td><AdminStatusTag :status="task.status" /></td>
<td><AdminStatusTag :status="task.systemBindingStatus" /></td>
<td><AdminStatusTag :status="task.userBindingStatus" /></td>
<td>
<div class="tile-tag-line">
<AdminStatusTag :status="resolveTaskAutoDeliveryStatus(task)" />
</div>
<div class="binding-copy">{{ formatTaskAutoDeliveryReason(task) }}</div>
</td>
<td>
<div>{{ formatTaskBindingCountSummary(task) }}</div>
<div class="binding-copy">角色 {{ formatBindingRoleSummary(task.bindingSummary.roleKeys) }}</div>
@@ -267,6 +330,10 @@ function formatTaskBindingCountSummary(task: AdminOrderDetail['tasks'][number])
.info-tile strong {
color: #1d3555;
}
.tile-tag-line {
display: flex;
align-items: center;
}
.binding-copy {
margin-top: 6px;
color: #64748b;
@@ -155,9 +155,11 @@ function formatTaskEventPayload(payload: Record<string, unknown>) {
payload.previousOutcome ? `切换原因 ${formatRedeemOutcomeLabel(String(payload.previousOutcome || ''))}` : '',
payload.deliveryReference ? `单号 ${payload.deliveryReference}` : '',
payload.platformOrderId ? `平台单 ${payload.platformOrderId}` : '',
payload.trigger ? `触发 ${formatAutoDeliveryTrigger(String(payload.trigger || ''))}` : '',
payload.reason ? `原因 ${payload.reason}` : '',
payload.responseStatus ? `HTTP ${payload.responseStatus}` : '',
payload.requestId ? `请求 ${payload.requestId}` : '',
payload.aldsType ? `发货类型 ${payload.aldsType}` : '',
payload.errorMessage ? `错误 ${payload.errorMessage}` : '',
].filter(Boolean)
@@ -191,6 +193,70 @@ function formatRedeemResolutionStatus(value: string) {
return value === 'success' ? '已完成' : value === 'failed' ? '失败收口' : (value || '-')
}
function formatAutoDeliveryTrigger(value: string) {
const normalized = String(value || '').trim()
switch (normalized) {
case 'claim_redeemed':
return '用户兑换完成后触发'
case 'manual_dispatch_completed':
return '人工履约完成后触发'
case 'task_delivered':
return '任务交付完成后触发'
default:
return normalized || '-'
}
}
function formatAutoDeliveryReason(value: string) {
const normalized = String(value || '').trim()
switch (normalized) {
case 'waiting_other_tasks':
return '订单下仍有任务未完成交付'
case 'already_sent':
return '该订单已经执行过自动发货'
case 'auto_delivery_disabled':
return '自动发货开关已关闭'
case 'missing_config':
return '缺少 Agiso 自动发货配置'
case 'request_failed':
return 'Agiso 接口返回失败'
case 'request_error':
return '请求 Agiso 接口时发生异常'
case 'task_not_delivered':
return '当前任务尚未进入已交付状态'
case 'not_supported':
return '当前订单不支持 Agiso 咸鱼自动发货'
default:
return normalized || '-'
}
}
function resolveTaskAutoDeliveryStatus(taskDetail: AdminTaskDetail) {
if (taskDetail.task.agisoAutoDelivery?.status) {
return taskDetail.task.agisoAutoDelivery.status
}
return taskDetail.task.deliveryStatus === 'delivered' ? 'pending' : 'waiting'
}
function resolveTaskAutoDeliverySummary(taskDetail: AdminTaskDetail) {
const autoDelivery = taskDetail.task.agisoAutoDelivery
if (autoDelivery?.errorMessage) {
return autoDelivery.errorMessage
}
if (autoDelivery?.reason) {
return formatAutoDeliveryReason(autoDelivery.reason)
}
return taskDetail.task.deliveryStatus === 'delivered'
? '当前任务已交付,等待订单下其他任务完成后统一自动发货'
: '当前任务尚未完成交付,暂不会触发自动发货'
}
onMounted(loadDetail)
onBeforeUnmount(clearScreenshotPreview)
</script>
@@ -306,8 +372,8 @@ onBeforeUnmount(clearScreenshotPreview)
</el-button>
</section>
<section class="table-card">
<h3>任务信息</h3>
<section class="table-card">
<h3>任务信息</h3>
<table class="data-table">
<tbody>
<tr><th>商品</th><td>{{ detail.orderItem?.skuName || '-' }}</td></tr>
@@ -337,9 +403,48 @@ onBeforeUnmount(clearScreenshotPreview)
<tr><th>截图路径</th><td>{{ detail.task.screenshotPath || '-' }}</td></tr>
</tbody>
</table>
</section>
<section
v-if="detail.order?.provider === 'agiso' && detail.order?.platform === 'xianyu'"
class="table-card"
>
<h3>咸鱼自动发货</h3>
<p class="section-copy">这里展示这条任务最近一次触发咸鱼自动发货的状态原因和接口回执摘要</p>
<div class="redeem-summary-grid">
<article class="redeem-summary-card">
<span>当前状态</span>
<div class="auto-delivery-tag-line">
<AdminStatusTag :status="resolveTaskAutoDeliveryStatus(detail)" />
</div>
</article>
<article class="redeem-summary-card">
<span>触发来源</span>
<strong>{{ formatAutoDeliveryTrigger(detail.task.agisoAutoDelivery?.trigger || '') }}</strong>
</article>
<article class="redeem-summary-card">
<span>最近执行</span>
<strong>{{ formatAdminDateTime(detail.task.agisoAutoDelivery?.updatedAt) }}</strong>
</article>
<article class="redeem-summary-card">
<span>接口回执</span>
<strong>
{{ detail.task.agisoAutoDelivery?.requestId || (detail.task.agisoAutoDelivery?.responseStatus ? `HTTP ${detail.task.agisoAutoDelivery.responseStatus}` : '-') }}
</strong>
</article>
</div>
<div class="auto-delivery-note">
<p>{{ resolveTaskAutoDeliverySummary(detail) }}</p>
<p v-if="detail.task.agisoAutoDelivery?.reason">原因{{ formatAutoDeliveryReason(detail.task.agisoAutoDelivery.reason) }}</p>
<p v-if="detail.task.agisoAutoDelivery?.responseStatus">HTTP{{ detail.task.agisoAutoDelivery.responseStatus }}</p>
<p v-if="detail.task.agisoAutoDelivery?.requestId">请求 ID{{ detail.task.agisoAutoDelivery.requestId }}</p>
<p v-if="detail.task.agisoAutoDelivery?.aldsType">发货类型{{ detail.task.agisoAutoDelivery.aldsType }}</p>
</div>
</section>
<section v-if="detail.redeemResolution" class="table-card">
<section v-if="detail.redeemResolution" class="table-card">
<h3>兑换重试链路</h3>
<p class="section-copy">这里记录自动兑换时每次使用过的 CDK判定结果以及是否切换到了新的同类型凭据</p>
@@ -545,6 +650,16 @@ onBeforeUnmount(clearScreenshotPreview)
.action-row { display: flex; flex-wrap: wrap; gap: 12px; }
.table-card h3 { margin: 0 0 12px; color: #1d3555; }
.section-copy { margin: 0 0 12px; color: #64748b; }
.auto-delivery-tag-line { display: flex; align-items: center; }
.auto-delivery-note {
margin-top: 14px;
padding: 14px 16px;
border-radius: 16px;
background: rgba(248,250,252,.92);
border: 1px solid rgba(86,108,138,.08);
}
.auto-delivery-note p { margin: 0 0 8px; color: #334155; }
.auto-delivery-note p:last-child { margin-bottom: 0; }
.data-table { width: 100%; border-collapse: collapse; }
.data-table th,.data-table td { padding: 12px 10px; text-align: left; border-bottom: 1px solid rgba(86,108,138,.08); }
.data-table th { width: 160px; color: #64748b; }