fix: align report workbench page actions
This commit is contained in:
@@ -5020,7 +5020,8 @@ npm run verify:phase8
|
|||||||
| TC-REPORT-WORKBENCH-010 | 在状态记录按批次、操作人、状态、入口、对象和时间搜索 | 返回真实状态记录及操作人;可追溯人工修改来源;分页、空数据、失败和历史无入口记录均正确展示 |
|
| TC-REPORT-WORKBENCH-010 | 在状态记录按批次、操作人、状态、入口、对象和时间搜索 | 返回真实状态记录及操作人;可追溯人工修改来源;分页、空数据、失败和历史无入口记录均正确展示 |
|
||||||
| TC-REPORT-WORKBENCH-011 | 桌面及390px窄屏查看四页和状态弹窗 | 桌面表格可扫描;窄屏核心主体、状态、今日发送和操作可访问,无按钮遮挡;控制台无新增错误,所有业务数据来自真实API/PostgreSQL |
|
| TC-REPORT-WORKBENCH-011 | 桌面及390px窄屏查看四页和状态弹窗 | 桌面表格可扫描;窄屏核心主体、状态、今日发送和操作可访问,无按钮遮挡;控制台无新增错误,所有业务数据来自真实API/PostgreSQL |
|
||||||
| TC-REPORT-WORKBENCH-012 | 查看企业签名表头和签名列表 | 签名列不显示升序、降序或其他排序按钮;列表不显示逐行待生成明细字段;默认按创建时间倒序 |
|
| TC-REPORT-WORKBENCH-012 | 查看企业签名表头和签名列表 | 签名列不显示升序、降序或其他排序按钮;列表不显示逐行待生成明细字段;默认按创建时间倒序 |
|
||||||
| TC-REPORT-WORKBENCH-013 | 通道报备明细当前页有10条数据,点击“全选当前页” | 当前页10条全部选中,按钮变为“取消全选”,批量数为10;翻页或查询后选择清空,不选中其他页面数据 |
|
| TC-REPORT-WORKBENCH-013 | 通道报备明细当前页有10条数据,点击右上角“全选当页” | 当前页10条全部选中,按钮变为“取消全选”,批量数为10;该按钮与“批量修改状态”保持统一操作区间距;翻页或查询后选择清空,不选中其他页面数据 |
|
||||||
|
| TC-REPORT-WORKBENCH-017 | 报备资料池当前页存在可生成资料,点击右上角“全选当页” | 仅选中本页全部可生成资料,按钮变为“取消全选”,“预检并生成”显示所选数量且与全选按钮保持统一操作区间距;不再显示表格上方的复选框式“选择本页全部可生成资料”,预检及生成接口契约不变 |
|
||||||
| TC-REPORT-WORKBENCH-014 | 生成同时包含签名和引流资料、覆盖多个通道的批次 | 每个通道生成一份简报;日期取批次生成日期,批次号一致;签名行和引流行分别按已确认格式展示,且可一键复制完整原文 |
|
| TC-REPORT-WORKBENCH-014 | 生成同时包含签名和引流资料、覆盖多个通道的批次 | 每个通道生成一份简报;日期取批次生成日期,批次号一致;签名行和引流行分别按已确认格式展示,且可一键复制完整原文 |
|
||||||
| TC-REPORT-WORKBENCH-015 | 通道配置0个、1个或多个名称为“短信内容”的字段 | 0个时简报短信内容为空;1个时取资料中该字段的实际值;多个时严格按`sortOrder ASC, createdAt ASC`取第一个字段在资料中的实际值,后续同名字段不参与;资料未提供首字段时保持为空,不得使用通道缺省值冒充资料内容 |
|
| TC-REPORT-WORKBENCH-015 | 通道配置0个、1个或多个名称为“短信内容”的字段 | 0个时简报短信内容为空;1个时取资料中该字段的实际值;多个时严格按`sortOrder ASC, createdAt ASC`取第一个字段在资料中的实际值,后续同名字段不参与;资料未提供首字段时保持为空,不得使用通道缺省值冒充资料内容 |
|
||||||
| TC-REPORT-WORKBENCH-016 | 批次生成后修改字段库或签名/引流资料 | 历史批次简报仍使用生成时固化的批次快照,不随当前资料改变;不新增数据库migration,不触发短信发送或队列 |
|
| TC-REPORT-WORKBENCH-016 | 批次生成后修改字段库或签名/引流资料 | 历史批次简报仍使用生成时固化的批次快照,不随当前资料改变;不新增数据库migration,不触发短信发送或队列 |
|
||||||
|
|||||||
@@ -4355,3 +4355,9 @@ git diff --check
|
|||||||
- 发布前恢复点为`/opt/cmpp-platform-backups/report-brief-testdata-20260903T013807Z`,约668MB,PostgreSQL custom dump、Redis RDB、运行与配置tar、标记及服务基线均通过SHA、`pg_restore --list`和tar可读性校验;精确Git归档SHA-256为`d17dcc5befbb9c0e7fb5439652fd375130ab3968a6ef95adb72d139bc1b251c3`。最终测试环境`.deployed-commit=068b8047ddbfbacf32b233b7b8cfb4e52fd04a6f`,95项migration无待执行项。
|
- 发布前恢复点为`/opt/cmpp-platform-backups/report-brief-testdata-20260903T013807Z`,约668MB,PostgreSQL custom dump、Redis RDB、运行与配置tar、标记及服务基线均通过SHA、`pg_restore --list`和tar可读性校验;精确Git归档SHA-256为`d17dcc5befbb9c0e7fb5439652fd375130ab3968a6ef95adb72d139bc1b251c3`。最终测试环境`.deployed-commit=068b8047ddbfbacf32b233b7b8cfb4e52fd04a6f`,95项migration无待执行项。
|
||||||
- 两个批次的简报均按批次创建时间显示“2026年9月3日”,包含签名`【LG压测】`和各自批次号;由于资料快照没有“短信内容”值,短信内容正确为空,证明默认值没有冒充资料。配置、批次、XLSX及简报核验JSON均保存在上述恢复点。
|
- 两个批次的简报均按批次创建时间显示“2026年9月3日”,包含签名`【LG压测】`和各自批次号;由于资料快照没有“短信内容”值,短信内容正确为空,证明默认值没有冒充资料。配置、批次、XLSX及简报核验JSON均保存在上述恢复点。
|
||||||
- 生成前后三条Redis Stream的完整基线文本逐字一致,未产生短信链路事件。发布后12项相关服务均active,API健康,发布窗口无error级journal。Chrome中现有测试标签停留在登录页且没有有效登录态,因此本轮没有把登录后页面视觉/复制交互标记为真实浏览器通过;该项由真实服务、数据库和MinIO数据验证覆盖,但仍与浏览器交互验收明确区分。
|
- 生成前后三条Redis Stream的完整基线文本逐字一致,未产生短信链路事件。发布后12项相关服务均active,API健康,发布窗口无error级journal。Chrome中现有测试标签停留在登录页且没有有效登录态,因此本轮没有把登录后页面视觉/复制交互标记为真实浏览器通过;该项由真实服务、数据库和MinIO数据验证覆盖,但仍与浏览器交互验收明确区分。
|
||||||
|
|
||||||
|
## 2026-09-03 报备工作台全选控件统一(本地修改)
|
||||||
|
|
||||||
|
- 通道报备明细右上角操作区改用现有统一`page-heading__actions`样式,修复“全选当页”和“批量修改状态”之间无间隔的问题;仅调整样式类名,不改变选择、翻页清空或批量状态接口。
|
||||||
|
- 报备资料池移除表格上方“选择本页全部可生成资料”复选框标签,替换为右上角统一幽灵按钮“全选当页/取消全选”,与“预检并生成”保持统一间距;仍只选择当前页资格预检通过的数据,后端预检及生成接口、参数和业务逻辑不变。
|
||||||
|
- 定向报备工作台组件3项、前端全量12文件54项、TypeScript及Vite生产构建通过;Vite仅保留既有Chart分块超过500kB提示。Browser插件不在本会话技能列表,按前端调试流程使用工作区Playwright Chrome运行本地生产预览;1600×1000下两个页面身份、非空、错误层、控制台及点击全选交互通过,两个按钮组间距实测均为8px,截图保存在工作区外,不纳入提交。页面数据仅用于本地布局和交互验证,不作为真实API验收结论。
|
||||||
|
|||||||
@@ -293,6 +293,16 @@ export function AdminReportMaterialsPage() {
|
|||||||
<h1>报备资料池</h1>
|
<h1>报备资料池</h1>
|
||||||
<p>这里按企业应用 × 签名或引流对象 × 资料版本展示待生成资料;生成批次后再拆成通道与运营商明细。</p>
|
<p>这里按企业应用 × 签名或引流对象 × 资料版本展示待生成资料;生成批次后再拆成通道与运营商明细。</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="page-heading__actions">
|
||||||
|
<Button
|
||||||
|
disabled={!eligibleItems.length}
|
||||||
|
onClick={() =>
|
||||||
|
setSelected(allSelected ? new Set() : new Set(eligibleItems.map((item) => item.id)))
|
||||||
|
}
|
||||||
|
variant="ghost"
|
||||||
|
>
|
||||||
|
{allSelected ? '取消全选' : '全选当页'}
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
disabled={busy || selected.size === 0}
|
disabled={busy || selected.size === 0}
|
||||||
icon={<Layers3 size={16} />}
|
icon={<Layers3 size={16} />}
|
||||||
@@ -301,19 +311,12 @@ export function AdminReportMaterialsPage() {
|
|||||||
{busy ? '生成中...' : `预检并生成(${selected.size})`}
|
{busy ? '生成中...' : `预检并生成(${selected.size})`}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{error ? <p className="form-error">{error}</p> : null}
|
{error ? <p className="form-error">{error}</p> : null}
|
||||||
{message ? <p className="form-success">{message}</p> : null}
|
{message ? <p className="form-success">{message}</p> : null}
|
||||||
<div className="page-stack">
|
<div className="page-stack">
|
||||||
{filter}
|
{filter}
|
||||||
<div className="surface">
|
<div className="surface">
|
||||||
<label className="table-actions">
|
|
||||||
<input
|
|
||||||
checked={allSelected}
|
|
||||||
onChange={() => setSelected(allSelected ? new Set() : new Set(eligibleItems.map((item) => item.id)))}
|
|
||||||
type="checkbox"
|
|
||||||
/>
|
|
||||||
选择本页全部可生成资料
|
|
||||||
</label>
|
|
||||||
<Table
|
<Table
|
||||||
columns={pendingColumns}
|
columns={pendingColumns}
|
||||||
data={pendingData.items}
|
data={pendingData.items}
|
||||||
|
|||||||
@@ -359,7 +359,7 @@ export function AdminReportTasksPage() {
|
|||||||
<h1>通道报备明细</h1>
|
<h1>通道报备明细</h1>
|
||||||
<p>按企业应用 × 签名/引流对象 × 通道 × 运营商展示,未生成任务的“未报备”明细也会显示。</p>
|
<p>按企业应用 × 签名/引流对象 × 通道 × 运营商展示,未生成任务的“未报备”明细也会显示。</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="page-heading-actions">
|
<div className="page-heading__actions">
|
||||||
<Button
|
<Button
|
||||||
disabled={!tasks.length}
|
disabled={!tasks.length}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
@@ -367,7 +367,7 @@ export function AdminReportTasksPage() {
|
|||||||
}
|
}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
>
|
>
|
||||||
{allCurrentPageSelected ? '取消全选' : '全选当前页'}
|
{allCurrentPageSelected ? '取消全选' : '全选当页'}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
disabled={!selected.size}
|
disabled={!selected.size}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import userEvent from '@testing-library/user-event';
|
|||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import { AdminReportBatchesPage } from './AdminReportBatchesPage';
|
import { AdminReportBatchesPage } from './AdminReportBatchesPage';
|
||||||
|
import { AdminReportMaterialsPage } from './AdminReportMaterialsPage';
|
||||||
import { AdminReportTasksPage } from './AdminReportTasksPage';
|
import { AdminReportTasksPage } from './AdminReportTasksPage';
|
||||||
|
|
||||||
const { adminApi, clipboardWriteText } = vi.hoisted(() => ({
|
const { adminApi, clipboardWriteText } = vi.hoisted(() => ({
|
||||||
@@ -11,9 +12,11 @@ const { adminApi, clipboardWriteText } = vi.hoisted(() => ({
|
|||||||
exportSingleReportMaterial: vi.fn(),
|
exportSingleReportMaterial: vi.fn(),
|
||||||
getReportMaterialBatch: vi.fn(),
|
getReportMaterialBatch: vi.fn(),
|
||||||
getSingleReportMaterialDetail: vi.fn(),
|
getSingleReportMaterialDetail: vi.fn(),
|
||||||
|
listPendingReportMaterials: vi.fn(),
|
||||||
listReportDetailsPage: vi.fn(),
|
listReportDetailsPage: vi.fn(),
|
||||||
listReportMaterialBatches: vi.fn(),
|
listReportMaterialBatches: vi.fn(),
|
||||||
listReportMaterialBatchTasks: vi.fn(),
|
listReportMaterialBatchTasks: vi.fn(),
|
||||||
|
preflightReportMaterialBatch: vi.fn(),
|
||||||
},
|
},
|
||||||
clipboardWriteText: vi.fn(),
|
clipboardWriteText: vi.fn(),
|
||||||
}));
|
}));
|
||||||
@@ -54,7 +57,7 @@ describe('report workbench pages', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
const selectPage = await screen.findByRole('button', { name: '全选当前页' });
|
const selectPage = await screen.findByRole('button', { name: '全选当页' });
|
||||||
await user.click(selectPage);
|
await user.click(selectPage);
|
||||||
expect(screen.getByRole('button', { name: '取消全选' })).toBeVisible();
|
expect(screen.getByRole('button', { name: '取消全选' })).toBeVisible();
|
||||||
expect(screen.getByRole('button', { name: '批量修改状态(2)' })).toBeEnabled();
|
expect(screen.getByRole('button', { name: '批量修改状态(2)' })).toBeEnabled();
|
||||||
@@ -65,6 +68,49 @@ describe('report workbench pages', () => {
|
|||||||
screen.getAllByRole('checkbox').forEach((checkbox) => expect(checkbox).not.toBeChecked());
|
screen.getAllByRole('checkbox').forEach((checkbox) => expect(checkbox).not.toBeChecked());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('uses the shared current-page selection button in the material pool heading', async () => {
|
||||||
|
const material = {
|
||||||
|
id: 'signature:signature-1',
|
||||||
|
tenantId: 'tenant-1',
|
||||||
|
signatureId: 'signature-1',
|
||||||
|
applicationId: 'app-1',
|
||||||
|
reportType: 'signature',
|
||||||
|
name: '测试签名',
|
||||||
|
detail: '测试用途',
|
||||||
|
materialVersion: 1,
|
||||||
|
changedAt: '2026-09-03T01:00:00.000Z',
|
||||||
|
tenant: { id: 'tenant-1', name: '测试企业' },
|
||||||
|
application: { id: 'app-1', name: '测试应用' },
|
||||||
|
};
|
||||||
|
adminApi.listPendingReportMaterials.mockResolvedValue({ items: [material], total: 1, page: 1, pageSize: 20 });
|
||||||
|
adminApi.preflightReportMaterialBatch.mockResolvedValue({
|
||||||
|
eligible: true,
|
||||||
|
eligibleTargetCount: 1,
|
||||||
|
skippedTargetCount: 0,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: material.id,
|
||||||
|
eligible: true,
|
||||||
|
blockedReasons: [],
|
||||||
|
targets: [{ eligible: true }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
render(
|
||||||
|
<MemoryRouter>
|
||||||
|
<AdminReportMaterialsPage />
|
||||||
|
</MemoryRouter>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const user = userEvent.setup();
|
||||||
|
await user.click(await screen.findByRole('button', { name: '全选当页' }));
|
||||||
|
expect(screen.getByRole('button', { name: '取消全选' })).toBeVisible();
|
||||||
|
expect(screen.getByRole('button', { name: '预检并生成(1)' })).toBeEnabled();
|
||||||
|
expect(screen.getByRole('checkbox', { name: '选择测试签名' })).toBeChecked();
|
||||||
|
expect(screen.queryByText('选择本页全部可生成资料')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
it('shows and copies the channel brief returned by the batch detail API', async () => {
|
it('shows and copies the channel brief returned by the batch detail API', async () => {
|
||||||
const batch = {
|
const batch = {
|
||||||
id: 'batch-1',
|
id: 'batch-1',
|
||||||
|
|||||||
Reference in New Issue
Block a user