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:
@@ -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()
|
||||
@@ -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([])
|
||||
})
|
||||
|
||||
@@ -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)') // 全站按钮主色统一 #4f78a5(d385d999 起)
|
||||
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([])
|
||||
})
|
||||
|
||||
|
||||
@@ -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()
|
||||
@@ -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', () => {
|
||||
// 权限配置弹窗用户表全量渲染无分页:补共享 OldPagination(10/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', () => {
|
||||
// 权限配置弹窗用户表全量渲染无分页:补共享 OldPagination(10/20/50/100)。
|
||||
const page = readSource('src/pages/tasks/ShopDataTasksPage.vue')
|
||||
assert.match(page, /:data="pagedPermissionItems"/, '权限表绑定分页切片')
|
||||
assert.match(page, /:total="permissionItems\.length"/, '权限表有分页器')
|
||||
})
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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 会把输入值转成 number;normalizePriceInput 需兼容(修复 "(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 = {
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
Reference in New Issue
Block a user