@@ -12,7 +12,7 @@
< el-form-item label = "订单号" > < el-input v-model = "query.orderCodesText" type="textarea" :autosize="{ minRows: 1, maxRows: 4 }" resize="none" class="order-codes-input" placeholder="每行一个订单号,最多 100 个" @keydown.ctrl.enter.prevent="search" / > < / el-form-item >
< el-form-item label = "解析状态" > < el-select v-model = "query.parseStatus" clearable placeholder="全部" style="width: 110px"><el-option label="成功" value="success" /><el-option label="失败" value="failed" / > < / el-select > < / el-form-item >
< el-form-item label = "处理阶段" > < el-select v-model = "query.processStage" clearable placeholder="全部" style="width: 132px"><el-option v-for="item in processStageOptions" :key="item.value" :label="item.label" :value="item.value" / > < / el-select > < / el-form-item >
< el-form-item class = "toolbar-actions" > < el-button type = "primary" :icon = "Search" @click ="search" > 查询 < / el -button > < el-button :icon = "RefreshLeft" @click ="reset" > 重置 < / el -button > < el-tooltip v-if = "canPurchase" :content="aiMatchButtonReason" :disabled="!aiMatchButtonReason" placement="top"><span class="action-button-wrap"><el-button :loading="specMatchLoading" :disabled="purchaseReadinessLoading || specMatchLoading || aiMatchCandidates.length === 0" @click="runBatchSpecMatch">AI 匹配<span class="action-count">{{ aiMatchCandidates.length }}</span></el-button></span></el-tooltip><el-button v-if="canPurchase" type="success" plain :disabled="imageSearchRows.length === 0" @click="openImageSearchBatch">图搜采集<span class="action-count">{{ imageSearchRows.length }}</span></el-button><el-button v-if="canPurchase" type="primary" :loading="purchaseReadinessLoading" :disabled="purchaseReadinessLoading || specMatchLoading || collectionCandidates.length === 0" @click="openCollectionBatch">创建采集<span class="action-count action-count-primary">{{ collectionCandidates.length }}</span></el-button><el-button v-if="canPurchase" type="primary" :loading="purchaseReadinessLoading" :disabled="purchaseReadinessLoading || specMatchLoading || purchaseCandidates.length === 0" @click="openPurchaseBatch">创建采购<span class="action-count action-count-primary" > {{ purchaseCandidates.length }} < / span > < / el -button > < / el-form-item >
< el-form-item class = "toolbar-actions" > < el-button type = "primary" :icon = "Search" @click ="search" > 查询 < / el -button > < el-button :icon = "RefreshLeft" @click ="reset" > 重置 < / el -button > < el-tooltip v-if = "canPurchase" :content="aiMatchButtonReason" :disabled="!aiMatchButtonReason" placement="top"><span class="action-button-wrap"><el-button :loading="specMatchLoading" :disabled="purchaseReadinessLoading || specMatchLoading || aiMatchCandidates.length === 0" @click="runBatchSpecMatch">AI 匹配<span class="action-count">{{ aiMatchCandidates.length }}</span></el-button></span></el-tooltip><el-button v-if="canPurchase" type="success" plain :disabled="imageSearchRows.length === 0" @click="openImageSearchBatch">图搜采集<span class="action-count">{{ imageSearchRows.length }}</span></el-button><el-button v-if="canPurchase" type="primary" :loading="purchaseReadinessLoading" :disabled="purchaseReadinessLoading || specMatchLoading || collectionCandidates.length === 0" @click="openCollectionBatch">创建采集<span class="action-count action-count-primary">{{ collectionCandidates.length }}</span></el-button><el-button v-if="canPurchase" type="primary" :loading="purchaseReadinessLoading" :disabled="purchaseReadinessLoading || specMatchLoading || purchaseCandidates.length === 0" @click="openPurchaseBatch">创建采购<span class="action-count action-count-primary">{{ purchaseCandidates.length }}</span></el-button><el-button v-if="canPurchase" :loading="returnMatchBatchLoading" :disabled="purchaseReadinessLoading || returnMatchBatchLoading || returnMatchCandidateIds.length === 0" @click="runBatchMatchReturns">匹配退货<span class="action-count" > {{ returnMatchCandidateIds.length }} < / span > < / el -button > < / el-form-item >
< / el-form >
< el-alert v-if = "canPurchase" title="先完成并保存规格匹配,商品才可以创建采购;AI 匹配只处理已解析规格、已关联 PDD 且具备完整可售 SKU 组合的明细。表头全选仅作用于当前页。" type="info" :closable="false" show-icon class="notice compact-notice" / >
< el-table ref = "productTable" v-loading = "loading" :data="products" row-key="id" border stripe empty-text="暂无 SYB 商品明细" @selection-change="handleSelectionChange" >
@@ -51,6 +51,28 @@
< div v-if = "quickBusyId === row.id || quickMessages[row.id]" class="muted" aria-live="polite" > {{ quickBusyId = = = row.id ? quickPhase : quickMessages [ row.id ] }} < / div >
< / template >
< / el -table -column >
< el-table-column label = "匹配到的退货商品" min -width = " 200 " >
< template # default = "{ row }" >
< template v-if = "returnMatchByProductId[row.id]" >
< div class = "return-match-cell" >
< el-image v-if = "returnMatchByProductId[row.id].image" :src="returnMatchByProductId[row.id].image" fit="cover" class="thumb" preview -teleported / >
< div >
< div > { { returnMatchByProductId [ row . id ] . orderSn || '—' } } < / div >
< div class = "muted" > { { returnMatchByProductId [ row . id ] . variationName || '—' } } < / div >
< div class = "muted" > 销毁截止 : { { formatMatchDeadline ( returnMatchByProductId [ row . id ] . destroyDeadline ) } } < / div >
< / div >
< / div >
< el-alert v-if = "returnMatchByProductId[row.id].syncStatus === 'missing'" title="退货已不在 yeeke 列表" type="error" :closable="false" show-icon class="return-missing-alert" / >
< el-alert v-if = "isDeadlinePassed(returnMatchByProductId[row.id].destroyDeadline)" title="退货已过销毁截止" type="error" :closable="false" show-icon class="return-missing-alert" / >
< div class = "quick-link-actions" >
< el-button type = "primary" link @click ="openMatchDetail(returnMatchByProductId[row.id].id)" > 查看对比 < / el -button >
< el-button v-if = "canPurchase" type="primary" link @click="openMatchDetail(returnMatchByProductId[row.id].id, true)" > 备注 < / el -button >
< el-button v-if = "canPurchase" type="danger" link @click="quickCancelMatch(returnMatchByProductId[row.id].id)" > 取消匹配 < / el -button >
< / div >
< / template >
< span v-else class = "muted" > 未匹配 < / span >
< / template >
< / el-table-column >
< el-table-column label = "操作" width = "132" fixed = "right" > < template # default = "{ row }" > < el-button type = "primary" link @click ="openDetail(row.id)" > 详情 < / el -button > < el-button v-if = "canPurchase && isPurchaseCandidate(row)" type="primary" link @click="openSinglePurchase(row)" > 采购 < / el -button > < / template > < / el-table-column >
< / el-table >
< pagination v-show = "total > 0" v-model:current-page="query.page" v-model:page-size="query.pageSize" :total="total" @pagination="load" / >
@@ -162,6 +184,82 @@
< template # footer > < el-button type = "primary" @click ="specMatchResult.open = false" > 关闭 < / el -button > < / template >
< / el-dialog >
<!-- 匹配退货批量结果 -- >
< el-dialog v-model = "returnMatchBatchResult.open" title="批量匹配退货结果" width="820px" >
< div class = "purchase-summary" aria -live = " polite " >
< span class = "success-text" > 成功匹配 < strong > { { returnMatchBatchResult . matchedCount } } < / strong > 条 < / span >
< span > 无候选 < strong > { { returnMatchBatchResult . noCandidateCount } } < / strong > 条 < / span >
< span > 阶段不参与跳过 < strong > { { returnMatchBatchResult . stageIneligibleCount } } < / strong > 条 < / span >
< span v-if = "returnMatchBatchResult.conflictCount" class="danger-text" > 并发冲突跳过 < strong > { { returnMatchBatchResult . conflictCount } } < / strong > 条 < / span >
< / div >
< el-table :data = "returnMatchBatchResult.items" border size = "small" max -height = " 420 " empty -text = " 没有匹配结果 " >
< el-table-column label = "SYB 商品 ID" prop = "sybProductId" width = "120" / >
< el-table-column label = "结果" width = "100" > < template # default = "{ row }" > < el-tag : type = "row.matched ? 'success' : 'warning'" > { { row . matched ? '已匹配' : '跳过' } } < / el-tag > < / template > < / el-table-column >
< el-table-column label = "原因" min -width = " 260 " > < template # default = "{ row }" > { { row . matched ? '—' : ( row . reason || matchReasonMeta ( row . reasonCode ) ) } } < / template > < / el-table-column >
< el-table-column label = "操作" width = "100" > < template # default = "{ row }" > < el-button v-if = "row.matched && row.matchId" type="primary" link @click="openMatchDetail(row.matchId)" > 查看对比 < / el -button > < / template > < / el-table-column >
< / el-table >
< template # footer > < el-button type = "primary" @click ="returnMatchBatchResult.open = false" > 关闭 < / el -button > < / template >
< / el-dialog >
<!-- 匹配详情对比 -- >
< el-dialog v-model = "matchDetail.open" title="退货匹配详情" width="900px" :close-on-click-modal="false" >
< div v-loading = "matchDetail.loading" >
< template v-if = "matchDetail.data" >
< el -alert : title = "'当前状态:' + matchStatusLabel(matchDetail.data.match.status)" : type = "matchDetail.data.match.status === 'confirmed' ? 'success' : matchDetail.data.match.status === 'cancelled' ? 'info' : 'warning'" :closable = "false" show -icon class = "notice" / >
< div class = "split" >
< div class = "split-col" >
< h3 class = "section-title" > SYB 商品 < / h3 >
< el-descriptions :column = "1" border size = "small" v-if = "matchDetail.data.syb" >
< el -descriptions -item label = "订单号" > { { matchDetail . data . syb . orderCode } } < / el-descriptions-item >
< el-descriptions-item label = "虾皮商品ID" > { { matchDetail . data . syb . shopeeItemId } } < / el-descriptions-item >
< el-descriptions-item label = "店铺" > { { matchDetail . data . syb . shopName } } < / el-descriptions-item >
< el-descriptions-item label = "标题" > { { matchDetail . data . syb . productTitle } } < / el-descriptions-item >
< el-descriptions-item label = "目标颜色/尺码" > { { matchDetail . data . syb . targetColor || '—' } } / { { matchDetail . data . syb . targetSize || '—' } } < / el-descriptions-item >
< el-descriptions-item label = "数量" > { { matchDetail . data . syb . quantity } } < / el-descriptions-item >
< el-descriptions-item label = "当前阶段" > < el-tag :type = "processMeta(matchDetail.data.syb.currentStage).type" > { { matchDetail . data . syb . currentStageLabel } } < / el-tag > < / el-descriptions-item >
< el-descriptions-item label = "归一化规格文字" > { { matchDetail . data . normalizedSybSpec } } < / el-descriptions-item >
< / el-descriptions >
< p v-else class = "muted" > SYB 商品不存在或已删除 < / p >
< / div >
< div class = "split-col" >
< h3 class = "section-title" > yeeke 退货商品 < / h3 >
< el-alert v-if = "matchDetail.data.yeeke && matchDetail.data.yeeke.syncStatus === 'missing'" title="退货已不在 yeeke 列表" type="error" :closable="false" show-icon class="notice" / >
< el-alert v-if = "matchDetail.data.yeeke && isDeadlinePassed(matchDetail.data.yeeke.destroyDeadline)" title="退货已过销毁截止,请核实退货是否仍在库" type="error" :closable="false" show-icon class="notice" / >
< el-descriptions :column = "1" border size = "small" v-if = "matchDetail.data.yeeke" >
< el -descriptions -item label = "退货订单号" > { { matchDetail . data . yeeke . orderSn } } < / el-descriptions-item >
< el-descriptions-item label = "商品ID" > { { matchDetail . data . yeeke . itemId } } < / el-descriptions-item >
< el-descriptions-item label = "规格ID" > { { matchDetail . data . yeeke . variationId } } < / el-descriptions-item >
< el-descriptions-item label = "店铺" > { { matchDetail . data . yeeke . shopName } } < / el-descriptions-item >
< el-descriptions-item label = "商品名" > { { matchDetail . data . yeeke . itemName } } < / el-descriptions-item >
< el-descriptions-item label = "规格文字" > { { matchDetail . data . yeeke . variationName } } < / el-descriptions-item >
< el-descriptions-item label = "数量" > { { matchDetail . data . yeeke . quantity } } < / el-descriptions-item >
< el-descriptions-item label = "销毁截止" > { { formatMatchDeadline ( matchDetail . data . yeeke . destroyDeadline ) } } < / el-descriptions-item >
< el-descriptions-item label = "归一化规格文字" > { { matchDetail . data . normalizedYeekeSpec } } < / el-descriptions-item >
< / el-descriptions >
< p v-else class = "muted" > 退货商品不存在或已删除 < / p >
< / div >
< / div >
< h3 class = "section-title" > 备注 < / h3 >
< el-input ref = "matchRemarkInput" v-model = "matchDetail.remarkText" type="textarea" :rows="2" maxlength="500" show-word-limit :disabled="!canPurchase" / >
< div class = "drawer-actions" >
< el-button v-if = "canPurchase" :loading="matchDetail.saving" @click="saveMatchRemark" > 保存备注 < / el -button >
< / div >
< h3 class = "section-title" > 操作日志 < / h3 >
< el-table :data = "matchDetail.data.logs" border size = "small" max -height = " 220 " empty -text = " 暂无日志 " >
< el-table-column label = "时间" width = "170" > < template # default = "{ row }" > { { formatMatchDeadline ( row . createdAt ) } } < / template > < / el-table-column >
< el-table-column label = "动作" width = "90" prop = "action" / >
< el-table-column label = "操作人" width = "100" prop = "operator" / >
< el-table-column label = "详情" min -width = " 200 " prop = "detail" / >
< / el-table >
< / template >
< / div >
< template # footer >
< el-button @click ="matchDetail.open = false" > 关闭 < / el -button >
< el-button v-if = "canPurchase && matchDetail.data && matchDetail.data.match.status === 'matched'" type="success" @click="confirmMatchDetail" > 确认 < / el -button >
< el-button v-if = "canPurchase && matchDetail.data && matchDetail.data.match.status !== 'cancelled'" type="danger" @click="cancelMatchDetail" > 取消匹配 < / el -button >
< / template >
< / el-dialog >
<!-- 详情 -- >
< el-drawer v-model = "detail.open" title="SYB 商品明细详情" size="960px" >
< div v-loading = "detail.loading" class="drawer-body" >
@@ -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 : 8 px ; flex - wrap : wrap ; margin : 4 px 0 }
. thumb { width : 44 px ; height : 44 px ; border - radius : 4 px ; object - fit : cover } . thumb . placeholder { display : flex ; align - items : center ; justify - content : center ; background : # f1f5f9 ; color : # 909399 ; font - size : 11 px }
. return - match - cell { display : flex ; gap : 8 px ; align - items : flex - start }
. link { color : # 1677 ff ; cursor : pointer }
. shopee - title { overflow : hidden ; text - overflow : ellipsis ; white - space : nowrap }
. notice { margin - bottom : 16 px }
. return - missing - alert { margin : 6 px 0 }
. compact - notice { margin - bottom : 12 px }
. purchase - summary { display : flex ; flex - wrap : wrap ; gap : 12 px 24 px ; margin - bottom : 16 px ; padding : 12 px 16 px ; border : 1 px solid # dbeafe ; border - radius : 8 px ; background : # f8fafc } . purchase - summary strong { font - variant - numeric : tabular - nums ; color : # 1 e40af } . success - text { color : # 166534 } . warning - text { color : # b45309 } . danger - text , . purchase - reason { color : # b91c1c } . purchase - reason { margin - top : 4 px ; font - size : 12 px ; line - height : 1.45 } . purchase - settings { margin - bottom : 12 px } . field - help { margin - left : 12 px ; color : # 909399 ; font - size : 12 px } . ellipsis { overflow : hidden ; text - overflow : ellipsis ; white - space : nowrap } . image - search - tag { margin - left : 6 px ; vertical - align : middle }
. spec - match - summary { gap : 8 px 24 px }