@@ -69,20 +104,62 @@
import { ElMessage } from 'element-plus'
import { Plus, RefreshLeft, Search } from '@element-plus/icons-vue'
import { createPddProduct, getPddProduct, listPddProducts, updatePddProduct } from '@/api/goauto/pdd-products'
+import { batchCreateCollectionTasks } from '@/api/goauto/collection-tasks'
+import { listCollectionRules } from '@/api/goauto/collection-rules'
+import { listDevices } from '@/api/goauto/devices'
let uid = 0
export default {
name: 'GoAutoPddProducts', setup() { return { Plus, RefreshLeft, Search } },
- data() { return { loading: false, products: [], total: 0, query: { page: 1, pageSize: 20, keyword: '', status: '' }, createDialog: { open: false, saving: false }, createData: { url: '' }, 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: '' }, 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' }] }} },
created() { this.load() },
methods: {
emptyEdit() { return { url: '', title: '', shopName: '', salesCount: null, reviewCount: null, status: 'active', specs: [] } },
- async load() { this.loading = true; try { const r = await listPddProducts(this.query); this.products = r.data.items; this.total = r.data.total } finally { this.loading = false } },
+ emptyBatch() { return { open: false, saving: false, step: 'confirm', products: [], options: { rules: [], devices: [] }, results: [], successCount: 0, failureCount: 0 } },
+ 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 } },
search() { this.query.page = 1; this.load() }, reset() { this.query = { page: 1, pageSize: 20, keyword: '', status: '' }; this.load() },
statusMeta(status) { return { pending: { label: '待完善', type: 'warning' }, active: { label: '正常', type: 'success' }, disabled: { label: '已停用', type: 'info' }}[status] || { label: status || '-', type: 'info' } },
countText(value) { return value === null || value === undefined ? '-' : Number(value).toLocaleString('zh-CN') }, roleLabel(role) { return { color: '颜色', size: '尺码', other: '其他' }[role] || role }, priceText(value) { return value === null || value === undefined ? '未填写' : `¥${(value / 100).toFixed(2)}` },
specsSummary(specs = []) { return specs.length ? specs.map(x => `${x.name} ${x.values.length}项`).join(';') : '待完善' },
priceSummary(specs = []) { const p = specs.filter(x => x.role === 'color').flatMap(x => x.values.map(v => v.priceCent).filter(v => v !== null && v !== undefined)); if (!p.length) return '-'; const min = Math.min(...p); const max = Math.max(...p); return min === max ? this.priceText(min) : `${this.priceText(min)}~${this.priceText(max)}` },
+ isProductSelectable(row) { return row.collectionSelectable === true },
+ collectionStateText(row) { return row.collectionSelectable ? '可创建采集任务' : (row.collectionDisabledReason || '当前不可创建采集任务') },
+ handleSelectionChange(rows) { this.selectedProducts = rows },
+ async openBatch() {
+ if (!this.selectedProducts.length) return
+ this.batch = { ...this.emptyBatch(), open: true, products: this.selectedProducts.map(item => ({ ...item })) }
+ this.batchData = { ruleId: null, deviceId: null }
+ try {
+ const [rules, devices] = await Promise.all([listCollectionRules({ page: 1, pageSize: 100 }), listDevices({ page: 1, pageSize: 100 })])
+ this.batch.options = { rules: rules.data.items, devices: devices.data.items }
+ if (!this.batch.options.rules.length) ElMessage.warning('没有可用采集规则,请先创建采集规则')
+ } catch (error) {
+ this.batch.open = false
+ throw error
+ }
+ this.$nextTick(() => this.$refs.batchForm?.clearValidate())
+ },
+ resetBatch() { this.batch = this.emptyBatch(); this.batchData = { ruleId: null, deviceId: null } },
+ async submitBatch() {
+ if (!await this.$refs.batchForm.validate().catch(() => false)) return
+ this.batch.saving = true
+ try {
+ const response = await batchCreateCollectionTasks({ requestId: crypto.randomUUID(), pddProductIds: this.batch.products.map(item => item.id), ruleId: this.batchData.ruleId, deviceId: this.batchData.deviceId || null })
+ const byID = new Map(this.batch.products.map(item => [item.id, item]))
+ this.batch.results = response.data.items.map(item => ({ ...byID.get(item.pddProductId), ...item }))
+ this.batch.successCount = response.data.successCount
+ this.batch.failureCount = response.data.failureCount
+ this.batch.step = 'result'
+ if (this.batch.failureCount) {
+ ElMessage.warning(`已创建 ${this.batch.successCount} 个,${this.batch.failureCount} 个未创建`)
+ } else {
+ ElMessage.success(`已创建 ${this.batch.successCount} 个采集任务`)
+ }
+ await this.load()
+ } finally {
+ this.batch.saving = false
+ }
+ },
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 } },
@@ -96,5 +173,5 @@ export default {
diff --git a/web/tests/e2e/pdd-product-module.spec.ts b/web/tests/e2e/pdd-product-module.spec.ts
index 5a5bf07..2b5abc6 100644
--- a/web/tests/e2e/pdd-product-module.spec.ts
+++ b/web/tests/e2e/pdd-product-module.spec.ts
@@ -9,6 +9,7 @@ const product = {
salesCount: 1200,
reviewCount: 36,
status: 'active',
+ collectionSelectable: true,
specs: [
{ name: '颜色分类', role: 'color', values: [{ name: '黑色', selectable: true, priceCent: 2590 }] },
{ name: '尺码', role: 'size', values: [{ name: 'XL', selectable: true }] },
@@ -62,3 +63,42 @@ test('PDD 商品列表、详情和编辑流程符合已确认原型', async ({ p
await expect.poll(() => updatePayload?.title).toBe('夏季防晒袖套(人工确认)');
expect(updatePayload.specs[0].values[0].priceCent).toBe(2590);
});
+
+test('PDD 商品当前页多选并批量创建独立采集任务', async ({ page, context }) => {
+ const second = { ...product, id: 2, goodsId: '719834019024', title: '第二件商品' };
+ const disabled = { ...product, id: 3, goodsId: '731370706977', title: '已停用商品', status: 'disabled', collectionSelectable: false, collectionDisabledReason: '商品已停用' };
+ let batchPayload: any;
+ await context.addCookies([{ name: 'Admin-Token', value: 'prototype-test-token', domain: 'localhost', path: '/' }]);
+ await page.route('**/api/**', async route => {
+ const url = new URL(route.request().url());
+ if (url.pathname.startsWith('/src/api/')) return route.continue();
+ if (url.pathname.endsWith('/api/v1/getinfo')) return route.fulfill({ json: { code: 200, data: { roles: ['admin'], name: '管理员', avatar: '', introduction: '', permissions: [] } } });
+ if (url.pathname.endsWith('/api/admin/v1/pdd-products')) return route.fulfill({ json: { code: 200, data: { items: [product, second, disabled], total: 3, page: 1, pageSize: 20 } } });
+ if (url.pathname.endsWith('/api/admin/v1/collection-rules')) return route.fulfill({ json: { code: 200, data: { items: [{ id: 9, name: 'PDD 商品详情采集' }], total: 1 } } });
+ if (url.pathname.endsWith('/api/admin/v1/devices')) return route.fulfill({ json: { code: 200, data: { items: [{ id: 4, name: 'samsung-SM-G9700', selectable: true }], total: 1 } } });
+ if (url.pathname.endsWith('/api/admin/v1/collection-tasks/batch')) {
+ batchPayload = route.request().postDataJSON();
+ return route.fulfill({ json: { code: 200, data: { successCount: 1, failureCount: 1, items: [{ pddProductId: 1, success: true, taskId: 101 }, { pddProductId: 2, success: false, code: 'PDD_PRODUCT_TASK_ACTIVE', message: '该商品已有待执行或执行中的任务' }] } } });
+ }
+ return route.fulfill({ json: { code: 200, data: [] } });
+ });
+
+ await page.goto('http://localhost:9527/#/pdd-products/index');
+ const rows = page.locator('.el-table__body-wrapper tbody tr');
+ await rows.nth(0).locator('.el-checkbox').click();
+ await rows.nth(1).locator('.el-checkbox').click();
+ await expect(rows.nth(2).getByRole('checkbox')).toBeDisabled();
+ await expect(page.getByText('已选择 2 条')).toBeVisible();
+
+ await page.getByRole('button', { name: '批量创建采集任务' }).click();
+ await page.getByLabel('采集规则').click();
+ await page.getByRole('option', { name: 'PDD 商品详情采集' }).click();
+ await page.getByRole('button', { name: '创建 2 个采集任务' }).click();
+
+ await expect.poll(() => batchPayload?.pddProductIds).toEqual([1, 2]);
+ expect(batchPayload.ruleId).toBe(9);
+ expect(batchPayload.deviceId).toBeNull();
+ await expect(page.getByText('创建完成:成功 1 个,失败 1 个')).toBeVisible();
+ await expect(page.getByText('采集任务 #101')).toBeVisible();
+ await expect(page.getByText('该商品已有待执行或执行中的任务')).toBeVisible();
+});