|
|
|
@@ -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">
|
|
|
|
@@ -75,7 +75,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">
|
|
|
|
@@ -333,6 +333,12 @@ import PddProductDetailDrawer from '../pdd-products/PddProductDetailDrawer.vue'
|
|
|
|
|
|
|
|
|
|
// 与服务端 image_search.go 的 imageSearchMaxBatchTasks 保持一致(单批去重后任务数上限,#280)。
|
|
|
|
|
const IMAGE_SEARCH_MAX_BATCH_TASKS = 50
|
|
|
|
|
// #339: 各批量按钮的服务端上限,用于勾选数超限时禁用按钮并提示原因。
|
|
|
|
|
// 创建采购 / AI 匹配:purchase/batch.go maxBatchPurchaseItems;创建采集(去重后 PDD 商品数):
|
|
|
|
|
// task/admin_service.go BatchCreate 的 100 上限;匹配退货:returnmatch/service.go maxBatchMatchItems。
|
|
|
|
|
const PURCHASE_BATCH_MAX = 100
|
|
|
|
|
const COLLECTION_BATCH_MAX = 100
|
|
|
|
|
const RETURN_MATCH_BATCH_MAX = 500
|
|
|
|
|
|
|
|
|
|
export default {
|
|
|
|
|
name: 'GoAutoSybProducts',
|
|
|
|
@@ -353,7 +359,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,
|
|
|
|
@@ -372,20 +378,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)) },
|
|
|
|
|
// #339: 创建采购上限保持 100(服务端 maxBatchPurchaseItems 不放宽),超出勾选数时禁用并提示。
|
|
|
|
|
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))] },
|
|
|
|
|
// #339: 创建采集按去重后的 PDD 商品数计数,与服务端 task.BatchCreate 的 100 上限口径一致。
|
|
|
|
|
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) },
|
|
|
|
|
// 服务端按去重后的蝦皮商品数创建任务(见 image_search.go 的 imageSearchMaxBatchTasks),
|
|
|
|
|
// 这里用相同口径预估,避免提交后才发现超限。
|
|
|
|
|
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} 个蝦皮商品` : '' },
|
|
|
|
|
// #339: 匹配退货服务端上限(returnmatch.maxBatchMatchItems),超出直接勾选数即禁用。
|
|
|
|
|
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() {
|
|
|
|
@@ -432,7 +452,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
|
|
|
|
@@ -477,16 +497,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] || '' },
|
|
|
|
|
// #339: the page can now show up to 500 rows, but the preview endpoint
|
|
|
|
|
// (previewPurchaseTasks -> purchase/batch.go maxBatchPurchaseItems) still
|
|
|
|
|
// caps a single request at 100 ids, so this splits the current page into
|
|
|
|
|
// sequential chunks of <=100 and merges their results. Any chunk failing
|
|
|
|
|
// falls back to the same page-wide failure state the single-request path
|
|
|
|
|
// used before, so rows never end up in a partially-updated mix of real
|
|
|
|
|
// and failed readiness.
|
|
|
|
|
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 })
|
|
|
|
|
if (generation !== this.loadGeneration) return
|
|
|
|
|
this.purchaseReadiness = Object.fromEntries(r.data.items.map(item => [item.sybProductId, item]))
|
|
|
|
|
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
|
|
|
|
|
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()
|
|
|
|
@@ -826,7 +859,7 @@ export default {
|
|
|
|
|
.search-form{display:flex;flex-wrap:nowrap;align-items:center;column-gap:8px;padding:12px 16px 0;margin-bottom:16px;border:1px solid #e5e7eb;border-radius:8px;background:#f8fafc}
|
|
|
|
|
.search-form :deep(.el-form-item){margin-right:0;margin-bottom:12px}
|
|
|
|
|
.search-form :deep(.el-form-item__content){flex-wrap:nowrap}
|
|
|
|
|
.shop-input{width:130px}.order-codes-input{width:210px}.unlinked-tag{margin-left:8px}
|
|
|
|
|
.shop-input{width:130px}.order-codes-input{width:168px}.unlinked-tag{margin-left:8px}
|
|
|
|
|
.shop-option{display:flex;align-items:center;justify-content:space-between;gap:12px}
|
|
|
|
|
.shop-option-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
|
|
|
.toolbar-actions :deep(.el-form-item__content){gap:8px}
|
|
|
|
|