@@ -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-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-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 || aiMatchButtonOverLimit " @click="runBatchSpecMatch">AI 匹配<span class="action-count">{{ aiMatchCandidates.length }}</span></el-button></span></el-tooltip><el-tooltip v-if="canPurchase" :content="imageSearchButtonReason" :disabled="!imageSearchButtonReason" placement="top"><span class="action-button-wrap"><el-button type="success" plain :disabled="imageSearchRows.length === 0 || imageSearchButtonOverLimit " @click="openImageSearchBatch">图搜采集<span class="action-count">{{ imageSearchRows.length }}</span></el-button></span></el-tooltip><el-tooltip v-if="canPurchase" :content="collectionButtonReason" :disabled="!collectionButtonReason" placement="top"><span class="action-button-wrap"><el-button type="primary" :loading="purchaseReadinessLoading" :disabled="purchaseReadinessLoading || specMatchLoading || collectionCandidates.length === 0 || collectionButtonOverLimit " @click="openCollectionBatch">创建采集<span class="action-count action-count-primary">{{ collectionCandidates.length }}</span></el-button></span></el-tooltip><el-tooltip v-if="canPurchase" :content="purchaseButtonReason" :disabled="!purchaseButtonReason" placement="top"><span class="action-button-wrap"><el-button type="primary" :loading="purchaseReadinessLoading" :disabled="purchaseReadinessLoading || specMatchLoading || purchaseCandidates.length === 0 || purchaseButtonOverLimit " @click="openPurchaseBatch">创建采购<span class="action-count action-count-primary">{{ purchaseCandidates.length }}</span></el-button></span></el-tooltip><el-tooltip v-if="canPurchase" :content="returnMatchButtonReason" :disabled="!returnMatchButtonReason" placement="top"><span class="action-button-wrap"><el-button :loading="returnMatchBatchLoading" :disabled="purchaseReadinessLoading || returnMatchBatchLoading || returnMatchCandidateIds.length === 0 || returnMatchButtonOverLimit " @click="runBatchMatchReturns">匹配退货<span class="action-count" > {{ returnMatchCandidateIds.length }} < / span > < / el -button > < / span > < / el-tooltip > < / 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" >
@@ -73,7 +73,7 @@
< / 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" / >
< pagination v-show = "total > 0" v-model:current-page="query.page" v-model:page-size="query.pageSize" :page-sizes="[20, 50, 100, 200, 500]" :total="total" @pagination="load" / >
< / el-card >
< el-dialog v-model = "quickPicker.open" :title="quickPicker.row ? '选择采集手机' : '切换采集手机'" width="460px" :close-on-click-modal="false" @close="quickCancelPicker" >
@@ -329,6 +329,12 @@ import PddProductDetailDrawer from '../pdd-products/PddProductDetailDrawer.vue'
/ / 与 服 务 端 i m a g e _ s e a r c h . g o 的 i m a g e S e a r c h M a x B a t c h T a s k s 保 持 一 致 ( 单 批 去 重 后 任 务 数 上 限 , # 2 8 0 ) 。
const IMAGE _SEARCH _MAX _BATCH _TASKS = 50
/ / # 3 3 9 : 各 批 量 按 钮 的 服 务 端 上 限 , 用 于 勾 选 数 超 限 时 禁 用 按 钮 并 提 示 原 因 。
/ / 创 建 采 购 / A I 匹 配 : p u r c h a s e / b a t c h . g o m a x B a t c h P u r c h a s e I t e m s ; 创 建 采 集 ( 去 重 后 P D D 商 品 数 ) :
/ / t a s k / a d m i n _ s e r v i c e . g o B a t c h C r e a t e 的 1 0 0 上 限 ; 匹 配 退 货 : r e t u r n m a t c h / s e r v i c e . g o m a x B a t c h M a t c h I t e m s 。
const PURCHASE _BATCH _MAX = 100
const COLLECTION _BATCH _MAX = 100
const RETURN _MATCH _BATCH _MAX = 500
export default {
name : 'GoAutoSybProducts' ,
@@ -349,7 +355,7 @@ export default {
imageSearchBatch : this . emptyImageSearchBatch ( ) ,
collectionBatchData : { ruleId : null , deviceId : null } ,
collectionBatchRules : { ruleId : [ { required : true , message : '请选择采集规则' , trigger : 'change' } ] } ,
query : { page : 1 , pageSize : 20 , shopName : '' , orderCodesText : '' , parseStatus : '' , processStage : '' } ,
query : { page : 1 , pageSize : 100 , shopName : '' , orderCodesText : '' , parseStatus : '' , processStage : '' } ,
returnMatchByProductId : { } ,
returnMatchLoading : false ,
returnMatchBatchLoading : false ,
@@ -368,20 +374,34 @@ export default {
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 ) } ,
aiMatchButtonOverLimit ( ) { return this . aiMatchCandidates . length > PURCHASE _BATCH _MAX } ,
aiMatchButtonReason ( ) {
if ( this . purchaseReadinessLoading ) return '正在检查 AI 匹配资格'
if ( ! this . selectedProducts . length ) return '请先勾选当前页中可处理的明细'
if ( this . aiMatchButtonOverLimit ) return ` 一次最多 ${ PURCHASE _BATCH _MAX } 条 `
if ( this . aiMatchCandidates . length ) return ''
const reasons = [ ... new Set ( this . selectedProducts . map ( row => this . purchaseReady ( row ) . aiMatchDisabledReason ) . filter ( Boolean ) ) ]
return reasons [ 0 ] || '所选明细不满足 AI 匹配前提'
} ,
purchaseCandidates ( ) { return this . selectedProducts . filter ( row => this . isPurchaseCandidate ( row ) ) } ,
/ / # 3 3 9 : 创 建 采 购 上 限 保 持 1 0 0 ( 服 务 端 m a x B a t c h P u r c h a s e I t e m s 不 放 宽 ) , 超 出 勾 选 数 时 禁 用 并 提 示 。
purchaseButtonOverLimit ( ) { return this . purchaseCandidates . length > PURCHASE _BATCH _MAX } ,
purchaseButtonReason ( ) { return this . purchaseButtonOverLimit ? ` 一次最多 ${ PURCHASE _BATCH _MAX } 条 ` : '' } ,
collectionCandidateRows ( ) { return this . selectedProducts . filter ( row => this . isCollectionCandidate ( row ) ) } ,
collectionCandidates ( ) { return [ ... new Set ( this . collectionCandidateRows . map ( row => this . purchaseReady ( row ) . pddProductId ) ) ] } ,
/ / # 3 3 9 : 创 建 采 集 按 去 重 后 的 P D D 商 品 数 计 数 , 与 服 务 端 t a s k . B a t c h C r e a t e 的 1 0 0 上 限 口 径 一 致 。
collectionButtonOverLimit ( ) { return this . collectionCandidates . length > COLLECTION _BATCH _MAX } ,
collectionButtonReason ( ) { return this . collectionButtonOverLimit ? ` 一次最多 ${ COLLECTION _BATCH _MAX } 个 PDD 商品 ` : '' } ,
imageSearchRows ( ) { return this . selectedProducts . filter ( row => row . shopeeProductId && row . imageUrl ) } ,
/ / 服 务 端 按 去 重 后 的 蝦 皮 商 品 数 创 建 任 务 ( 见 i m a g e _ s e a r c h . g o 的 i m a g e S e a r c h M a x B a t c h T a s k s ) ,
/ / 这 里 用 相 同 口 径 预 估 , 避 免 提 交 后 才 发 现 超 限 。
imageSearchMaxBatchTasks ( ) { return IMAGE _SEARCH _MAX _BATCH _TASKS } ,
imageSearchButtonTaskCount ( ) { return new Set ( this . imageSearchRows . map ( row => row . shopeeProductId ) ) . size } ,
imageSearchButtonOverLimit ( ) { return this . imageSearchButtonTaskCount > IMAGE _SEARCH _MAX _BATCH _TASKS } ,
imageSearchButtonReason ( ) { return this . imageSearchButtonOverLimit ? ` 一次最多 ${ IMAGE _SEARCH _MAX _BATCH _TASKS } 个蝦皮商品 ` : '' } ,
/ / # 3 3 9 : 匹 配 退 货 服 务 端 上 限 ( r e t u r n m a t c h . m a x B a t c h M a t c h I t e m s ) , 超 出 直 接 勾 选 数 即 禁 用 。
returnMatchButtonOverLimit ( ) { return this . returnMatchCandidateIds . length > RETURN _MATCH _BATCH _MAX } ,
returnMatchButtonReason ( ) { return this . returnMatchButtonOverLimit ? ` 一次最多 ${ RETURN _MATCH _BATCH _MAX } 条 ` : '' } ,
imageSearchBatchTaskCount ( ) { return new Set ( this . imageSearchBatch . rows . map ( row => row . shopeeProductId ) ) . size } ,
imageSearchBatchOverLimit ( ) { return this . imageSearchBatchTaskCount > IMAGE _SEARCH _MAX _BATCH _TASKS } ,
imageSearchBatchDurationText ( ) {
@@ -428,7 +448,7 @@ export default {
}
} ,
search ( ) { this . query . page = 1 ; this . load ( ) } ,
reset ( ) { this . query = { page : 1 , pageSize : 20 , shopName : '' , orderCodesText : '' , parseStatus : '' , processStage : '' } ; this . load ( ) } ,
reset ( ) { this . query = { page : 1 , pageSize : 100 , shopName : '' , orderCodesText : '' , parseStatus : '' , processStage : '' } ; this . load ( ) } ,
normalizeShopName ( value ) { return String ( value || '' ) . normalize ( 'NFKC' ) . trim ( ) . toLocaleLowerCase ( ) } ,
async ensureShopOptions ( ) {
if ( this . shopOptionsLoaded ) return
@@ -473,16 +493,29 @@ export default {
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' } , 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 ] || '' } ,
/ / # 3 3 9 : t h e p a g e c a n n o w s h o w u p t o 5 0 0 r o w s , b u t t h e p r e v i e w e n d p o i n t
/ / ( p r e v i e w P u r c h a s e T a s k s - > p u r c h a s e / b a t c h . g o m a x B a t c h P u r c h a s e I t e m s ) s t i l l
/ / c a p s a s i n g l e r e q u e s t a t 1 0 0 i d s , s o t h i s s p l i t s t h e c u r r e n t p a g e i n t o
/ / s e q u e n t i a l c h u n k s o f < = 1 0 0 a n d m e r g e s t h e i r r e s u l t s . A n y c h u n k f a i l i n g
/ / f a l l s b a c k t o t h e s a m e p a g e - w i d e f a i l u r e s t a t e t h e s i n g l e - r e q u e s t p a t h
/ / u s e d b e f o r e , s o r o w s n e v e r e n d u p i n a p a r t i a l l y - u p d a t e d m i x o f r e a l
/ / a n d f a i l e d r e a d i n e s s .
async loadPurchaseReadiness ( ids , requestOptions = { } , generation = this . loadGeneration , selectedIDsOverride = null ) {
if ( generation !== this . loadGeneration ) return
const selectedIDs = selectedIDsOverride || new Set ( this . selectedProducts . map ( row => row . id ) )
this . purchaseReadiness = { }
if ( ! this . canPurchase || ! ids . length ) { this . purchaseReadinessLoading = false ; return }
this . purchaseReadinessLoading = true
const chunkSize = 100
const merged = { }
try {
const r = await previewPurchaseTasks ( { sybProductIds : ids } , { ... requestOptions , suppressErrorMessage : true } )
for ( let start = 0 ; start < ids . length ; start += chunkSize ) {
const chunk = ids . slice ( start , start + chunkSize )
const r = await previewPurchaseTasks ( { sybProductIds : chunk } , { ... requestOptions , suppressErrorMessage : true } )
if ( generation !== this . loadGeneration ) return
this . purchaseReadiness = Object . fromEntries ( r . data . items . map ( item => [ item . sybProductId , item ] ) )
for ( const item of r . data . items ) merged [ item . sybProductId ] = item
}
this . purchaseReadiness = merged
await this . $nextTick ( )
const valid = this . products . filter ( row => selectedIDs . has ( row . id ) && this . isSelectableCandidate ( row ) )
this . $refs . productTable ? . clearSelection ( )