feat(后台管理): 实体管理列表统一展示创建时间/更新时间
用户管理、菜单管理、不符合ASIN、数据去重总数据、查询ASIN、最低价ASIN、 商品类目、密钥管理共 8 个实体管理列表补齐两列。分组管理/店铺密钥/店铺管理 此前已带创建+修改时间,任务列表与统计报表(撞款监控、密钥用量、日志、 记录与版本)不含实体更新语义,均未改动。 关键点——时间列必须由数据库维护,否则新列是假的: 这些表的更新走 selectById → 改字段 → updateById,实体带着读出的旧 updated_at 一起写回。MySQL 规则是「UPDATE 显式给某列赋值时不触发该列的 ON UPDATE 自动更新」,不禁写就会把旧值写回去,更新时间永远冻结在首次写入 时刻。按 V125(biz_file_result)既有样板,给 7 个实体标注 @TableField(insertStrategy=NEVER, updateStrategy=NEVER)。 - V131:users / columns 补 updated_at(幂等 ADD COLUMN,仿 V125 写法)。 存量行被回填为迁移执行时刻,非真实历史变更时间(历史上无记录,无法还原) - 实体/VO:AdminUserEntity、PermissionMenuEntity、InvalidAsinDataEntity、 DedupeTotalDataEntity、ProductCategoryEntity、QueryAsinEntity、 SkipPriceAsinEntity 加/改写 updatedAt;AdminUserItemVo、PermissionMenuItemVo、 InvalidAsinDataItemVo、DedupeTotalDataItemVo 补 updatedAt; AdminUserSecretRowVo 补 createdAt(行级首次配置时间 = 三模块最早) - 查询ASIN/最低价ASIN 后端 VO 与前端 model 本就有两字段,仅补渲染 - 前端 8 页表格加列,同步修正空态/加载行的 colspan(手写表格,不同步会错位) - 测试:align-query-asin / align-skip-price 原断言「不允许有更新时间列」 (像素复刻旧版),按新需求改为断言两列存在;新增 e2e list-time-columns 覆盖 8 页表头与真实时间值渲染
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
import { expect, test, type Page } from '@playwright/test'
|
||||
|
||||
// 后台管理列表「创建时间 + 更新时间」两列验收(2026-09-19 需求):
|
||||
// 8 个实体管理列表统一展示创建/更新时间,数据取自各列表 VO。
|
||||
// mock 夹具统一给 createdAt=2026-03-01 09:15:00、updatedAt=2026-09-18 16:42:30,
|
||||
// 页面统一经 formatDateTime 归一为「YYYY-MM-DD HH:mm:ss」。
|
||||
|
||||
const CREATED = '2026-03-01 09:15:00'
|
||||
const UPDATED = '2026-09-18 16:42:30'
|
||||
|
||||
const PAGES: Array<{ title: string; path: string; heading: string }> = [
|
||||
{ title: '用户管理', path: '/admin-vue/account/users', heading: '用户列表' },
|
||||
{ title: '菜单管理', path: '/admin-vue/account/menus', heading: '菜单' },
|
||||
{ title: '密钥管理', path: '/admin-vue/account/user-secrets', heading: '用户密钥列表' },
|
||||
{ title: '不符合ASIN数据', path: '/admin-vue/asin-center/invalid', heading: '品牌数据列表' },
|
||||
{ title: '数据去重总数据', path: '/admin-vue/asin-center/registry', heading: '数据去重总数据' },
|
||||
{ title: '查询ASIN', path: '/admin-vue/asin-center/query', heading: '查询' },
|
||||
{ title: '最低价ASIN', path: '/admin-vue/asin-center/skip-price', heading: '最低价' },
|
||||
{ title: '商品类目', path: '/admin-vue/asin-center/categories', heading: '商品类目' },
|
||||
]
|
||||
|
||||
async function openPage(page: Page, path: string) {
|
||||
await page.goto(path)
|
||||
await expect(page.locator('.panel-box table tbody tr').first()).toBeVisible({ timeout: 15000 })
|
||||
}
|
||||
|
||||
for (const spec of PAGES) {
|
||||
test(`${spec.title}:表格展示创建时间与更新时间`, async ({ page }) => {
|
||||
const errors: string[] = []
|
||||
page.on('pageerror', (error) => errors.push(error.message))
|
||||
|
||||
await openPage(page, spec.path)
|
||||
|
||||
const table = page.locator('.panel-box table').first()
|
||||
// 表头出现两列
|
||||
await expect(table.locator('thead')).toContainText('创建时间')
|
||||
await expect(table.locator('thead')).toContainText('更新时间')
|
||||
// 数据行渲染出真实时间值(非空、非占位符)
|
||||
await expect(table.locator('tbody')).toContainText(CREATED)
|
||||
await expect(table.locator('tbody')).toContainText(UPDATED)
|
||||
|
||||
expect(errors).toEqual([])
|
||||
})
|
||||
}
|
||||
@@ -220,13 +220,183 @@ const server = createServer((req, res) => {
|
||||
return json(res, {
|
||||
success: true,
|
||||
data: {
|
||||
items: [{ id: 1, username: 'admin', role: 'super_admin', creatorUsername: 'system', createdAt: '2026-01-01 00:00:00' }],
|
||||
items: [{ id: 1, username: 'admin', role: 'super_admin', creatorUsername: 'system', created_at: '2026-03-01 09:15:00', updated_at: '2026-09-18 16:42:30' }],
|
||||
total: 1,
|
||||
},
|
||||
})
|
||||
}
|
||||
if (url === '/api/admin/permission-menus') {
|
||||
return json(res, { success: true, data: { items: [] } })
|
||||
// 菜单管理列表:一条根节点,带创建/更新时间(验收时间列)。
|
||||
return json(res, {
|
||||
success: true,
|
||||
data: [
|
||||
{
|
||||
id: 1,
|
||||
name: '用户管理',
|
||||
column_key: 'admin_users',
|
||||
parent_id: null,
|
||||
menu_type: 'admin',
|
||||
route_path: 'account/users',
|
||||
sort_order: 10,
|
||||
created_at: '2026-03-01T09:15:00',
|
||||
updated_at: '2026-09-18T16:42:30',
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
if (url === '/api/admin/invalid-asin-data') {
|
||||
return json(res, {
|
||||
success: true,
|
||||
data: {
|
||||
items: [
|
||||
{
|
||||
id: 1,
|
||||
dataValue: 'B0INVALID1',
|
||||
brand: '测试品牌',
|
||||
groupId: 1,
|
||||
groupName: '测试分组',
|
||||
recordSource: 'MANUAL',
|
||||
createdAt: '2026-03-01 09:15:00',
|
||||
updatedAt: '2026-09-18 16:42:30',
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
},
|
||||
})
|
||||
}
|
||||
if (url === '/api/admin/dedupe-total-data') {
|
||||
return json(res, {
|
||||
success: true,
|
||||
data: {
|
||||
items: [
|
||||
{
|
||||
id: 1,
|
||||
dataValue: 'B0DEDUPE01',
|
||||
country: 'DE',
|
||||
groupId: 1,
|
||||
groupName: '测试分组',
|
||||
uploaderUserId: 1,
|
||||
username: 'admin',
|
||||
createdAt: '2026-03-01 09:15:00',
|
||||
updatedAt: '2026-09-18 16:42:30',
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
},
|
||||
})
|
||||
}
|
||||
if (url === '/api/admin/query-asins') {
|
||||
return json(res, {
|
||||
success: true,
|
||||
data: {
|
||||
items: [
|
||||
{
|
||||
id: 1,
|
||||
groupId: 1,
|
||||
groupName: '测试分组',
|
||||
shopName: '测试店铺',
|
||||
asinDe: 'B0QUERY001',
|
||||
asinUk: '',
|
||||
asinFr: '',
|
||||
asinIt: '',
|
||||
asinEs: '',
|
||||
createdAt: '2026-03-01 09:15:00',
|
||||
updatedAt: '2026-09-18 16:42:30',
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
},
|
||||
})
|
||||
}
|
||||
if (url === '/api/admin/skip-price-asins') {
|
||||
return json(res, {
|
||||
success: true,
|
||||
data: {
|
||||
items: [
|
||||
{
|
||||
id: 1,
|
||||
groupId: 1,
|
||||
groupName: '测试分组',
|
||||
shopName: '测试店铺',
|
||||
asinDe: 'B0SKIP0001',
|
||||
minimumPriceDe: 9.99,
|
||||
asinUk: '',
|
||||
minimumPriceUk: null,
|
||||
asinFr: '',
|
||||
minimumPriceFr: null,
|
||||
asinIt: '',
|
||||
minimumPriceIt: null,
|
||||
asinEs: '',
|
||||
minimumPriceEs: null,
|
||||
createdAt: '2026-03-01 09:15:00',
|
||||
updatedAt: '2026-09-18 16:42:30',
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
},
|
||||
})
|
||||
}
|
||||
if (url === '/api/admin/product-categories/children') {
|
||||
return json(res, {
|
||||
success: true,
|
||||
data: {
|
||||
items: [
|
||||
{
|
||||
id: 1,
|
||||
parentId: null,
|
||||
name: '护肤品',
|
||||
categoryKey: 'skincare',
|
||||
sortOrder: 10,
|
||||
description: '测试备注',
|
||||
isBuiltin: true,
|
||||
childCount: 0,
|
||||
level: 0,
|
||||
path: '护肤品',
|
||||
createdAt: '2026-03-01 09:15:00',
|
||||
updatedAt: '2026-09-18 16:42:30',
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
hasMore: false,
|
||||
},
|
||||
})
|
||||
}
|
||||
if (url === '/api/admin/user-secrets') {
|
||||
const emptyModule = { moduleKey: '', moduleLabel: '', masked: '', full: '', exists: false, checkStatus: 'unknown', checkCode: '', checkMessage: '', checkLatencyMs: null, checkedAt: null, updatedAt: null }
|
||||
return json(res, {
|
||||
success: true,
|
||||
data: {
|
||||
items: [
|
||||
{
|
||||
userId: 1,
|
||||
username: 'admin',
|
||||
groups: [],
|
||||
leaderUsername: '',
|
||||
similarAsin: { ...emptyModule, moduleKey: 'similar-asin', moduleLabel: '货源查询密钥' },
|
||||
appearancePatent: { ...emptyModule, moduleKey: 'appearance-patent', moduleLabel: '外观专利密钥' },
|
||||
proxy: { ...emptyModule, moduleKey: 'proxy', moduleLabel: '代理设置' },
|
||||
status: 'unknown',
|
||||
statusMessage: '',
|
||||
createdAt: '2026-03-01 09:15:00',
|
||||
updatedAt: '2026-09-18 16:42:30',
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
page: 1,
|
||||
pageSize: 15,
|
||||
groupOptions: [],
|
||||
},
|
||||
})
|
||||
}
|
||||
if (url === '/api/admin/shop-manage-groups') {
|
||||
return json(res, { success: true, data: { items: [] } })
|
||||
|
||||
@@ -30,6 +30,8 @@ export interface AdminUserSecretRow {
|
||||
proxy: AdminUserSecretModule
|
||||
status: string
|
||||
statusMessage: string
|
||||
/** 首次配置时间(三模块中最早);从未配置为 null。 */
|
||||
createdAt: string | null
|
||||
updatedAt: string | null
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,8 @@ export function toAdminUserItem(raw: unknown): AdminUser | null {
|
||||
if (createdById !== null) item.createdById = createdById
|
||||
const createdAt = text(r.created_at)
|
||||
if (createdAt) item.createdAt = createdAt
|
||||
const updatedAt = text(r.updated_at)
|
||||
if (updatedAt) item.updatedAt = updatedAt
|
||||
const creator = text(r.creator_username)
|
||||
if (creator) item.creatorUsername = creator
|
||||
const abbr = text(r.pinyin_abbr)
|
||||
|
||||
@@ -241,6 +241,7 @@ onMounted(loadMenus)
|
||||
<th style="width: 100px">菜单类型</th>
|
||||
<th style="width: 120px">上级菜单</th>
|
||||
<th style="width: 170px">创建时间</th>
|
||||
<th style="width: 170px">更新时间</th>
|
||||
<th style="width: 170px">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -271,6 +272,7 @@ onMounted(loadMenus)
|
||||
<td><span class="menu-type-text">{{ menuTypeLabel(row.menuType) }}</span></td>
|
||||
<td>{{ parentNameOf(row) }}</td>
|
||||
<td>{{ formatDateTime(row.createdAt) }}</td>
|
||||
<td>{{ formatDateTime(row.updatedAt) }}</td>
|
||||
<td class="ops-cell">
|
||||
<button class="btn btn-sm" type="button" @click="openEdit(row)">编辑</button>
|
||||
<button
|
||||
@@ -286,10 +288,10 @@ onMounted(loadMenus)
|
||||
</tr>
|
||||
</template>
|
||||
<tr v-else-if="loading">
|
||||
<td colspan="7" class="empty-tip">加载中...</td>
|
||||
<td colspan="8" class="empty-tip">加载中...</td>
|
||||
</tr>
|
||||
<tr v-else>
|
||||
<td colspan="7" class="empty-tip">{{ filterName || filterType ? '暂无匹配菜单' : '暂无菜单,请先在上方新增' }}</td>
|
||||
<td colspan="8" class="empty-tip">{{ filterName || filterType ? '暂无匹配菜单' : '暂无菜单,请先在上方新增' }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -233,6 +233,8 @@ onMounted(load)
|
||||
<th style="width: 230px">外观专利密钥</th>
|
||||
<th style="width: 380px">代理设置</th>
|
||||
<th style="width: 130px">状态</th>
|
||||
<th style="width: 170px">创建时间</th>
|
||||
<th style="width: 170px">更新时间</th>
|
||||
<th style="width: 170px">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -278,6 +280,8 @@ onMounted(load)
|
||||
{{ rowStatusMeta(row.status).label }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ formatDateTime(row.createdAt) }}</td>
|
||||
<td>{{ formatDateTime(row.updatedAt) }}</td>
|
||||
<td class="ops-cell">
|
||||
<button
|
||||
class="btn btn-sm"
|
||||
@@ -292,10 +296,10 @@ onMounted(load)
|
||||
</tr>
|
||||
</template>
|
||||
<tr v-else-if="loading">
|
||||
<td :colspan="isSuperAdmin ? 8 : 7" class="empty-tip">加载中...</td>
|
||||
<td :colspan="isSuperAdmin ? 10 : 9" class="empty-tip">加载中...</td>
|
||||
</tr>
|
||||
<tr v-else>
|
||||
<td :colspan="isSuperAdmin ? 8 : 7" class="empty-tip">{{ keyword || statusFilter || groupFilter ? '暂无匹配记录' : '暂无用户密钥记录' }}</td>
|
||||
<td :colspan="isSuperAdmin ? 10 : 9" class="empty-tip">{{ keyword || statusFilter || groupFilter ? '暂无匹配记录' : '暂无用户密钥记录' }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -169,6 +169,7 @@ onMounted(loadUsers)
|
||||
<th style="width: 140px">角色</th>
|
||||
<th style="width: 160px">所属管理员</th>
|
||||
<th style="width: 180px">创建时间</th>
|
||||
<th style="width: 180px">更新时间</th>
|
||||
<th style="width: 160px">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -180,6 +181,7 @@ onMounted(loadUsers)
|
||||
<td>{{ roleLabel(row.role) }}</td>
|
||||
<td>{{ row.creatorUsername || '-' }}</td>
|
||||
<td>{{ formatDateTime(row.createdAt) }}</td>
|
||||
<td>{{ formatDateTime(row.updatedAt) }}</td>
|
||||
<td class="ops-cell">
|
||||
<button
|
||||
class="btn btn-sm"
|
||||
@@ -203,10 +205,10 @@ onMounted(loadUsers)
|
||||
</tr>
|
||||
</template>
|
||||
<tr v-else-if="loading">
|
||||
<td colspan="6" class="empty-tip">加载中...</td>
|
||||
<td colspan="7" class="empty-tip">加载中...</td>
|
||||
</tr>
|
||||
<tr v-else>
|
||||
<td colspan="6" class="empty-tip">{{ userListEmptyHint() }}</td>
|
||||
<td colspan="7" class="empty-tip">{{ userListEmptyHint() }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -15,6 +15,7 @@ export interface MenuManageNode {
|
||||
routePath: string
|
||||
sortOrder: number
|
||||
createdAt?: string
|
||||
updatedAt?: string
|
||||
children?: MenuManageNode[]
|
||||
}
|
||||
|
||||
@@ -90,6 +91,8 @@ export function parseMenuManageItem(raw: unknown): MenuManageNode | null {
|
||||
if (rootColumnKey) node.rootColumnKey = rootColumnKey
|
||||
const createdAt = text(record.created_at)
|
||||
if (createdAt) node.createdAt = createdAt
|
||||
const updatedAt = text(record.updated_at)
|
||||
if (updatedAt) node.updatedAt = updatedAt
|
||||
return node
|
||||
}
|
||||
|
||||
|
||||
@@ -202,6 +202,7 @@ onMounted(() => {
|
||||
<th v-if="isSuperAdmin" style="width: 140px">分组</th>
|
||||
<th style="width: 110px">来源</th>
|
||||
<th style="width: 170px">创建时间</th>
|
||||
<th style="width: 170px">更新时间</th>
|
||||
<th style="width: 150px">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -218,6 +219,7 @@ onMounted(() => {
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ formatDateTime(row.createdAt) }}</td>
|
||||
<td>{{ formatDateTime(row.updatedAt) }}</td>
|
||||
<td class="ops-cell">
|
||||
<button class="btn btn-sm" type="button" @click="openEdit(row)">编辑</button>
|
||||
<button class="btn btn-sm btn-danger" type="button" @click="remove(row)">删除</button>
|
||||
@@ -225,10 +227,10 @@ onMounted(() => {
|
||||
</tr>
|
||||
</template>
|
||||
<tr v-else-if="loading">
|
||||
<td :colspan="isSuperAdmin ? 7 : 6" class="empty-tip">加载中...</td>
|
||||
<td :colspan="isSuperAdmin ? 8 : 7" class="empty-tip">加载中...</td>
|
||||
</tr>
|
||||
<tr v-else>
|
||||
<td :colspan="isSuperAdmin ? 7 : 6" class="empty-tip">暂无数据</td>
|
||||
<td :colspan="isSuperAdmin ? 8 : 7" class="empty-tip">暂无数据</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -375,6 +375,7 @@ onMounted(() => {
|
||||
<th>用户名</th>
|
||||
<th v-if="isSuperAdmin">分组</th>
|
||||
<th>创建时间</th>
|
||||
<th>更新时间</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -387,6 +388,7 @@ onMounted(() => {
|
||||
<td>{{ row.username || '-' }}</td>
|
||||
<td v-if="isSuperAdmin">{{ row.groupName || '未分组' }}</td>
|
||||
<td>{{ formatDateTime(row.createdAt) }}</td>
|
||||
<td>{{ formatDateTime(row.updatedAt) }}</td>
|
||||
<td class="ops-cell">
|
||||
<button class="btn btn-sm" type="button" @click="openEdit(row)">编辑</button>
|
||||
<button class="btn btn-sm btn-danger" type="button" @click="removeRow(row)">删除</button>
|
||||
@@ -394,10 +396,10 @@ onMounted(() => {
|
||||
</tr>
|
||||
</template>
|
||||
<tr v-else-if="loading">
|
||||
<td :colspan="isSuperAdmin ? 7 : 6" class="empty-tip">加载中...</td>
|
||||
<td :colspan="isSuperAdmin ? 8 : 7" class="empty-tip">加载中...</td>
|
||||
</tr>
|
||||
<tr v-else>
|
||||
<td :colspan="isSuperAdmin ? 7 : 6" class="empty-tip">暂无总数据</td>
|
||||
<td :colspan="isSuperAdmin ? 8 : 7" class="empty-tip">暂无总数据</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
} from './product-category-model.ts'
|
||||
import { categoryDraftError, createProductCategoryDraft, toCategorySaveRequest, type ProductCategoryDraft } from './product-category-editor-model.ts'
|
||||
import { buildProductCategoryExportUrl } from './product-category-export.ts'
|
||||
import { formatDateTime } from '@/utils/datetime'
|
||||
import OldPagination from '@/components/OldPagination.vue'
|
||||
|
||||
const loading = ref(false)
|
||||
@@ -330,6 +331,8 @@ onMounted(loadTree)
|
||||
<th style="width: 90px">排序</th>
|
||||
<th style="width: 120px">来源</th>
|
||||
<th>备注</th>
|
||||
<th style="width: 170px">创建时间</th>
|
||||
<th style="width: 170px">更新时间</th>
|
||||
<th style="width: 150px">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -337,7 +340,7 @@ onMounted(loadTree)
|
||||
<template v-if="treeRows.length">
|
||||
<template v-for="row in treeRows" :key="String(row.node.id)">
|
||||
<tr v-if="isCategoryLoadMoreNode(row.node)" class="load-more-row">
|
||||
<td colspan="6">
|
||||
<td colspan="8">
|
||||
<div class="load-more-cell">
|
||||
<span class="tree-indent" :style="{ width: `${(row.depth + 1) * 24}px` }"></span>
|
||||
<button class="btn btn-sm btn-secondary" type="button" :disabled="moreLoading(row.node)" @click="loadMoreChildren(row.node)">
|
||||
@@ -376,6 +379,8 @@ onMounted(loadTree)
|
||||
<span v-if="row.node.description" class="node-desc" :title="row.node.description">{{ row.node.description }}</span>
|
||||
<span v-else class="dim">—</span>
|
||||
</td>
|
||||
<td>{{ formatDateTime(row.node.createdAt) }}</td>
|
||||
<td>{{ formatDateTime(row.node.updatedAt) }}</td>
|
||||
<td class="ops-cell">
|
||||
<button class="btn btn-sm" type="button" @click="openEdit(row.node)">编辑</button>
|
||||
<button
|
||||
@@ -392,10 +397,10 @@ onMounted(loadTree)
|
||||
</template>
|
||||
</template>
|
||||
<tr v-else-if="loading">
|
||||
<td colspan="6" class="empty-tip">加载中...</td>
|
||||
<td colspan="8" class="empty-tip">加载中...</td>
|
||||
</tr>
|
||||
<tr v-else>
|
||||
<td colspan="6" class="empty-tip">暂无商品类目</td>
|
||||
<td colspan="8" class="empty-tip">暂无商品类目</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody v-else>
|
||||
@@ -411,6 +416,8 @@ onMounted(loadTree)
|
||||
<span v-if="row.description" class="node-desc" :title="row.description">{{ row.description }}</span>
|
||||
<span v-else class="dim">—</span>
|
||||
</td>
|
||||
<td>{{ formatDateTime(row.createdAt) }}</td>
|
||||
<td>{{ formatDateTime(row.updatedAt) }}</td>
|
||||
<td class="ops-cell">
|
||||
<button class="btn btn-sm" type="button" @click="openEdit(row)">编辑</button>
|
||||
<button
|
||||
@@ -426,10 +433,10 @@ onMounted(loadTree)
|
||||
</tr>
|
||||
</template>
|
||||
<tr v-else-if="searchLoading">
|
||||
<td colspan="6" class="empty-tip">搜索中...</td>
|
||||
<td colspan="8" class="empty-tip">搜索中...</td>
|
||||
</tr>
|
||||
<tr v-else>
|
||||
<td colspan="6" class="empty-tip">暂无搜索结果</td>
|
||||
<td colspan="8" class="empty-tip">暂无搜索结果</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import CopyText from '@/components/CopyText.vue'
|
||||
import { formatDateTime } from '@/utils/datetime'
|
||||
import { createQueryAsin, fetchQueryAsinList, fetchShopNamesByGroup } from './query-asin-api.ts'
|
||||
import { QUERY_ASIN_COUNTRIES, queryAsinDisplayRows, type QueryAsinItem } from './query-asin-model.ts'
|
||||
import { asinCountryLabel } from './asin-country.ts'
|
||||
@@ -394,6 +395,8 @@ onMounted(() => {
|
||||
<th style="width: 15%">店铺名</th>
|
||||
<th style="width: 24%">ASIN</th>
|
||||
<th style="width: 12%">国家</th>
|
||||
<th style="width: 170px">创建时间</th>
|
||||
<th style="width: 170px">更新时间</th>
|
||||
<th style="width: 9%">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -413,6 +416,8 @@ onMounted(() => {
|
||||
</td>
|
||||
<td>{{ asinCountryLabel(row.country || '') }}</td>
|
||||
<template v-if="row.isFirst">
|
||||
<td :rowspan="row.rowspan">{{ formatDateTime(row.item.createdAt) }}</td>
|
||||
<td :rowspan="row.rowspan">{{ formatDateTime(row.item.updatedAt) }}</td>
|
||||
<td :rowspan="row.rowspan" class="asin-col-actions">
|
||||
<button class="btn btn-sm" type="button" @click="openConfig(row.item as QueryAsinItem)">配置</button>
|
||||
<button class="btn btn-sm btn-danger" type="button" @click="removeShopAsin(row.item as QueryAsinItem)">删除</button>
|
||||
@@ -421,10 +426,10 @@ onMounted(() => {
|
||||
</tr>
|
||||
</template>
|
||||
<tr v-else-if="loading">
|
||||
<td :colspan="isSuperAdmin ? 6 : 5" class="empty-tip">加载中...</td>
|
||||
<td :colspan="isSuperAdmin ? 8 : 7" class="empty-tip">加载中...</td>
|
||||
</tr>
|
||||
<tr v-else>
|
||||
<td :colspan="isSuperAdmin ? 6 : 5" class="empty-tip">暂无数据</td>
|
||||
<td :colspan="isSuperAdmin ? 8 : 7" class="empty-tip">暂无数据</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import CopyText from '@/components/CopyText.vue'
|
||||
import { formatDateTime } from '@/utils/datetime'
|
||||
import { createSkipPriceAsin, fetchSkipPriceList } from './skip-price-api.ts'
|
||||
import { skipPriceDisplayRows, type SkipPriceItem } from './skip-price-model.ts'
|
||||
import { asinCountryLabel } from './asin-country.ts'
|
||||
@@ -447,6 +448,8 @@ onMounted(() => {
|
||||
<th style="width: 24%">ASIN</th>
|
||||
<th style="width: 12%">国家</th>
|
||||
<th style="width: 10%">最低价</th>
|
||||
<th style="width: 170px">创建时间</th>
|
||||
<th style="width: 170px">更新时间</th>
|
||||
<th style="width: 9%">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -467,6 +470,8 @@ onMounted(() => {
|
||||
<td>{{ asinCountryLabel(row.country || '') }}</td>
|
||||
<td class="price-cell">{{ row.minimumPrice !== '' ? row.minimumPrice : '-' }}</td>
|
||||
<template v-if="row.isFirst">
|
||||
<td :rowspan="row.rowspan">{{ formatDateTime(row.item.createdAt) }}</td>
|
||||
<td :rowspan="row.rowspan">{{ formatDateTime(row.item.updatedAt) }}</td>
|
||||
<td :rowspan="row.rowspan" class="asin-col-actions">
|
||||
<button class="btn btn-sm" type="button" @click="openConfig(row.item as SkipPriceItem)">配置</button>
|
||||
<button class="btn btn-sm btn-danger" type="button" @click="removeRow(row.item as SkipPriceItem)">删除</button>
|
||||
@@ -475,10 +480,10 @@ onMounted(() => {
|
||||
</tr>
|
||||
</template>
|
||||
<tr v-else-if="loading">
|
||||
<td :colspan="isSuperAdmin ? 7 : 6" class="empty-tip">加载中...</td>
|
||||
<td :colspan="isSuperAdmin ? 9 : 8" class="empty-tip">加载中...</td>
|
||||
</tr>
|
||||
<tr v-else>
|
||||
<td :colspan="isSuperAdmin ? 7 : 6" class="empty-tip">暂无数据</td>
|
||||
<td :colspan="isSuperAdmin ? 9 : 8" class="empty-tip">暂无数据</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -11,6 +11,7 @@ export interface DedupeTotalItem {
|
||||
uploaderUserId: number | null
|
||||
username: string
|
||||
createdAt?: string
|
||||
updatedAt?: string
|
||||
}
|
||||
|
||||
export interface DedupeTotalPageResult {
|
||||
@@ -51,6 +52,8 @@ export function toDedupeTotalItem(raw: unknown): DedupeTotalItem | null {
|
||||
}
|
||||
const createdAt = text(record.createdAt ?? record.created_at)
|
||||
if (createdAt) item.createdAt = createdAt
|
||||
const updatedAt = text(record.updatedAt ?? record.updated_at)
|
||||
if (updatedAt) item.updatedAt = updatedAt
|
||||
return item
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ export interface ProductCategoryNode {
|
||||
childCount: number
|
||||
level: number | null
|
||||
path: string
|
||||
createdAt?: string
|
||||
updatedAt?: string
|
||||
children: ProductCategoryNode[]
|
||||
}
|
||||
|
||||
@@ -72,6 +74,10 @@ export function toProductCategoryNode(raw: unknown): ProductCategoryNode | null
|
||||
path: text(record.path),
|
||||
children: [],
|
||||
}
|
||||
const createdAt = text(record.createdAt ?? record.created_at)
|
||||
if (createdAt) node.createdAt = createdAt
|
||||
const updatedAt = text(record.updatedAt ?? record.updated_at)
|
||||
if (updatedAt) node.updatedAt = updatedAt
|
||||
if (Array.isArray(record.children)) {
|
||||
node.children = record.children
|
||||
.map((child) => toProductCategoryNode(child))
|
||||
|
||||
@@ -12,6 +12,7 @@ export interface InvalidAsinItem {
|
||||
groupName: string
|
||||
recordSource: string
|
||||
createdAt?: string
|
||||
updatedAt?: string
|
||||
}
|
||||
|
||||
export interface InvalidAsinPageResult {
|
||||
@@ -74,6 +75,8 @@ export function toInvalidAsinItem(raw: unknown): InvalidAsinItem | null {
|
||||
}
|
||||
const createdAt = text(record.createdAt ?? record.created_at)
|
||||
if (createdAt) item.createdAt = createdAt
|
||||
const updatedAt = text(record.updatedAt ?? record.updated_at)
|
||||
if (updatedAt) item.updatedAt = updatedAt
|
||||
return item
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ export interface AdminUser {
|
||||
createdById?: number | null
|
||||
creatorUsername?: string
|
||||
createdAt?: string
|
||||
updatedAt?: string
|
||||
pinyinAbbr?: string
|
||||
}
|
||||
|
||||
|
||||
@@ -82,7 +82,9 @@ test('align_query_asin_page_layout_wiring', () => {
|
||||
assert.match(page, /新增 ASIN/, '顶栏补新增 ASIN 按钮')
|
||||
assert.match(page, /:rowspan="row\.rowspan"/, '表格用原生 rowspan 合并(像素复刻旧版)')
|
||||
assert.match(page, /CopyText/, 'ASIN 单元格点击复制')
|
||||
assert.doesNotMatch(page, /更新时间/, '去掉更新时间列(参考无此列)')
|
||||
// 需求变更:后台管理列表统一展示「创建时间 + 更新时间」,覆盖早期「不加更新时间」的像素对齐。
|
||||
assert.match(page, /创建时间/, '表格含创建时间列')
|
||||
assert.match(page, /更新时间/, '表格含更新时间列')
|
||||
assert.match(page, /queryAsinDisplayRows/, '行展开走纯模型')
|
||||
assert.match(page, /请完整填写分组、店铺名、国家和 ASIN/, '新增弹窗校验文案对齐')
|
||||
assert.match(page, /请先选择分组/, '店铺下拉未选分组时占位对齐')
|
||||
|
||||
@@ -82,7 +82,9 @@ test('align_skip_price_page_layout_wiring', () => {
|
||||
assert.match(page, /新增 ASIN/, '顶栏补新增 ASIN 按钮')
|
||||
assert.match(page, /:rowspan="row\.rowspan"/, '表格用原生 rowspan 合并(像素复刻旧版)')
|
||||
assert.match(page, /CopyText/, 'ASIN 单元格点击复制')
|
||||
assert.doesNotMatch(page, /更新时间/, '去掉更新时间列(参考无此列)')
|
||||
// 需求变更:后台管理列表统一展示「创建时间 + 更新时间」,覆盖早期「不加更新时间」的像素对齐。
|
||||
assert.match(page, /创建时间/, '表格含创建时间列')
|
||||
assert.match(page, /更新时间/, '表格含更新时间列')
|
||||
assert.match(page, /skipPriceDisplayRows/, '行展开走纯模型')
|
||||
assert.match(page, /请完整填写分组、店铺名、国家和 ASIN/, '新增弹窗校验文案对齐')
|
||||
assert.match(page, /最低价格式不正确/, '最低价格式校验对齐')
|
||||
|
||||
Reference in New Issue
Block a user