refactor(#51): compact SYB product list layout

This commit is contained in:
QiuSW
2026-08-20 15:42:15 +08:00
parent badb78fa68
commit d392ed54c4
2 changed files with 42 additions and 16 deletions
+11 -16
View File
@@ -5,10 +5,10 @@
<el-form :model="query" :inline="true" class="search-form" @submit.prevent="search">
<el-form-item><el-tooltip :disabled="isAdmin" content="只有管理员可以开始导入" placement="bottom"><span><el-button type="primary" :icon="Download" :disabled="!isAdmin" @click="openImport">从 SYB 导入</el-button></span></el-tooltip></el-form-item>
<el-form-item><el-button @click="$router.push('/syb-sync-runs/index')">同步记录</el-button></el-form-item>
<el-form-item><el-button @click="$router.push('/syb-shops/index')">店铺管理</el-button></el-form-item>
<el-form-item><span class="selection-count" aria-live="polite">已选择 <strong>{{ selectedProducts.length }}</strong> 条</span></el-form-item>
<el-form-item label="搜索"><el-input v-model="query.keyword" placeholder="货运单号、虾皮商品ID或商品标题" clearable @keyup.enter="search" /></el-form-item>
<el-form-item label="解析状态"><el-select v-model="query.parseStatus" clearable placeholder="全部" style="width: 140px"><el-option label="成功" value="success" /><el-option label="存疑" value="uncertain" /><el-option label="失败" value="failed" /></el-select></el-form-item>
<el-form-item><el-button type="primary" :icon="Search" @click="search">查询</el-button><el-button :icon="RefreshLeft" @click="reset">重置</el-button></el-form-item>
<el-form-item><el-button type="primary" :icon="Search" @click="search">查询</el-button><el-button :icon="RefreshLeft" @click="reset">重置</el-button><el-button :disabled="selectedProducts.length === 0" @click="openReparseBatch">重新解析</el-button></el-form-item>
</el-form>
<div v-if="activeRun" class="sync-banner" :class="`is-${activeRun.status}`" aria-live="polite">
<div>
@@ -19,23 +19,18 @@
</div>
<el-button type="primary" link @click="$router.push({ path: '/syb-sync-runs/index', query: { runId: activeRun.id }})">查看详情</el-button>
</div>
<div class="batch-toolbar" aria-live="polite">
<span>已选择 <strong>{{ selectedProducts.length }}</strong> 条</span>
<el-button :disabled="selectedProducts.length === 0" @click="openReparseBatch">重新解析</el-button>
<span class="muted">「批量创建采购任务」入口由 #44 注入本页,本模块只提供明细数据与勾选能力。</span>
</div>
<el-table ref="productTable" v-loading="loading" :data="products" row-key="id" border stripe empty-text="暂无 SYB 商品明细" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="48" />
<el-table-column label="参考图" width="76"><template #default="{ row }"><el-image v-if="row.imageUrl" :src="row.imageUrl" fit="cover" class="thumb" :preview-src-list="[row.imageUrl]" preview-teleported /><div v-else class="thumb placeholder">无图</div></template></el-table-column>
<el-table-column label="货运单号" prop="orderCode" min-width="150" />
<el-table-column label="虾皮商品" min-width="200"><template #default="{ row }"><a class="link" href="javascript:void(0)" @click="openShopeeDetail(row.shopeeProductId)">{{ row.shopeeItemId }} ↗</a><div class="muted">{{ row.productTitle || '未取到标题' }}</div></template></el-table-column>
<el-table-column label="货运单号" prop="orderCode" min-width="120" />
<el-table-column label="虾皮商品" min-width="421"><template #default="{ row }"><a class="link" href="javascript:void(0)" @click="openShopeeDetail(row.shopeeProductId)">{{ row.shopeeItemId }} ↗</a><div class="muted">{{ row.productTitle || '未取到标题' }}</div></template></el-table-column>
<el-table-column label="店铺" prop="shopName" min-width="100" />
<el-table-column label="目标颜色 / 尺码" min-width="160"><template #default="{ row }"><div :class="{ 'missing': !row.targetColor }">颜色:{{ row.targetColor || '—' }}</div><div :class="{ 'missing': !row.targetSize }">尺码:{{ row.targetSize || '—' }}</div></template></el-table-column>
<el-table-column label="数量" prop="quantity" width="70" />
<el-table-column label="目标颜色 / 尺码" min-width="128"><template #default="{ row }"><div :class="{ 'missing': !row.targetColor }">颜色:{{ row.targetColor || '—' }}</div><div :class="{ 'missing': !row.targetSize }">尺码:{{ row.targetSize || '—' }}</div></template></el-table-column>
<el-table-column label="数量" prop="quantity" width="56" />
<el-table-column label="售价" width="110"><template #default="{ row }">{{ priceText(row) }}</template></el-table-column>
<el-table-column label="虾皮关联" width="100"><template #default="{ row }"><el-tag :type="row.shopeeProductId ? 'success' : 'info'">{{ row.shopeeProductId ? '已关联' : '未关联' }}</el-tag></template></el-table-column>
<el-table-column label="解析状态" width="160"><template #default="{ row }"><el-tag :type="parseMeta(row.parseStatus).type">{{ parseMeta(row.parseStatus).label }}</el-tag><el-tag v-if="row.manuallyConfirmed" size="small" type="success" effect="plain" style="margin-left:4px">人工已确认</el-tag></template></el-table-column>
<el-table-column label="操作" width="90" fixed="right"><template #default="{ row }"><el-button type="primary" link @click="openDetail(row.id)">详情</el-button></template></el-table-column>
<el-table-column label="虾皮关联" width="80"><template #default="{ row }"><el-tag :type="row.shopeeProductId ? 'success' : 'info'">{{ row.shopeeProductId ? '已关联' : '未关联' }}</el-tag></template></el-table-column>
<el-table-column label="解析状态" width="80"><template #default="{ row }"><el-tag :type="parseMeta(row.parseStatus).type">{{ parseMeta(row.parseStatus).label }}</el-tag><el-tag v-if="row.manuallyConfirmed" size="small" type="success" effect="plain" style="margin-left:4px">人工已确认</el-tag></template></el-table-column>
<el-table-column label="操作" width="45" fixed="right"><template #default="{ row }"><el-button type="primary" link @click="openDetail(row.id)">详情</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" />
<p class="scope-note">本页不展示采购任务、PDD 订单号、快递单号和支付状态。</p>
@@ -304,9 +299,9 @@ export default {
.page-card { min-height: calc(100vh - 124px); }
.page-heading { display:flex;align-items:flex-start;justify-content:space-between;gap:16px;margin-bottom:16px;h1{margin:0 0 6px;font-size:24px;color:#1f2937}p{margin:0;color:#606266;line-height:1.5} }
.search-form{padding:16px 16px 0;margin-bottom:16px;border:1px solid #e5e7eb;border-radius:8px;background:#f8fafc}
.batch-toolbar{display:flex;align-items:center;gap:12px;min-height:48px;padding:8px 12px;margin-bottom:12px;border:1px solid #dbeafe;border-radius:8px;background:#f8fafc}
.selection-count{display:inline-flex;align-items:center;min-height:32px;white-space:nowrap;color:#606266}
.selection-count strong{margin:0 4px;font-variant-numeric:tabular-nums;color:#1e40af}
.sync-banner{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:12px 16px;margin-bottom:12px;border:1px solid #bfdbfe;border-radius:8px;background:#eff6ff}.sync-banner>div{display:flex;flex-wrap:wrap;gap:8px 16px}.sync-banner.is-failed,.sync-banner.is-interrupted{border-color:#fecaca;background:#fef2f2}.sync-banner.is-succeeded{border-color:#bbf7d0;background:#f0fdf4}
.batch-toolbar strong{font-variant-numeric:tabular-nums;color:#1e40af}
.primary{font-weight:600;color:#1f2937}.muted{font-size:12px;color:#909399}
.missing{color:#b91c1c}
.thumb{width:44px;height:44px;border-radius:4px;object-fit:cover}.thumb.placeholder{display:flex;align-items:center;justify-content:center;background:#f1f5f9;color:#909399;font-size:11px}
+31
View File
@@ -0,0 +1,31 @@
import { expect, test } from '@playwright/test'
test('SYB 商品工具栏与列宽按紧凑布局显示', async({ page, context }) => {
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/syb-products')) return route.fulfill({ json: { code: 200, data: { items: [{ id: 1, orderCode: 'SYB001', shopeeItemId: '26154802794', shopeeProductId: 9, productTitle: '用于验证加宽后的虾皮商品标题区域', shopName: '大碼臻選', targetColor: '黑色', targetSize: 'XL', quantity: 2, unitPriceCent: 1000, parseStatus: 'success', manuallyConfirmed: false }], total: 1, page: 1, pageSize: 20 }}})
return route.fulfill({ json: { code: 200, data: [] }})
})
await page.goto('http://localhost:9527/#/syb-products/index')
await expect(page.getByRole('button', { name: '店铺管理' })).toHaveCount(0)
await expect(page.getByText('已选择 0 条', { exact: true })).toBeVisible()
await expect(page.locator('.batch-toolbar')).toHaveCount(0)
const reset = await page.getByRole('button', { name: '重置' }).boundingBox()
const reparse = await page.getByRole('button', { name: '重新解析' }).boundingBox()
expect(reset).not.toBeNull(); expect(reparse).not.toBeNull()
expect(Math.abs((reset?.y || 0) - (reparse?.y || 0))).toBeLessThan(2)
expect(reparse?.x || 0).toBeGreaterThan((reset?.x || 0) + (reset?.width || 0))
await expect(page.getByRole('button', { name: '重新解析' })).toBeDisabled()
await page.locator('.el-table__body-wrapper tbody tr').first().locator('.el-checkbox').click()
await expect(page.getByText('已选择 1 条', { exact: true })).toBeVisible()
await expect(page.getByRole('button', { name: '重新解析' })).toBeEnabled()
const widths = await page.locator('.el-table__header-wrapper colgroup col').evaluateAll(cols => cols.map(col => Number(col.getAttribute('width'))))
expect(widths.slice(0, 11)).toEqual([48, 76, 120, 421, 100, 128, 56, 110, 80, 80, 45])
})