@@ -102,11 +120,19 @@ import { createPddProduct, getPddProduct, listPddProducts, updatePddProduct } fr
import { batchCreateCollectionTasks } from '@/api/goauto/collection-tasks'
import { listCollectionRules } from '@/api/goauto/collection-rules'
import { listDevices } from '@/api/goauto/devices'
+import { createStockPurchaseTask } from '@/api/goauto/purchase-tasks'
let uid = 0
export default {
name: 'GoAutoPddProducts', setup() { return { Plus, RefreshLeft, Search } },
- data() { return { loading: false, products: [], selectedProducts: [], total: 0, query: { page: 1, pageSize: 20, keyword: '', status: '' }, location: this.emptyLocation(), createDialog: { open: false, saving: false }, createData: { url: '' }, batch: this.emptyBatch(), batchData: { ruleId: null, deviceId: null }, batchRules: { ruleId: [{ required: true, message: '请选择采集规则', trigger: 'change' }] }, detail: { open: false, loading: false, product: null }, edit: { open: false, loading: false, saving: false, productId: null }, editData: this.emptyEdit(), urlRules: { url: [{ required: true, message: '请输入 PDD 商品链接', trigger: 'blur' }, { max: 4096, message: '链接不能超过 4096 个字符', trigger: 'blur' }] }} },
+ data() { return { loading: false, products: [], selectedProducts: [], total: 0, query: { page: 1, pageSize: 20, keyword: '', status: '' }, location: this.emptyLocation(), createDialog: { open: false, saving: false }, createData: { url: '' }, batch: this.emptyBatch(), batchData: { ruleId: null, deviceId: null }, batchRules: { ruleId: [{ required: true, message: '请选择采集规则', trigger: 'change' }] }, detail: { open: false, loading: false, product: null }, stock: this.emptyStock(), stockData: this.emptyStockData(), stockRules: { color: [{ required: true, message: '请选择颜色', trigger: 'change' }], size: [{ required: true, message: '请选择尺码', trigger: 'change' }], quantity: [{ required: true, message: '请输入数量', trigger: 'change' }], executionMode: [{ required: true, message: '请选择执行模式', trigger: 'change' }], minPriceYuan: [{ required: true, message: '请输入允许单价下限', trigger: 'change' }], maxPriceYuan: [{ required: true, message: '请输入允许单价上限', trigger: 'change' }] }, edit: { open: false, loading: false, saving: false, productId: null }, editData: this.emptyEdit(), urlRules: { url: [{ required: true, message: '请输入 PDD 商品链接', trigger: 'blur' }, { max: 4096, message: '链接不能超过 4096 个字符', trigger: 'blur' }] }} },
+ computed: {
+ canPurchase() { const roles = this.$store.getters.roles || []; return roles.includes('admin') || roles.includes('purchaser') },
+ stockColorOptions() { return this.stockSpecValues('color').filter(item => item.priceCent !== null && item.priceCent !== undefined && item.priceCent >= 0) },
+ stockSizeOptions() { return this.stockSpecValues('size') },
+ stockRequiresSize() { return (this.detail.product?.specs || []).some(item => item.role === 'size') },
+ canCreateStockPurchase() { return this.canPurchase && this.detail.product?.status === 'active' && this.stockColorOptions.length > 0 && (!this.stockRequiresSize || this.stockSizeOptions.length > 0) }
+ },
created() {
const productId = Number(this.$route.query.productId)
if (Number.isInteger(productId) && productId > 0) this.loadLocatedProduct(productId)
@@ -117,6 +143,9 @@ export default {
emptyLocation() { return { active: false, productId: null, goodsId: '', error: '' } },
emptyEdit() { return { url: '', title: '', shopName: '', salesCount: null, reviewCount: null, status: 'active', specs: [] } },
emptyBatch() { return { open: false, saving: false, step: 'confirm', products: [], options: { rules: [], devices: [] }, results: [], successCount: 0, failureCount: 0 } },
+ emptyStock() { return { open: false, saving: false, error: '' } },
+ emptyStockData() { return { color: '', size: '', quantity: 1, minPriceYuan: null, maxPriceYuan: null, executionMode: 'live' } },
+ stockSpecValues(role) { return (this.detail.product?.specs || []).filter(item => item.role === role).flatMap(item => item.values || []).filter(item => item.selectable && String(item.name || '').trim()).map(item => ({ ...item, name: String(item.name).trim() })) },
async load() { this.loading = true; this.selectedProducts = []; this.$refs.productTable?.clearSelection(); try { const r = await listPddProducts(this.query); this.products = r.data.items; this.total = r.data.total } finally { this.loading = false } },
async clearLocation() {
if (!this.location.active && !this.$route.query.productId) return
@@ -199,6 +228,20 @@ export default {
openCreate() { this.createDialog = { open: true, saving: false }; this.createData = { url: '' }; this.$nextTick(() => this.$refs.createForm?.clearValidate()) },
async createProduct() { if (!await this.$refs.createForm.validate().catch(() => false)) return; this.createDialog.saving = true; try { const r = await createPddProduct({ requestId: crypto.randomUUID(), url: this.createData.url.trim() }); ElMessage.success('PDD 商品已添加'); this.createDialog.open = false; await this.load(); await this.openDetail(r.data.product.id) } finally { this.createDialog.saving = false } },
async openDetail(id) { this.detail = { open: true, loading: true, product: null }; try { const r = await getPddProduct(id); this.detail.product = r.data.product } finally { this.detail.loading = false } },
+ openStockPurchase() { if (!this.canCreateStockPurchase) return; const color = this.stockColorOptions[0]; const price = color.priceCent / 100; this.stock = { open: true, saving: false, error: '' }; this.stockData = { color: color.name, size: this.stockSizeOptions[0]?.name || '', quantity: 1, minPriceYuan: price, maxPriceYuan: price, executionMode: 'live' }; this.$nextTick(() => this.$refs.stockForm?.clearValidate()) },
+ resetStockPurchase() { this.stock = this.emptyStock(); this.stockData = this.emptyStockData() },
+ onStockColorChange(name) { const color = this.stockColorOptions.find(item => item.name === name); if (!color) return; const price = color.priceCent / 100; this.stockData.minPriceYuan = price; this.stockData.maxPriceYuan = price; this.stock.error = '' },
+ async submitStockPurchase() {
+ if (!await this.$refs.stockForm.validate().catch(() => false)) return
+ const minUnitPriceCent = Math.round(this.stockData.minPriceYuan * 100); const maxUnitPriceCent = Math.round(this.stockData.maxPriceYuan * 100)
+ if (minUnitPriceCent < 0 || maxUnitPriceCent < minUnitPriceCent) { this.stock.error = '允许单价上限不能低于下限'; return }
+ this.stock.saving = true; this.stock.error = ''
+ try {
+ const response = await createStockPurchaseTask({ requestId: crypto.randomUUID(), executionMode: this.stockData.executionMode, pddProductId: this.detail.product.id, color: this.stockData.color, size: this.stockData.size || undefined, quantity: this.stockData.quantity, minUnitPriceCent, maxUnitPriceCent }, { suppressErrorMessage: true })
+ ElMessage.success('备货采购任务已创建'); this.stock.open = false; this.detail.open = false
+ await this.$router.push({ path: '/purchase-tasks/index', query: { taskId: response.data.id }})
+ } catch (error) { this.stock.error = error?.response?.data?.message || error?.response?.data?.msg || error?.message || '创建失败,请稍后重试' } finally { this.stock.saving = false }
+ },
async openEdit(id) { this.detail.open = false; this.edit = { open: true, loading: true, saving: false, productId: id }; try { const r = await getPddProduct(id); const p = r.data.product; this.editData = { url: p.url, title: p.title || '', shopName: p.shopName || '', salesCount: p.salesCount, reviewCount: p.reviewCount, status: p.status === 'disabled' ? 'disabled' : 'active', specs: (p.specs || []).map(d => ({ ...d, localId: ++uid, values: d.values.map(v => ({ ...v, localId: ++uid, priceYuan: v.priceCent === null || v.priceCent === undefined ? null : v.priceCent / 100 })) })) } } finally { this.edit.loading = false } },
addDimension() { this.editData.specs.push({ localId: ++uid, name: '', role: 'other', values: [] }) }, addValue(d) { d.values.push({ localId: ++uid, name: '', selectable: true, priceYuan: null }) },
move(items, index, offset) { const target = index + offset; if (target < 0 || target >= items.length) return; const [item] = items.splice(index, 1); items.splice(target, 0, item) },
@@ -209,5 +252,5 @@ export default {
diff --git a/web/src/views/goauto/purchase-tasks/index.vue b/web/src/views/goauto/purchase-tasks/index.vue
index fc40b8b..bafad54 100644
--- a/web/src/views/goauto/purchase-tasks/index.vue
+++ b/web/src/views/goauto/purchase-tasks/index.vue
@@ -5,6 +5,7 @@
重新加载
+
@@ -16,8 +17,9 @@
CG-{{ row.id }}
{{ modeLabel(row.executionMode) }}
+ {{ taskTypeLabel(row.taskType) }}
{{ row.shopeeOrderNoSnapshot || '—' }}
- {{ row.shopeeTitleSnapshot || `虾皮商品 ${row.shopeeItemIdSnapshot || '—'}` }}
SYB-{{ row.sybProductId || '—' }} · {{ row.shopeeShopNameSnapshot || '店铺未记录' }}
+ —
{{ row.shopeeTitleSnapshot || `虾皮商品 ${row.shopeeItemIdSnapshot || '—'}` }}
SYB-{{ row.sybProductId || '—' }} · {{ row.shopeeShopNameSnapshot || '店铺未记录' }}
{{ row.pddTitleSnapshot || '标题未记录' }}
goods_id {{ row.pddGoodsIdSnapshot || '—' }}
{{ specText(row.mappedColorSnapshot, row.mappedSizeSnapshot) }}
目标:{{ specText(row.targetColorSnapshot, row.targetSizeSnapshot) }} · {{ row.quantity }} 件
{{ row.deviceName || '尚未分派' }}
账号:{{ row.pddAccountRefSnapshot || '不强制记录' }}
@@ -33,12 +35,13 @@
- CG-{{ detail.task.id }}{{ statusLabel(detail.task.status) }}{{ modeLabel(detail.task.executionMode) }}
状态更新时间:{{ formatTime(detail.task.statusChangedAt) }}
+ CG-{{ detail.task.id }}{{ taskTypeLabel(detail.task.taskType) }}{{ statusLabel(detail.task.status) }}{{ modeLabel(detail.task.executionMode) }}
状态更新时间:{{ formatTime(detail.task.statusChangedAt) }}
+
任务快照
- SYB-{{ detail.task.sybProductId || '—' }}{{ detail.task.shopeeOrderNoSnapshot || '—' }}
- {{ detail.task.shopeeItemIdSnapshot || '—' }}{{ detail.task.shopeeShopNameSnapshot || '未记录' }}
- {{ detail.task.shopeeTitleSnapshot || '未记录' }}{{ detail.task.pddGoodsIdSnapshot || '—' }}
+ {{ taskTypeLabel(detail.task.taskType) }}{{ detail.task.taskType === 'stock' ? '—' : `SYB-${detail.task.sybProductId || '—'}` }}
+ {{ detail.task.taskType === 'stock' ? '—' : (detail.task.shopeeOrderNoSnapshot || '—') }}{{ detail.task.taskType === 'stock' ? '—' : (detail.task.shopeeItemIdSnapshot || '—') }}
+ {{ detail.task.taskType === 'stock' ? '—' : (detail.task.shopeeShopNameSnapshot || '未记录') }}{{ detail.task.taskType === 'stock' ? '—' : (detail.task.shopeeTitleSnapshot || '未记录') }}{{ detail.task.pddGoodsIdSnapshot || '—' }}
{{ detail.task.pddTitleSnapshot || '未记录' }}
{{ specText(detail.task.targetColorSnapshot, detail.task.targetSizeSnapshot) }}{{ specText(detail.task.mappedColorSnapshot, detail.task.mappedSizeSnapshot) }}
{{ detail.task.quantity }} 件{{ specSourceLabel(detail.task.specSource) }} 查看映射
@@ -50,7 +53,7 @@
订单{{ detail.task.pddOrderNo || '尚未取得订单号' }}{{ detail.task.orderSubmittedAt ? `下单时间 ${formatTime(detail.task.orderSubmittedAt)}` : '获取订单号失败即采购失败' }}
支付复核{{ paymentLabel(detail.task.paymentReviewStatus) }}{{ detail.task.paymentReviewedAt ? formatTime(detail.task.paymentReviewedAt) : '仅人工记录,系统不支付' }}
物流{{ logisticsLabel(detail.task.logisticsStatus) }}{{ detail.task.trackingNo || '尚未取得快递单号' }}
- SYB 回填{{ writebackLabel(detail.task.writebackStatus) }}{{ detail.task.writebackAt ? formatTime(detail.task.writebackAt) : '由独立回填流程处理' }}
+ SYB 回填{{ writebackLabel(detail.task.writebackStatus) }}{{ detail.task.writebackAt ? formatTime(detail.task.writebackAt) : '由独立回填流程处理' }}
执行记录
@@ -131,7 +134,7 @@ export default {
data() {
return {
loading: false, loadError: '', tasks: [], total: 0, statuses: statusOptions,
- query: { page: 1, pageSize: 20, taskId: '', shopeeOrderNo: '', status: '', executionMode: 'live', sybProductId: '', pddOrderNo: '' },
+ query: { page: 1, pageSize: 20, taskId: '', taskType: '', shopeeOrderNo: '', status: '', executionMode: 'live', sybProductId: '', pddOrderNo: '' },
detail: { open: false, loading: false, task: null, attempts: [] },
retrySelection: [],
retryDialog: { open: false, saving: false, items: [] },
@@ -145,13 +148,13 @@ export default {
detailAlert() {
const task = this.detail.task || {}
if (task.status === 'order_result_unknown') return { type: 'error', title: '订单结果未知,禁止重复采购', description: '请先到 PDD 人工核对订单,再在本页处理结果。' }
- if (task.status === 'order_created') return { type: 'success', title: '订单已创建', description: '后续支付复核、快递单号采集和 SYB 回填相互独立;GoAuto 不提供支付功能。' }
+ if (task.status === 'order_created') return { type: 'success', title: '订单已创建', description: task.taskType === 'stock' ? '后续支付复核和快递单号采集相互独立;GoAuto 不提供支付功能。' : '后续支付复核、快递单号采集和 SYB 回填相互独立;GoAuto 不提供支付功能。' }
if (task.status === 'failed') { const next = task.retryable ? '可返回列表勾选此任务并点击重试。' : (task.retryDisabledReason || '当前任务不能直接重试。'); return { type: 'error', title: '采购失败', description: `${task.errorMessage || '请查看执行记录。'} ${next}` } }
if (task.executionMode === 'rehearsal') return { type: 'info', title: '这是演练任务', description: '演练只验证流程,不允许提交订单。' }
return { type: 'info', title: '任务正在采购流程中', description: '设备串行执行,进入提交订单阶段后不能直接取消。' }
},
- canAuthorizeRepurchase() { const t = this.detail.task; return t && ['order_created', 'cancelled'].includes(t.status) && Boolean(t.pddOrderNo) && t.paymentReviewStatus !== 'paid' && !t.rePurchaseAuthorizedAt },
- canSelectWriteback() { const t = this.detail.task; return t && t.status === 'order_created' && t.paymentReviewStatus === 'paid' && t.writebackStatus === 'not_selected' },
+ canAuthorizeRepurchase() { const t = this.detail.task; return t && t.taskType !== 'stock' && ['order_created', 'cancelled'].includes(t.status) && Boolean(t.pddOrderNo) && t.paymentReviewStatus !== 'paid' && !t.rePurchaseAuthorizedAt },
+ canSelectWriteback() { const t = this.detail.task; return t && t.taskType !== 'stock' && t.status === 'order_created' && t.paymentReviewStatus === 'paid' && t.writebackStatus === 'not_selected' },
canCancel() { const t = this.detail.task; return t && !['running', 'order_submit_started', 'cancelled'].includes(t.status) },
canSubmitRepurchase() { return this.repurchase.reason.trim() && this.repurchase.confirmText === '允许重新采购' },
canSubmitUnknown() { return this.unknown.status === 'order_created' ? this.unknown.pddOrderNo.trim() && this.unknown.orderSubmittedAt : this.unknown.reason.trim() },
@@ -173,21 +176,23 @@ 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: '', shopeeOrderNo: '', status: '', executionMode: 'live', sybProductId: '', pddOrderNo: '' }; this.load() },
+ reset() { this.query = { page: 1, pageSize: 20, taskId: '', taskType: '', 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 || '—' },
statusType(value) { return { pending: 'info', spec_probe_pending: 'warning', running: 'warning', rehearsal_completed: 'success', order_submit_started: 'warning', order_created: 'success', order_result_unknown: 'danger', failed: 'danger', cancelled: 'info' }[value] || 'info' },
+ taskTypeLabel(value) { return value === 'stock' ? '备货采购' : '订单采购' },
+ taskTypeTag(value) { return value === 'stock' ? 'warning' : 'primary' },
modeLabel(value) { return value === 'live' ? '正式' : '演练' },
paymentLabel(value) { return { pending: '待人工复核', paid: '已支付', unpaid: '未支付' }[value] || '未记录' },
logisticsLabel(value) { return { pending: '待获取快递单号', collected: '已获取快递单号', failed: '获取失败' }[value] || '未开始' },
writebackLabel(value) { return { not_selected: '未选为回填候选', pending: '等待回填', running: '正在回填', succeeded: '回填成功', failed: '回填失败' }[value] || '未开始' },
- specSourceLabel(value) { return { manual: '人工映射', ai: 'AI 决策', exact_match: '精确匹配' }[value] || value || '未记录' },
+ specSourceLabel(value) { return { manual: '人工映射', ai: 'AI 决策', exact_match: '精确匹配', direct_select: '直接选择' }[value] || value || '未记录' },
specText(color, size) { return [color, size].filter(Boolean).join(' / ') || '未记录' },
priceText(cents, currency) { if (cents === null || cents === undefined) return '—'; return `${currency || ''} ${(cents / 100).toFixed(2)}` },
formatTime(value) { if (!value) return '—'; return new Date(value).toLocaleString('zh-CN', { hour12: false }) },
requestId() { return window.crypto.randomUUID() },
- isRetrySelectable(row) { return row.status === 'failed' && row.retryable === true },
+ isRetrySelectable(row) { return row.taskType !== 'stock' && row.status === 'failed' && row.retryable === true },
retryRowClass({ row }) { return row.status === 'failed' && !row.retryable ? 'retry-disabled-row' : '' },
handleRetrySelection(rows) { this.retrySelection = rows },
openRetryDialog() { if (!this.retrySelection.length) return; this.retryDialog = { open: true, saving: false, items: [...this.retrySelection] } },