@@ -227,6 +325,7 @@ import { listDevices } from '@/api/goauto/devices'
import { createPurchaseTasksBatch, matchPurchaseSpecsBatch, previewPurchaseTasks } from '@/api/goauto/purchase-tasks'
import { batchCreateCollectionTasks, batchCreateImageSearchCollectionTasks } from '@/api/goauto/collection-tasks'
import { listCollectionRules } from '@/api/goauto/collection-rules'
+import { listReturnMatches, batchMatchReturns, getReturnMatch, confirmReturnMatch, cancelReturnMatch, remarkReturnMatch } from '@/api/goauto/return-matches'
import { createRequestId } from '@/utils/request-id'
import { readPurchaseDevice, rememberPurchaseDevice } from '@/utils/purchase-device-preference'
import ShopeeProductDetailDrawer from '../shopee-products/ShopeeProductDetailDrawer.vue'
@@ -255,6 +354,11 @@ export default {
collectionBatchData: { ruleId: null, deviceId: null },
collectionBatchRules: { ruleId: [{ required: true, message: '请选择采集规则', trigger: 'change' }] },
query: { page: 1, pageSize: 20, shopName: '', orderCodesText: '', parseStatus: '', processStage: '' },
+ returnMatchByProductId: {},
+ returnMatchLoading: false,
+ returnMatchBatchLoading: false,
+ returnMatchBatchResult: { open: false, items: [], matchedCount: 0, noCandidateCount: 0, stageIneligibleCount: 0, conflictCount: 0 },
+ matchDetail: { open: false, loading: false, saving: false, id: null, data: null, remarkText: '' },
detail: { open: false, loading: false, item: null },
correct: { open: false, saving: false, productId: null, targetColor: '', targetSize: '' },
shopeeDetail: { open: false, productId: null, targetColor: '', action: '' },
@@ -265,7 +369,8 @@ export default {
purchaseDeviceUnavailable() { return !!this.purchaseDialog.deviceId && !this.purchaseDevices.some(device => device.id === this.purchaseDialog.deviceId) },
purchaseImageSearchLinkedCount() { return this.purchaseDialog.items.filter(item => item.imageSearchLinked).length },
canPurchase() { const roles = this.$store.getters.roles || []; return roles.includes('admin') || roles.includes('purchaser') || roles.includes('after_sales') },
- processStageOptions() { return [{ value: 'manual_action', label: '待人工处理' }, { value: 'pdd_unlinked', label: '未关联 PDD' }, { value: 'pdd_pending', label: 'PDD 待采集' }, { value: 'pdd_collecting', label: 'PDD 采集中' }, { value: 'pdd_collection_failed', label: 'PDD 采集失败' }, { value: 'color_mapping', label: '规格待匹配' }, { value: 'purchase_ready', label: '可创建采购' }, { value: 'task_created', label: '已创建任务' }, { value: 'purchase_succeeded', label: '采购成功' }, { value: 'order_review', label: '待人工核对' }] },
+ processStageOptions() { return [{ value: 'manual_action', label: '待人工处理' }, { value: 'pdd_unlinked', label: '未关联 PDD' }, { value: 'pdd_pending', label: 'PDD 待采集' }, { value: 'pdd_collecting', label: 'PDD 采集中' }, { value: 'pdd_collection_failed', label: 'PDD 采集失败' }, { value: 'color_mapping', label: '规格待匹配' }, { value: 'purchase_ready', label: '可创建采购' }, { value: 'task_created', label: '已创建任务' }, { value: 'purchase_succeeded', label: '采购成功' }, { value: 'order_review', label: '待人工核对' }, { value: 'return_pending', label: '退货待确认' }, { value: 'return_used', label: '已用退货' }] },
+ returnMatchCandidateIds() { return this.selectedProducts.filter(row => this.isReturnMatchCandidate(row)).map(row => row.id) },
aiMatchCandidates() { return this.selectedProducts.filter(row => this.purchaseReady(row).aiMatchEligible === true) },
aiMatchButtonReason() {
if (this.purchaseReadinessLoading) return '正在检查 AI 匹配资格'
@@ -313,6 +418,7 @@ export default {
this.total = r.data.total
this.loading = false
void this.loadPurchaseReadiness(this.products.map(item => item.id), requestOptions, generation)
+ void this.loadReturnMatches(this.products.map(item => item.id), generation)
} catch (error) {
if (generation !== this.loadGeneration) return
if (allowNetworkRetry && error?.message === 'Network Error' && !error?.response) {
@@ -357,13 +463,19 @@ export default {
// 不能要求已关联——另外三个判据都要求,于是图搜入口对它最该服务的那类商品
// 一直不可达(#291)。这里与 imageSearchRows 的过滤条件保持一致。
isImageSearchCandidate(row) { return Boolean(row.shopeeProductId && row.imageUrl) },
+ // #338: 参与匹配退货的处理阶段范围(服务端 returnmatch.participatingStages 同一口径)。
+ isReturnMatchCandidate(row) {
+ const participating = new Set(['pdd_unlinked', 'pdd_pending', 'pdd_collecting', 'pdd_collection_failed', 'color_mapping', 'purchase_ready'])
+ return participating.has(this.purchaseReady(row).processStage)
+ },
+ isActivelyReturnMatched(row) { const stage = this.purchaseReady(row).processStage; return stage === 'return_pending' || stage === 'return_used' },
// `[必须]` 可勾选不等于可采购。放宽的只有勾选:purchaseCandidates / collectionCandidates
- // / aiMatchCandidates 仍各自用自己的判据过滤,采购门禁不受影响。
- isSelectableCandidate(row) { return this.isPurchaseCandidate(row) || this.isCollectionCandidate(row) || this.isAIMatchCandidate(row) || this.isImageSearchCandidate(row) },
+ // / aiMatchCandidates / returnMatch 仍各自用自己的判据过滤,采购门禁不受影响。
+ isSelectableCandidate(row) { return this.isPurchaseCandidate(row) || this.isCollectionCandidate(row) || this.isAIMatchCandidate(row) || this.isImageSearchCandidate(row) || this.isReturnMatchCandidate(row) },
rowSelectable(row) { return this.canPurchase && !this.purchaseReadinessLoading && this.isSelectableCandidate(row) },
purchaseReady(row) { return this.purchaseReadiness[row.id] || { sybProductId: row.id, eligible: false, reason: this.purchaseReadinessLoading ? '正在检查' : '请刷新后重试' } },
purchasePriceText(item) { if (item.minUnitPriceCent === undefined || item.maxUnitPriceCent === undefined) return ''; return `允许单价 ¥${(item.minUnitPriceCent / 100).toFixed(2)}~¥${(item.maxUnitPriceCent / 100).toFixed(2)}` },
- processMeta(stage) { return { manual_action: { label: '待人工处理', type: 'warning' }, pdd_unlinked: { label: '未关联 PDD', type: 'info' }, pdd_pending: { label: 'PDD 待采集', type: 'info' }, pdd_collecting: { label: 'PDD 采集中', type: 'primary' }, pdd_collection_failed: { label: 'PDD 采集失败', type: 'danger' }, color_mapping: { label: '规格待匹配', type: 'warning' }, purchase_ready: { label: '可创建采购', type: 'success' }, task_created: { label: '已创建任务', type: 'primary' }, purchase_succeeded: { label: '采购成功', type: 'success' }, order_review: { label: '待人工核对', type: 'danger' }}[stage] || { label: '待人工处理', type: 'warning' } },
+ processMeta(stage) { return { manual_action: { label: '待人工处理', type: 'warning' }, pdd_unlinked: { label: '未关联 PDD', type: 'info' }, pdd_pending: { label: 'PDD 待采集', type: 'info' }, pdd_collecting: { label: 'PDD 采集中', type: 'primary' }, pdd_collection_failed: { label: 'PDD 采集失败', type: 'danger' }, color_mapping: { label: '规格待匹配', type: 'warning' }, purchase_ready: { label: '可创建采购', type: 'success' }, task_created: { label: '已创建任务', type: 'primary' }, purchase_succeeded: { label: '采购成功', type: 'success' }, order_review: { label: '待人工核对', type: 'danger' }, return_pending: { label: '退货待确认', type: 'warning' }, return_used: { label: '已用退货', type: 'info' }}[stage] || { label: '待人工处理', type: 'warning' } },
purchaseActionLabel(item) { return { open_pdd_link: '去关联', open_mapping: '去匹配', open_shopee: '查看蝦皮商品', open_pdd: '查看 PDD 商品', open_task: '查看任务', reparse: '查看并处理', select_device: '重新选择设备', refresh: '刷新' }[item.nextAction] || '' },
async loadPurchaseReadiness(ids, requestOptions = {}, generation = this.loadGeneration, selectedIDsOverride = null) {
if (generation !== this.loadGeneration) return
@@ -428,6 +540,109 @@ export default {
},
formatConfidence(value) { return Number.isFinite(value) ? `${Math.round(value * 100)}%` : '—' },
+ // ---------------- 退货匹配 (#338) ----------------
+ formatMatchDeadline(value) { return value ? new Date(value).toLocaleString() : '—' },
+ // #338: an already-matched return whose destroy deadline has passed is only flagged, never auto-cancelled.
+ isDeadlinePassed(value) { return Boolean(value) && new Date(value).getTime() <= Date.now() },
+ matchStatusLabel(status) { return { matched: '待确认', confirmed: '已确认', cancelled: '已取消' }[status] || status },
+ async loadReturnMatches(ids, generation = this.loadGeneration) {
+ this.returnMatchByProductId = {}
+ if (!ids.length) return
+ this.returnMatchLoading = true
+ try {
+ const r = await listReturnMatches({ sybProductId: ids }, { suppressErrorMessage: true })
+ if (generation !== this.loadGeneration) return
+ const map = {}
+ for (const item of (r.data?.items || [])) {
+ if (item.status === 'cancelled') continue
+ if (!map[item.sybProductId]) map[item.sybProductId] = item
+ }
+ this.returnMatchByProductId = map
+ } catch {
+ // 展示失败不影响主列表;用户可重新查询重试。
+ } finally {
+ if (generation === this.loadGeneration) this.returnMatchLoading = false
+ }
+ },
+ matchReasonMeta(code) {
+ return {
+ stage_ineligible: '阶段不参与',
+ no_candidate: '无候选',
+ conflict: '并发冲突'
+ }[code] || code
+ },
+ async runBatchMatchReturns() {
+ const ids = this.returnMatchCandidateIds
+ if (!ids.length || this.returnMatchBatchLoading) return
+ this.returnMatchBatchLoading = true
+ try {
+ const r = await batchMatchReturns({ sybProductIds: ids })
+ const items = r.data.items || []
+ this.returnMatchBatchResult = {
+ open: true, items,
+ matchedCount: r.data.matchedCount || 0,
+ noCandidateCount: items.filter(i => i.reasonCode === 'no_candidate').length,
+ stageIneligibleCount: items.filter(i => i.reasonCode === 'stage_ineligible').length,
+ conflictCount: items.filter(i => i.reasonCode === 'conflict').length
+ }
+ await this.load()
+ } finally {
+ this.returnMatchBatchLoading = false
+ }
+ },
+ async openMatchDetail(matchId, focusRemark = false) {
+ this.matchDetail = { open: true, loading: true, saving: false, id: matchId, data: null, remarkText: '' }
+ try {
+ const r = await getReturnMatch(matchId)
+ this.matchDetail.data = r.data
+ this.matchDetail.remarkText = r.data.match.remark || ''
+ } finally {
+ this.matchDetail.loading = false
+ }
+ if (focusRemark) this.$nextTick(() => this.$refs.matchRemarkInput?.focus())
+ },
+ async refreshMatchDetail() {
+ const r = await getReturnMatch(this.matchDetail.id)
+ this.matchDetail.data = r.data
+ this.matchDetail.remarkText = r.data.match.remark || ''
+ await this.load()
+ },
+ async saveMatchRemark() {
+ this.matchDetail.saving = true
+ try {
+ await remarkReturnMatch(this.matchDetail.id, { remark: this.matchDetail.remarkText })
+ ElMessage.success('已保存备注')
+ await this.refreshMatchDetail()
+ } finally {
+ this.matchDetail.saving = false
+ }
+ },
+ async confirmMatchDetail() {
+ try {
+ await this.$confirm('确认后该 SYB 商品状态变为「已用退货」,仍不能创建采购任务。确定继续?', '确认匹配', { type: 'warning' })
+ } catch { return }
+ await confirmReturnMatch(this.matchDetail.id)
+ ElMessage.success('已确认匹配')
+ await this.refreshMatchDetail()
+ },
+ async cancelMatchDetail() {
+ try {
+ await this.$confirm('取消后该 SYB 商品恢复可创建采购,对应退货商品回到可用池。确定取消匹配?', '取消匹配', { type: 'warning', confirmButtonClass: 'el-button--danger' })
+ } catch { return }
+ await cancelReturnMatch(this.matchDetail.id)
+ ElMessage.success('已取消匹配')
+ this.matchDetail.open = false
+ await this.load()
+ },
+ async quickCancelMatch(matchId) {
+ try {
+ await this.$confirm('取消后该 SYB 商品恢复可创建采购,对应退货商品回到可用池。确定取消匹配?', '取消匹配', { type: 'warning', confirmButtonClass: 'el-button--danger' })
+ } catch { return }
+ await cancelReturnMatch(matchId)
+ ElMessage.success('已取消匹配')
+ await this.load()
+ },
+
// ---------------- 批量创建采购任务 ----------------
async openSinglePurchase(row) {
this.$refs.productTable?.clearSelection()
@@ -625,9 +840,11 @@ export default {
.missing{color:#b91c1c}
.quick-link-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin:4px 0}
.thumb{width:44px;height:44px;border-radius:4px;object-fit:cover}.thumb.placeholder{display:flex;align-items:center;justify-content:center;background:#f1f5f9;color:#909399;font-size:11px}
+.return-match-cell{display:flex;gap:8px;align-items:flex-start}
.link{color:#1677ff;cursor:pointer}
.shopee-title{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.notice{margin-bottom:16px}
+.return-missing-alert{margin:6px 0}
.compact-notice{margin-bottom:12px}
.purchase-summary{display:flex;flex-wrap:wrap;gap:12px 24px;margin-bottom:16px;padding:12px 16px;border:1px solid #dbeafe;border-radius:8px;background:#f8fafc}.purchase-summary strong{font-variant-numeric:tabular-nums;color:#1e40af}.success-text{color:#166534}.warning-text{color:#b45309}.danger-text,.purchase-reason{color:#b91c1c}.purchase-reason{margin-top:4px;font-size:12px;line-height:1.45}.purchase-settings{margin-bottom:12px}.field-help{margin-left:12px;color:#909399;font-size:12px}.ellipsis{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.image-search-tag{margin-left:6px;vertical-align:middle}
.spec-match-summary{gap:8px 24px}
diff --git a/web/src/views/goauto/yeeke-returns/index.vue b/web/src/views/goauto/yeeke-returns/index.vue
index 5edf6d8..714a9c9 100644
--- a/web/src/views/goauto/yeeke-returns/index.vue
+++ b/web/src/views/goauto/yeeke-returns/index.vue
@@ -22,6 +22,11 @@
+
+
+
+
+
查询重置
@@ -61,9 +66,28 @@
{{ claimStatusMeta(row.claimStatus).label }}
+
+
+ 不可用(yeeke 列表中已不存在)
+ —
+
+
{{ formatTime(row.claimTime) }}
{{ formatTime(row.destroyDeadLine) }}
{{ formatTime(row.lastSyncedAt) }}
+
+ {{ matchStatusMeta(row.matchStatus).label }}
+
+
+
+
+ {{ row.occupyingSybOrderCode }} ↗
+ {{ row.occupyingSybStageLabel }}
+ 已过销毁截止
+
+ —
+
+
详情
@@ -139,8 +163,15 @@ export default {
loading: false, loadError: '', items: [], total: 0, createTimeRange: null,
summary: { lastSuccessAt: '', lastRun: null }, summaryError: '',
shopOptions: [],
- query: { page: 1, pageSize: 20, orderSn: '', shop: '', claimStatus: '', createTimeFrom: '', createTimeTo: '' },
+ query: { page: 1, pageSize: 20, orderSn: '', shop: '', claimStatus: '', createTimeFrom: '', createTimeTo: '', matchStatus: '' },
detail: { open: false, loading: false, placeholder: false, item: null, pkg: null, siblings: [] },
+ // #338: 匹配状态是只读信息筛选,未勾选/无写操作,按钮门禁规则里
+ // 「纯只读筛选可以照常展示」的部分——不需要 canPurchase 判断。
+ matchStatusOptions: [
+ { label: '未匹配', value: 'unmatched' },
+ { label: '退货待确认', value: 'matched' },
+ { label: '已用退货', value: 'confirmed' }
+ ],
// yeeke's only confirmed claim-status value is the raw string "1"
// (HAR evidence, #336 requirement doc). Any other value the sync sees
// is preserved verbatim and flagged status_unrecognized — never
@@ -159,8 +190,12 @@ export default {
// unmapped value we have not seen yet — show it as-is rather than a
// bare digit or a guessed Chinese label.
claimStatusMeta(status) { return status === '1' ? { label: '已认领', type: 'success' } : { label: status ? `其他:${status}` : '—', type: 'info' } },
+ matchStatusMeta(status) { return { unmatched: { label: '未匹配', type: 'info' }, matched: { label: '退货待确认', type: 'warning' }, confirmed: { label: '已用退货', type: 'success' }}[status] || { label: status || '—', type: 'info' } },
+ openSybProduct(sybProductId) { this.$router.push({ path: '/syb-products/index', query: { sybProductId } }) },
statusMeta(status) { return { running: { label: '执行中', type: 'primary' }, succeeded: { label: '成功', type: 'success' }, failed: { label: '失败', type: 'danger' }, interrupted: { label: '已中断', type: 'warning' }}[status] || { label: status || '-', type: 'info' } },
formatTime(value) { if (!value) return '—'; return new Date(value).toLocaleString('zh-CN', { hour12: false }) },
+ // #338: a matched return past its destroy deadline is flagged only, never auto-released.
+ isDeadlinePassed(value) { return Boolean(value) && new Date(value).getTime() <= Date.now() },
async load() {
this.loading = true; this.loadError = ''
try {
@@ -202,7 +237,7 @@ export default {
},
reset() {
this.createTimeRange = null
- this.query = { page: 1, pageSize: 20, orderSn: '', shop: '', claimStatus: '', createTimeFrom: '', createTimeTo: '' }
+ this.query = { page: 1, pageSize: 20, orderSn: '', shop: '', claimStatus: '', createTimeFrom: '', createTimeTo: '', matchStatus: '' }
this.load()
},
async openDetail(row) {
@@ -245,4 +280,5 @@ export default {
.item-title{font-size:15px;font-weight:600;color:#1f2937;margin-bottom:4px}
.muted{font-size:12px;color:#909399}
@media(max-width:800px){.page-heading{flex-direction:column}}
+.deadline-passed{color:var(--el-color-danger);font-size:12px}
diff --git a/web/src/views/goauto/yeeke-sync-runs/index.vue b/web/src/views/goauto/yeeke-sync-runs/index.vue
index 4eedeb7..0c23a25 100644
--- a/web/src/views/goauto/yeeke-sync-runs/index.vue
+++ b/web/src/views/goauto/yeeke-sync-runs/index.vue
@@ -41,6 +41,8 @@
+
+
{{ row.errorMessage || '—' }}