Commit 29a9f2d0 authored by liguangyu06's avatar liguangyu06
Browse files

Add SPD Playwright UI automation framework for GitLab CI and Jenkins.

Keep credentials, browser cache, and Cursor skills out of the repository so clones can run from .env.example and npm ci.
parents
import { readFileSync } from 'node:fs';
import path from 'node:path';
import type { TestInfo } from '@playwright/test';
import { test } from '@playwright/test';
import { isCi } from '../config/ci.config';
import { extractCaseId } from './case-meta';
import type { FailureClass } from './failure-classify';
export const QUARANTINE_TAG = '@quarantine';
export type QuarantineMode = 'quarantine' | 'observe';
export type QuarantineItem = {
/** STAT-XXX-NNN,与用例标题匹配 */
id?: string;
titleIncludes?: string;
fileIncludes?: string;
/** quarantine:主门禁隔离;observe:仍执行,只打标跟踪 */
mode: QuarantineMode;
reason: string;
owner?: string;
since: string;
ticket?: string;
category?: FailureClass;
};
export type QuarantineFile = {
version: number;
items: QuarantineItem[];
};
let cached: QuarantineFile | null = null;
export function loadQuarantine(): QuarantineFile {
if (cached) return cached;
const file = path.resolve(process.cwd(), 'config/quarantine.json');
try {
const parsed = JSON.parse(readFileSync(file, 'utf8')) as QuarantineFile;
cached = { version: parsed.version ?? 1, items: Array.isArray(parsed.items) ? parsed.items : [] };
} catch {
cached = { version: 1, items: [] };
}
return cached;
}
export function matchQuarantine(testInfo: Pick<TestInfo, 'title' | 'file'>): QuarantineItem | undefined {
const caseId = extractCaseId(testInfo.title);
const file = testInfo.file.replace(/\\/g, '/');
return loadQuarantine().items.find((item) => {
if (item.id && caseId && item.id === caseId) return true;
if (item.id && testInfo.title.includes(item.id)) return true;
const titleOk = item.titleIncludes ? testInfo.title.includes(item.titleIncludes) : false;
const fileOk = item.fileIncludes ? file.includes(item.fileIncludes) : !item.fileIncludes;
if (item.titleIncludes && titleOk && fileOk) return true;
if (!item.id && !item.titleIncludes && item.fileIncludes && fileOk) return true;
return false;
});
}
/** CI 主门禁默认隔离 quarantine 项;RUN_QUARANTINE=1 时改为只跑隔离项。 */
export function shouldSkipQuarantinedInMain(): boolean {
if (process.env.RUN_QUARANTINE === '1') return false;
if (process.env.QUARANTINE_POLICY === 'never') return false;
if (process.env.QUARANTINE_POLICY === 'always') return true;
return isCi();
}
export function applyQuarantine(testInfo: TestInfo): void {
const item = matchQuarantine(testInfo);
const quarantineOnly = process.env.RUN_QUARANTINE === '1';
if (quarantineOnly && !item) {
testInfo.annotations.push({ type: 'quarantine', description: 'suite-exclude' });
test.skip(true, 'quarantine 套件:不在隔离清单中');
return;
}
if (!item) return;
testInfo.annotations.push({
type: 'quarantine',
description: `${item.mode}: ${item.reason}`,
});
if (item.ticket) {
testInfo.annotations.push({ type: 'ticket', description: item.ticket });
}
if (item.mode === 'observe') return;
if (!shouldSkipQuarantinedInMain()) return;
test.skip(true, `quarantine: ${item.reason}`);
}
import { expect, type FrameLocator, type Locator, type Page, type Response } from '@playwright/test';
import {
collectPageHealthIssues,
type PageHealthScope,
} from './page-health.util';
const MAX_ISSUES = 30;
const STATIC_ASSET_PATTERN = /\.(js|css|png|jpe?g|gif|svg|webp|woff2?|ico|map)(\?|$)/i;
/** 需要监控的业务接口(排除静态资源)。 */
function isTrackableApiUrl(url: string): boolean {
if (STATIC_ASSET_PATTERN.test(url)) return false;
if (url.startsWith('data:') || url.startsWith('blob:')) return false;
return (
/\/api\//i.test(url) ||
/\/spd\//i.test(url) ||
/(export|query|list|report|search|async)/i.test(url)
);
}
function trimUrl(url: string): string {
return url.length > 180 ? `${url.slice(0, 177)}...` : url;
}
async function readBusinessError(response: Response): Promise<string | null> {
const contentType = response.headers()['content-type'] ?? '';
if (!contentType.includes('json')) return null;
try {
const body = await response.json();
if (!body || typeof body !== 'object') return null;
const record = body as Record<string, unknown>;
const code = record.code ?? record.statusCode ?? record.errCode;
const success = record.success;
const message = record.msg ?? record.message ?? record.error ?? record.errMsg;
if (typeof success === 'boolean' && !success) {
return String(message ?? 'success=false');
}
if (typeof code === 'number' && code !== 200 && code !== 0) {
return String(message ?? `业务码=${code}`);
}
if (typeof code === 'string' && !['200', '0', 'success'].includes(code)) {
return String(message ?? `业务码=${code}`);
}
} catch {
return null;
}
return null;
}
export class RuntimeHealthGuard {
private readonly apiIssues: string[] = [];
private readonly allowedIssuePatterns: RegExp[] = [];
private readonly page: Page;
private readonly extraRoots: Array<Locator | FrameLocator>;
constructor(page: Page, extraRoots: Array<Locator | FrameLocator> = []) {
this.page = page;
this.extraRoots = extraRoots;
this.attachListeners(page);
}
private pushIssue(issue: string): void {
if (this.apiIssues.length >= MAX_ISSUES) return;
if (this.apiIssues.includes(issue)) return;
this.apiIssues.push(issue);
}
private attachListeners(target: Page): void {
target.on('requestfailed', (request) => {
if (!isTrackableApiUrl(request.url())) return;
const failure = request.failure()?.errorText ?? 'unknown';
// 切换统计维度/重复查询时,浏览器会 abort 上一次 in-flight 请求,非业务失败
if (/ERR_ABORTED/i.test(failure)) return;
this.pushIssue(`接口请求失败: ${request.method()} ${trimUrl(request.url())} (${failure})`);
});
target.on('response', (response) => {
void this.handleResponse(response);
});
}
private async handleResponse(response: Response): Promise<void> {
const url = response.url();
if (!isTrackableApiUrl(url)) return;
const status = response.status();
if (status >= 500) {
this.pushIssue(`接口 HTTP ${status}: ${trimUrl(url)}`);
return;
}
if (status >= 400) {
this.pushIssue(`接口 HTTP ${status}: ${trimUrl(url)}`);
return;
}
const businessError = await readBusinessError(response);
if (businessError) {
this.pushIssue(`接口业务报错 ${trimUrl(url)}: ${businessError}`);
}
}
/** 反向用例可登记预期报错(如登录失败),收尾健康检查不再把它们当缺陷。 */
allowExpected(pattern: RegExp): void {
this.allowedIssuePatterns.push(pattern);
}
private isAllowedIssue(issue: string): boolean {
return this.allowedIssuePatterns.some((pattern) => pattern.test(issue));
}
filterUnexpected(issues: readonly string[]): string[] {
return issues.filter((issue) => !this.isAllowedIssue(issue));
}
getApiIssues(): readonly string[] {
return this.apiIssues;
}
async collectAllIssues(contextRoots?: Array<Locator | FrameLocator>): Promise<string[]> {
const roots = contextRoots ?? [...this.extraRoots];
const scope: PageHealthScope = { page: this.page, roots };
const uiIssues = await collectPageHealthIssues(scope);
for (const frame of this.page.frames()) {
if (frame === this.page.mainFrame()) continue;
for (const selector of [
'.ant-message-error',
'.ant-notification-notice-error',
'.ant-alert-error',
'.ant-result-error',
]) {
const locator = frame.locator(selector);
const count = await locator.count().catch(() => 0);
for (let i = 0; i < count; i += 1) {
const item = locator.nth(i);
if (!(await item.isVisible().catch(() => false))) continue;
const text = (await item.innerText().catch(() => '')).trim();
if (text) uiIssues.push(`[iframe] ${text}`);
}
}
}
return [...new Set([...uiIssues, ...this.apiIssues])].slice(0, MAX_ISSUES);
}
async assertClean(context: string, contextRoots?: Array<Locator | FrameLocator>): Promise<void> {
const issues = this.filterUnexpected(await this.collectAllIssues(contextRoots));
expect(
issues,
`${context}:检测到页面/接口/系统报错,用例判为失败(不允许 skip 掩盖)`,
).toEqual([]);
}
}
const guardByPage = new WeakMap<Page, RuntimeHealthGuard>();
export function attachRuntimeHealthGuard(
page: Page,
extraRoots: Array<Locator | FrameLocator> = [],
): RuntimeHealthGuard {
const existing = guardByPage.get(page);
if (existing) return existing;
const guard = new RuntimeHealthGuard(page, extraRoots);
guardByPage.set(page, guard);
return guard;
}
export function getRuntimeHealthGuard(page: Page): RuntimeHealthGuard | undefined {
return guardByPage.get(page);
}
/** 页面 + 接口 + 系统统一健康断言(所有用例 skip 前/结束时调用)。 */
export async function assertRuntimeHealthy(
scope: PageHealthScope,
context: string,
guard?: RuntimeHealthGuard,
): Promise<void> {
const raw = guard ? await guard.collectAllIssues(scope.roots) : await collectPageHealthIssues(scope);
const issues = guard ? guard.filterUnexpected(raw) : raw;
expect(
issues,
`${context}:检测到页面/接口/系统报错,用例判为失败(不允许 skip 掩盖)`,
).toEqual([]);
}
import { expect, type FrameLocator, type Locator, type Page } from '@playwright/test';
import { timeouts } from '../config/timeouts.config';
export const ANT_SPIN_SELECTOR = '.ant-spin-spinning';
export const SEARCH_LOADING_SELECTOR = '.anticon-loading, .ant-btn-loading-icon';
export const TABLE_EMPTY_TEXT = /暂无数据|没有数据|暂无/;
export type TableReadyState = 'loading' | 'ready' | 'empty' | 'idle';
function asLocator(
root: Page | Locator | FrameLocator,
selector: string,
): Locator {
return root.locator(selector);
}
/** 元素可见(操作等待,TimeoutError 可被操作层重试;业务断言请用 expect)。 */
export async function waitVisible(locator: Locator, timeout = timeouts.expect): Promise<void> {
await locator.waitFor({ state: 'visible', timeout });
}
export async function waitHidden(locator: Locator, timeout = timeouts.expect): Promise<void> {
await locator.waitFor({ state: 'hidden', timeout });
}
/** Ant Spin 消失;没有 spin 时不阻塞。 */
export async function waitAntSpinGone(
root: Page | Locator | FrameLocator,
timeout = timeouts.searchLoading,
): Promise<void> {
await asLocator(root, ANT_SPIN_SELECTOR)
.first()
.waitFor({ state: 'hidden', timeout })
.catch(() => undefined);
}
/** 查询按钮 loading:先给一个短窗口出现,再等到消失。 */
export async function waitSearchButtonIdle(
searchBtn: Locator,
timeout = timeouts.searchLoading,
): Promise<void> {
const loadingIcon = searchBtn.locator(SEARCH_LOADING_SELECTOR);
await loadingIcon.waitFor({ state: 'visible', timeout: 2_000 }).catch(() => undefined);
await loadingIcon.waitFor({ state: 'hidden', timeout }).catch(() => undefined);
}
export async function isSearchButtonLoading(searchBtn: Locator): Promise<boolean> {
const loadingIcon = searchBtn.locator(SEARCH_LOADING_SELECTOR);
return (
(await loadingIcon.count()) > 0 &&
(await loadingIcon.first().isVisible().catch(() => false))
);
}
export async function isAntSpinVisible(root: Page | Locator | FrameLocator): Promise<boolean> {
const spin = asLocator(root, ANT_SPIN_SELECTOR);
return (await spin.count()) > 0 && (await spin.first().isVisible().catch(() => false));
}
export async function isTableEmptyVisible(
root: Page | Locator | FrameLocator,
): Promise<boolean> {
const empty = asLocator(root, '.ant-empty, .ant-table-placeholder, .ant-table-tbody').getByText(
TABLE_EMPTY_TEXT,
);
return (await empty.count()) > 0 && (await empty.first().isVisible().catch(() => false));
}
/**
* 列表就绪:loading 结束即可(有行 / 空态 / idle 都算结束)。
* 由 Page 提供 probe,避免 core 依赖具体定位器。
*/
export async function waitUntilNotLoading(
probe: () => Promise<TableReadyState>,
timeout = timeouts.tableReady,
): Promise<void> {
await expect.poll(probe, { timeout }).not.toBe('loading');
}
/**
* 动画/布局沉降:优先等两帧 rAF,避免固定 sleep。
* 仍保留极短上限,给表格滚动、列宽拖拽用。
*/
export async function waitUiSettle(page: Page, fallbackMs = 200): Promise<void> {
if (page.isClosed()) return;
try {
await page.evaluate(
() =>
new Promise<void>((resolve) => {
requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
}),
);
} catch {
await page.waitForTimeout(fallbackMs).catch(() => undefined);
}
}
import { buildSortColumnCases, type CenterReportPageData } from './center-report-shared.types';
import type { CenterReportDrawerField } from './center-report-shared.types';
import type { DeptQuickFieldDef } from './dept-report-field-defs';
export const anaChgColumns = {
field: '起始时间',
productName: '产品名称',
spec: '规格型号',
supplierName: '进院供应商',
field2: '二级供应商',
consumableType: '耗材类型',
id: '阳采ID',
productCode: '产品编号',
field3: '产品俗称',
field4: '器械注册人-术语配置',
field5: '单位',
field6: '原单价',
field7: '现单价',
field8: '数量',
field9: '总差额',
} as const;
export const anaChgComboFullQuickFields: readonly DeptQuickFieldDef[] = [
{ key: 'field', label: '起始时间', type: 'date' },
{ key: 'productName', label: '产品名称', type: 'text' },
{ key: 'spec', label: '规格型号', type: 'text' },
];
export const anaChgComboQuickFields: readonly DeptQuickFieldDef[] = [
{ key: 'field', label: '起始时间', type: 'date' },
{ key: 'productName', label: '产品名称', type: 'text' },
{ key: 'spec', label: '规格型号', type: 'text' },
];
export const anaChgComboDrawerFields: readonly CenterReportDrawerField[] = [
{ key: 'productName', type: 'text', label: '产品名称' },
{ key: 'spec', type: 'text', label: '规格型号' },
{ key: 'supplierName', type: 'text', label: '进院供应商' },
{ key: 'field2', type: 'text', label: '二级供应商' },
{ key: 'consumableType', type: 'select', label: '耗材类型' },
];
export const anaChgComboAssertKeys = ['productName'] as const;
export const anaChgDomainQuickFields: readonly DeptQuickFieldDef[] = [
{ key: 'field', label: '起始时间', type: 'date' },
{ key: 'productName', label: '产品名称', type: 'text' },
{ key: 'spec', label: '规格型号', type: 'text' },
];
export const anaChgDomainAssertKeys = ['field', 'productName', 'spec'] as const;
export const anaChgSortColumns = [
'产品编号',
'产品名称',
'产品俗称',
'规格型号',
'器械注册人-术语配置',
'单位',
'进院供应商',
'二级供应商',
'耗材类型',
'数量',
] as const;
export const anaChgData: CenterReportPageData & {
mockFieldLabel?: string;
drawerResetFieldLabel?: string;
drawerResetFieldKey?: string;
} = {
reportPath: '/spd/form/statistics/report/statistics/chg',
iframeSrc: 'statistics/chg',
productNameSelector: '#form_item_goodsName',
columns: anaChgColumns,
coreTableHeaders: ['产品编号', '产品名称', '产品俗称', '规格型号', '原单价', '现单价', '数量', '总差额'],
summaryKeywords: ['选中总数量', '选中总差额'],
columnAliases: {
supplierName: ['进院供应商名称'],
field4: ['器械注册人'],
field5: ['采购计划号'],
},
horizontalScrollAnchorColumn: '产品编号',
horizontalScrollTargetColumn: '器械注册人-术语配置',
horizontalScrollDragDelta: 320,
mockNotExistProduct: '__NOT_EXIST_PRODUCT__99999',
mockFieldLabel: '产品名称',
drawerResetFieldLabel: '产品名称',
drawerResetFieldKey: 'productName',
paginationPageSizeFrom: '50 条/页',
paginationPageSizeTo: '100 条/页',
sampleRowIndex: 0,
sortColumns: anaChgSortColumns,
sortCasePrefix: 'STAT-ACH',
sortCaseStartIndex: 10,
hasAdvancedFilter: true,
hasExport: true,
hasExpandRow: false,
hasBatchPrint: false,
hasToolbarPrint: false,
hasFilterToggle: false,
exportTaskName: '调价统计报表',
exportDirectDownload: false,
exportSelectFirstRow: false,
exportQueryFromSample: true,
columnSettingsToggleCandidates: ['备注'],
comboSampleScrollColumns: ['进院供应商'],
comboSamplePickKeys: ['field', 'productName', 'spec', 'supplierName', 'field2', 'consumableType'],
comboSampleMaxRowScan: 5,
drawerFields: anaChgComboDrawerFields,
};
export const anaChgSortCases = buildSortColumnCases('STAT-ACH', anaChgSortColumns, 10);
import { buildSortColumnCases, type CenterReportPageData } from './center-report-shared.types';
import type { CenterReportDrawerField } from './center-report-shared.types';
import type { DeptQuickFieldDef } from './dept-report-field-defs';
export const anaDeptColumns = {
field: '时间',
campus: '院区名称',
deptName: '科室',
} as const;
export const anaDeptComboFullQuickFields: readonly DeptQuickFieldDef[] = [
{ key: 'field', label: '时间', type: 'date' },
{ key: 'campus', label: '院区名称', type: 'select' },
{ key: 'deptName', label: '科室', type: 'select' },
];
export const anaDeptComboQuickFields: readonly DeptQuickFieldDef[] = [
{ key: 'field', label: '时间', type: 'date' },
{ key: 'campus', label: '院区名称', type: 'select' },
{ key: 'deptName', label: '科室', type: 'select' },
];
export const anaDeptComboDrawerFields: readonly CenterReportDrawerField[] = [
{ key: 'campus', type: 'select', label: '院区名称' },
{ key: 'deptName', type: 'select', label: '科室' },
];
export const anaDeptComboAssertKeys = ['field', 'campus'] as const;
export const anaDeptDomainQuickFields: readonly DeptQuickFieldDef[] = [
{ key: 'field', label: '时间', type: 'date' },
{ key: 'campus', label: '院区名称', type: 'select' },
];
export const anaDeptDomainAssertKeys = ['field', 'campus'] as const;
export const anaDeptData: CenterReportPageData & {
mockFieldLabel?: string;
drawerResetFieldLabel?: string;
drawerResetFieldKey?: string;
} = {
reportPath: '/spd/form/statistics/report/statistics/dept',
iframeSrc: 'statistics/dept',
productNameSelector: '#form_item_branchId',
columns: anaDeptColumns,
coreTableHeaders: ['科室', '院区名称', '时间'],
columnAliases: {
deptName: ['请领科室', '科室名称'],
},
horizontalScrollAnchorColumn: '时间',
horizontalScrollTargetColumn: '科室',
horizontalScrollDragDelta: 320,
mockNotExistProduct: '__NOT_EXIST_DEPT__99999',
mockFieldLabel: '科室',
drawerResetFieldLabel: '院区名称',
drawerResetFieldKey: 'campus',
paginationPageSizeFrom: '50 条/页',
paginationPageSizeTo: '100 条/页',
sampleRowIndex: 0,
hasAdvancedFilter: false,
hasExport: true,
hasExpandRow: false,
hasBatchPrint: false,
hasToolbarPrint: false,
hasFilterToggle: false,
isChart: true,
searchBtnLabel: '搜 索',
exportTaskName: '财务数据分析-科室成本',
exportDirectDownload: false,
exportSelectFirstRow: false,
exportQueryFromSample: true,
columnSettingsToggleCandidates: ['备注'],
comboSampleScrollColumns: ['科室'],
comboSamplePickKeys: ['field', 'campus', 'deptName'],
comboSampleMaxRowScan: 5,
drawerFields: anaDeptComboDrawerFields,
};
import { buildSortColumnCases, type CenterReportPageData } from './center-report-shared.types';
import type { CenterReportDrawerField } from './center-report-shared.types';
import type { DeptQuickFieldDef } from './dept-report-field-defs';
export const anaProvColumns = {
field: '时间',
supplierName: '进院供应商名称',
campus: '院区名称',
} as const;
export const anaProvComboFullQuickFields: readonly DeptQuickFieldDef[] = [
{ key: 'field', label: '时间', type: 'date' },
{ key: 'supplierName', label: '进院供应商名称', type: 'select' },
];
export const anaProvComboQuickFields: readonly DeptQuickFieldDef[] = [
{ key: 'field', label: '时间', type: 'date' },
{ key: 'supplierName', label: '进院供应商名称', type: 'select' },
];
export const anaProvComboDrawerFields: readonly CenterReportDrawerField[] = [
{ key: 'supplierName', type: 'select', label: '进院供应商名称' },
];
export const anaProvComboAssertKeys = ['field', 'campus'] as const;
export const anaProvDomainQuickFields: readonly DeptQuickFieldDef[] = [
{ key: 'field', label: '时间', type: 'date' },
];
export const anaProvDomainAssertKeys = ['field'] as const;
export const anaProvData: CenterReportPageData & {
mockFieldLabel?: string;
drawerResetFieldLabel?: string;
drawerResetFieldKey?: string;
} = {
reportPath: '/spd/form/statistics/report/statistics/prov',
iframeSrc: 'statistics/prov',
productNameSelector: '#form_item_supplierName',
columns: anaProvColumns,
coreTableHeaders: ['进院供应商名称', '时间'],
columnAliases: {
supplierName: ['进院供应商'],
},
horizontalScrollAnchorColumn: '时间',
horizontalScrollTargetColumn: '进院供应商名称',
horizontalScrollDragDelta: 320,
mockNotExistProduct: '__NOT_EXIST_SUPPLIER__99999',
mockFieldLabel: '进院供应商',
drawerResetFieldLabel: '进院供应商名称',
drawerResetFieldKey: 'supplierName',
paginationPageSizeFrom: '50 条/页',
paginationPageSizeTo: '100 条/页',
sampleRowIndex: 0,
hasAdvancedFilter: false,
hasExport: true,
hasExpandRow: false,
hasBatchPrint: false,
hasToolbarPrint: false,
hasFilterToggle: false,
isChart: true,
searchBtnLabel: '搜 索',
exportTaskName: '财务数据分析-供应商入账',
exportDirectDownload: false,
exportSelectFirstRow: false,
exportQueryFromSample: true,
columnSettingsToggleCandidates: ['备注'],
comboSampleScrollColumns: ['进院供应商'],
comboSamplePickKeys: ['field', 'supplierName'],
comboSampleMaxRowScan: 5,
drawerFields: anaProvComboDrawerFields,
};
/** 入库汇总查询 · 列表列名(动态取数)。 */
export const centerReportEntryColumns = {
productCode: '产品编号',
productName: '产品名称',
unit: '单位',
spec: '规格型号',
manufacturer: '生产厂家',
inboundQty: '入库数量',
inboundAmount: '入库金额',
billType: '单据类型',
campus: '院区名称',
sourceUnit: '来源单位',
batchNo: '批号',
supplier: '进院供应商',
billMode: '单据形态',
} as const;
export type CenterReportEntryColumnKey = keyof typeof centerReportEntryColumns;
export type EntryListSampleRow = Partial<Record<CenterReportEntryColumnKey, string>> & {
accountDate?: string;
};
/** 高级筛选抽屉字段(entry 页扫描字段)。 */
export const centerReportEntryDrawerFields: ReadonlyArray<{
key: CenterReportEntryColumnKey;
type: 'text' | 'select';
label: string;
}> = [
{ key: 'campus', type: 'select', label: '院区名称' },
{ key: 'sourceUnit', type: 'text', label: '来源单位' },
{ key: 'billMode', type: 'select', label: '单据形态' },
{ key: 'batchNo', type: 'text', label: '批号' },
{ key: 'supplier', type: 'text', label: '进院供应商' },
];
function buildSortColumnCases(): Array<{ id: string; column: string }> {
return centerReportEntrySortColumns.map((column, index) => ({
id: `STAT-IES-${String(11 + index).padStart(3, '0')}`,
column,
}));
}
export const centerReportEntrySortColumns = [
'产品编号',
'产品名称',
'单位',
'规格型号',
'耗材类型',
'入库数量',
'入库金额',
'医保编码',
'规格',
'型号',
] as const;
/** 子表表头(行展开后)。 */
export const centerReportEntrySubTableHeaders = [
'入库单号',
'记账日期',
'单据类型',
'院区名称',
'来源单位',
'制单人',
'查看',
] as const;
/** 入库汇总查询 · 测试数据(与 testcase CSV 对齐)。 */
export const centerReportEntryData = {
entryPath: '/spd/form/centerReport/report/centerPages/entry',
columns: centerReportEntryColumns,
drawerFields: centerReportEntryDrawerFields,
dimensions: ['区分包装', '区分批号', '区分来源单位'] as const,
coreTableHeaders: [
'产品编号',
'产品名称',
'单位',
'规格型号',
'生产厂家',
'入库数量',
'入库金额',
'医保编码',
] as const,
subTableHeaders: centerReportEntrySubTableHeaders,
subTableOrderNoHeader: '入库单号',
detailTabKeywords: ['详情', '入库', '单据'] as const,
summaryKeywords: ['入库品种', '入库单元数', '入库数量', '入库金额'] as const,
outboundSummaryKeywords: ['出库数量', '物流出库金额'] as const,
mockNotExistProduct: '__NOT_EXIST_PRODUCT__99999',
sampleRowIndex: 0,
sortableColumnCandidates: [...centerReportEntrySortColumns],
sortColumnCases: buildSortColumnCases(),
columnSettingsToggleCandidates: ['产品俗称', '线上线下标记'],
horizontalScrollAnchorColumn: '产品编号',
horizontalScrollTargetColumn: '型号',
horizontalScrollDragDelta: 320,
paginationPageSizeFrom: '50 条/页',
paginationPageSizeTo: '100 条/页',
exportTaskName: '中心库入库汇总报表',
exportDirectDownload: true,
};
import type { CenterReportPageData } from './center-report-shared.types';
import type { DeptQuickFieldDef } from './dept-report-field-defs';
export const centerReportLmmPsiColumns = {
productCode: '产品编号',
productName: '产品名称',
spec: '规格型号',
manufacturer: '生产厂家',
campus: '院区名称',
storageArea: '库区名称',
entryDate: '入库日期',
openingQty: '期初数量',
purchaseInboundQty: '采购入库数量',
requisitionOutboundQty: '请领出库数量',
} as const;
/** STAT-LMP-004 快捷区可见字段(入账日期/院区不在列表列中,组合查询时不改页面默认区间与院区)。 */
export const centerReportLmmPsiComboQuickFields: readonly DeptQuickFieldDef[] = [
{ key: 'storageArea', label: '库区名称', type: 'select' },
];
/** STAT-LMP-004 展开筛选区字段。 */
export const centerReportLmmPsiComboExpandedFields: readonly DeptQuickFieldDef[] = [
{ key: 'productName', label: '产品名称', type: 'text' },
{ key: 'spec', label: '规格型号', type: 'text' },
{ key: 'manufacturer', label: '生产厂家', type: 'text' },
];
export const centerReportLmmPsiComboAssertKeys = [
'productCode',
'productName',
'spec',
'campus',
'storageArea',
'manufacturer',
] as const;
/** 组合查询前从主表补采可能需横向滚动才可见的列。 */
export const centerReportLmmPsiSampleScrollColumns = ['生产厂家'] as const;
export const centerReportLmmPsiData: CenterReportPageData = {
reportPath: '/spd/form/centerReport/report/centerPages/lmmPsi',
iframeSrc: 'centerPages/lmmPsi',
productNameSelector: '#table_search_form_goodsName',
columns: centerReportLmmPsiColumns,
columnAliases: {
entryDate: ['入库日期', '入账日期'],
},
coreTableHeaders: [
'库区名称',
'产品编号',
'产品名称',
'规格型号',
'期初数量',
'采购入库数量',
'请领出库数量',
],
mockNotExistMaterial: '__NOT_EXIST_MATERIAL__99999',
mockNotExistProduct: '__NOT_EXIST_MATERIAL__99999',
paginationPageSizeFrom: '50 条/页',
paginationPageSizeTo: '100 条/页',
sampleRowIndex: 0,
hasAdvancedFilter: false,
hasExport: true,
hasExpandRow: false,
hasBatchPrint: false,
hasFilterToggle: true,
exportTaskName: '中心库进销存(后勤)报表',
columnSettingsToggleCandidates: ['期初金额', '退库入库数量', '采购入库金额'],
horizontalScrollAnchorColumn: '产品编号',
horizontalScrollTargetColumn: '请领出库数量',
horizontalScrollDragDelta: 320,
};
import type { CenterReportDrawerField } from './center-report-shared.types';
import type { CenterReportPageData } from './center-report-shared.types';
import { lmmCenterSummaryMatch } from './center-report-shared.types';
import type { DeptQuickFieldDef } from './dept-report-field-defs';
export const centerReportLmmSheetColumns = {
materialName: '物资名称',
materialCode: '物资编码',
accountDate: '记账日期',
consumableCategory: '耗材分类',
campus: '院区名称',
billType: '单据类型',
inboundQty: '入库数量',
outboundQty: '出库数量',
batchNo: '批号',
orderNo: '单号',
amount: '金额',
generationStyle: '生成方式',
supplier: '进院供应商',
purchaser: '采购员',
sourceTarget: '来源/目标',
storageArea: '库区名称',
} as const;
/** STAT-LMS-006 快捷区:首行样本 → 查询条件 */
export const centerReportLmmSheetComboQuickFields: readonly DeptQuickFieldDef[] = [
{ key: 'materialName', label: '物资名称', type: 'text' },
{ key: 'accountDate', label: '记账日期', type: 'date' },
{ key: 'consumableCategory', label: '物资分类', type: 'cascader' },
];
export { centerLmmMaterialSyncFields as centerReportLmmSheetQuickDrawerSyncFields } from './dept-report-field-defs';
/** STAT-LMS-006 高级筛选抽屉(单据类型为 checkbox 组,页面对 billType 单独处理) */
export const centerReportLmmSheetComboDrawerFields: readonly CenterReportDrawerField[] = [
{ key: 'orderNo', type: 'text', label: '单号' },
{ key: 'campus', type: 'select', label: '院区名称' },
{ key: 'billType', type: 'select', label: '单据类型' },
{ key: 'generationStyle', type: 'select', label: '生成方式' },
{ key: 'supplier', type: 'text', label: '进院供应商' },
{ key: 'purchaser', type: 'text', label: '采购员' },
{ key: 'sourceTarget', type: 'select', label: '来源/目标' },
{ key: 'storageArea', type: 'select', label: '库区名称' },
];
export const centerReportLmmSheetComboAssertKeys = [
'materialCode',
'materialName',
'accountDate',
'consumableCategory',
'billType',
'orderNo',
'campus',
'purchaser',
'sourceTarget',
'storageArea',
] as const;
export const centerReportLmmSheetData: CenterReportPageData = {
reportPath: '/spd/form/centerReport/report/centerPages/lmmSheet',
iframeSrc: 'centerPages/lmmSheet',
productNameSelector: '#form_item_goodsName',
columns: centerReportLmmSheetColumns,
columnAliases: {
materialName: ['物资名称', '产品名称'],
consumableCategory: ['耗材分类', '物资分类'],
materialCode: ['物资编码', '物资编号'],
sourceTarget: ['来源/去向', '来源/目标'],
},
coreTableHeaders: ['物资编号', '物资名称', '来源/目标', '单据类型', '物资分类', '结算方式'],
summaryKeywords: [...lmmCenterSummaryMatch.metricLabels],
summaryMatch: lmmCenterSummaryMatch,
drawerFields: centerReportLmmSheetComboDrawerFields,
mockNotExistMaterial: '__NOT_EXIST_MATERIAL__99999',
mockNotExistProduct: '__NOT_EXIST_MATERIAL__99999',
paginationPageSizeFrom: '50 条/页',
paginationPageSizeTo: '100 条/页',
sampleRowIndex: 0,
hasAdvancedFilter: true,
hasExport: true,
hasExpandRow: false,
hasBatchPrint: false,
hasFilterToggle: false,
exportTaskName: '中心库出入库明细(后勤)报表',
columnSettingsToggleCandidates: ['产地', '制单人', '科室名称'],
horizontalScrollAnchorColumn: '物资名称',
horizontalScrollTargetColumn: '进院供应商',
horizontalScrollDragDelta: 320,
};
import { buildSortColumnCases, type CenterReportPageData } from './center-report-shared.types';
import type { CenterReportDrawerField } from './center-report-shared.types';
import type { DeptQuickFieldDef, QuickDrawerSyncField } from './dept-report-field-defs';
export const centerReportNetEntryColumns = {
goodsCode: '产品编码',
goodsName: '产品名称',
campus: '院区',
supplier: '进院供应商',
spec: '规格型号',
consumableType: '耗材类型',
billDate: '单据日期',
isCharge: '是否计费',
isCollective: '是否集采',
} as const;
/** 页面表头核验可排序列(2026-09-09 实测:默认可见 14 列均可排序;规格型号/27位医保编码默认不展示,不写排序用例)。 */
export const centerReportNetEntrySortColumns = [
'院区',
'产品编码',
'产品名称',
'单位',
'耗材类型',
'是否集采',
'是否计费',
'厂家',
'进院供应商',
'供应商编码',
'医保单件名称',
'注册证号',
'数量',
'金额',
] as const;
export const centerReportNetEntrySortCases = buildSortColumnCases(
'STAT-NES',
centerReportNetEntrySortColumns,
9,
);
export const centerReportNetEntryData: CenterReportPageData = {
reportPath: '/spd/form/centerReport/report/centerPages/netEntrySummary',
iframeSrc: 'centerPages/netEntrySummary',
productNameSelector: '#form_item_goodsName',
goodsCodeSelector: '#form_item_goodsCode',
columns: centerReportNetEntryColumns,
columnAliases: { goodsCode: ['产品编码', '物资编码'], goodsName: ['产品名称', '物资名称'] },
coreTableHeaders: ['院区', '产品编码', '产品名称', '是否集采', '是否计费', '进院供应商'],
extraTableHeaders: ['27位医保编码', '医保单件名称'],
dimensions: ['供应商', '物资编码', '是否计费', '是否集采'],
drawerFields: [
{ key: 'campus', type: 'select', label: '院区' },
{ key: 'supplier', type: 'text', label: '供应商' },
],
sortColumns: centerReportNetEntrySortColumns,
sortCasePrefix: 'STAT-NES',
sortCaseStartIndex: 9,
horizontalScrollAnchorColumn: '院区',
horizontalScrollTargetColumn: '金额',
horizontalScrollDragDelta: 320,
mockNotExistCode: '__NOT_EXIST_CODE__99999',
mockNotExistProduct: '__NOT_EXIST_CODE__99999',
paginationPageSizeFrom: '50 条/页',
paginationPageSizeTo: '100 条/页',
sampleRowIndex: 0,
hasAdvancedFilter: true,
hasExport: true,
hasExpandRow: false,
hasBatchPrint: false,
hasFilterToggle: false,
exportTaskName: '净入库汇总报表',
columnSettingsToggleCandidates: ['27位医保编码', '规格型号', '生产厂家'],
};
export const centerReportNetEntryComboQuickFields: readonly DeptQuickFieldDef[] = [
{ key: 'goodsCode', label: '物资编码', type: 'text' },
{ key: 'goodsName', label: '物资名称', type: 'text' },
{ key: 'billDate', label: '单据日期', type: 'date' },
];
/** STAT-NES-007 高级筛选抽屉全量字段(与页面扫描一致)。 */
export const centerReportNetEntryComboDrawerFields: readonly CenterReportDrawerField[] = [
{ key: 'billDate', type: 'text', label: '单据日期' },
{ key: 'campus', type: 'select', label: '院区' },
{ key: 'goodsCode', type: 'text', label: '物资编码' },
{ key: 'goodsName', type: 'text', label: '物资名称' },
{ key: 'spec', type: 'text', label: '规格型号' },
{ key: 'consumableType', type: 'select', label: '耗材类型' },
{ key: 'supplier', type: 'text', label: '供应商' },
{ key: 'isCharge', type: 'select', label: '是否收费' },
{ key: 'isCollective', type: 'select', label: '是否集采' },
];
export const centerReportNetEntryComboAssertKeys = [
'goodsCode',
'goodsName',
'campus',
'consumableType',
'supplier',
'isCharge',
'isCollective',
] as const;
export const centerReportNetEntrySyncFields: readonly QuickDrawerSyncField[] = [
{ quickLabel: '物资编码', drawerLabel: '物资编码', type: 'text', sampleKey: 'goodsCode' },
{ quickLabel: '物资名称', drawerLabel: '物资名称', type: 'text', sampleKey: 'goodsName' },
{ quickLabel: '单据日期', drawerLabel: '单据日期', type: 'date', sampleKey: 'billDate' },
];
import { buildSortColumnCases, type CenterReportPageData } from './center-report-shared.types';
import type { CenterReportDrawerField } from './center-report-shared.types';
import type { DeptQuickFieldDef, QuickDrawerSyncField } from './dept-report-field-defs';
export const centerReportNetOutColumns = {
goodsCode: '产品编码',
goodsName: '产品名称',
campus: '院区',
parentDept: '上级科室名称',
parentDeptCode: '上级科室编码',
deptName: '科室名称',
deptCode: '科室编码',
consumableType: '耗材类型',
spec: '规格型号',
billDate: '单据日期',
} as const;
/** 页面表头核验可排序列(2026-08-31 实测,医保单件名称/HIS收费编码无 sorter)。 */
export const centerReportNetOutSortColumns = [
'院区',
'上级科室编码',
'上级科室名称',
'科室编码',
'科室名称',
'产品编码',
'产品名称',
'规格型号',
'单位',
'耗材类型',
'厂家',
'27位医保编码',
'数量',
'金额',
] as const;
export const centerReportNetOutSortCases = buildSortColumnCases(
'STAT-NOS',
centerReportNetOutSortColumns,
9,
);
export const centerReportNetOutData: CenterReportPageData = {
reportPath: '/spd/form/centerReport/report/centerPages/netOutSummary',
iframeSrc: 'centerPages/netOutSummary',
productNameSelector: '#form_item_goodsName',
goodsCodeSelector: '#form_item_goodsCode',
columns: centerReportNetOutColumns,
columnAliases: {
goodsCode: ['产品编码', '物资编码'],
goodsName: ['产品名称', '物资名称'],
parentDept: ['上级科室名称', '上级科室'],
parentDeptCode: ['上级科室编码'],
deptName: ['科室名称'],
deptCode: ['科室编码'],
},
coreTableHeaders: ['院区', '产品编码', '产品名称', '耗材类型'],
extraTableHeaders: ['上级科室名称', '科室名称', '27位医保编码', 'HIS收费编码'],
dimensions: ['上级科室编码', '科室名称', '物资编码', '耗材类型'],
drawerFields: [
{ key: 'campus', type: 'select', label: '院区' },
{ key: 'goodsCode', type: 'text', label: '物资编码' },
{ key: 'spec', type: 'text', label: '规格型号' },
],
sortColumns: centerReportNetOutSortColumns,
sortCasePrefix: 'STAT-NOS',
sortCaseStartIndex: 9,
horizontalScrollAnchorColumn: '院区',
horizontalScrollTargetColumn: '金额',
horizontalScrollDragDelta: 320,
mockNotExistCode: '__NOT_EXIST_CODE__99999',
mockNotExistProduct: '__NOT_EXIST_CODE__99999',
paginationPageSizeFrom: '50 条/页',
paginationPageSizeTo: '100 条/页',
sampleRowIndex: 0,
hasAdvancedFilter: true,
hasExport: true,
hasExpandRow: false,
hasBatchPrint: false,
hasFilterToggle: false,
exportTaskName: '净出库汇总报表',
columnSettingsToggleCandidates: ['27位医保编码', 'HIS收费编码', '规格型号'],
};
export const centerReportNetOutComboQuickFields: readonly DeptQuickFieldDef[] = [
{ key: 'goodsCode', label: '物资编码', type: 'text' },
{ key: 'goodsName', label: '物资名称', type: 'text' },
{ key: 'billDate', label: '单据日期', type: 'date' },
];
/** STAT-NOS-007 高级筛选抽屉全量字段(与页面扫描一致)。 */
export const centerReportNetOutComboDrawerFields: readonly CenterReportDrawerField[] = [
{ key: 'billDate', type: 'text', label: '单据日期' },
{ key: 'campus', type: 'select', label: '院区' },
{ key: 'goodsCode', type: 'text', label: '物资编码' },
{ key: 'goodsName', type: 'text', label: '物资名称' },
{ key: 'spec', type: 'text', label: '规格型号' },
{ key: 'consumableType', type: 'select', label: '耗材类型' },
{ key: 'deptName', type: 'select', label: '科室' },
{ key: 'parentDeptCode', type: 'text', label: '上级科室编码' },
];
export const centerReportNetOutComboAssertKeys = [
'goodsCode',
'goodsName',
'campus',
'consumableType',
'deptName',
'parentDept',
] as const;
export const centerReportNetOutSyncFields: readonly QuickDrawerSyncField[] = [
{ quickLabel: '物资编码', drawerLabel: '物资编码', type: 'text', sampleKey: 'goodsCode' },
{ quickLabel: '物资名称', drawerLabel: '物资名称', type: 'text', sampleKey: 'goodsName' },
{ quickLabel: '单据日期', drawerLabel: '单据日期', type: 'date', sampleKey: 'billDate' },
];
import {
buildSortColumnCases,
centerReportOutSummaryMatch,
type CenterReportPageData,
} from './center-report-shared.types';
export const centerReportOutColumns = {
productCode: '产品编号',
productName: '产品名称',
unit: '单位',
spec: '规格型号',
manufacturer: '生产厂家',
consumableType: '耗材类型',
outboundQty: '出库数量',
outboundAmount: '出库金额',
accountDate: '记账日期',
billType: '单据类型',
campus: '院区名称',
outboundTarget: '出库目标',
batchNo: '批号',
supplier: '进院供应商',
billMode: '单据形态',
} as const;
export type CenterReportOutColumnKey = keyof typeof centerReportOutColumns;
export type CenterReportOutSampleRow = Partial<Record<CenterReportOutColumnKey, string>>;
/** 行展开子表列 → sample key */
export const centerReportOutSubTableColumns: Partial<Record<CenterReportOutColumnKey, string>> = {
accountDate: '记账日期',
billType: '单据类型',
campus: '院区名称',
outboundTarget: '出库目标',
};
export const centerReportOutDrawerFields: ReadonlyArray<{
key: CenterReportOutColumnKey;
type: 'text' | 'select';
label: string;
}> = [
{ key: 'campus', type: 'select', label: '院区名称' },
{ key: 'outboundTarget', type: 'select', label: '出库目标' },
{ key: 'billMode', type: 'select', label: '单据形态' },
{ key: 'batchNo', type: 'text', label: '批号' },
{ key: 'supplier', type: 'text', label: '进院供应商' },
];
export const centerReportOutSortColumns = [
'产品编号',
'产品名称',
'单位',
'规格型号',
'耗材类型',
'出库数量',
'出库金额',
'医保编码',
'规格',
'型号',
] as const;
export const centerReportOutData: CenterReportPageData = {
reportPath: '/spd/form/centerReport/report/centerPages/out',
iframeSrc: 'centerPages/out',
productNameSelector: '#form_item_goodsName',
columns: centerReportOutColumns,
coreTableHeaders: ['产品编号', '产品名称', '出库数量', '出库金额', '耗材类型'],
summaryKeywords: ['出库品种', '出库单元数', '出库数量', '合计出库金额'],
forbiddenSummaryKeywords: ['入库品种', '入库数量'],
dimensions: ['区分包装', '区分批号'],
drawerFields: centerReportOutDrawerFields,
subTableHeaders: ['出库单号', '记账日期', '单据类型', '院区名称', '出库目标', '制单日期', '制单人', '查看'],
subTableOrderNoKeywords: ['出库单号', '单号'],
detailTabKeywords: ['详情', '出库', '单据'],
sortColumns: centerReportOutSortColumns,
sortCasePrefix: 'STAT-OOS',
sortCaseStartIndex: 11,
mockNotExistProduct: '__NOT_EXIST_PRODUCT__99999',
columnSettingsToggleCandidates: ['产品俗称', '线上线下标记'],
horizontalScrollAnchorColumn: '产品编号',
horizontalScrollTargetColumn: '医保编码',
horizontalScrollDragDelta: 320,
paginationPageSizeFrom: '50 条/页',
paginationPageSizeTo: '100 条/页',
sampleRowIndex: 0,
quantityColumnLabels: ['出库数量', '数量'],
mainQuantitySampleKey: 'outboundQty',
hasAdvancedFilter: true,
hasExport: true,
hasExpandRow: true,
hasBatchPrint: true,
hasFilterToggle: false,
exportTaskName: '中心库出库汇总报表',
summaryMatch: centerReportOutSummaryMatch,
};
export const centerReportOutSortCases = buildSortColumnCases(
'STAT-OOS',
centerReportOutSortColumns,
11,
);
import type { CenterReportDrawerField } from './center-report-shared.types';
import type { CenterReportPageData } from './center-report-shared.types';
export const centerReportPsiColumns = {
productCode: '产品编号',
productName: '产品名称',
spec: '规格型号',
consumableType: '耗材类型',
consumableCategory: '耗材分类',
manufacturer: '生产厂家',
campus: '院区名称',
openingQty: '期初数量',
netInQty: '净入数量',
netOutQty: '净出数量',
entryDate: '入账日期',
} as const;
/** STAT-PSI-004 高级筛选抽屉全量字段(与页面扫描一致)。 */
export const centerReportPsiComboDrawerFields: readonly CenterReportDrawerField[] = [
{ key: 'consumableType', type: 'select', label: '耗材类型' },
{ key: 'consumableCategory', type: 'cascader', label: '耗材分类' },
{ key: 'productName', type: 'text', label: '产品名称' },
{ key: 'spec', type: 'text', label: '规格型号' },
{ key: 'manufacturer', type: 'text', label: '生产厂家' },
];
export const centerReportPsiComboAssertKeys = [
'productCode',
'productName',
'spec',
'consumableType',
'consumableCategory',
'manufacturer',
] as const;
/** 组合查询前从主表补采可能需横向滚动才可见的列。 */
export const centerReportPsiSampleScrollColumns = ['耗材分类', '生产厂家'] as const;
export const centerReportPsiData: CenterReportPageData = {
reportPath: '/spd/form/centerReport/report/centerPages/psi',
iframeSrc: 'centerPages/psi',
productNameSelector: '#form_item_goodsKeyword',
columns: centerReportPsiColumns,
coreTableHeaders: ['产品编号', '产品名称', '规格型号', '期初数量', '净入数量', '净出数量'],
extraTableHeaders: ['耗材类型', '耗材分类', '生产厂家', '期初金额', '净入金额'],
drawerFields: [
{ key: 'campus', type: 'select', label: '院区名称' },
...centerReportPsiComboDrawerFields,
],
mockNotExistProduct: '__NOT_EXIST_PRODUCT__99999',
paginationPageSizeFrom: '50 条/页',
paginationPageSizeTo: '100 条/页',
sampleRowIndex: 0,
hasAdvancedFilter: true,
hasExport: true,
hasExpandRow: false,
hasBatchPrint: false,
hasFilterToggle: false,
exportTaskName: '中心库产品进销存报表',
columnSettingsToggleCandidates: ['期初金额', '调拨入库数量', '净入金额'],
horizontalScrollAnchorColumn: '产品编号',
horizontalScrollTargetColumn: '净出数量',
horizontalScrollDragDelta: 320,
};
export {
centerReportPsiComboQuickFields,
centerReportPsiSyncFields,
} from './dept-report-field-defs';
export type DrawerFieldType = 'text' | 'select' | 'cascader' | 'date';
export type CenterReportDrawerField = {
key: string;
type: DrawerFieldType;
label: string;
};
export type CenterReportPageData = {
reportPath: string;
iframeSrc: string;
/** 产品名称输入框选择器(iframe 内) */
productNameSelector: string;
/** 物资/产品编码输入框(净入/净出库等多维度页) */
goodsCodeSelector?: string;
columns: Record<string, string>;
columnAliases?: Partial<Record<string, readonly string[]>>;
coreTableHeaders: readonly string[];
summaryKeywords?: readonly string[];
forbiddenSummaryKeywords?: readonly string[];
dimensions?: readonly string[];
drawerFields?: readonly CenterReportDrawerField[];
subTableHeaders?: readonly string[];
subTableOrderNoKeywords?: readonly string[];
detailTabKeywords?: readonly string[];
sortColumns?: readonly string[];
sortCasePrefix?: string;
sortCaseStartIndex?: number;
mockNotExistProduct: string;
mockNotExistCode?: string;
mockNotExistMaterial?: string;
mockNotExistDept?: string;
mockNotExistSupplier?: string;
/** 空列表反例写入的筛选项标签(产品名称 / 单号 / 科室等) */
mockFieldLabel?: string;
mockDateStart?: string;
mockDateEnd?: string;
mockPreferFutureDate?: boolean;
drawerResetFieldLabel?: string;
drawerResetFieldKey?: string;
hasBarcodeExport?: boolean;
exportSuccessMessage?: string;
columnSettingsToggleCandidates?: readonly string[];
/** 双表页:左侧列表列设置 toggle 候选列 */
leftColumnSettingsToggleCandidates?: readonly string[];
/** 双表页:右侧明细列表列设置 toggle 候选列(未配置时回退 columnSettingsToggleCandidates) */
detailColumnSettingsToggleCandidates?: readonly string[];
/**
* 列设置勾选名 → 表头断言名(双行/合并表头时,分组列对应末行叶列)。
* 例:HIS计费情况 → HIS收费编码
*/
columnSettingsToggleAssertHeaders?: Readonly<Record<string, string>>;
paginationPageSizeFrom: string;
paginationPageSizeTo: string;
sampleRowIndex: number;
quantityColumnLabels?: readonly string[];
/** extractSampleRow 中主表数量字段 key(主子表勾稽用) */
mainQuantitySampleKey?: string;
hasAdvancedFilter: boolean;
hasExport: boolean;
hasExpandRow: boolean;
hasBatchPrint: boolean;
hasToolbarPrint?: boolean;
/** 工具栏打印需先勾选明细行(如结算单发票状态查询) */
printRequiresDetailSelection?: boolean;
hasFilterToggle: boolean;
/** 图表分析页(无列表样本,查询按钮常为「搜 索」) */
isChart?: boolean;
/**
* open/expectLoaded 时是否等待主表就绪。
* 默认 false:只等查询区控件,列表就绪交给显式 search(),避免每条用例双等表。
*/
waitTableOnOpen?: boolean;
/** iframe 内主查询按钮文案,默认「查 询」;财务分析等页为「搜 索」 */
searchBtnLabel?: '查 询' | '搜 索';
extraTableHeaders?: readonly string[];
/** 导出后在下载中心(任务中心)显示的任务名称;未配置时校验最近导出任务 */
exportTaskName?: string;
/** @deprecated 导出已自动兼容浏览器直下载与下载中心,一般无需配置 */
exportDirectDownload?: boolean;
/** 直接下载导出前:有数据则勾选首行,无数据则跳过选择 */
exportSelectFirstRow?: boolean;
/** 导出前用首行样本填入快捷区查询(无数据则直接导出) */
exportQueryFromSample?: boolean;
/** 横向拖拉滚动:锚点列、目标列与拖拉距离 */
horizontalScrollAnchorColumn?: string;
horizontalScrollTargetColumn?: string;
horizontalScrollDragDelta?: number;
/** 底部汇总与明细行勾稽(STAT-*-006 / LMS-008 / DLS-007 等) */
summaryMatch?: SummaryMatchProfile;
/** 组合查询:抽屉字段仅当首行样本有值时才写入(避免自动选首项导致无结果) */
comboOnlyFilledDrawerFields?: boolean;
/**
* 组合查询不写入的抽屉 key(级联选错、单据号截断、汇总页单号改粒度等会把结果 AND 空)。
*/
comboSkipDrawerKeys?: readonly string[];
/** 主表 ant-table-wrapper 索引(双表页如科室消耗排名:0=科室列表) */
tableWrapperIndex?: number;
/** 列排序/排序控件定位使用的表格索引(双表页如科室消耗排名取明细表) */
sortTableWrapperIndex?: number;
/** 明细表 wrapper 索引(双表页:1=右侧产品明细;横向滚动/产品行断言用) */
detailTableWrapperIndex?: number;
/** mock 反例字段仅在高级筛选抽屉(如 UDI码) */
mockFieldInDrawer?: boolean;
/** 组合查询:横向滚动补采列(表头 label) */
comboSampleScrollColumns?: readonly string[];
/** 组合查询:多行扫描时评估样本完整度的字段 key(默认用 comboAssertKeys) */
comboSamplePickKeys?: readonly string[];
/** 组合查询:快捷区日期 key 缺失时,从 table 样本 key 推导单日范围 */
comboSampleDateFromKey?: { targetKey: string; sourceKey: string };
/** 组合查询:多行扫描上限(默认 5) */
comboSampleMaxRowScan?: number;
/** 快捷区字段 key → iframe 内 input#id(label 定位失败时兜底) */
quickFieldInputIds?: Partial<Record<string, string>>;
/** 高级筛选抽屉字段 key → input#id(label 定位失败时兜底) */
drawerFieldInputIds?: Partial<Record<string, string>>;
/** 组合查询:抽屉字段 key 从样本其它 key 复制(如 field5 ← field9);目标 key 始终以源 key 为准。 */
comboSampleFieldAliases?: Partial<Record<string, string>>;
/** 选择首行后底部汇总与该行对应字段勾稽 */
firstRowSummaryMatch?: FirstRowSummaryMatch;
};
/** 主表列 → 底部合计标签(选中行小计 / 收窄后查询合计)。 */
export type FirstRowSummaryMetric = {
key: string;
label: string;
summaryLabels: readonly string[];
/** 该指标按绝对值对照(红冲负值 vs 底部绝对值)。 */
abs?: boolean;
};
export type FirstRowSummaryMatch = {
metrics: readonly FirstRowSummaryMetric[];
requiredKey: string;
fields: readonly { key: string; label: string; type: 'text' | 'select' | 'date' | 'cascader' }[];
/** 只收窄这些 key(含 required)。不配则 required + 所有有值的 fields */
narrowKeys?: readonly string[];
footerKeywords?: readonly string[];
/** 全部指标按绝对值对照 */
absAll?: boolean;
};
/** 明细页底部汇总 ↔ 列表行聚合对照配置 */
export type SummaryMatchProfile = {
metricLabels: readonly string[];
varietyLabel?: string;
inboundUnitLabel?: string;
inboundQtyLabel: string;
outboundUnitLabel?: string;
outboundQtyLabel: string;
inboundAmountLabel?: string;
outboundAmountLabel?: string;
productKeys: readonly string[];
batchNoKey?: string;
orderNoKey?: string;
orderNoDrawerLabel?: string;
varietyFromKey?: string;
inboundQtyKey: string;
outboundQtyKey: string;
amountKey?: string;
quickDateKey?: string;
quickDateLabel?: string;
extraDrawerFilters?: ReadonlyArray<{
sampleKey: string;
drawerLabel: string;
type: 'text' | 'select' | 'cascader';
}>;
};
export const detailSheetSummaryMatch = {
metricLabels: [
'品种',
'入库单元数',
'入库数量',
'出库单元数',
'出库数量',
'物流入库金额',
'物流出库金额',
],
varietyLabel: '品种',
inboundUnitLabel: '入库单元数',
inboundQtyLabel: '入库数量',
outboundUnitLabel: '出库单元数',
outboundQtyLabel: '出库数量',
inboundAmountLabel: '物流入库金额',
outboundAmountLabel: '物流出库金额',
productKeys: ['productCode', 'productName'],
batchNoKey: 'batchNo',
orderNoKey: 'orderNo',
orderNoDrawerLabel: '单号',
varietyFromKey: 'productCode',
inboundQtyKey: 'inboundQty',
outboundQtyKey: 'outboundQty',
amountKey: 'amount',
quickDateKey: 'accountDate',
quickDateLabel: '记账日期',
} as const satisfies SummaryMatchProfile;
export const deptSheetSummaryMatch = {
metricLabels: ['入库数量', '出库数量', '物流入库金额', '物流出库金额'],
varietyLabel: '品种',
inboundQtyLabel: '入库数量',
outboundQtyLabel: '出库数量',
inboundAmountLabel: '物流入库金额',
outboundAmountLabel: '物流出库金额',
productKeys: ['productCode', 'productName'],
batchNoKey: 'batchNo',
orderNoKey: 'orderNo',
orderNoDrawerLabel: '单号',
varietyFromKey: 'productCode',
inboundQtyKey: 'inboundQty',
outboundQtyKey: 'outboundQty',
amountKey: 'amount',
quickDateKey: 'accountDate',
quickDateLabel: '记账日期',
extraDrawerFilters: [{ sampleKey: 'deptName', drawerLabel: '科室', type: 'select' }],
} as const satisfies SummaryMatchProfile;
export const lmmCenterSummaryMatch = {
metricLabels: ['入库数量', '出库数量', '物流入库金额', '物流出库金额'],
inboundQtyLabel: '入库数量',
outboundQtyLabel: '出库数量',
inboundAmountLabel: '物流入库金额',
outboundAmountLabel: '物流出库金额',
productKeys: ['materialCode', 'materialName'],
batchNoKey: 'batchNo',
orderNoKey: 'orderNo',
orderNoDrawerLabel: '单号',
inboundQtyKey: 'inboundQty',
outboundQtyKey: 'outboundQty',
amountKey: 'amount',
} as const satisfies SummaryMatchProfile;
export const lmmDeptSummaryMatch = {
metricLabels: ['入库数量', '出库数量', '物流入库金额', '物流出库金额'],
inboundQtyLabel: '入库数量',
outboundQtyLabel: '出库数量',
inboundAmountLabel: '物流入库金额',
outboundAmountLabel: '物流出库金额',
productKeys: ['materialCode', 'materialName'],
batchNoKey: 'batchNo',
orderNoKey: 'orderNo',
orderNoDrawerLabel: '单号',
inboundQtyKey: 'inboundQty',
outboundQtyKey: 'outboundQty',
amountKey: 'amount',
} as const satisfies SummaryMatchProfile;
/** 中心库出库汇总页:底部汇总 ↔ 主表明细聚合 */
export const centerReportOutSummaryMatch = {
metricLabels: ['出库品种', '出库单元数', '出库数量', '合计出库金额'],
varietyLabel: '出库品种',
outboundUnitLabel: '出库单元数',
outboundQtyLabel: '出库数量',
outboundAmountLabel: '合计出库金额',
inboundQtyLabel: '入库数量',
productKeys: ['productCode', 'productName'],
batchNoKey: 'batchNo',
varietyFromKey: 'productCode',
inboundQtyKey: 'inboundQty',
outboundQtyKey: 'outboundQty',
amountKey: 'outboundAmount',
quickDateKey: 'accountDate',
quickDateLabel: '记账日期',
} as const satisfies SummaryMatchProfile;
export function buildSortColumnCases(
prefix: string,
columns: readonly string[],
startIndex: number,
): Array<{ id: string; column: string }> {
return columns.map((column, index) => ({
id: `${prefix}-${String(startIndex + index).padStart(3, '0')}`,
column,
}));
}
/** 日常抽样排序列(均匀取首/中/尾),其余挂 @full。 */
export function pickSortSampleCases<T extends { id: string }>(
cases: readonly T[],
max = 3,
): T[] {
if (cases.length <= max) return [...cases];
if (max <= 1) return [cases[0]!];
const picked: T[] = [];
for (let i = 0; i < max; i += 1) {
const idx = Math.round((i * (cases.length - 1)) / (max - 1));
picked.push(cases[idx]!);
}
return [...new Map(picked.map((c) => [c.id, c])).values()];
}
This diff is collapsed.
import { buildSortColumnCases, type CenterReportPageData } from './center-report-shared.types';
export const centerReportTurnoverColumns = {
productCode: '产品编号',
productName: '产品名称',
spec: '规格型号',
manufacturer: '生产厂家',
consumableType: '耗材类型',
campus: '院区名称',
stockQty: '库存数量',
outboundQty: '出库数量',
turnoverRate: '周转率',
turnoverDays: '周转天数',
periodDays: '时间段天数',
} as const;
export type CenterReportTurnoverSampleRow = Partial<
Record<keyof typeof centerReportTurnoverColumns, string>
>;
export const centerReportTurnoverComboQuickFields = [
{ key: 'productName' as const, label: '产品名称', type: 'text' as const },
{ key: 'campus' as const, label: '院区名称', type: 'select' as const },
{ key: 'spec' as const, label: '规格型号', type: 'text' as const },
{ key: 'consumableType' as const, label: '耗材类型', type: 'select' as const },
];
export const centerReportTurnoverSortColumns = [
'期初数量',
'期末数量',
'平均库存',
'出库数量',
'库存周转天数',
'库存周转次数',
'库存周转率',
] as const;
export const centerReportTurnoverData: CenterReportPageData = {
reportPath: '/spd/form/centerReport/report/statistics/pro',
iframeSrc: 'statistics/pro',
productNameSelector: '#table_search_form_goodsName',
columns: centerReportTurnoverColumns,
coreTableHeaders: ['产品编号', '产品名称', '规格型号', '生产厂家', '耗材类型', '默认供应商'],
extraTableHeaders: ['时间段天数', '期初数量', '期末数量', '平均库存', '出库数量', '库存周转天数', '库存周转次数', '库存周转率'],
sortColumns: centerReportTurnoverSortColumns,
sortCasePrefix: 'STAT-PTR',
sortCaseStartIndex: 10,
columnSettingsToggleCandidates: ['单位', '默认供应商'],
mockNotExistProduct: '__NOT_EXIST_PRODUCT__99999',
horizontalScrollAnchorColumn: '产品编号',
horizontalScrollTargetColumn: '库存周转率',
horizontalScrollDragDelta: 320,
paginationPageSizeFrom: '50 条/页',
paginationPageSizeTo: '100 条/页',
sampleRowIndex: 0,
hasAdvancedFilter: false,
hasExport: true,
hasExpandRow: false,
hasBatchPrint: false,
hasFilterToggle: true,
exportTaskName: '产品周转率报表',
};
export const centerReportTurnoverSortCases = buildSortColumnCases(
'STAT-PTR',
centerReportTurnoverSortColumns,
10,
);
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
import { buildSortColumnCases, type CenterReportPageData } from './center-report-shared.types';
import type { CenterReportDrawerField } from './center-report-shared.types';
import type { DeptQuickFieldDef } from './dept-report-field-defs';
export const consumePatientColumns = {
patientName: '患者姓名',
patientUniqueNo: '住院唯一编号',
patientId: '患者ID',
field: '年龄',
field2: '性别',
} as const;
export const consumePatientComboQuickFields: readonly DeptQuickFieldDef[] = [
{ key: 'patientName', label: '患者姓名', type: 'text' },
{ key: 'patientUniqueNo', label: '住院唯一编号', type: 'text' },
];
export const consumePatientComboDrawerFields: readonly CenterReportDrawerField[] = [
// 无额外抽屉字段
];
export const consumePatientComboAssertKeys = ['patientName', 'patientUniqueNo'] as const;
export const consumePatientData: CenterReportPageData & {
mockFieldLabel?: string;
drawerResetFieldLabel?: string;
drawerResetFieldKey?: string;
isChart?: boolean;
} = {
reportPath: '/spd/form/consume/report/consume/patient',
iframeSrc: 'consume/patient',
productNameSelector: '#form_item_patientInfo',
columns: consumePatientColumns,
coreTableHeaders: ['患者ID', '患者姓名', '年龄', '性别', '住院唯一编号', '操作'],
horizontalScrollAnchorColumn: '患者姓名',
horizontalScrollTargetColumn: '住院唯一编号',
horizontalScrollDragDelta: 320,
mockNotExistProduct: '__NOT_EXIST_PATIENT_NO__99999',
mockFieldLabel: '住院唯一编号',
paginationPageSizeFrom: '50 条/页',
paginationPageSizeTo: '100 条/页',
sampleRowIndex: 0,
hasAdvancedFilter: false,
hasExport: false,
hasExpandRow: false,
hasBatchPrint: false,
hasToolbarPrint: false,
hasFilterToggle: false,
isChart: false,
exportTaskName: '患者消耗追溯',
exportDirectDownload: false,
exportSelectFirstRow: false,
exportQueryFromSample: true,
columnSettingsToggleCandidates: ['性别'],
comboSampleScrollColumns: ['住院唯一编号'],
comboSamplePickKeys: ['patientName', 'patientUniqueNo'],
drawerFields: consumePatientComboDrawerFields,
};
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment