feat(#67): add Shopee order snapshots to purchase tasks

This commit is contained in:
QiuSW
2026-08-22 10:41:05 +08:00
parent 0ceb248233
commit 9490bddf8f
17 changed files with 173 additions and 30 deletions
@@ -5,6 +5,7 @@
<el-alert v-if="loadError" :title="loadError" type="error" :closable="false" show-icon class="notice"><template #default><el-button type="primary" link @click="load">重新加载</el-button></template></el-alert>
<el-form :model="query" :inline="true" class="search-form" @submit.prevent="search">
<el-form-item label="任务"><el-input v-model="query.taskId" class="task-input" clearable placeholder="编号" @keyup.enter="search" /></el-form-item>
<el-form-item label="蝦皮订单号"><el-input v-model="query.shopeeOrderNo" class="shopee-order-input" clearable placeholder="订单号" @keyup.enter="search" /></el-form-item>
<el-form-item label="状态"><el-select v-model="query.status" clearable placeholder="全部状态" style="width:160px"><el-option v-for="item in statuses" :key="item.value" :label="item.label" :value="item.value" /></el-select></el-form-item>
<el-form-item label="SYB 商品"><el-input v-model="query.sybProductId" class="syb-input" clearable placeholder="商品 ID" @keyup.enter="search" /></el-form-item>
<el-form-item label="PDD 订单号"><el-input v-model="query.pddOrderNo" class="order-input" clearable placeholder="订单号" @keyup.enter="search" /></el-form-item>
@@ -15,6 +16,7 @@
<el-table ref="taskTable" v-loading="loading" :data="tasks" row-key="id" border stripe empty-text="暂无采购任务" :row-class-name="retryRowClass" @selection-change="handleRetrySelection">
<el-table-column type="selection" width="48" :selectable="isRetrySelectable" />
<el-table-column label="任务" width="105"><template #default="{ row }"><div class="primary">CG-{{ row.id }}</div><el-tag size="small" effect="plain" :type="row.executionMode === 'live' ? 'success' : 'info'">{{ modeLabel(row.executionMode) }}</el-tag></template></el-table-column>
<el-table-column label="蝦皮订单号" min-width="160"><template #default="{ row }"><span class="order-number">{{ row.shopeeOrderNoSnapshot || '—' }}</span></template></el-table-column>
<el-table-column label="SYB / 虾皮商品" min-width="220"><template #default="{ row }"><div class="primary ellipsis">{{ row.shopeeTitleSnapshot || `虾皮商品 ${row.shopeeItemIdSnapshot || '—'}` }}</div><div class="muted">SYB-{{ row.sybProductId || '—' }} · {{ row.shopeeShopNameSnapshot || '店铺未记录' }}</div></template></el-table-column>
<el-table-column label="PDD 商品" min-width="190"><template #default="{ row }"><div class="primary ellipsis">{{ row.pddTitleSnapshot || '标题未记录' }}</div><div class="muted">goods_id {{ row.pddGoodsIdSnapshot || '—' }}</div></template></el-table-column>
<el-table-column label="采购规格 / 数量" min-width="190"><template #default="{ row }"><div>{{ specText(row.mappedColorSnapshot, row.mappedSizeSnapshot) }}</div><div class="muted">目标:{{ specText(row.targetColorSnapshot, row.targetSizeSnapshot) }} · {{ row.quantity }} 件</div></template></el-table-column>
@@ -34,8 +36,9 @@
<div class="detail-heading"><div><span class="task-number">CG-{{ detail.task.id }}</span><el-tag :type="statusType(detail.task.status)">{{ statusLabel(detail.task.status) }}</el-tag><el-tag effect="plain" :type="detail.task.executionMode === 'live' ? 'success' : 'info'">{{ modeLabel(detail.task.executionMode) }}</el-tag></div><span class="muted">状态更新时间:{{ formatTime(detail.task.statusChangedAt) }}</span></div>
<section><h3>任务快照</h3><el-descriptions :column="2" border>
<el-descriptions-item label="SYB 商品">SYB-{{ detail.task.sybProductId || '—' }}</el-descriptions-item><el-descriptions-item label="虾皮商品 ID">{{ detail.task.shopeeItemIdSnapshot || '—' }}</el-descriptions-item>
<el-descriptions-item label="虾皮标题" :span="2">{{ detail.task.shopeeTitleSnapshot || '未记录' }}</el-descriptions-item><el-descriptions-item label="虾皮店铺">{{ detail.task.shopeeShopNameSnapshot || '未记录' }}</el-descriptions-item><el-descriptions-item label="PDD goods_id">{{ detail.task.pddGoodsIdSnapshot || '—' }}</el-descriptions-item>
<el-descriptions-item label="SYB 商品">SYB-{{ detail.task.sybProductId || '—' }}</el-descriptions-item><el-descriptions-item label="蝦皮订单号">{{ detail.task.shopeeOrderNoSnapshot || '—' }}</el-descriptions-item>
<el-descriptions-item label="虾皮商品 ID">{{ detail.task.shopeeItemIdSnapshot || '—' }}</el-descriptions-item><el-descriptions-item label="虾皮店铺">{{ detail.task.shopeeShopNameSnapshot || '未记录' }}</el-descriptions-item>
<el-descriptions-item label="虾皮标题" :span="2">{{ detail.task.shopeeTitleSnapshot || '未记录' }}</el-descriptions-item><el-descriptions-item label="PDD goods_id">{{ detail.task.pddGoodsIdSnapshot || '—' }}</el-descriptions-item>
<el-descriptions-item label="PDD 标题" :span="2">{{ detail.task.pddTitleSnapshot || '未记录' }}</el-descriptions-item>
<el-descriptions-item label="目标规格">{{ specText(detail.task.targetColorSnapshot, detail.task.targetSizeSnapshot) }}</el-descriptions-item><el-descriptions-item label="执行规格">{{ specText(detail.task.mappedColorSnapshot, detail.task.mappedSizeSnapshot) }}</el-descriptions-item>
<el-descriptions-item label="数量">{{ detail.task.quantity }} 件</el-descriptions-item><el-descriptions-item label="映射来源">{{ specSourceLabel(detail.task.specSource) }} <el-button v-if="detail.task.shopeeProductId" type="primary" link @click="openShopeeMapping">查看映射</el-button></el-descriptions-item>
@@ -128,7 +131,7 @@ export default {
data() {
return {
loading: false, loadError: '', tasks: [], total: 0, statuses: statusOptions,
query: { page: 1, pageSize: 20, taskId: '', status: '', executionMode: 'live', sybProductId: '', pddOrderNo: '' },
query: { page: 1, pageSize: 20, taskId: '', shopeeOrderNo: '', status: '', executionMode: 'live', sybProductId: '', pddOrderNo: '' },
detail: { open: false, loading: false, task: null, attempts: [] },
retrySelection: [],
retryDialog: { open: false, saving: false, items: [] },
@@ -170,7 +173,7 @@ export default {
},
cleanQuery() { return Object.fromEntries(Object.entries(this.query).filter(([, value]) => value !== '')) },
search() { this.query.page = 1; this.load() },
reset() { this.query = { page: 1, pageSize: 20, taskId: '', status: '', executionMode: 'live', sybProductId: '', pddOrderNo: '' }; this.load() },
reset() { this.query = { page: 1, pageSize: 20, taskId: '', shopeeOrderNo: '', status: '', executionMode: 'live', sybProductId: '', pddOrderNo: '' }; this.load() },
async openDetail(row) { this.detail = { open: true, loading: true, task: null, attempts: [] }; try { await this.refreshDetail(row.id) } finally { this.detail.loading = false } },
async refreshDetail(id = this.detail.task?.id) { const r = await getPurchaseTask(id); this.detail.task = r.data.task; this.detail.attempts = r.data.attempts || [] },
statusLabel(value) { return statusOptions.find(item => item.value === value)?.label || value || '—' },
@@ -222,5 +225,5 @@ export default {
</script>
<style lang="scss" scoped>
.page-card{min-height:calc(100vh - 124px)}.search-form{display:flex;align-items:flex-end;padding:16px 16px 0;margin-bottom:16px;border:1px solid #e5e7eb;border-radius:8px;background:#f8fafc;white-space:nowrap}.retry-action{margin-left:auto;margin-right:0}.selected-copy{margin-right:10px;color:#606266}.selected-copy strong{color:#337ecc;font-variant-numeric:tabular-nums}.task-input{width:72px}.syb-input{width:100px}.order-input{width:120px}.notice{margin-bottom:18px}.primary{color:#303133;font-weight:600}.muted{color:#909399;font-size:12px;line-height:1.5}.ellipsis{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.retry-disabled{display:block;margin-top:4px;color:#b88230;font-size:11px;line-height:1.2;cursor:help}.dialog-help{margin:14px 0 0;color:#606266;line-height:1.6}.drawer-body{min-height:240px;padding:0 4px 28px}.detail-heading{display:flex;align-items:center;justify-content:space-between;gap:16px;margin-bottom:18px}.detail-heading>div{display:flex;align-items:center;gap:8px}.task-number{font-size:22px;font-weight:700;color:#1f2937}.drawer-body section{margin-top:24px}.drawer-body h3{margin:0 0 12px;color:#303133;font-size:17px}.state-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}.state-card{display:flex;min-height:104px;flex-direction:column;gap:7px;padding:16px;border:1px solid #e5e7eb;border-radius:8px;background:#f8fafc}.state-card span,.state-card small{color:#606266}.state-card strong{color:#1f2937;font-size:16px;overflow-wrap:anywhere}.drawer-actions{position:sticky;bottom:0;z-index:2;display:flex;justify-content:flex-end;gap:8px;margin:24px -4px -28px;padding:16px 4px;background:#fff;border-top:1px solid #e5e7eb}:deep(.el-table .retry-disabled-row){color:#909399;background:#fafafa}:deep(.el-table .retry-disabled-row .el-checkbox){cursor:not-allowed}@media(max-width:1200px){.search-form{overflow-x:auto}.retry-action{margin-left:24px}}@media(max-width:768px){.search-form{white-space:normal}.state-grid{grid-template-columns:1fr}.detail-heading{align-items:flex-start;flex-direction:column}.drawer-actions{flex-wrap:wrap}}
.page-card{min-height:calc(100vh - 124px)}.search-form{display:flex;align-items:flex-end;padding:16px 16px 0;margin-bottom:16px;border:1px solid #e5e7eb;border-radius:8px;background:#f8fafc;white-space:nowrap}.retry-action{margin-left:auto;margin-right:0}.selected-copy{margin-right:10px;color:#606266}.selected-copy strong{color:#337ecc;font-variant-numeric:tabular-nums}.task-input{width:72px}.shopee-order-input{width:140px}.syb-input{width:100px}.order-input{width:120px}.order-number{font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.notice{margin-bottom:18px}.primary{color:#303133;font-weight:600}.muted{color:#909399;font-size:12px;line-height:1.5}.ellipsis{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.retry-disabled{display:block;margin-top:4px;color:#b88230;font-size:11px;line-height:1.2;cursor:help}.dialog-help{margin:14px 0 0;color:#606266;line-height:1.6}.drawer-body{min-height:240px;padding:0 4px 28px}.detail-heading{display:flex;align-items:center;justify-content:space-between;gap:16px;margin-bottom:18px}.detail-heading>div{display:flex;align-items:center;gap:8px}.task-number{font-size:22px;font-weight:700;color:#1f2937}.drawer-body section{margin-top:24px}.drawer-body h3{margin:0 0 12px;color:#303133;font-size:17px}.state-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}.state-card{display:flex;min-height:104px;flex-direction:column;gap:7px;padding:16px;border:1px solid #e5e7eb;border-radius:8px;background:#f8fafc}.state-card span,.state-card small{color:#606266}.state-card strong{color:#1f2937;font-size:16px;overflow-wrap:anywhere}.drawer-actions{position:sticky;bottom:0;z-index:2;display:flex;justify-content:flex-end;gap:8px;margin:24px -4px -28px;padding:16px 4px;background:#fff;border-top:1px solid #e5e7eb}:deep(.el-table .retry-disabled-row){color:#909399;background:#fafafa}:deep(.el-table .retry-disabled-row .el-checkbox){cursor:not-allowed}@media(max-width:1200px){.search-form{overflow-x:auto}.retry-action{margin-left:24px}}@media(max-width:768px){.search-form{white-space:normal}.state-grid{grid-template-columns:1fr}.detail-heading{align-items:flex-start;flex-direction:column}.drawer-actions{flex-wrap:wrap}}
</style>
+3 -3
View File
@@ -7,7 +7,7 @@
<el-form-item><el-button @click="$router.push('/syb-sync-runs/index')">同步记录</el-button></el-form-item>
<el-form-item><span class="selection-count" aria-live="polite">已选择 <strong>{{ selectedProducts.length }}</strong> 条</span></el-form-item>
<el-form-item v-if="canPurchase"><el-radio-group v-model="batchMode" size="small" aria-label="批量操作用途" @change="changeBatchMode"><el-radio-button value="purchase">创建采购</el-radio-button><el-radio-button value="reparse">重新解析</el-radio-button></el-radio-group></el-form-item>
<el-form-item label="搜索"><el-input v-model="query.keyword" placeholder="货运单号、虾皮商品ID或商品标题" clearable @keyup.enter="search" /></el-form-item>
<el-form-item label="搜索"><el-input v-model="query.keyword" placeholder="订单号、虾皮商品ID或商品标题" clearable @keyup.enter="search" /></el-form-item>
<el-form-item label="解析状态"><el-select v-model="query.parseStatus" clearable placeholder="全部" style="width: 140px"><el-option label="成功" value="success" /><el-option label="存疑" value="uncertain" /><el-option label="失败" value="failed" /></el-select></el-form-item>
<el-form-item><el-button type="primary" :icon="Search" @click="search">查询</el-button><el-button :icon="RefreshLeft" @click="reset">重置</el-button><el-button v-if="batchMode === 'reparse'" :disabled="selectedProducts.length === 0" @click="openReparseBatch">重新解析</el-button><el-button v-if="canPurchase && batchMode === 'purchase'" type="primary" :disabled="selectedProducts.length === 0" @click="openPurchaseBatch">创建采购任务</el-button></el-form-item>
</el-form>
@@ -24,7 +24,7 @@
<el-table ref="productTable" v-loading="loading" :data="products" row-key="id" border stripe empty-text="暂无 SYB 商品明细" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="48" :selectable="rowSelectable" />
<el-table-column label="参考图" width="76"><template #default="{ row }"><el-image v-if="row.imageUrl" :src="row.imageUrl" fit="cover" class="thumb" :preview-src-list="[row.imageUrl]" preview-teleported /><div v-else class="thumb placeholder">无图</div></template></el-table-column>
<el-table-column label="货运单号" prop="orderCode" min-width="120" />
<el-table-column label="订单号" prop="orderCode" min-width="120" />
<el-table-column label="虾皮商品" min-width="421"><template #default="{ row }"><a class="link" href="javascript:void(0)" @click="openShopeeDetail(row.shopeeProductId)">{{ row.shopeeItemId }} ↗</a><div class="muted">{{ row.productTitle || '未取到标题' }}</div></template></el-table-column>
<el-table-column label="店铺" prop="shopName" min-width="100" />
<el-table-column label="目标颜色 / 尺码" min-width="128"><template #default="{ row }"><div :class="{ 'missing': !row.targetColor }">颜色:{{ row.targetColor || '—' }}</div><div :class="{ 'missing': !row.targetSize }">尺码:{{ row.targetSize || '—' }}</div></template></el-table-column>
@@ -119,7 +119,7 @@
<div class="split-col">
<h3 class="section-title">结构化字段</h3>
<el-descriptions :column="1" border size="small">
<el-descriptions-item label="货运单号">{{ detail.item.orderCode }}</el-descriptions-item>
<el-descriptions-item label="订单号">{{ detail.item.orderCode }}</el-descriptions-item>
<el-descriptions-item label="来源明细ID">{{ detail.item.detailId }}</el-descriptions-item>
<el-descriptions-item label="虾皮商品ID"><a class="link" href="javascript:void(0)" @click="openShopeeDetail(detail.item.shopeeProductId)">{{ detail.item.shopeeItemId }} ↗</a></el-descriptions-item>
<el-descriptions-item label="商品标题">{{ detail.item.productTitle || '未取到标题' }}</el-descriptions-item>