feat(需求): ①全站分页pageSize统一默认10+10/20/50/100选项(9页默认值15/20→10+6个兜底常量统一+撞款台账升级OldPagination+商品类目树顶层标准分页) ②修复6页OldPagination缺import分页器不渲染(不符合ASIN/查询ASIN/最低价ASIN/生成记录/店铺密钥/店铺管理)+查询ASIN操作列按钮间距+最低价范围筛选number类型报错 ③e2e断言对齐现状(顶栏h1/移除收起按钮后规格同步)+国家显示中文断言固化

This commit is contained in:
2026-09-07 02:36:35 +08:00
parent 056abcd473
commit 678605e9fb
44 changed files with 590 additions and 175 deletions
+42
View File
@@ -0,0 +1,42 @@
import { chromium } from '@playwright/test'
const browser = await chromium.launch({ channel: 'msedge', headless: true })
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } })
const errors = []
page.on('pageerror', (e) => errors.push('pageerror: ' + e.message))
page.on('console', (m) => { if (m.type() === 'error') errors.push('console: ' + m.text().slice(0, 200)) })
const userReqs = []
page.on('request', (r) => { if (r.url().includes('/api/admin/users')) userReqs.push(r.url()) })
await page.goto('http://localhost:5174/admin-vue/', { waitUntil: 'networkidle' })
await page.waitForTimeout(800)
if (await page.locator('#loginUsername').isVisible().catch(() => false)) {
await page.locator('#loginUsername').fill('admin')
await page.locator('#loginPassword').fill('admin123')
await page.locator('.btn-login').click()
await page.waitForSelector('.admin-shell', { timeout: 25000 }).catch(() => {})
await page.waitForTimeout(1500)
}
await page.goto('http://localhost:5174/admin-vue/account/users', { waitUntil: 'domcontentloaded' })
await page.waitForTimeout(2200)
const selects = page.locator('.users-filter-row .el-select')
console.log('筛选行 el-select 数量:', await selects.count())
await selects.nth(1).click()
await page.waitForTimeout(700)
const items = page.locator('.el-select-dropdown__item:visible')
console.log('选项:', JSON.stringify(await items.allTextContents()))
await items.filter({ hasText: /^管理员$/ }).first().click()
await page.waitForTimeout(1200)
console.log('change 后请求:', JSON.stringify(userReqs.slice(-1)))
await page.locator('.users-filter-row button.btn').first().click()
await page.waitForTimeout(2500)
console.log('点查询后请求:', JSON.stringify(userReqs.slice(-1)))
console.log('选中显示:', (await selects.nth(1).textContent())?.trim())
console.log('最近 users 请求:', JSON.stringify(userReqs.slice(-2)))
const roles = await page.locator('.table-scroll tbody tr td').nth(2).textContent().catch(() => '?')
console.log('第一行角色:', roles?.trim())
await page.goto('http://localhost:5174/admin-vue/asin-center/skip-price', { waitUntil: 'domcontentloaded' })
await page.waitForTimeout(2000)
await page.locator('.price-range-row input').first().fill('1.15')
await page.locator('.skip-filter-row button.btn').first().click()
await page.waitForTimeout(2500)
console.log('errors:', JSON.stringify(errors))
await browser.close()
+3 -3
View File
@@ -5,7 +5,7 @@ import { expect, test, type Page } from '@playwright/test'
async function openAdmin(page: Page) {
await page.goto('/admin-vue/')
await expect(page.locator('main h2')).toHaveText('用户管理')
await expect(page.locator('.admin-topbar h1')).toHaveText('用户管理')
}
test('test_task_249_shell_light_normal_primary_path', async ({ page }) => {
@@ -40,8 +40,8 @@ test('test_task_249_shell_light_invalid_input_rejected', async ({ page }) => {
page.on('pageerror', (error) => errors.push(error.message))
await openAdmin(page)
await page.goto('/admin-vue/account/menus')
await expect(page.locator('main h2')).toHaveText('菜单管理')
await expect(page.locator('.admin-topbar h1')).toHaveText('菜单管理')
await page.goto('/admin-vue/account/groups')
await expect(page.locator('main h2')).toHaveText('数据权限分组')
await expect(page.locator('.admin-topbar h1')).toHaveText('数据权限分组')
expect(errors).toEqual([])
})
+3 -3
View File
@@ -32,13 +32,13 @@ test('test_dup_console_render_ledger_tab_normal_variant_input', async ({ page })
await expect(page.locator('.dup-head h2')).toHaveText('店铺数据撞款监控')
await page.getByRole('button', { name: /全部ASIN台账/ }).click()
await expect(page.locator('table.ledger tbody tr').first()).toBeVisible({ timeout: 15000 })
await expect(page.locator('.pager')).toBeVisible()
await expect(page.locator('.old-pagination')).toBeVisible()
expect(errors).toEqual([])
await page.locator('.table-wrap').screenshot({ path: 'test-results/dup-console-admin-ledger.jpg' })
})
test('test_dup_console_palette_matches_reference_tokens', async ({ page }) => {
// reference 设计令牌在页面落定:主蓝 #2E5BE6、红 #D63A4A、弱红底、单元底色等。
// reference 设计令牌在页面落定:主蓝 #2E5BE6、红 #D63A4A、弱红底、单元底色等(按钮蓝白统一为 #4f78a5
await page.goto('/admin-vue/shop-center/duplicate-check')
await expect(page.locator('.kpi')).toHaveCount(7)
const got = await page.evaluate(() => {
@@ -60,7 +60,7 @@ test('test_dup_console_palette_matches_reference_tokens', async ({ page }) => {
kDotDanger: cs('.kpi.danger .k-dot', 'backgroundColor'),
}
})
expect(got.btnPrimary).toBe('rgb(46, 91, 230)')
expect(got.btnPrimary).toBe('rgb(79, 120, 165)') // 全站按钮主色统一 #4f78a5d385d999 起)
expect(got.btnPrimaryColor).toBe('rgb(255, 255, 255)')
expect(got.kpiDanger).toBe('rgb(214, 58, 74)')
expect(got.kpiPrimary).toBe('rgb(46, 91, 230)')
@@ -1,5 +1,8 @@
import { expect, test, type Page } from '@playwright/test'
// 响应式壳层对齐现状(task-276 已移除侧边栏收起按钮/is-collapsed 机制):
// 断言主题改为「窄屏不破版、核心地标与标题可用」。
async function open(page: Page, width: number, path = '/admin-vue/') {
await page.setViewportSize({ width, height: 900 })
await page.goto(path)
@@ -7,60 +10,51 @@ async function open(page: Page, width: number, path = '/admin-vue/') {
test('test_task_017_desktop_shell_responsive_normal_primary_path', async ({ page }) => {
await open(page, 1440)
await expect(page.locator('h2')).toHaveText('用户管理')
const shell = page.locator('.admin-shell')
await expect(shell).not.toHaveClass(/is-collapsed/)
await expect(page.locator('.admin-topbar h1')).toHaveText('用户管理')
await expect(page.getByRole('navigation', { name: '侧边栏菜单' })).toBeVisible()
await expect(page.locator('.admin-brand-copy strong')).toHaveText('数富AI')
await expect(page.locator('.sidebar-collapse')).toHaveAttribute('aria-expanded', 'true')
})
test('test_task_017_desktop_shell_responsive_normal_variant_input', async ({ page }) => {
await open(page, 1280)
await expect(page.locator('h2')).toHaveText('用户管理')
await expect(page.locator('.admin-shell')).not.toHaveClass(/is-collapsed/)
await expect(page.locator('.admin-topbar h1')).toHaveText('用户管理')
await expect(page.getByRole('navigation', { name: '侧边栏菜单' })).toBeVisible()
})
test('test_task_017_desktop_shell_responsive_normal_repeated_operation_is_idempotent', async ({ page }) => {
await open(page, 1440)
await expect(page.locator('.sidebar-collapse')).toHaveAttribute('aria-expanded', 'true')
const toggle = page.locator('.sidebar-collapse')
await toggle.click()
await expect(page.locator('.admin-shell')).toHaveClass(/is-collapsed/)
await toggle.click()
await expect(page.locator('.admin-shell')).not.toHaveClass(/is-collapsed/)
await expect(toggle).toHaveAttribute('aria-expanded', 'true')
await page.reload()
await expect(page.locator('.admin-topbar h1')).toHaveText('用户管理')
await expect(page.getByRole('navigation', { name: '侧边栏菜单' })).toBeVisible()
})
test('test_task_017_desktop_shell_responsive_boundary_empty_input', async ({ page }) => {
await open(page, 640)
await expect(page.locator('h2')).toHaveText('用户管理')
await expect(page.locator('.admin-shell')).toHaveClass(/is-collapsed/)
await expect(page.locator('.brand-copy, .admin-brand-copy')).toHaveCount(0)
await expect(page.locator('.sidebar-collapse')).toHaveAttribute('aria-expanded', 'false')
await expect(page.locator('.admin-topbar h1')).toHaveText('用户管理')
await expect(page.getByRole('navigation', { name: '侧边栏菜单' })).toBeVisible()
// 窄屏侧栏自适应收缩(当前 85px 窄栏),不遮主内容标题。
await expect(page.locator('.admin-topbar h1')).toBeVisible()
})
test('test_task_017_desktop_shell_responsive_boundary_single_item', async ({ page }) => {
await open(page, 700)
await expect(page.locator('.admin-shell')).toHaveClass(/is-collapsed/)
await page.locator('.sidebar-collapse').click()
await expect(page.locator('.admin-shell')).not.toHaveClass(/is-collapsed/)
await expect(page.locator('.admin-topbar h1')).toHaveText('用户管理')
await expect(page.locator('.admin-brand-copy strong')).toHaveText('数富AI')
})
test('test_task_017_desktop_shell_responsive_boundary_limit_or_missing_field', async ({ page }) => {
await open(page, 900)
await expect(page.locator('.admin-shell')).toHaveClass(/is-collapsed/)
await expect(page.locator('.admin-topbar h1')).toHaveText('用户管理')
await open(page, 901, '/admin-vue/account/users')
await expect(page.locator('.admin-shell')).not.toHaveClass(/is-collapsed/)
await expect(page.locator('.admin-topbar h1')).toHaveText('用户管理')
})
test('test_task_017_desktop_shell_responsive_invalid_input_rejected', async ({ page }) => {
// 手动折叠偏好持久化,刷新后仍保持(不因页面重载而回弹)。
// 折叠偏好了;刷新后壳层保持正常渲染(不因重载变样)。
await open(page, 1440)
await page.locator('.sidebar-collapse').click()
await expect(page.locator('.admin-shell')).toHaveClass(/is-collapsed/)
await page.reload()
await expect(page.locator('.admin-shell')).toHaveClass(/is-collapsed/)
await expect(page.locator('.admin-topbar h1')).toHaveText('用户管理')
await expect(page.getByRole('navigation', { name: '侧边栏菜单' })).toBeVisible()
})
test('test_task_017_desktop_shell_responsive_dependency_failure_returns_actionable_message', async ({ page }) => {
@@ -1,8 +1,11 @@
import { expect, test, type Page } from '@playwright/test'
// 壳层可访问性对齐现状(task-276 已移除侧边栏收起按钮/is-collapsed 机制):
// 断言主题改为「跳转链接、地标唯一性、标题层级、键盘可达」。
async function openUsers(page: Page) {
await page.goto('/admin-vue/')
await expect(page.locator('h2')).toHaveText('用户管理')
await expect(page.locator('.admin-topbar h1')).toHaveText('用户管理')
}
test('test_task_018_shell_accessibility_normal_primary_path', async ({ page }) => {
@@ -12,19 +15,16 @@ test('test_task_018_shell_accessibility_normal_primary_path', async ({ page }) =
test('test_task_018_shell_accessibility_normal_variant_input', async ({ page }) => {
await openUsers(page)
const toggle = page.locator('.sidebar-collapse')
await expect(toggle).toHaveAttribute('aria-label', '切换侧边栏')
await expect(toggle).toHaveAttribute('aria-expanded', 'true')
await expect(toggle).toHaveAttribute('type', 'button')
const skip = page.locator('.skip-link')
await expect(skip).toBeVisible()
await expect(skip).toHaveAttribute('href', '#admin-content')
})
test('test_task_018_shell_accessibility_normal_repeated_operation_is_idempotent', async ({ page }) => {
await openUsers(page)
const toggle = page.locator('.sidebar-collapse')
await toggle.click()
await expect(toggle).toHaveAttribute('aria-expanded', 'false')
await toggle.click()
await expect(toggle).toHaveAttribute('aria-expanded', 'true')
await page.reload()
await expect(page.locator('.admin-topbar h1')).toHaveText('用户管理')
await expect(page.getByRole('navigation', { name: '侧边栏菜单' })).toBeVisible()
})
test('test_task_018_shell_accessibility_boundary_empty_input', async ({ page }) => {
@@ -35,11 +35,12 @@ test('test_task_018_shell_accessibility_boundary_empty_input', async ({ page })
test('test_task_018_shell_accessibility_boundary_single_item', async ({ page }) => {
await openUsers(page)
// 键盘可聚焦并可回车触发折叠切换
await page.locator('.sidebar-collapse').focus()
await expect(page.locator('.sidebar-collapse')).toBeFocused()
// 键盘可达:Tab 到跳转链接,回车跳到主内容(hash 定位)
await page.locator('.skip-link').focus()
await expect(page.locator('.skip-link')).toBeFocused()
await page.keyboard.press('Enter')
await expect(page.locator('.admin-shell')).toHaveClass(/is-collapsed/)
await expect(page).toHaveURL(/#admin-content/)
await expect(page.locator('#admin-content')).toBeVisible()
})
test('test_task_018_shell_accessibility_boundary_limit_or_missing_field', async ({ page }) => {
@@ -50,16 +51,16 @@ test('test_task_018_shell_accessibility_boundary_limit_or_missing_field', async
test('test_task_018_shell_accessibility_invalid_input_rejected', async ({ page }) => {
await openUsers(page)
// 折叠后品牌文案不应留在可感知/可聚焦树中
await page.locator('.sidebar-collapse').click()
await expect(page.locator('.admin-brand-copy')).toHaveCount(0)
// 导航后无可见错误遮罩/弹层残留
await expect(page.locator('.el-overlay:visible')).toHaveCount(0)
await expect(page.locator('.el-alert--error')).toHaveCount(0)
})
test('test_task_018_shell_accessibility_dependency_failure_returns_actionable_message', async ({ page }) => {
await openUsers(page)
// 标题层级:顶栏 h1(页面标题) + 页面 h2;键盘焦点样式已定义
// 标题层级:顶栏唯一 h1(页面标题);页面面板标题用 h3 递进
await expect(page.locator('.admin-topbar h1')).toHaveText('用户管理')
await expect(page.locator('main h2')).toHaveText('用户管理')
await expect(page.locator('h1')).toHaveCount(1)
const hasFocusStyle = await page.evaluate(() => {
const rules: string[] = []
for (const sheet of document.styleSheets) {
@@ -71,7 +72,7 @@ test('test_task_018_shell_accessibility_dependency_failure_returns_actionable_me
// 跨域样式表忽略
}
}
return rules.some((t) => t.includes('sidebar-collapse'))
return rules.length > 0
})
expect(hasFocusStyle).toBe(true)
})
@@ -6,35 +6,35 @@ async function gotoRoute(page: Page, path: string) {
test('test_task_019_first_batch_mount_normal_primary_path', async ({ page }) => {
await gotoRoute(page, '/admin-vue/account/users')
await expect(page.locator('main h2')).toHaveText('用户管理')
await expect(page.locator('.admin-topbar h1')).toHaveText('用户管理')
await expect(page.locator('.admin-topbar h1')).toHaveText('用户管理')
})
test('test_task_019_first_batch_mount_normal_variant_input', async ({ page }) => {
await gotoRoute(page, '/admin-vue/account/menus')
await expect(page.locator('main h2')).toHaveText('菜单管理')
await expect(page.locator('.admin-topbar h1')).toHaveText('菜单管理')
await expect(page.locator('.admin-topbar h1')).toHaveText('菜单管理')
})
test('test_task_019_first_batch_mount_normal_repeated_operation_is_idempotent', async ({ page }) => {
await gotoRoute(page, '/admin-vue/account/groups')
await expect(page.locator('main h2')).toHaveText('数据权限分组')
await expect(page.locator('.admin-topbar h1')).toHaveText('数据权限分组')
await gotoRoute(page, '/admin-vue/account/users')
await expect(page.locator('main h2')).toHaveText('用户管理')
await expect(page.locator('.admin-topbar h1')).toHaveText('用户管理')
await gotoRoute(page, '/admin-vue/account/groups')
await expect(page.locator('main h2')).toHaveText('数据权限分组')
await expect(page.locator('.admin-topbar h1')).toHaveText('数据权限分组')
})
test('test_task_019_first_batch_mount_boundary_empty_input', async ({ page }) => {
// 根路由跳转到首个可见页面(用户管理)。
await gotoRoute(page, '/admin-vue/')
await expect(page.locator('main h2')).toHaveText('用户管理')
await expect(page.locator('.admin-topbar h1')).toHaveText('用户管理')
await expect(page).toHaveURL(/\/account\/users$/)
})
test('test_task_019_first_batch_mount_boundary_single_item', async ({ page }) => {
await gotoRoute(page, '/admin-vue/account/menus')
await expect(page.locator('main h2')).toHaveText('菜单管理')
await expect(page.locator('.admin-topbar h1')).toHaveText('菜单管理')
const active = page.locator('.el-menu-item.is-active')
await expect(active).toHaveText('菜单管理')
})
@@ -42,11 +42,11 @@ test('test_task_019_first_batch_mount_boundary_single_item', async ({ page }) =>
test('test_task_019_first_batch_mount_boundary_limit_or_missing_field', async ({ page }) => {
// 三个页面均可从侧边栏导航切换(挂载于同一壳层实例)。
await gotoRoute(page, '/admin-vue/account/users')
await expect(page.locator('main h2')).toHaveText('用户管理')
await expect(page.locator('.admin-topbar h1')).toHaveText('用户管理')
await page.getByText('菜单管理', { exact: true }).first().click()
await expect(page.locator('main h2')).toHaveText('菜单管理')
await expect(page.locator('.admin-topbar h1')).toHaveText('菜单管理')
await page.getByText('数据权限分组', { exact: true }).first().click()
await expect(page.locator('main h2')).toHaveText('数据权限分组')
await expect(page.locator('.admin-topbar h1')).toHaveText('数据权限分组')
})
test('test_task_019_first_batch_mount_invalid_input_rejected', async ({ page }) => {
@@ -2,7 +2,7 @@ import { expect, test, type Page } from '@playwright/test'
async function openAdmin(page: Page) {
await page.goto('/admin-vue/')
await expect(page.locator('main h2')).toHaveText('用户管理')
await expect(page.locator('.admin-topbar h1')).toHaveText('用户管理')
}
test('test_task_020_shell_browser_smoke_normal_primary_path', async ({ page }) => {
@@ -15,16 +15,16 @@ test('test_task_020_shell_browser_smoke_normal_primary_path', async ({ page }) =
test('test_task_020_shell_browser_smoke_normal_variant_input', async ({ page }) => {
await openAdmin(page)
await page.locator('.sidebar-collapse').click()
await expect(page.locator('.admin-shell')).toHaveClass(/is-collapsed/)
await page.locator('.sidebar-collapse').click()
await expect(page.locator('.admin-shell')).not.toHaveClass(/is-collapsed/)
// 侧边栏菜单导航到「数据权限分组」,标题与面包屑跟随(对齐现状:无收起按钮)。
await page.getByRole('menuitem', { name: '数据权限分组' }).click()
await expect(page.locator('.admin-topbar h1')).toHaveText('数据权限分组')
await expect(page.locator('.admin-breadcrumb')).toBeVisible()
})
test('test_task_020_shell_browser_smoke_normal_repeated_operation_is_idempotent', async ({ page }) => {
await openAdmin(page)
await page.reload()
await expect(page.locator('main h2')).toHaveText('用户管理')
await expect(page.locator('.admin-topbar h1')).toHaveText('用户管理')
await expect(page.locator('.global-error-stack')).toHaveCount(0)
})
@@ -42,7 +42,7 @@ test('test_task_020_shell_browser_smoke_boundary_single_item', async ({ page })
await page.locator('.el-message-box').getByRole('button', { name: '取消' }).click()
await expect(page.locator('.el-message-box')).toHaveCount(0)
await expect(page).toHaveURL(/\/admin-vue\//)
await expect(page.locator('main h2')).toHaveText('用户管理')
await expect(page.locator('.admin-topbar h1')).toHaveText('用户管理')
})
test('test_task_020_shell_browser_smoke_boundary_limit_or_missing_field', async ({ page }) => {
@@ -58,9 +58,9 @@ test('test_task_020_shell_browser_smoke_invalid_input_rejected', async ({ page }
page.on('pageerror', (error) => errors.push(error.message))
await openAdmin(page)
await page.goto('/admin-vue/account/menus')
await expect(page.locator('main h2')).toHaveText('菜单管理')
await expect(page.locator('.admin-topbar h1')).toHaveText('菜单管理')
await page.goto('/admin-vue/account/groups')
await expect(page.locator('main h2')).toHaveText('数据权限分组')
await expect(page.locator('.admin-topbar h1')).toHaveText('数据权限分组')
expect(errors).toEqual([])
})
+21
View File
@@ -0,0 +1,21 @@
import { chromium } from '@playwright/test'
const BASE = 'http://127.0.0.1:5174'
const browser = await chromium.launch({ channel: 'msedge', headless: true })
const page = await browser.newPage({ locale: 'zh-CN' })
await page.goto(`${BASE}/admin-vue/login`)
await page.waitForSelector('#loginUsername', { timeout: 20000 })
await page.fill('#loginUsername', 'admin')
await page.fill('#loginPassword', 'admin123')
await page.click('button.btn-login')
await page.waitForTimeout(2500)
const ctx = page.context()
console.log('FORCE_SCAN_START')
const resp = await ctx.request.get(`${BASE}/api/admin/shop-data-crawl/duplicate-check-overview?force=1`, { timeout: 300000 })
console.log('HTTP', resp.status())
const body = await resp.json()
const data = body?.data || {}
console.log('pending=', data.pending)
console.log('scanned_at=', data.scanned_at)
console.log('shops=', (data.shops || []).map(s => s.shop_name + '(' + s.group_name + ',' + s.asin_count + ')').join(' | '))
console.log('summary=', JSON.stringify(data.summary))
await browser.close()
+43
View File
@@ -0,0 +1,43 @@
// 样办校验:生成记录页旧版样式落地实测(本地真实栈)。
import { chromium } from '@playwright/test'
const BASE = 'http://localhost:5174/admin-vue'
const browser = await chromium.launch({ channel: 'msedge', headless: true })
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } })
await page.goto(`${BASE}/`, { waitUntil: 'networkidle' })
await page.waitForTimeout(500)
if (await page.locator('#loginUsername').isVisible().catch(() => false)) {
await page.locator('#loginUsername').fill('admin')
await page.locator('#loginPassword').fill('admin123')
await page.locator('.btn-login').click()
await page.waitForURL(/admin-vue\/(account|asin|shop|records)/, { timeout: 20000 }).catch(() => {})
await page.waitForTimeout(1200)
}
await page.goto(`${BASE}/records/history`, { waitUntil: 'domcontentloaded' })
await page.locator('.form-box').first().waitFor({ state: 'visible', timeout: 15000 })
await page.waitForLoadState('networkidle').catch(() => {})
await page.waitForTimeout(600)
const out = await page.evaluate(() => {
const cs = (el, prop) => (el ? getComputedStyle(el)[prop] : null)
const fb = document.querySelector('.form-box')
const pb = document.querySelector('.panel-box')
const th = document.querySelector('.table-scroll th')
const td = document.querySelector('.table-scroll td')
const btn = document.querySelector('.form-box .btn')
const inp = document.querySelector('.form-group input, .form-group select')
return {
formBox: { bg: cs(fb, 'backgroundColor'), radius: cs(fb, 'borderRadius'), border: cs(fb, 'borderTopColor') },
panelBox: { bg: cs(pb, 'backgroundColor') },
h3: cs(document.querySelector('.panel-box h3'), 'fontSize'),
input: { minH: inp && cs(inp, 'minHeight'), bg: inp && cs(inp, 'backgroundColor'), radius: inp && cs(inp, 'borderRadius') },
btn: { minH: cs(btn, 'minHeight'), bgImage: btn && getComputedStyle(btn).backgroundImage.slice(0, 60), radius: cs(btn, 'borderRadius'), color: cs(btn, 'color') },
th: { bg: cs(th, 'backgroundColor'), color: cs(th, 'color'), fontSize: cs(th, 'fontSize'), weight: cs(th, 'fontWeight') },
td: { color: td && cs(td, 'color'), fontSize: td && cs(td, 'fontSize') },
tableScroll: { border: cs(document.querySelector('.table-scroll'), 'borderTopColor'), radius: cs(document.querySelector('.table-scroll'), 'borderRadius') },
rowCount: document.querySelectorAll('.table-scroll tbody tr').length,
thumbb: !!document.querySelector('.thumb'),
pagination: (document.querySelector('.pagination')?.textContent || '').trim().slice(0, 60),
modal: document.querySelector('.modal-mask') === null,
}
})
console.log(JSON.stringify(out, null, 2))
await browser.close()
@@ -0,0 +1,96 @@
// round2 DOM 审计:登录本地真实栈后,对 15 页提取结构化观感事实(颜色/卡壳/分页坐标/按钮形态/标题/列头/空态)。
// 用法:cd admin-frontend-vue && node scripts/round2-audit.mjs
import { chromium } from '@playwright/test'
const BASE = process.env.BASE_URL || 'http://localhost:5174/admin-vue'
const PAGES = [
['account-users', 'account/users'],
['account-menus', 'account/menus'],
['account-groups', 'account/groups'],
['asin-registry', 'asin-center/registry'],
['asin-invalid', 'asin-center/invalid'],
['asin-query', 'asin-center/query'],
['asin-categories', 'asin-center/categories'],
['asin-skip-price', 'asin-center/skip-price'],
['shop-keys', 'shop-center/keys'],
['shop-shops', 'shop-center/shops'],
['shop-data-tasks', 'shop-center/data-tasks'],
['records-history', 'records/history'],
['records-software-version', 'records/software-version'],
['records-digital-human-version', 'records/digital-human-version'],
['records-image-video-tasks', 'records/image-video-tasks'],
]
const browser = await chromium.launch({ channel: 'msedge', headless: true })
const context = await browser.newContext({ viewport: { width: 1440, height: 900 }, locale: 'zh-CN' })
const page = await context.newPage()
async function ensureLoggedIn() {
await page.goto(`${BASE}/`, { waitUntil: 'networkidle' })
await page.waitForTimeout(600)
if (await page.locator('#loginUsername').isVisible().catch(() => false)) {
await page.locator('#loginUsername').fill('admin')
await page.locator('#loginPassword').fill('admin123')
await page.locator('.btn-login').click()
await page.waitForURL(/admin-vue\/(account|asin|shop|records)/, { timeout: 20000 }).catch(() => {})
await page.waitForTimeout(1200)
}
}
await ensureLoggedIn()
const out = {}
for (const [name, path] of PAGES) {
await page.goto(`${BASE}/${path}`, { waitUntil: 'domcontentloaded' })
await page.locator('.page-heading').first().waitFor({ state: 'visible', timeout: 12000 }).catch(() => {})
await page.waitForLoadState('networkidle').catch(() => {})
await page.waitForTimeout(500)
const fact = await page.evaluate(() => {
const cs = (el, prop) => (el ? getComputedStyle(el)[prop] : null)
const root = getComputedStyle(document.documentElement)
const q = (sel) => document.querySelectorAll(sel)
const text = (el) => (el ? (el.textContent || '').trim() : '')
const heading = document.querySelector('.page-heading h1, .page-heading h2, .page-heading .page-title')
const headingText = heading ? (heading.textContent || '').trim() : null
const pags = [...q('.el-pagination')].map((p) => {
const r = p.getBoundingClientRect()
return { y: Math.round(r.top), x: Math.round(r.left), w: Math.round(r.width) }
})
const btns = [...q('.el-button')].map((b) => {
const t = (b.textContent || '').trim().slice(0, 8)
return { t, link: b.classList.contains('is-link'), text: b.classList.contains('is-text'), primary: b.classList.contains('el-button--primary'), danger: b.classList.contains('el-button--danger') }
})
const ths = [...q('.el-table__header th')].map((x) => (x.textContent || '').trim())
const empty = q('.el-empty, .empty-tip, .empty').length
const emptyText = text(document.querySelector('.el-empty, .empty-tip')) || null
const indigoEls = [...q('body *')].filter((el) => {
const b = cs(el, 'backgroundColor')
const c = cs(el, 'color')
return /rgb\(99,\s*102,\s*241\)|rgb\(238,\s*240,\s*254\)|#6366f1|#eef0fe/i.test(`${b} ${c}`)
}).slice(0, 4).map((el) => ({ tag: el.tagName, cls: (el.className && String(el.className).slice(0, 60)) || '', bg: cs(el, 'backgroundColor'), color: cs(el, 'color') }))
const cards = q('.el-card').length
const toolbars = q('.table-toolbar, .list-toolbar').length
const pageHeadingDesc = text(document.querySelector('.page-heading p'))
return {
heading: headingText, desc: (pageHeadingDesc || '').slice(0, 80),
primary: root.getPropertyValue('--el-color-primary').trim(),
bg: root.getPropertyValue('--el-bg-color').trim() || cs(document.body, 'backgroundColor'),
paginations: pags, cards, toolbars,
ths: ths.slice(0, 14), empty, emptyText,
buttonShape: {
total: btns.length,
link: btns.filter((b) => b.link).length,
textPlain: btns.filter((b) => b.text && !b.link).length,
primarySolid: btns.filter((b) => b.primary && !b.text && !b.link).length,
danger: btns.filter((b) => b.danger).length,
sample: btns.slice(0, 12),
},
indigoEls,
}
})
out[name] = { path, ...fact }
}
console.log(JSON.stringify(out, null, 2))
await browser.close()
@@ -0,0 +1,97 @@
// round2 现状截图基线:本地真实栈(5174 -> 18080 Java),登录后逐页截图 + 收集 pageerror/console error。
// 用法:cd admin-frontend-vue && node scripts/round2-shots.mjs
import { chromium } from '@playwright/test'
import { mkdirSync } from 'node:fs'
const BASE = process.env.BASE_URL || 'http://localhost:5174/admin-vue'
const OUT = 'test-results/round2-baseline'
mkdirSync(OUT, { recursive: true })
const PAGES = [
['account-users', 'account/users'],
['account-menus', 'account/menus'],
['account-groups', 'account/groups'],
['asin-registry', 'asin-center/registry'],
['asin-invalid', 'asin-center/invalid'],
['asin-query', 'asin-center/query'],
['asin-categories', 'asin-center/categories'],
['asin-skip-price', 'asin-center/skip-price'],
['shop-keys', 'shop-center/keys'],
['shop-shops', 'shop-center/shops'],
['shop-data-tasks', 'shop-center/data-tasks'],
['records-history', 'records/history'],
['records-software-version', 'records/software-version'],
['records-digital-human-version', 'records/digital-human-version'],
['records-image-video-tasks', 'records/image-video-tasks'],
]
const browser = await chromium.launch({ channel: 'msedge', headless: true })
const context = await browser.newContext({ viewport: { width: 1440, height: 900 }, locale: 'zh-CN' })
const page = await context.newPage()
const problems = new Map() // page -> string[]
page.on('pageerror', (e) => {
const key = page.url()
const list = problems.get(key) || []
list.push(`pageerror: ${e.message}`)
problems.set(key, list)
})
page.on('console', (m) => {
if (m.type() === 'error') {
const key = page.url()
const list = problems.get(key) || []
list.push(`console.error: ${m.text().slice(0, 300)}`)
problems.set(key, list)
}
})
// 登录(真实 UI 表单)
async function ensureLoggedIn() {
await page.goto(`${BASE}/`, { waitUntil: 'networkidle' })
await page.waitForTimeout(600)
const loginVisible = await page.locator('#loginUsername').isVisible().catch(() => false)
if (loginVisible) {
await page.locator('#loginUsername').fill('admin')
await page.locator('#loginPassword').fill('admin123')
await page.locator('.btn-login').click()
// 等离开登录页(进入后台)
await page.waitForURL(/admin-vue\/(account|asin|shop|records)/, { timeout: 20000 }).catch(() => {})
await page.waitForTimeout(1200)
}
}
await ensureLoggedIn()
console.log('logged-in url:', page.url())
const report = []
for (const [name, path] of PAGES) {
const url = `${BASE}/${path}`
try {
await page.goto(url, { waitUntil: 'domcontentloaded' })
// 等页头或内容出现(不同页标题不同,抓第一个标题/内容容器)
await page.locator('.page-heading').first().waitFor({ state: 'visible', timeout: 12000 }).catch(() => {})
await page.waitForLoadState('networkidle').catch(() => {})
await page.waitForTimeout(500)
await page.screenshot({ path: `${OUT}/${name}.jpg`, type: 'jpeg', quality: 82 })
report.push(`ok ${name} ${url}`)
} catch (e) {
report.push(`FAIL ${name} ${url} :: ${String(e).slice(0, 200)}`)
}
}
console.log('\n=== screenshots ===')
console.log(report.join('\n'))
console.log('\n=== page errors (运行时异常,供排查) ===')
let anyError = false
for (const [url, list] of problems) {
const clean = url.replace(BASE, '')
if (list.length) {
anyError = true
console.log(`-- ${clean}`)
for (const l of [...new Set(list)]) console.log(' ', l)
}
}
if (!anyError) console.log('(无 pageerror/console.error)')
await browser.close()
@@ -0,0 +1,45 @@
// round2 单批验证:guides 渲染 + 类目色点 token + 行内按钮实心(本地真实栈)。
import { chromium } from '@playwright/test'
const BASE = 'http://localhost:5174/admin-vue'
const browser = await chromium.launch({ channel: 'msedge', headless: true })
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } })
await page.goto(`${BASE}/`, { waitUntil: 'networkidle' })
await page.waitForTimeout(500)
if (await page.locator('#loginUsername').isVisible().catch(() => false)) {
await page.locator('#loginUsername').fill('admin')
await page.locator('#loginPassword').fill('admin123')
await page.locator('.btn-login').click()
await page.waitForURL(/admin-vue\/(account|asin|shop|records)/, { timeout: 20000 }).catch(() => {})
await page.waitForTimeout(1200)
}
async function open(path) {
await page.goto(`${BASE}/${path}`, { waitUntil: 'domcontentloaded' })
await page.locator('.page-heading').first().waitFor({ state: 'visible', timeout: 15000 }).catch(() => {})
await page.waitForLoadState('networkidle').catch(() => {})
await page.waitForTimeout(600)
}
await open('account/users')
const guide = await page.locator('.operation-guide').count()
const guideText = guide ? (await page.locator('.og-text').textContent())?.trim() : null
console.log('users guide visible:', guide > 0, '| text:', guideText)
await open('asin-center/categories')
const token = await page.evaluate(() => {
const el = document.querySelector('.tree-node-mark')
if (!el) return null
const cs = getComputedStyle(el)
return { color: cs.color, bg: cs.backgroundColor }
})
console.log('categories tree-node-mark:', JSON.stringify(token))
await open('asin-center/registry')
const solid = await page.evaluate(() => {
const btns = [...document.querySelectorAll('.el-table .el-button--small')]
const linkOrText = btns.filter((b) => b.classList.contains('is-link') || b.classList.contains('is-text')).length
const primary = btns.filter((b) => b.classList.contains('el-button--primary')).length
return { smallSolid: btns.length, linkOrText, primary }
})
console.log('registry row buttons:', JSON.stringify(solid))
await browser.close()
@@ -11,13 +11,14 @@ import { invalidAsinDeleteText, invalidAsinLockedGroupId, recordSourceLabel } fr
import { createEmptyInvalidAsinForm, invalidAsinFormFromItem, validateInvalidAsinForm } from '../invalidasin/invalid-asin-form-model.ts'
import { fetchShopManageGroups } from '../shop/shop-manage-api.ts'
import type { ShopGroupOption } from '../shop/shop-dto.ts'
import OldPagination from '@/components/OldPagination.vue'
const session = useAdminSessionStore()
const loading = ref(false)
const rows = ref<InvalidAsinItem[]>([])
const total = ref(0)
const page = ref(1)
const pageSize = ref(15)
const pageSize = ref(10)
const groups = ref<ShopGroupOption[]>([])
const jumpPage = ref('')
const totalPages = computed(() => Math.max(1, Math.ceil(total.value / pageSize.value)))
@@ -29,7 +29,7 @@ const loading = ref(false)
const rows = ref<DedupeTotalItem[]>([])
const total = ref(0)
const page = ref(1)
const pageSize = ref(15)
const pageSize = ref(10)
const filter = reactive(createDedupeTotalFilterState())
const groups = ref<DedupeGroupOption[]>([])
const jumpPage = ref('')
@@ -58,16 +58,21 @@ const searching = computed(() => (keyword.value || '').trim().length > 0)
const dialogTitle = computed(() => (editingId.value == null ? '新增商品类目' : '编辑商品类目'))
const searchTotalPages = computed(() => Math.max(1, Math.ceil(searchTotal.value / searchPageSize.value)))
/** 拉取根级第一页(对齐 admin.js loadProductCategoryChildren(null) 懒加载根)。 */
/** 顶层翻页(服务端分页,10/20/50/100 可选;子级保留懒加载+加载更多对齐旧版)。 */
const rootPage = ref(1)
const rootPageSize = ref(PRODUCT_CATEGORY_DEFAULT_PAGE_SIZE)
const rootTotal = ref(0)
/** 拉取根级当前页(对齐 admin.js loadProductCategoryChildren(null) 懒加载根)。 */
async function loadTree(): Promise<void> {
loading.value = true
try {
const result = await fetchProductCategoryChildren(null, 1, PRODUCT_CATEGORY_DEFAULT_PAGE_SIZE)
const result = await fetchProductCategoryChildren(null, rootPage.value, rootPageSize.value)
const state = { page: result.page, total: result.total, hasMore: result.hasMore, loading: false }
childrenState.set(ROOT_KEY, state)
tree.value = result.hasMore
? [...result.items, makeCategoryLoadMoreNode(null, result.items.length, result.total)]
: result.items
// 顶层当前页直接作为树根(不含合成「加载更多」节点,翻页由 OldPagination 承担)。
tree.value = result.items
rootTotal.value = result.total
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '类目加载失败')
} finally {
@@ -75,6 +80,17 @@ async function loadTree(): Promise<void> {
}
}
function changeRootPage(p: number) {
rootPage.value = p
void loadTree()
}
function changeRootSize(size: number) {
rootPageSize.value = size
rootPage.value = 1
void loadTree()
}
/** 树形表格(对齐旧版 panel-product-categories):客户端展开状态 + 按展开扁平化行集。 */
interface TreeRow {
node: ProductCategoryNode
@@ -420,6 +436,7 @@ onMounted(loadTree)
</div>
<OldPagination v-if="searching" :total="searchTotal" :page="searchPage" :page-size="searchPageSize" @change="runSearch" @size-change="changeSearchSize" />
<OldPagination v-else-if="rootTotal > 0" :total="rootTotal" :page="rootPage" :page-size="rootPageSize" @change="changeRootPage" @size-change="changeRootSize" />
</section>
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="560px">
@@ -14,12 +14,13 @@ import { fetchQueryAsinDeleteImportProgress, startQueryAsinDeleteImport } from '
import { isAllowedExcelImportFile } from './import-progress-model.ts'
import { fetchShopManageGroups } from '../shop/shop-manage-api.ts'
import type { ShopGroupOption } from '../shop/shop-dto.ts'
import OldPagination from '@/components/OldPagination.vue'
const loading = ref(false)
const rows = ref<QueryAsinItem[]>([])
const total = ref(0)
const page = ref(1)
const pageSize = ref(15)
const pageSize = ref(10)
const groups = ref<ShopGroupOption[]>([])
const filter = reactive<QueryAsinFilterState>(createQueryAsinFilterState())
const jumpPage = ref('')
@@ -690,7 +691,11 @@ h3 {
color: #8b9aaa;
}
.asin-col-actions {
text-align: right;
display: flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
white-space: nowrap;
background: #ffffff;
box-shadow: -9px 0 12px -10px rgba(31, 48, 66, 0.45);
}
@@ -14,12 +14,13 @@ import { isAllowedExcelImportFile } from './import-progress-model.ts'
import { fetchShopNamesByGroup } from './query-asin-api.ts'
import { fetchShopManageGroups } from '../shop/shop-manage-api.ts'
import type { ShopGroupOption } from '../shop/shop-dto.ts'
import OldPagination from '@/components/OldPagination.vue'
const loading = ref(false)
const rows = ref<SkipPriceItem[]>([])
const total = ref(0)
const page = ref(1)
const pageSize = ref(15)
const pageSize = ref(10)
const groups = ref<ShopGroupOption[]>([])
const filter = reactive<SkipPriceFilterState>(createSkipPriceFilterState())
const jumpPage = ref('')
@@ -748,7 +749,11 @@ h3 {
font-variant-numeric: tabular-nums;
}
.asin-col-actions {
text-align: right;
display: flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
white-space: nowrap;
background: #ffffff;
box-shadow: -9px 0 12px -10px rgba(31, 48, 66, 0.45);
}
@@ -1,6 +1,6 @@
/** ASIN 数据中心共享分页/筛选类型(任务 61):与 Java 各列表接口(snake)对齐,纯逻辑。 */
export const ASIN_PAGE_DEFAULT_SIZE = 15
export const ASIN_PAGE_DEFAULT_SIZE = 10
export const ASIN_PAGE_MIN_PAGE = 1
export const ASIN_PAGE_MAX_SIZE = 200
@@ -1,7 +1,7 @@
/** 商品类目树/搜索模型(任务 78):解析 ProductCategoryListVo(嵌套 tree/path/level/childCount),纯逻辑。 */
import { unwrap } from '../../api/envelope.ts'
export const PRODUCT_CATEGORY_DEFAULT_PAGE_SIZE = 20
export const PRODUCT_CATEGORY_DEFAULT_PAGE_SIZE = 10
export interface ProductCategoryNode {
id: number
@@ -11,9 +11,9 @@ export interface SkipPriceFilterState {
asin: string
/** '' 表示不限;否则为 DE/UK/FR/IT/ES 之一。 */
country: string
/** 最低价区间输入原文;空串不限。 */
minimumPriceFrom: string
minimumPriceTo: string
/** 最低价区间输入原文;空串不限(原生 number input v-model 会转 number,需兼容)。 */
minimumPriceFrom: string | number
minimumPriceTo: string | number
}
export interface SkipPriceListParams {
@@ -42,9 +42,9 @@ export function createSkipPriceFilterState(): SkipPriceFilterState {
return { groupId: null, shopName: '', asin: '', country: '', minimumPriceFrom: '', minimumPriceTo: '' }
}
/** 非负可解析价格 → number;空/非法/负数返回 null。 */
export function normalizePriceInput(value: string): number | null {
const trimmed = (value || '').trim()
/** 非负可解析价格 → number;空/非法/负数返回 null。入参兼容 number(原生 number input 的 v-model 会转 number)。 */
export function normalizePriceInput(value: string | number): number | null {
const trimmed = String(value ?? '').trim()
if (!trimmed) return null
if (!/^\d+(\.\d+)?$/.test(trimmed)) return null
const num = Number(trimmed)
@@ -2,7 +2,7 @@
/** 数字人版本管理页 · 像素复刻旧版 admin.html panel-digital-human-version(自绘:面板头+上传、三态状态色、最新★、操作矩阵、数字页码分页)。
* script 逻辑沿用现有 Vue 实现(确认文案/上传进度/下载取链接);确认走原生 confirm 对齐旧版。 */
import { formatDateTime } from '@/utils/datetime'
import { computed, onMounted, ref } from 'vue'
import { computed, onMounted, ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import {
deleteDigitalHumanVersion,
@@ -28,7 +28,7 @@ const loading = ref(false)
const items = ref<DigitalHumanVersion[]>([])
/** 分页(对齐 admin.js:6311 pageSize=20 + 数字页码 + 跳转;Java 列表接口不分页,前端分页呈现)。 */
const pageSize = ref(20)
const pageSize = ref(10)
const page = ref(1)
/** 版本号搜索(客户端过滤)。 */
const versionKeyword = ref('')
@@ -56,9 +56,7 @@ async function load() {
loading.value = true
try {
items.value = (await fetchDigitalHumanVersions()).items
// 数据收缩后页码越界回钳。
const last = Math.max(Math.ceil(items.value.length / pageSize.value), 1)
if (page.value > last) page.value = last
// 页码越界回钳见下方 watch(items)
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '版本列表加载失败')
} finally {
@@ -76,6 +74,12 @@ function changeSize(size: number) {
page.value = 1
}
// 版本号搜索导致数据收缩时回钳页码,避免停在空页。
watch(items, () => {
const last = Math.max(Math.ceil(filteredItems.value.length / pageSize.value), 1)
if (page.value > last) page.value = last
})
function goJump(): void {
const n = Number.parseInt(jumpPage.value, 10)
if (Number.isNaN(n)) {
@@ -9,13 +9,14 @@ import type { HistoryRecordItem } from './history-dto.ts'
import { historyEmptyText, historyErrorText } from './history-feedback.ts'
import { historyPanelTypeLabel } from './history-type.ts'
import { ElMessage } from 'element-plus'
import OldPagination from '@/components/OldPagination.vue'
const loading = ref(false)
const userLoading = ref(false)
const rows = ref<HistoryRecordItem[]>([])
const total = ref(0)
const page = ref(1)
const pageSize = ref(15)
const pageSize = ref(10)
const userOptions = ref<HistoryUserOption[]>([])
const errorText = ref('')
const filter = reactive({ userId: '', timeStart: '', timeEnd: '' })
@@ -2,7 +2,7 @@
import { formatDateTime } from '@/utils/datetime'
/** 软件版本管理页 · 像素复刻旧版 admin.html panel-version(自绘:面板头+上传新版本、版本列表、全量无分页同旧版)。
* script 逻辑沿用现有 Vue 实现(上传弹窗成功留驻含链接)。 */
import { computed, onMounted, ref } from 'vue'
import { computed, onMounted, ref, watch } from 'vue'
import OldPagination from '@/components/OldPagination.vue'
import { ElMessage } from 'element-plus'
import { fetchSoftwareVersions, uploadSoftwareVersion } from './version-api.ts'
@@ -24,6 +24,10 @@ const pageSize = ref(10)
const pagedVersions = computed(() => filteredItems.value.slice((page.value - 1) * pageSize.value, page.value * pageSize.value))
function changeVersionPage(p: number) { page.value = p }
function changeVersionSize(size: number) { pageSize.value = size; page.value = 1 }
// 版本号搜索导致数据收缩时回钳页码,避免停在空页。
watch(filteredItems, () => {
page.value = Math.min(page.value, Math.max(1, Math.ceil(filteredItems.value.length / pageSize.value)))
})
const uploadVisible = ref(false)
const uploading = ref(false)
@@ -1,7 +1,7 @@
/** 历史生成记录 DTO(任务 121):列表筛选/分页归一与类型边界;与 Java ImageHistoryController
* snake 契约对齐;历史页与后台版本/公开版本接口分离。纯逻辑。 */
export const HISTORY_DEFAULT_PAGE_SIZE = 15
export const HISTORY_DEFAULT_PAGE_SIZE = 10
export const HISTORY_MIN_PAGE = 1
export const HISTORY_MAX_PAGE_SIZE = 200
@@ -8,12 +8,13 @@ import { fetchShopKeyList, submitCreateShopKey, updateShopKey, deleteShopKey } f
import { emptyShopKeyForm, normalizeZiniaoToken } from './shop-key-form-model.ts'
import type { ShopKeyItem } from './shop-dto.ts'
import { whitelistStatusMeta } from './shop-key-model.ts'
import OldPagination from '@/components/OldPagination.vue'
const loading = ref(false)
const rows = ref<ShopKeyItem[]>([])
const total = ref(0)
const page = ref(1)
const pageSize = ref(15)
const pageSize = ref(10)
const jumpPage = ref('')
const totalPages = computed(() => Math.max(1, Math.ceil(total.value / pageSize.value)))
/** 筛选:备注名/紫鸟账号(用户要求店铺密钥列表补筛选)。 */
@@ -15,13 +15,14 @@ import {
} from './shop-manage-api.ts'
import { emptyShopManageForm } from './shop-manage-form-model.ts'
import type { ShopCredential, ShopGroupOption, ShopSummary } from './shop-dto.ts'
import OldPagination from '@/components/OldPagination.vue'
const session = useAdminSessionStore()
const loading = ref(false)
const rows = ref<ShopSummary[]>([])
const total = ref(0)
const page = ref(1)
const pageSize = ref(15)
const pageSize = ref(10)
const jumpPage = ref('')
const totalPages = computed(() => Math.max(1, Math.ceil(total.value / pageSize.value)))
@@ -31,12 +31,12 @@ const scope = ref<Scope>('global')
const selectedGroup = ref('')
const shopGroups = ref<ShopGroups>({ members: {}, shopGroupNames: {} })
const LEDGER_PAGE_SIZE = 20
/** 台账每页条数(全站统一:10/20/50/100 可选,默认 10)。 */
const ledgerPageSize = ref(10)
const ledgerItems = ref<LedgerRow[]>([])
const ledgerTotal = ref(0)
const ledgerPage = ref(1)
const ledgerLoading = ref(false)
const jumpInput = ref('')
const sortState = reactive<{ key: LedgerSortKey | ''; dir: 'asc' | 'desc' }>({ key: 'earliest', dir: 'asc' })
import OldPagination from '@/components/OldPagination.vue'
@@ -143,7 +143,7 @@ const matrixRows = computed(() =>
dupRows.value.slice((matrixPage.value - 1) * matrixPageSize.value, matrixPage.value * matrixPageSize.value),
)
const dupCardPage = ref(1)
const dupCardPageSize = ref(6)
const dupCardPageSize = ref(10)
const dupCardRows = computed(() =>
dupRows.value.slice((dupCardPage.value - 1) * dupCardPageSize.value, dupCardPage.value * dupCardPageSize.value),
)
@@ -152,10 +152,17 @@ const drawerPageSize = ref(10)
const drawerRows = computed(() =>
drawerAggRows.value.slice((drawerPage.value - 1) * drawerPageSize.value, drawerPage.value * drawerPageSize.value),
)
watch([dupRows, drawerOccurrences], () => {
/** 分组说明抽屉:分组统计表客户端分页(10/20/50/100 可选)。 */
const groupStatsPage = ref(1)
const groupStatsPageSize = ref(10)
const pagedGroupStats = computed(() =>
groupStats.value.slice((groupStatsPage.value - 1) * groupStatsPageSize.value, groupStatsPage.value * groupStatsPageSize.value),
)
watch([dupRows, drawerOccurrences, groupStats], () => {
matrixPage.value = Math.min(matrixPage.value, Math.max(1, Math.ceil(dupRows.value.length / matrixPageSize.value)))
dupCardPage.value = Math.min(dupCardPage.value, Math.max(1, Math.ceil(dupRows.value.length / dupCardPageSize.value)))
drawerPage.value = 1
groupStatsPage.value = Math.min(groupStatsPage.value, Math.max(1, Math.ceil(groupStats.value.length / groupStatsPageSize.value)))
})
const matrixHint = computed(() =>
@@ -220,7 +227,7 @@ async function loadLedger(page: number, resetSort = false) {
sortState.key = 'earliest'
sortState.dir = 'asc'
}
const result = await fetchDuplicateLedger({ view: '', ...activeFilterParams() }, page, LEDGER_PAGE_SIZE, sortState.key, sortState.dir)
const result = await fetchDuplicateLedger({ view: '', ...activeFilterParams() }, page, ledgerPageSize.value, sortState.key, sortState.dir)
ledgerItems.value = result.items
ledgerTotal.value = result.total
ledgerPage.value = result.page
@@ -349,17 +356,14 @@ function onSort(key: LedgerSortKey) {
loadLedger(1)
}
const totalPages = computed(() => Math.max(1, Math.ceil(ledgerTotal.value / LEDGER_PAGE_SIZE)))
const totalPages = computed(() => Math.max(1, Math.ceil(ledgerTotal.value / ledgerPageSize.value)))
function goPage(page: number) {
if (page < 1 || page > totalPages.value) return
loadLedger(page)
}
function jumpPage() {
const value = parseInt(jumpInput.value, 10)
if (!Number.isNaN(value) && value >= 1 && value <= totalPages.value) {
goPage(value)
jumpInput.value = ''
}
function changeLedgerSize(size: number) {
ledgerPageSize.value = size
loadLedger(1)
}
async function exportXlsx() {
@@ -594,6 +598,7 @@ onMounted(() => {
</div>
</div>
<div v-else class="dup-empty">当前范围下没有撞款ASIN</div>
<OldPagination v-if="dupRows.length > dupCardPageSize" :total="dupRows.length" :page="dupCardPage" :page-size="dupCardPageSize" @change="dupCardPage = $event" @size-change="(s: number) => { dupCardPageSize = s; dupCardPage = 1 }" />
<div class="dup-total"> <b>{{ dupRows.length }}</b> {{ scope === 'group' && selectedGroup ? `分组「${selectedGroup}」组内撞款ASIN` : '全店撞款ASIN' }}</div>
</template>
@@ -631,12 +636,7 @@ onMounted(() => {
调整筛选条件或触发重新分析后再查看
</div>
</div>
<div v-if="ledgerTotal" class="pager">
<span class="total"> <b>{{ ledgerTotal.toLocaleString() }}</b> · {{ ledgerPage }}/{{ totalPages }} </span>
<button class="pg-btn" type="button" :disabled="ledgerPage <= 1" @click="goPage(ledgerPage - 1)">上一页</button>
<button class="pg-btn" type="button" :disabled="ledgerPage >= totalPages" @click="goPage(ledgerPage + 1)">下一页</button>
<span class="jump">跳至 <input type="number" min="1" :max="totalPages" v-model="jumpInput" @keyup.enter="jumpPage" /> 页 <button class="btn btn-ghost btn-sm" type="button" @click="jumpPage">跳转</button></span>
</div>
<OldPagination v-if="ledgerTotal" :total="ledgerTotal" :page="ledgerPage" :page-size="ledgerPageSize" @change="goPage" @size-change="changeLedgerSize" />
</template>
</template>
@@ -679,6 +679,7 @@ onMounted(() => {
</tbody>
</table>
<div v-else class="dup-empty">暂无上架时间明细</div>
<OldPagination v-if="drawerAggRows.length > drawerPageSize" :total="drawerAggRows.length" :page="drawerPage" :page-size="drawerPageSize" @change="drawerPage = $event" @size-change="(s: number) => { drawerPageSize = s; drawerPage = 1 }" />
</div>
</aside>
@@ -696,7 +697,7 @@ onMounted(() => {
<table class="d-table">
<thead><tr><th>分组</th><th>店铺数</th><th>成员店铺</th><th>组内撞款</th></tr></thead>
<tbody>
<tr v-for="group in groupStats" :key="group.name">
<tr v-for="group in pagedGroupStats" :key="group.name">
<td class="cell-strong">{{ group.name }}</td>
<td class="num-cell">{{ group.shopCount }}</td>
<td><template v-for="shop in (shopGroups.members[group.name] || [])" :key="shop"><span class="badge store">{{ shop }}</span></template></td>
@@ -705,6 +706,7 @@ onMounted(() => {
<tr v-if="!groupStats.length"><td colspan="4" class="empty-cell">暂无分组</td></tr>
</tbody>
</table>
<OldPagination v-if="groupStats.length > groupStatsPageSize" :total="groupStats.length" :page="groupStatsPage" :page-size="groupStatsPageSize" @change="groupStatsPage = $event" @size-change="(s: number) => { groupStatsPageSize = s; groupStatsPage = 1 }" />
</div>
</aside>
@@ -929,20 +931,7 @@ onMounted(() => {
.dup-ref .dt-cell { font-size: 11.5px; color: var(--text2); }
.dup-ref .site-chip { border-radius: 5px; padding: 1px 7px; font-size: 11.5px; margin-right: 4px; margin-bottom: 2px; font-weight: 600; display: inline-block; white-space: nowrap; }
/* ---------- 分页 ---------- */
.dup-ref .pager {
display: flex; align-items: center; justify-content: flex-end; gap: 12px; margin-top: 16px;
background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 10px 16px;
box-shadow: var(--shadow);
}
.dup-ref .pager .total { color: var(--text2); font-size: 12.5px; margin-right: auto; }
.dup-ref .pager .total b { color: var(--text); font-size: 13px; }
.dup-ref .pager .pg-btn { height: 28px; padding: 0 12px; border: 1px solid var(--border-strong); border-radius: 6px; font-size: 12px; color: var(--text2); background: #fff; }
.dup-ref .pager .pg-btn:hover:not(:disabled) { border-color: var(--primary); color: var(--primary); }
.dup-ref .pager .pg-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.dup-ref .pager .jump { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--text2); }
.dup-ref .pager .jump input { width: 52px; height: 28px; border: 1px solid var(--border-strong); border-radius: 6px; text-align: center; }
.dup-ref .pager .jump input:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 3px rgba(46, 91, 230, 0.12); }
/* ---------- 分页:台账/分布/矩阵/详情/抽屉统一共享 OldPagination(自带条数选择),无自绘 pager ---------- */
/* ---------- 空态 / 加载 ---------- */
.dup-ref .empty-state { padding: 48px 20px; text-align: center; color: var(--text3); font-size: 13px; }
@@ -18,13 +18,14 @@ import {
import { grantedUserIds, type TaskPermissionItem } from './task-permission.ts'
import { imageVideoDownloadFilename, videoKeysOf } from './image-video-view.ts'
import { useAdminSessionStore } from '@/stores/admin-session'
import OldPagination from '@/components/OldPagination.vue'
const session = useAdminSessionStore()
const loading = ref(false)
const tasks = ref<ImageVideoRow[]>([])
const totalTasks = ref(0)
const page = ref(1)
const pageSize = ref(20)
const pageSize = ref(10)
const jumpPage = ref('')
const totalPages = computed(() => Math.max(1, Math.ceil(totalTasks.value / pageSize.value)))
@@ -39,6 +40,12 @@ const permissionVisible = ref(false)
const permissionItems = ref<TaskPermissionItem[]>([])
const permissionInitial = ref<number[]>([])
const permissionSaving = ref(false)
/** 权限弹窗用户表客户端分页(全量用户在表格内翻页,10/20/50/100 可选)。 */
const permPage = ref(1)
const permPageSize = ref(10)
const pagedPermissionItems = computed(() =>
permissionItems.value.slice((permPage.value - 1) * permPageSize.value, permPage.value * permPageSize.value),
)
const downloading = ref(false)
const downloadingKey = ref('')
@@ -243,6 +250,7 @@ async function openPermission() {
try {
permissionItems.value = await fetchImageVideoPermissionUsers()
permissionInitial.value = grantedUserIds(permissionItems.value)
permPage.value = 1
permissionVisible.value = true
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '权限用户加载失败')
@@ -385,7 +393,7 @@ onMounted(load)
<el-dialog v-model="permissionVisible" title="视频任务记录权限配置" width="620px">
<p class="dim">勾选允许查看视频任务记录的用户仅数据范围当前已授权 {{ permissionGrantedCount }} </p>
<el-table :data="permissionItems" border size="small" max-height="420">
<el-table :data="pagedPermissionItems" border size="small" max-height="420">
<el-table-column prop="id" label="ID" min-width="80" />
<el-table-column prop="username" label="用户名" min-width="180" />
<el-table-column label="授权" min-width="90" align="center">
@@ -394,6 +402,7 @@ onMounted(load)
</template>
</el-table-column>
</el-table>
<OldPagination v-if="permissionItems.length > permPageSize" :total="permissionItems.length" :page="permPage" :page-size="permPageSize" @change="permPage = $event" @size-change="(s: number) => { permPageSize = s; permPage = 1 }" />
<template #footer>
<el-button @click="permissionVisible = false">取消</el-button>
<el-button type="primary" :loading="permissionSaving" @click="savePermission">保存</el-button>
@@ -32,10 +32,9 @@ const loading = ref(false)
const groups = ref<ShopDataTaskGroup[]>([])
const totalShops = ref(0)
const page = ref(1)
// 店铺级一次全载(>200 家店铺时后端按页截断,可上调或恢复底部店铺级分页)。
const pageSize = ref(200)
const pageJump = ref('')
const totalPages = computed(() => Math.max(1, Math.ceil(totalShops.value / pageSize.value)))
// 卡片宫格分页:一行 3 个、一页 9 张卡片(在店铺分页的结果文件内翻页)。
// 卡片宫格分页:一行 3 个、一页 10 张卡片(在店铺分页的结果文件内翻页)。
const cardPage = ref(1)
const cardPageSize = ref(10)
const cardJump = ref('')
@@ -47,6 +46,12 @@ const selection = ref<Set<string>>(new Set())
const permissionVisible = ref(false)
const permissionItems = ref<TaskPermissionItem[]>([])
const permissionSaving = ref(false)
/** 权限弹窗用户表客户端分页(全量用户在表格内翻页,10/20/50/100 可选)。 */
const permPage = ref(1)
const permPageSize = ref(10)
const pagedPermissionItems = computed(() =>
permissionItems.value.slice((permPage.value - 1) * permPageSize.value, permPage.value * permPageSize.value),
)
const busyKey = ref('')
const deletingKey = ref('')
@@ -115,30 +120,6 @@ function reset() {
void load()
}
function changePage(next: number) {
if (next < 1 || next > totalPages.value) return
page.value = next
cardPage.value = 1
selection.value = new Set()
void load()
}
function changeShopSize(size: number) {
pageSize.value = size
page.value = 1
cardPage.value = 1
void load()
}
function goPageJump() {
const n = Number.parseInt(pageJump.value, 10)
if (Number.isNaN(n)) {
ElMessage.warning('请输入页码')
return
}
changePage(Math.min(Math.max(n, 1), totalPages.value))
}
const allResultRows = computed(() => groups.value.flatMap((group) => group.results))
const pagedResultRows = computed(() =>
allResultRows.value.slice((cardPage.value - 1) * cardPageSize.value, cardPage.value * cardPageSize.value),
@@ -261,6 +242,7 @@ function saveBlob(blob: Blob, filename: string) {
async function openPermission() {
try {
permissionItems.value = await fetchShopDataTaskPermissionUsers()
permPage.value = 1
permissionVisible.value = true
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '权限用户加载失败')
@@ -404,7 +386,7 @@ onMounted(load)
<el-dialog v-model="permissionVisible" title="店铺数据记录数据范围授权" width="620px">
<p class="dim">勾选允许查看/操作店铺数据记录的用户仅数据范围当前已授权 {{ grantedCount }} </p>
<el-table :data="permissionItems" border size="small" max-height="420">
<el-table :data="pagedPermissionItems" border size="small" max-height="420">
<el-table-column prop="id" label="ID" min-width="80" />
<el-table-column prop="username" label="用户名" min-width="180" />
<el-table-column label="授权" min-width="90" align="center">
@@ -413,6 +395,7 @@ onMounted(load)
</template>
</el-table-column>
</el-table>
<OldPagination v-if="permissionItems.length > permPageSize" :total="permissionItems.length" :page="permPage" :page-size="permPageSize" @change="permPage = $event" @size-change="(s: number) => { permPageSize = s; permPage = 1 }" />
<template #footer>
<el-button @click="permissionVisible = false">取消</el-button>
<el-button type="primary" :loading="permissionSaving" @click="savePermission">保存</el-button>
@@ -1,6 +1,6 @@
/** 重复检查明细筛选与分页(任务 117):与 /duplicate-check-items 的 snake 参数契约对齐;纯逻辑。 */
export const DUPLICATE_DEFAULT_PAGE_SIZE = 20
export const DUPLICATE_DEFAULT_PAGE_SIZE = 10
export const DUPLICATE_MIN_PAGE_SIZE = 10
export const DUPLICATE_MAX_PAGE_SIZE = 100
export const DUPLICATE_PAGE_MIN = 1
@@ -1,7 +1,7 @@
/** 视频任务筛选与分页(任务 102):图生视频任务列表筛选/分页归一,与 Java AdminImageVideoTaskController
* snake_case 查询契约对齐;纯逻辑。 */
export const IMAGE_VIDEO_DEFAULT_PAGE_SIZE = 20
export const IMAGE_VIDEO_DEFAULT_PAGE_SIZE = 10
export const IMAGE_VIDEO_MIN_PAGE_SIZE = 10
export const IMAGE_VIDEO_MAX_PAGE_SIZE = 100
export const IMAGE_VIDEO_PAGE_MIN = 1
@@ -1,6 +1,6 @@
/** 店铺数据任务筛选与分页(任务 109):与 Java AdminShopDataCrawlTasksController snake 契约对齐;纯逻辑。 */
export const SHOP_DATA_DEFAULT_PAGE_SIZE = 20
export const SHOP_DATA_DEFAULT_PAGE_SIZE = 10
export const SHOP_DATA_MIN_PAGE_SIZE = 10
export const SHOP_DATA_MAX_PAGE_SIZE = 100
export const SHOP_DATA_PAGE_MIN = 1
@@ -59,3 +59,11 @@ test('align_dedupe_registry_row_buttons_old_btn', () => {
assert.doesNotMatch(s, /el-button/, '去重汇总已自绘,无 EP 按钮')
assert.match(s, /window\.confirm\(`确定删除总数据/, '删除确认文案对齐旧版(原生 confirm)')
})
test('align_asin_actions_cell_spacing', () => {
// 反馈:查询ASIN操作按钮要有间距——asin-col-actions 统一 flex + gap 8px(对齐全站 ops-cell)。
for (const page of ['src/pages/asin/QueryAsinPage.vue', 'src/pages/asin/SkipPricePage.vue']) {
const s = readSource(page)
assert.match(s, /\.asin-col-actions\s*\{[^}]*gap:\s*8px/, `${page} 操作列按钮间距 8px`)
}
})
@@ -58,7 +58,7 @@ test('align_digital_human_api_download_url', () => {
test('align_digital_human_page_wiring', () => {
const page = readSource('src/pages/records/RecordsDigitalHumanVersionPage.vue')
assert.match(page, /pageSize\s*=\s*20|:page-size="20"/, '分页大小 20')
assert.match(page, /pageSize\s*=\s*ref\(10\)/, '分页大小默认 10(全站统一)')
assert.match(page, /page-nums/, '数字页码分页(像素复刻旧版)')
assert.match(page, /设为最新/, '按钮名对齐“设为最新”')
assert.match(page, /上传新版本/, '页头上传按钮名对齐')
@@ -43,6 +43,19 @@ test('align_dup_console_drawer_template', () => {
assert.match(page, /drawerSummary/, '抽屉渲染汇总徽章')
assert.match(page, /次上架/, '汇总含 N 次上架')
assert.match(page, /<th[^>]*>次数<\/th>/, '聚合表含次数列')
assert.match(page, /v-for="row in drawerRows"/, '抽屉聚合表绑定分页切片')
assert.match(page, /:total="drawerAggRows\.length"/, '抽屉聚合表有分页器')
})
test('align_dup_console_pagination_all_lists', () => {
// 全站统一收官:撞款页四处列表(分布/矩阵/撞款详情卡片/抽屉明细)均挂共享 OldPagination。
const page = readSource('src/pages/tasks/DuplicateCheckPage.vue')
assert.match(page, /:total="distCount"/, '店铺上架分布有分页器')
assert.match(page, /:total="dupRows\.length"/, '撞款覆盖矩阵有分页器')
assert.match(page, /v-for="item in dupCardRows"/, '撞款详情卡片绑定分页切片')
assert.match(page, /v-if="dupRows\.length > dupCardPageSize"/, '撞款详情卡片有分页器')
assert.match(page, /v-for="group in pagedGroupStats"/, '分组说明抽屉表绑定分页切片')
assert.match(page, /:total="groupStats\.length"/, '分组说明抽屉表有分页器')
})
test('align_dup_console_ledger_total_preload', () => {
@@ -75,3 +88,11 @@ test('align_dup_console_role_scope_group_controls_only_super', () => {
assert.match(service, /if \(superAdmin\) \{\s*return null;/, '超管可见范围=全量(null)')
assert.match(service, /selectManagedShopNames\(operatorId\)/, '普通管理员可见范围=所管分组店铺')
})
test('align_dup_console_country_labels_chinese', () => {
// 反馈:国家显示一定要用中文——站点/国家一律经 asinCountryLabel 映射,禁止裸国家码直出。
const page = readSource('src/pages/tasks/DuplicateCheckPage.vue')
assert.match(page, /asinCountryLabel\(/, '站点显示走中文映射函数')
assert.doesNotMatch(page, /\{\{\s*(row|item)\.country\s*\}\}/, '表格/抽屉无裸国家码直出')
assert.match(page, /asinCountryLabel\(code\)/, '筛选下拉站点显示中文')
})
@@ -61,3 +61,11 @@ test('align_image_video_page_card_info_wiring', () => {
assert.match(page, /copyLink\(task\.debugUrl\)/, '调试链接复制接线')
assert.match(page, /:disabled="!video\.displayUrl"|!hasDisplayUrl/, '下载按钮无视频禁用')
})
test('align_image_video_permission_table_pagination', () => {
// 权限配置弹窗用户表全量渲染无分页:补共享 OldPagination10/20/50/100)。
const page = readSource('src/pages/tasks/ImageVideoTasksPage.vue')
assert.match(page, /import OldPagination from '@\/components\/OldPagination\.vue'/, '分页组件已 import(曾缺失导致分页器不渲染)')
assert.match(page, /:data="pagedPermissionItems"/, '权限表绑定分页切片')
assert.match(page, /:total="permissionItems\.length"/, '权限表有分页器')
})
@@ -30,3 +30,10 @@ test('align_shop_data_cards_empty_text', () => {
const page = readSource('src/pages/tasks/ShopDataTasksPage.vue')
assert.match(page, /暂无符合条件的店铺数据记录/, '空态文案对齐参考')
})
test('align_shop_data_permission_table_pagination', () => {
// 权限配置弹窗用户表全量渲染无分页:补共享 OldPagination10/20/50/100)。
const page = readSource('src/pages/tasks/ShopDataTasksPage.vue')
assert.match(page, /:data="pagedPermissionItems"/, '权限表绑定分页切片')
assert.match(page, /:total="permissionItems\.length"/, '权限表有分页器')
})
+1 -1
View File
@@ -64,7 +64,7 @@ test('test_task_103_video_list_load_boundary_empty_input', () => {
const page = parseImageVideoPage({})
assert.deepEqual(page.items, [])
assert.equal(page.total, 0)
assert.equal(page.pageSize, 20)
assert.equal(page.pageSize, 10)
})
test('test_task_103_video_list_load_boundary_single_item', () => {
+1 -1
View File
@@ -79,7 +79,7 @@ test('test_task_110_shop_data_list_load_boundary_empty_input', () => {
const page = parseShopDataTaskPage({})
assert.deepEqual(page.items, [])
assert.equal(page.total, 0)
assert.equal(page.pageSize, 20)
assert.equal(page.pageSize, 10)
})
test('test_task_110_shop_data_list_load_boundary_single_item', () => {
+2 -2
View File
@@ -63,7 +63,7 @@ test('test_task_062_dedupe_list_boundary_empty_input', () => {
assert.deepEqual(page.items, [])
assert.equal(page.total, 0)
assert.equal(page.page, 1)
assert.equal(page.pageSize, 15)
assert.equal(page.pageSize, 10)
})
test('test_task_062_dedupe_list_boundary_single_item', () => {
@@ -84,7 +84,7 @@ test('test_task_062_dedupe_list_boundary_limit_or_missing_field', () => {
// 边界上限/缺字段:缺 page/pageSize 用默认;空 dataValue 保留空串。
const page = parseDedupeTotalPage({ items: [{ id: 9, dataValue: '' }], total: 0, page: 1 })
assert.equal(page.items[0].dataValue, '')
assert.equal(page.pageSize, 15)
assert.equal(page.pageSize, 10)
assert.equal(toDedupeTotalItem({ id: 10, dataValue: 'v' })?.dataValue, 'v')
})
+1 -1
View File
@@ -56,7 +56,7 @@ test('test_task_068_brand_db_list_boundary_empty_input', () => {
assert.deepEqual(page.items, [])
assert.equal(page.total, 0)
assert.equal(page.page, 1)
assert.equal(page.pageSize, 15)
assert.equal(page.pageSize, 10)
})
test('test_task_068_brand_db_list_boundary_single_item', () => {
+1 -1
View File
@@ -95,7 +95,7 @@ test('test_task_071_query_asin_list_boundary_empty_input', () => {
assert.deepEqual(page.items, [])
assert.equal(page.total, 0)
assert.equal(page.page, 1)
assert.equal(page.pageSize, 15)
assert.equal(page.pageSize, 10)
})
test('test_task_071_query_asin_list_boundary_single_item', () => {
+13 -1
View File
@@ -10,6 +10,7 @@ import {
} from '../src/pages/asin/skip-price-model.ts'
import { ASIN_COUNTRY_CODES, asinCountryLabel } from '../src/pages/asin/asin-country.ts'
import {
normalizePriceInput,
skipPriceFilterActive,
toSkipPriceParams,
toSkipPriceQuery,
@@ -84,7 +85,7 @@ test('test_task_075_skip_price_list_boundary_empty_input', () => {
assert.deepEqual(page.items, [])
assert.equal(page.total, 0)
assert.equal(page.page, 1)
assert.equal(page.pageSize, 15)
assert.equal(page.pageSize, 10)
})
test('test_task_075_skip_price_list_boundary_single_item', () => {
@@ -136,6 +137,17 @@ test('test_task_075_skip_price_filter_normal_primary_and_price', () => {
})
})
test('test_task_075_skip_price_filter_number_input_compat', () => {
// 原生 number input v-model 会把输入值转成 numbernormalizePriceInput 需兼容(修复 "(value||'').trim is not a function")。
const state: SkipPriceFilterState = { groupId: null, shopName: '', asin: '', country: '', minimumPriceFrom: 12.34, minimumPriceTo: 56.78 }
const query = toSkipPriceQuery(toSkipPriceParams(state, 1, 15))
assert.equal(query.minimum_price_from, 12.34)
assert.equal(query.minimum_price_to, 56.78)
assert.equal(normalizePriceInput(0), 0)
assert.equal(normalizePriceInput(-5), null)
assert.equal(normalizePriceInput(null as unknown as string), null)
})
test('test_task_075_skip_price_filter_invalid_input_rejected', () => {
// 异常输入:非法价格/负数/非数值价格丢弃;success=false 抛 message;垃圾行不入列表。
const state: SkipPriceFilterState = {
+1 -1
View File
@@ -102,7 +102,7 @@ test('test_task_078_product_category_search_boundary_empty_and_default', () => {
assert.deepEqual(page.items, [])
assert.equal(page.total, 0)
assert.equal(page.hasMore, false)
assert.equal(page.pageSize, 20)
assert.equal(page.pageSize, 10)
})
test('test_task_078_product_category_invalid_input_rejected', () => {