Polish admin table labels and button layout
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
2026-05-09 12:47:27 +08:00
parent e390b72a0c
commit 8669a996ca
5 changed files with 1194 additions and 27 deletions

View File

@@ -0,0 +1,107 @@
/* @vitest-environment jsdom */
import {render, screen, waitFor} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import {beforeEach, expect, test, vi} from 'vitest';
import {
getAdminDatabaseTableRows,
getAdminDatabaseTables,
} from '../api/adminApiClient';
import {AdminDatabaseTablesPage} from './AdminDatabaseTablesPage';
vi.mock('../api/adminApiClient', () => ({
formatAdminApiError: vi.fn((error: unknown) =>
error instanceof Error ? error.message : '请求失败',
),
getAdminDatabaseTableRows: vi.fn(),
getAdminDatabaseTables: vi.fn(),
isAdminApiError: vi.fn(() => false),
}));
beforeEach(() => {
window.location.hash = '#tables?table=profile_referral_relation';
vi.mocked(getAdminDatabaseTables).mockResolvedValue({
fetchErrors: [],
tables: ['profile_referral_relation'],
});
vi.mocked(getAdminDatabaseTableRows).mockResolvedValue({
columns: ['invitee_user_id', 'inviter_user_id', 'invite_code', 'bound_at'],
limit: 100,
rows: [
{
cells: {
bound_at: '2026-05-02T00:00:00Z',
invitee_user_id: 'u-b',
invite_code: 'INV-1001',
inviter_user_id: 'u-a',
},
raw: [
'u-b',
'u-a',
'INV-1001',
'2026-05-02T00:00:00Z',
],
},
{
cells: {
bound_at: '2026-05-01T00:00:00Z',
invitee_user_id: 'u-a',
invite_code: 'INV-1002',
inviter_user_id: 'u-c',
},
raw: ['u-a', 'u-c', 'INV-1002', '2026-05-01T00:00:00Z'],
},
{
cells: {
bound_at: '2026-05-03T00:00:00Z',
invitee_user_id: 'u-c',
invite_code: 'INV-1003',
inviter_user_id: 'u-a',
},
raw: ['u-c', 'u-a', 'INV-1003', '2026-05-03T00:00:00Z'],
},
],
tableName: 'profile_referral_relation',
totalReturned: 3,
});
});
test('后台表查询页支持宽表滚动容器和表头排序', async () => {
const user = userEvent.setup();
const {container} = render(
<AdminDatabaseTablesPage token="admin-token" onUnauthorized={vi.fn()} />,
);
await screen.findByText('u-b');
const tableWrap = container.querySelector('.admin-table-wrap');
expect(tableWrap?.querySelector('.admin-database-table')).not.toBeNull();
expect(screen.getByRole('option', {name: '邀请关系profile_referral_relation'}).getAttribute('title')).toBe(
'原始表名profile_referral_relation。邀请关系记录表。',
);
expect(screen.getByText('已选表邀请关系profile_referral_relation')).toBeTruthy();
expect(screen.getByRole('heading', {name: '邀请关系'}).getAttribute('title')).toBe(
'原始表名profile_referral_relation。邀请关系记录表。',
);
expect(screen.getByRole('button', {name: '被邀请人ID'}).getAttribute('title')).toBe(
'原始字段名invitee_user_id。被邀请人的用户标识。点击可按此列排序。',
);
expect(readFirstColumnValues(container)).toEqual(['u-b', 'u-a', 'u-c']);
await user.click(screen.getByRole('button', {name: '邀请人ID'}));
await waitFor(() => {
expect(readFirstColumnValues(container)).toEqual(['u-b', 'u-c', 'u-a']);
});
await user.click(screen.getByRole('button', {name: '邀请人ID'}));
await waitFor(() => {
expect(readFirstColumnValues(container)).toEqual(['u-a', 'u-b', 'u-c']);
});
});
function readFirstColumnValues(container: HTMLElement) {
return Array.from(container.querySelectorAll('tbody tr')).map(
(row) => row.querySelector('td')?.textContent?.trim() ?? '',
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -350,11 +350,6 @@ button:disabled {
font-weight: 650;
}
.admin-query-reset-button {
width: auto;
padding: 0 12px;
}
.admin-field {
display: grid;
min-width: 0;
@@ -603,6 +598,13 @@ button:disabled {
background: #eef3f6;
}
.admin-ghost-button.admin-query-reset-button {
width: auto;
min-width: 76px;
height: 40px;
padding: 0 12px;
}
.admin-text-button {
display: inline;
border: 0;
@@ -650,7 +652,10 @@ button:disabled {
}
.admin-table-wrap {
max-width: 100%;
overflow: auto;
scrollbar-gutter: stable;
-webkit-overflow-scrolling: touch;
}
.admin-table {
@@ -687,6 +692,65 @@ button:disabled {
min-width: 1180px;
}
.admin-database-table {
width: max-content;
min-width: 100%;
table-layout: fixed;
}
.admin-database-table th,
.admin-database-table td {
width: 220px;
min-width: 220px;
max-width: 220px;
}
.admin-database-table th:last-child,
.admin-database-table td:last-child {
width: 112px;
min-width: 112px;
max-width: 112px;
}
.admin-table-sort-button {
display: inline-flex;
align-items: center;
gap: 6px;
max-width: 100%;
border: 0;
color: #667682;
background: transparent;
padding: 0;
text-align: left;
font-size: 12px;
font-weight: 800;
}
.admin-table-sort-button span {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
}
.admin-table-sort-button svg {
flex: 0 0 auto;
}
.admin-table-cell-ellipsis {
display: block;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.admin-table-sort-button:hover,
.admin-table-sort-button:focus-visible,
.admin-table-sort-button[data-active="true"] {
color: #0f5666;
outline: none;
}
.admin-json-preview {
max-width: 360px;
max-height: 160px;

View File

@@ -78,9 +78,13 @@ Query
页面能力:
- 表选择下拉,支持 URL hash `#tables?table=xxx` 直达指定表。
- 表选择下拉展示中文表名并保留原始表名,支持 URL hash `#tables?table=xxx` 直达指定表。
- 查询表单表名、关键词、JSON 条件、条数。
- 查询结果表格横向滚动,移动端不撑坏布局。
- 查询结果标题和已选表摘要展示中文表名,鼠标悬浮显示原始表名和表说明,方便运营识别真实数据域。
- 表头支持点击排序,排序只作用于当前已拉取的行数据,不改变后端 SQL。
- 表头展示中文字段名,鼠标悬浮显示原始字段名、字段说明和排序提示,方便运营阅读且保留排障所需的真实列名。
- 单元格内容过长时在表格内单行省略,完整内容可通过悬浮标题或行详情弹层查看。
- 每行提供“详情”按钮,以独立弹层展示完整 JSON。
- 总览表统计行点击后跳转到 `#tables?table={tableName}`

View File

@@ -7,6 +7,8 @@ export default defineConfig({
include: [
'src/**/*.test.ts',
'src/**/*.test.tsx',
'apps/admin-web/src/**/*.test.ts',
'apps/admin-web/src/**/*.test.tsx',
'scripts/**/*.test.ts',
'packages/shared/src/**/*.test.ts',
],