Commit 6c31a476 authored by liguangyu06's avatar liguangyu06
Browse files

同步基础管理与统计报表用例文档,并更新 Playwright 脚本与 PO。

对齐 CSV/MD/测试点与现网列名及组合查询;补充厂商资质子 Tab 与基础模块复跑脚本。
parent aa8987ce
......@@ -79,7 +79,7 @@ scripts/run-ci.mjs Jenkins / 定时执行入口
| `pages/` | 业务操作、取数/快照(如 `snapshotList`) | 用例主断言、流程编排、裸密码 |
| `tests/` | 调用 Page 方法 + **`expect` 断言业务结果** | `page.locator(...)`(反例探测除外)、访问 `locators` |
厂商管理样板:`tests/spd/vendor-query.spec.ts` + `tests/spd/helpers/vendor-assert.helper.ts`(断言在用例;PO 无 `expect`)。
厂商管理样板:`tests/spd/vendor-query.spec.ts`(9 条,与 `testcase/…厂商查询…csv` 一致)+ `pages/spd/vendor-query.page.ts`(列设置见 `pages/spd/helpers/column-settings.helper.ts`)+ `tests/spd/helpers/vendor-assert.helper.ts`(列表/空态断言在用例层)。
**智能等待**:查询按钮 loading、Ant Spin、表体有行/空态,统一走 `core/smart-wait.ts`。动画沉降用 `waitUiSettle`(双 rAF),不要在 Page 里写 `waitForTimeout`。
......
......@@ -202,10 +202,19 @@ export class BasePage {
return retryAction(async () => (await locator.innerText()).trim(), { label: '读取文本' });
}
/** 搜索条作用域:有 iframe 用首个 iframe,否则用主文档。 */
/** 搜索条作用域:仅当 iframe 内存在业务筛选表单时才用 iframe,避免误进空 iframe。 */
async queryScope(): Promise<FilterScope> {
const frames = this.page.locator('iframe');
if ((await frames.count().catch(() => 0)) > 0) return this.page.frameLocator('iframe').first();
const frameCount = await frames.count().catch(() => 0);
for (let i = 0; i < frameCount; i += 1) {
const frame = this.page.frameLocator(`iframe >> nth=${i}`);
const hasForm = await frame
.locator('.ant-form-item, .ant-form')
.first()
.isVisible()
.catch(() => false);
if (hasForm) return frame;
}
return this.page;
}
......@@ -236,7 +245,16 @@ export class BasePage {
await this.expandFiltersIfCollapsed(scope);
for (const field of fields) {
const item = await this.resolveFilterItem(scope, field.label);
const labels = field.aliases?.length ? [field.label, ...field.aliases] : [field.label];
let item: Locator | null = null;
let resolvedLabel = field.label;
for (const candidate of labels) {
item = await this.resolveFilterItem(scope, candidate);
if (item) {
resolvedLabel = candidate;
break;
}
}
if (!item) {
missing.push(field.label);
if (options.skipMissing === false) {
......@@ -245,16 +263,16 @@ export class BasePage {
continue;
}
if (field.kind === 'select') {
await this.pickFirstOption(scope, item, field.label);
await this.pickFirstOption(scope, item, resolvedLabel);
} else if (field.kind === 'dateRange') {
await this.fillDateRange(scope, item, field.label);
await this.fillDateRange(scope, item, resolvedLabel);
} else {
const input = item.locator('input').first();
await this.fill(input, field.value ?? '', `组合查询:${field.label}`, {
fallbacks: [scope.getByPlaceholder(new RegExp(field.label)).first()],
await this.fill(input, field.value ?? '样', `组合查询:${resolvedLabel}`, {
fallbacks: [scope.getByPlaceholder(new RegExp(resolvedLabel)).first()],
});
}
applied.push(field.label);
applied.push(resolvedLabel);
}
logger.step(`组合查询:已填 ${applied.length}/${fields.length} 项,未找到 ${missing.join('、') || '无'}`);
......@@ -284,21 +302,35 @@ export class BasePage {
* 搜索条中的单个字段:按 label[for] 精确定位到同级控件容器。
* antd 的 label for / 控件 id 一一对应,比 hasText 更可靠(避免「物料名称」误命中含同名文字的外层)。
*/
protected filterLabelCandidates(label: string): string[] {
const aliasMap: Record<string, string[]> = {
进院供应商: ['进院供应商', '进院供应商名称', '销售商(自定义)', '销售商', '供应商'],
销售商: ['销售商', '销售商(自定义)', '销售商名称', '进院供应商', '供应商'],
供应商: ['供应商', '进院供应商', '销售商(自定义)', '销售商'],
};
const extra = aliasMap[label];
return extra ? [...new Set([label, ...extra])] : [label];
}
protected async resolveFilterItem(scope: FilterScope, label: string): Promise<Locator | null> {
const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const exactLabel = new RegExp(`^\\s*${escaped}\\s*$`);
const labels = scope.locator('.ant-form-item label').filter({ hasText: exactLabel });
const count = await labels.count().catch(() => 0);
for (let i = 0; i < count; i += 1) {
const lab = labels.nth(i);
const id = await lab.getAttribute('for').catch(() => null);
if (!id) continue;
const item = scope.locator('.ant-form-item').filter({ has: scope.locator(`#${CSS_ESCAPE(id)}`) }).first();
if (await item.isVisible().catch(() => false)) return item;
for (const candidate of this.filterLabelCandidates(label)) {
const escaped = candidate.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const exactLabel = new RegExp(`^\\s*${escaped}\\s*$`);
const labels = scope.locator('.ant-form-item label').filter({ hasText: exactLabel });
const count = await labels.count().catch(() => 0);
for (let i = 0; i < count; i += 1) {
const lab = labels.nth(i);
const id = await lab.getAttribute('for').catch(() => null);
if (!id) continue;
const item = scope.locator('.ant-form-item').filter({ has: scope.locator(`#${CSS_ESCAPE(id)}`) }).first();
if (await item.isVisible().catch(() => false)) return item;
}
const fallback = scope
.locator('.ant-form-item')
.filter({ has: scope.locator('label', { hasText: exactLabel }) })
.first();
if (await fallback.isVisible().catch(() => false)) return fallback;
}
// 退化:label 所在表单项(仅当 label 文本完全相等时)
const fallback = scope.locator('.ant-form-item').filter({ has: scope.locator('label', { hasText: exactLabel }) }).first();
if (await fallback.isVisible().catch(() => false)) return fallback;
return null;
}
......@@ -306,11 +338,33 @@ export class BasePage {
* 打开下拉并取第一个可选项。浮层挂在**当前 scope 的 document.body**——
* iframe 页面的浮层在 iframe 内,主页面查不到,因此 scope 必须传对。
*/
protected async activeSelectDropdown(scope: FilterScope): Promise<Locator> {
await expect
.poll(
async () => {
for (const root of [scope, this.page]) {
const panel = root.locator('.ant-select-dropdown:not(.ant-select-dropdown-hidden)').last();
if (await panel.isVisible().catch(() => false)) return true;
}
return false;
},
{ timeout: 15_000 },
)
.toBeTruthy();
for (const root of [scope, this.page]) {
const panel = root.locator('.ant-select-dropdown:not(.ant-select-dropdown-hidden)').last();
if (await panel.isVisible().catch(() => false)) return panel;
}
return scope.locator('.ant-select-dropdown:not(.ant-select-dropdown-hidden)').last();
}
protected async pickFirstOption(scope: FilterScope, item: Locator, label: string): Promise<string> {
const dropdown = scope.locator('.ant-select-dropdown:not(.ant-select-dropdown-hidden)').last();
await this.click(item.locator('.ant-select-selector').first(), `组合查询:展开「${label}」`);
const dropdown = await this.activeSelectDropdown(scope);
const option = dropdown.locator('.ant-select-item-option:not(.ant-select-item-option-disabled)').first();
await option.waitFor({ state: 'visible', timeout: timeouts.expect });
await expect
.poll(async () => option.isVisible().catch(() => false), { timeout: timeouts.expect })
.toBeTruthy();
const text = ((await option.innerText().catch(() => '')) ?? '').replace(/\s/g, '');
await this.click(option, `组合查询:选择「${label}」`);
await this.waitSettle(300);
......@@ -358,6 +412,8 @@ const CSS_ESCAPE = (id: string) => id.replace(/([^\w-])/g, '\\$1');
export type FilterField = {
/** 搜索条上的字段名,需与页面 label 文本一致 */
label: string;
/** 页面 label 别名(按顺序尝试定位) */
aliases?: string[];
kind: 'text' | 'select' | 'dateRange';
/** kind=text 时的输入值;其余可省略 */
value?: string;
......
......@@ -8,11 +8,21 @@ const COLUMN_HEADER_ALIAS_GROUPS: readonly (readonly string[])[] = [
['医保单件名称', '单件名称', '医保名称'],
['HIS编码', 'HIS码', 'His编码'],
['结算单标签', '标签'],
['物料编码', '产品编号', '产品编码', '物资编码', '商品编码', '物料编号'],
['物料编号', '物料编码', '产品编号', '产品编码', '物资编码', '商品编码'],
['物料名称', '产品名称', '物资名称', '商品名称', '产品俗称'],
['规格型号', '规格', '产品规格'],
['产品批号', '批号'],
['进院供应商', '进院供应商名称', '默认进院供应商', '供应商名称', '供应商', '销售商', '销售商名称'],
[
'销售商(自定义)',
'销售商(自定义)名称',
'进院供应商',
'进院供应商名称',
'默认销售商',
'默认进院供应商',
'默认供应商',
'销售商',
'销售商名称',
],
['单号', '变更单号', '申请单号', '授权单号'],
['方案编码', '评价方案编码'],
['方案名称', '评价方案名称'],
......@@ -23,7 +33,7 @@ const COLUMN_HEADER_ALIAS_GROUPS: readonly (readonly string[])[] = [
['序号', '行号'],
['注册证号(新)', '注册证号', '新注册证号'],
['启用云仓', '云仓', '是否启用云仓'],
['二级供应商名称', '二级供应商', '供应商名称'],
['供应商', '供应商名称', '二级供应商', '二级供应商名称', '默认二级供应商'],
['院区', '院区名称'],
['产品类型', '耗材类型', '物资类型'],
];
......
import type { Locator } from '@playwright/test';
import { columnHeaderMatchTokens, columnHeaderTextsMatch } from './column-header-match.util';
import { columnHeaderMatchTokens, scoreColumnHeaderMatch } from './column-header-match.util';
/** 与 table-scroll-layout.helper 一致:不参与列对齐的单元格。 */
export function isUtilityTableCellClass(className: string): boolean {
......@@ -16,10 +16,23 @@ export function isUtilityTableCellClass(className: string): boolean {
}
export function resolveColumnIndexFromLabels(headerLabels: string[], columnName: string): number {
let index = headerLabels.findIndex((item) => columnHeaderTextsMatch(item, columnName));
const pickBest = (name: string): number => {
let bestIndex = -1;
let bestScore = 0;
for (let i = 0; i < headerLabels.length; i += 1) {
const score = scoreColumnHeaderMatch(headerLabels[i], name);
if (score > bestScore) {
bestScore = score;
bestIndex = i;
}
}
return bestScore > 0 ? bestIndex : -1;
};
let index = pickBest(columnName);
if (index >= 0) return index;
for (const token of columnHeaderMatchTokens(columnName)) {
index = headerLabels.findIndex((item) => columnHeaderTextsMatch(item, token));
index = pickBest(token);
if (index >= 0) return index;
}
return -1;
......@@ -42,6 +55,12 @@ export async function dataHeaderLabelsFromCells(headerCells: Locator): Promise<s
if (!raw) {
raw = ((await th.innerText({ timeout: 1_500 }).catch(() => '')) || '').trim();
}
if (!raw) {
raw = ((await th.getAttribute('title')) ?? '').trim();
}
if (!raw) {
raw = ((await th.getAttribute('aria-label')) ?? '').trim();
}
labels.push(raw.replace(/\s/g, ''));
}
return labels;
......
/** 宽表横向滚动时,扣除 ant-table 左右固定列占用的可视区域。 */
export type RectLike = { left: number; right: number; width: number };
export function computeTableScrollHostInsets(
hostRect: Pick<RectLike, 'left' | 'right'>,
fixedLeft: Pick<RectLike, 'left' | 'width'> | null,
fixedRight: Pick<RectLike, 'right' | 'width'> | null,
basePad = 48,
): { leftPad: number; rightPad: number } {
let leftPad = basePad;
let rightPad = basePad;
if (fixedLeft && fixedLeft.width > 0 && fixedLeft.left <= hostRect.left + 4) {
leftPad = Math.max(leftPad, fixedLeft.width + 16);
}
if (fixedRight && fixedRight.width > 0 && fixedRight.right >= hostRect.right - 4) {
rightPad = Math.max(rightPad, fixedRight.width + 16);
}
return { leftPad, rightPad };
}
/** 为露出表头 th,相对当前 scrollLeft 的增量;无需滚动则 null。 */
export function deltaScrollLeftForTableHeader(
thRect: Pick<RectLike, 'left' | 'right'>,
hostRect: Pick<RectLike, 'left' | 'right'>,
insets: { leftPad: number; rightPad: number },
): number | null {
if (thRect.left < hostRect.left + insets.leftPad) {
return -(hostRect.left - thRect.left + insets.leftPad);
}
if (thRect.right > hostRect.right - insets.rightPad) {
return thRect.right - (hostRect.right - insets.rightPad);
}
return null;
}
/** 供 page.evaluate 内联:与 computeTableScrollHostInsets / deltaScrollLeftForTableHeader 保持一致。 */
export function readTableScrollInsetsInDom(root: HTMLElement, hostRect: DOMRect): { leftPad: number; rightPad: number } {
const fixedLeft = root.querySelector('.ant-table-fixed-left') as HTMLElement | null;
const fixedRight = root.querySelector('.ant-table-fixed-right') as HTMLElement | null;
return computeTableScrollHostInsets(
hostRect,
fixedLeft ? fixedLeft.getBoundingClientRect() : null,
fixedRight ? fixedRight.getBoundingClientRect() : null,
);
}
export function headerOverlapsFixedRight(thRect: Pick<RectLike, 'left' | 'right'>, fixedRightRect: RectLike | null): boolean {
if (!fixedRightRect || fixedRightRect.width <= 0) return false;
return thRect.right > fixedRightRect.left + 2 && thRect.left < fixedRightRect.right;
}
export function deltaScrollToClearFixedRight(
thRect: Pick<RectLike, 'left' | 'right'>,
fixedRightRect: RectLike | null,
): number | null {
if (!headerOverlapsFixedRight(thRect, fixedRightRect)) return null;
return thRect.right - fixedRightRect!.left + 32;
}
export function applyScrollLeftDeltaForHeaderTh(
root: HTMLElement,
matchedTh: HTMLElement,
scrollContainers: Iterable<Element>,
): void {
const fixedRight = root.querySelector('.ant-table-fixed-right') as HTMLElement | null;
const fixedRightRect = fixedRight ? fixedRight.getBoundingClientRect() : null;
for (let pass = 0; pass < 3; pass += 1) {
const thRect = matchedTh.getBoundingClientRect();
let moved = false;
for (const container of Array.from(scrollContainers)) {
const host = container as HTMLElement;
if (host.scrollWidth <= host.clientWidth + 4) continue;
const hostRect = host.getBoundingClientRect();
const insets = readTableScrollInsetsInDom(root, hostRect);
const delta = deltaScrollLeftForTableHeader(thRect, hostRect, insets);
if (delta != null) {
host.scrollLeft = Math.max(0, host.scrollLeft + delta);
moved = true;
}
const clearFixed = deltaScrollToClearFixedRight(matchedTh.getBoundingClientRect(), fixedRightRect);
if (clearFixed != null) {
host.scrollLeft = Math.max(0, host.scrollLeft + clearFixed);
moved = true;
}
}
if (!moved) break;
if (!headerOverlapsFixedRight(matchedTh.getBoundingClientRect(), fixedRightRect)) break;
}
}
......@@ -6,8 +6,8 @@ export const anaChgColumns = {
field: '起始时间',
productName: '物料名称',
spec: '规格型号',
supplierName: '进院供应商',
field2: '二级供应商',
supplierName: '销售商(自定义)',
field2: '供应商',
consumableType: '耗材类型',
id: '阳采ID',
productCode: '物料编号',
......@@ -35,8 +35,8 @@ export const anaChgComboQuickFields: readonly DeptQuickFieldDef[] = [
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: 'supplierName', type: 'text', label: '销售商(自定义)' },
{ key: 'field2', type: 'text', label: '供应商' },
{ key: 'consumableType', type: 'select', label: '耗材类型' },
];
......@@ -56,8 +56,8 @@ export const anaChgSortColumns = [
'规格型号',
'器械注册人-术语配置',
'单位',
'进院供应商',
'二级供应商',
'销售商(自定义)',
'供应商',
'耗材类型',
'数量',
] as const;
......@@ -74,7 +74,7 @@ export const anaChgData: CenterReportPageData & {
coreTableHeaders: ['物料编号','物料名称', '产品俗称', '规格型号', '原单价', '现单价', '数量', '总差额'],
summaryKeywords: ['选中总数量', '选中总差额'],
columnAliases: {
supplierName: ['进院供应商名称'],
supplierName: ['销售商(自定义)名称'],
field4: ['器械注册人'],
field5: ['采购计划号'],
},
......@@ -105,7 +105,7 @@ export const anaChgData: CenterReportPageData & {
exportQueryFromSample: true,
columnSettingsToggleCandidates: ['备注'],
comboSampleScrollColumns: ['进院供应商'],
comboSampleScrollColumns: ['销售商(自定义)'],
comboSamplePickKeys: ['field', 'productName', 'spec', 'supplierName', 'field2', 'consumableType'],
comboSampleMaxRowScan: 5,
drawerFields: anaChgComboDrawerFields,
......
......@@ -4,22 +4,22 @@ import type { DeptQuickFieldDef } from './dept-report-field-defs';
export const anaProvColumns = {
field: '时间',
supplierName: '进院供应商名称',
supplierName: '销售商(自定义)名称',
campus: '院区名称',
} as const;
export const anaProvComboFullQuickFields: readonly DeptQuickFieldDef[] = [
{ key: 'field', label: '时间', type: 'date' },
{ key: 'supplierName', label: '进院供应商名称', type: 'select' },
{ key: 'supplierName', label: '销售商(自定义)名称', type: 'select' },
];
export const anaProvComboQuickFields: readonly DeptQuickFieldDef[] = [
{ key: 'field', label: '时间', type: 'date' },
{ key: 'supplierName', label: '进院供应商名称', type: 'select' },
{ key: 'supplierName', label: '销售商(自定义)名称', type: 'select' },
];
export const anaProvComboDrawerFields: readonly CenterReportDrawerField[] = [
{ key: 'supplierName', type: 'select', label: '进院供应商名称' },
{ key: 'supplierName', type: 'select', label: '销售商(自定义)名称' },
];
export const anaProvComboAssertKeys = ['field', 'campus'] as const;
......@@ -39,20 +39,20 @@ export const anaProvData: CenterReportPageData & {
iframeSrc: 'statistics/prov',
productNameSelector: '#form_item_supplierName',
columns: anaProvColumns,
coreTableHeaders: ['进院供应商名称', '时间'],
coreTableHeaders: ['销售商(自定义)名称', '时间'],
columnAliases: {
supplierName: ['进院供应商'],
supplierName: ['销售商(自定义)'],
},
horizontalScrollAnchorColumn: '时间',
horizontalScrollTargetColumn: '进院供应商名称',
horizontalScrollTargetColumn: '销售商(自定义)名称',
horizontalScrollDragDelta: 320,
mockNotExistProduct: '__NOT_EXIST_SUPPLIER__99999',
/** 图表页快捷区常仅有「时间」,用未来区间反例空态。 */
mockFieldLabel: '时间',
mockPreferFutureDate: true,
drawerResetFieldLabel: '进院供应商名称',
drawerResetFieldLabel: '销售商(自定义)名称',
drawerResetFieldKey: 'supplierName',
paginationPageSizeFrom: '50 条/页',
paginationPageSizeTo: '100 条/页',
......@@ -71,7 +71,7 @@ export const anaProvData: CenterReportPageData & {
exportQueryFromSample: true,
columnSettingsToggleCandidates: ['备注'],
comboSampleScrollColumns: ['进院供应商'],
comboSampleScrollColumns: ['销售商(自定义)'],
comboSamplePickKeys: ['field', 'supplierName'],
comboSampleMaxRowScan: 5,
drawerFields: anaProvComboDrawerFields,
......
......@@ -11,7 +11,7 @@ export const centerReportEntryColumns = {
campus: '院区名称',
sourceUnit: '来源单位',
batchNo: '批号',
supplier: '进院供应商',
supplier: '销售商(自定义)',
billMode: '单据形态',
} as const;
......@@ -31,7 +31,7 @@ export const centerReportEntryDrawerFields: ReadonlyArray<{
{ key: 'sourceUnit', type: 'text', label: '来源单位' },
{ key: 'billMode', type: 'select', label: '单据形态' },
{ key: 'batchNo', type: 'text', label: '批号' },
{ key: 'supplier', type: 'text', label: '进院供应商' },
{ key: 'supplier', type: 'text', label: '销售商(自定义)' },
];
function buildSortColumnCases(): Array<{ id: string; column: string }> {
......
......@@ -16,7 +16,7 @@ export const centerReportLmmSheetColumns = {
orderNo: '单号',
amount: '金额',
generationStyle: '生成方式',
supplier: '进院供应商',
supplier: '销售商(自定义)',
purchaser: '采购员',
sourceTarget: '来源/目标',
storageArea: '库区名称',
......@@ -37,7 +37,7 @@ export const centerReportLmmSheetComboDrawerFields: readonly CenterReportDrawerF
{ key: 'campus', type: 'select', label: '院区名称' },
{ key: 'billType', type: 'select', label: '单据类型' },
{ key: 'generationStyle', type: 'select', label: '生成方式' },
{ key: 'supplier', type: 'text', label: '进院供应商' },
{ key: 'supplier', type: 'text', label: '销售商(自定义)' },
{ key: 'purchaser', type: 'text', label: '采购员' },
{ key: 'sourceTarget', type: 'select', label: '来源/目标' },
{ key: 'storageArea', type: 'select', label: '库区名称' },
......@@ -62,7 +62,7 @@ export const centerReportLmmSheetData: CenterReportPageData = {
productNameSelector: '#form_item_goodsName',
columns: centerReportLmmSheetColumns,
columnAliases: {
materialName: ['物资名称', '产品名称'],
materialName: ['物资名称', '物料名称'],
consumableCategory: ['耗材分类', '物资分类'],
materialCode: ['物资编码', '物资编号'],
sourceTarget: ['来源/去向', '来源/目标'],
......@@ -84,6 +84,6 @@ export const centerReportLmmSheetData: CenterReportPageData = {
exportTaskName: '中心库出入库明细(后勤)报表',
columnSettingsToggleCandidates: ['产地', '制单人', '科室名称'],
horizontalScrollAnchorColumn: '物资名称',
horizontalScrollTargetColumn: '进院供应商',
horizontalScrollTargetColumn: '销售商(自定义)',
horizontalScrollDragDelta: 320,
};
......@@ -3,10 +3,10 @@ import type { CenterReportDrawerField } from './center-report-shared.types';
import type { DeptQuickFieldDef, QuickDrawerSyncField } from './dept-report-field-defs';
export const centerReportNetEntryColumns = {
goodsCode: '产品编码',
goodsName: '产品名称',
goodsCode: '物料编码',
goodsName: '物料名称',
campus: '院区',
supplier: '进院供应商',
supplier: '销售商(自定义)',
spec: '规格型号',
consumableType: '耗材类型',
billDate: '单据日期',
......@@ -16,14 +16,14 @@ export const centerReportNetEntryColumns = {
/** 可排序列以 data/scan/netEntrySummary-sort-columns.json 为准(默认表头无「院区」,不写排序用例)。 */
export const centerReportNetEntrySortColumns = [
'产品编码',
'产品名称',
'物料编码',
'物料名称',
'单位',
'耗材类型',
'是否集采',
'是否计费',
'厂家',
'进院供应商',
'销售商(自定义)',
'供应商编码',
'医保单件名称',
'注册证号',
......@@ -43,8 +43,8 @@ export const centerReportNetEntryData: CenterReportPageData = {
productNameSelector: '#form_item_goodsName',
goodsCodeSelector: '#form_item_goodsCode',
columns: centerReportNetEntryColumns,
columnAliases: { goodsCode: ['产品编码', '物资编码'], goodsName: ['产品名称', '物资名称'] },
coreTableHeaders: ['院区', '产品编码', '产品名称', '是否集采', '是否计费', '进院供应商'],
columnAliases: { goodsCode: ['物料编码', '物资编码'], goodsName: ['物料名称', '物资名称'] },
coreTableHeaders: ['院区', '物料编码', '物料名称', '是否集采', '是否计费', '销售商(自定义)'],
extraTableHeaders: ['27位医保编码', '医保单件名称'],
dimensions: ['供应商', '物资编码', '是否计费', '是否集采'],
drawerFields: [
......
......@@ -3,8 +3,8 @@ import type { CenterReportDrawerField } from './center-report-shared.types';
import type { DeptQuickFieldDef, QuickDrawerSyncField } from './dept-report-field-defs';
export const centerReportNetOutColumns = {
goodsCode: '产品编码',
goodsName: '产品名称',
goodsCode: '物料编码',
goodsName: '物料名称',
campus: '院区',
parentDept: '上级科室名称',
parentDeptCode: '上级科室编码',
......@@ -22,8 +22,8 @@ export const centerReportNetOutSortColumns = [
'上级科室名称',
'科室编码',
'科室名称',
'产品编码',
'产品名称',
'物料编码',
'物料名称',
'单位',
'耗材类型',
'厂家',
......@@ -45,14 +45,14 @@ export const centerReportNetOutData: CenterReportPageData = {
goodsCodeSelector: '#form_item_goodsCode',
columns: centerReportNetOutColumns,
columnAliases: {
goodsCode: ['产品编码', '物资编码'],
goodsName: ['产品名称', '物资名称'],
goodsCode: ['物料编码', '物资编码'],
goodsName: ['物料名称', '物资名称'],
parentDept: ['上级科室名称', '上级科室'],
parentDeptCode: ['上级科室编码'],
deptName: ['科室名称'],
deptCode: ['科室编码'],
},
coreTableHeaders: ['院区', '产品编码', '产品名称', '耗材类型'],
coreTableHeaders: ['院区', '物料编码', '物料名称', '耗材类型'],
extraTableHeaders: ['上级科室名称', '科室名称', '27位医保编码', 'HIS收费编码'],
dimensions: ['上级科室编码', '科室名称', '物资编码', '耗材类型'],
drawerFields: [
......
......@@ -18,7 +18,7 @@ export const centerReportOutColumns = {
campus: '院区名称',
outboundTarget: '出库目标',
batchNo: '批号',
supplier: '进院供应商',
supplier: '销售商(自定义)',
billMode: '单据形态',
} as const;
......@@ -42,7 +42,7 @@ export const centerReportOutDrawerFields: ReadonlyArray<{
{ key: 'outboundTarget', type: 'select', label: '出库目标' },
{ key: 'billMode', type: 'select', label: '单据形态' },
{ key: 'batchNo', type: 'text', label: '批号' },
{ key: 'supplier', type: 'text', label: '进院供应商' },
{ key: 'supplier', type: 'text', label: '销售商(自定义)' },
];
export const centerReportOutSortColumns = [
......
......@@ -9,9 +9,9 @@ export type CenterReportDrawerField = {
export type CenterReportPageData = {
reportPath: string;
iframeSrc: string;
/** 产品名称输入框选择器(iframe 内) */
/** 物料名称输入框选择器(iframe 内) */
productNameSelector: string;
/** 物资/产品编码输入框(净入/净出库等多维度页) */
/** 物资/物料编码输入框(净入/净出库等多维度页) */
goodsCodeSelector?: string;
columns: Record<string, string>;
columnAliases?: Partial<Record<string, readonly string[]>>;
......@@ -33,7 +33,7 @@ export type CenterReportPageData = {
mockNotExistMaterial?: string;
mockNotExistDept?: string;
mockNotExistSupplier?: string;
/** 空列表反例写入的筛选项标签(产品名称 / 单号 / 科室等) */
/** 空列表反例写入的筛选项标签(物料名称 / 单号 / 科室等) */
mockFieldLabel?: string;
mockDateStart?: string;
mockDateEnd?: string;
......@@ -77,6 +77,11 @@ export type CenterReportPageData = {
waitTableOnOpen?: boolean;
/** iframe 内主查询按钮文案,默认「查 询」;财务分析等页为「搜 索」 */
searchBtnLabel?: '查 询' | '搜 索';
/**
* 进页后主表不自动出数/出列,须点「查 询」后才有列表字段与数据。
* 为 true 时取样本、表头断言等会先 search(),不依赖「已有行则跳过查询」优化。
*/
listRequiresExplicitSearch?: boolean;
extraTableHeaders?: readonly string[];
/** 导出后在下载中心(任务中心)显示的任务名称;未配置时校验最近导出任务 */
exportTaskName?: string;
......
......@@ -2,13 +2,14 @@ import {
detailSheetSummaryMatch,
type CenterReportDrawerField,
} from './center-report-shared.types';
import { reportAccountDateQuickRange } from './report-default-date-range';
/** 出入库明细查询 · 列表列名与抽屉字段映射(动态取数用)。 */
export const centerReportSheetColumns = {
productCode: '物料编号',
productName: '物料名称',
batchNo: '批号',
supplier: '进院供应商',
supplier: '销售商(自定义)',
campus: '院区名称',
accountDate: '记账日期',
billType: '单据类型',
......@@ -43,6 +44,7 @@ export const centerReportSheetColumnAliases: Partial<
sourceTarget: ['来源/去向', '来源/目标'],
orderNo: ['单据号', '单号'],
operationDate: ['制单日期', '制单时间'],
supplier: ['进院供应商', '销售商(自定义)名称'],
};
/** 高级筛选抽屉表单项 label(与列表列名不一致时)。 */
......@@ -75,7 +77,7 @@ export const centerReportSheetDrawerFields: ReadonlyArray<{
{ key: 'chargeable', type: 'select', label: '是否收费' },
{ key: 'focusControl', type: 'select', label: '是否重点管控' },
{ key: 'focusControlType', type: 'select', label: '重点管控分类' },
{ key: 'supplier', type: 'text', label: '进院供应商' },
{ key: 'supplier', type: 'text', label: '销售商(自定义)' },
{ key: 'operationDate', type: 'dateRange', label: '制单日期' },
{ key: 'sourceType', type: 'select', label: '入库来源' },
{ key: 'manufacturer', type: 'text', label: '生产厂家' },
......@@ -87,8 +89,8 @@ export const centerReportSheetSortColumns = [
'单位',
'备注',
'批号',
'进院供应商',
'二级供应商',
'销售商(自定义)',
'供应商',
'院区名称',
'耗材分类',
'记账日期',
......@@ -127,10 +129,11 @@ export const centerReportSheetData = {
reportPath: '/spd/form/centerReport/report/centerPages/sheet',
sheetPath: '/spd/form/centerReport/report/centerPages/sheet',
iframeSrc: 'centerPages/sheet',
defaultQuickDateRange: reportAccountDateQuickRange,
productNameSelector: '#form_item_goodsName',
columns: centerReportSheetColumns,
columnAliases: centerReportSheetColumnAliases,
coreTableHeaders: ['物料编号','物料名称', '批号', '进院供应商', '院区名称'],
coreTableHeaders: ['物料编号','物料名称', '批号', '销售商(自定义)', '院区名称'],
summaryKeywords: detailSheetSummaryMatch.metricLabels,
summaryMatch: detailSheetSummaryMatch,
drawerLabels: centerReportSheetDrawerLabels,
......@@ -154,7 +157,7 @@ export const centerReportSheetData = {
mockBatchNo: 'MOCK_BATCH_001',
mockInvalidPage: '99999',
/** 列设置 toggle 候选非核心列 */
columnSettingsToggleCandidates: ['备注', '产品俗称', '二级供应商'],
columnSettingsToggleCandidates: ['备注', '产品俗称', '供应商'],
/** 横向拖拉滚动:锚点列与滚动后应可见的远侧列 */
horizontalScrollAnchorColumn: '物料编号',
horizontalScrollTargetColumn: '医保编码',
......
import { buildSortColumnCases, type CenterReportPageData } from './center-report-shared.types';
import { reportStatPeriodQuickRange } from './report-default-date-range';
export const centerReportTurnoverColumns = {
productCode: '物料编号',
productName: '物料名称',
spec: '规格型号',
manufacturer: '生产厂家',
manufacturer: '器械注册人',
consumableType: '耗材类型',
campus: '院区名称',
stockQty: '库存数量',
......@@ -28,7 +29,7 @@ export const centerReportTurnoverComboQuickFields = [
export const centerReportTurnoverSortColumns = [
'期初数量',
'期末数量',
'平均库存',
'平均库存数量',
'出库数量',
'库存周转天数',
'库存周转次数',
......@@ -38,14 +39,29 @@ export const centerReportTurnoverSortColumns = [
export const centerReportTurnoverData: CenterReportPageData = {
reportPath: '/spd/form/centerReport/report/statistics/pro',
iframeSrc: 'statistics/pro',
defaultQuickDateRange: reportStatPeriodQuickRange,
productNameSelector: '#table_search_form_goodsName',
columns: centerReportTurnoverColumns,
coreTableHeaders: ['物料编号','物料名称', '规格型号', '生产厂家', '耗材类型', '默认供应商'],
extraTableHeaders: ['时间段天数', '期初数量', '期末数量', '平均库存', '出库数量', '库存周转天数', '库存周转次数', '库存周转率'],
columnAliases: {
productName: ['物料名称'],
manufacturer: ['生产厂家'],
},
mockFieldLabel: '物料名称',
coreTableHeaders: ['物料编号', '物料名称', '规格型号', '器械注册人', '耗材类型', '默认销售商'],
extraTableHeaders: [
'时间段天数',
'期初数量',
'期末数量',
'平均库存数量',
'出库数量',
'库存周转天数',
'库存周转次数',
'库存周转率',
],
sortColumns: centerReportTurnoverSortColumns,
sortCasePrefix: 'STAT-PTR',
sortCaseStartIndex: 10,
columnSettingsToggleCandidates: ['单位', '默认供应商'],
columnSettingsToggleCandidates: ['单位', '默认销售商'],
mockNotExistProduct: '__NOT_EXIST_PRODUCT__99999',
horizontalScrollAnchorColumn: '物料编号',
horizontalScrollTargetColumn: '库存周转率',
......
......@@ -26,7 +26,6 @@ export const consumeDeptRankDeptSortColumns = ['消耗数量', '消耗金额'] a
/** 右侧产品明细可排序列(不含序号、占比)。 */
export const consumeDeptRankDetailSortColumns = ['物料名称',
'规格型号',
'器械注册人-术语配置',
'单价',
'消耗数量',
......
......@@ -21,8 +21,8 @@ export const consumeHighColumns = {
his: 'HIS计费日期',
field3: '消耗方式',
manufacturer: '生产厂家',
supplierName: '进院供应商名称',
subSupplierName: '二级供应商名称',
supplierName: '销售商(自定义)名称',
subSupplierName: '供应商名称',
consumableCategory: '耗材分类',
field4: '特殊高值标记',
batchNo: '批号',
......@@ -42,8 +42,8 @@ export const consumeHighColumns = {
id2: '条码ID',
field11: '备注',
27: '27位医保编码',
supplierName2: '进院供应商',
field12: '二级供应商',
supplierName2: '销售商(自定义)',
field12: '供应商',
field13: '数量',
field14: '结算单价',
field15: '结算金额',
......@@ -78,7 +78,7 @@ export const consumeHighComboDrawerFields: readonly CenterReportDrawerField[] =
{ key: 'isVolume', type: 'select', label: '是否带量' },
{ key: 'onlineOfflineFlag', type: 'select', label: '线上线下标记' },
{ key: 'spec', type: 'text', label: '规格型号' },
{ key: 'supplierName', type: 'text', label: '进院供应商名称' },
{ key: 'supplierName', type: 'text', label: '销售商(自定义)名称' },
];
export const consumeHighComboAssertKeys = ['patientName', 'udi', 'productName'] as const;
......@@ -95,10 +95,10 @@ export const consumeHighData: CenterReportPageData & {
columns: consumeHighColumns,
coreTableHeaders: ['物料编号','物料名称', '产品俗称', '单位', 'UDI码', '条码ID', '原厂码', 'HIS编码'],
columnAliases: {
supplierName: ['进院供应商'],
supplierName: ['销售商(自定义)'],
field5: ['采购计划号'],
},
sortColumns: ['物料编号','物料名称', 'UDI码', '条码ID', '原厂码', 'HIS编码', '备注', '规格型号', '27位医保编码', '进院供应商', '结算单价', '库存科室', '记账时间', 'HIS计费时间', '患者名称'],
sortColumns: ['物料编号','物料名称', 'UDI码', '条码ID', '原厂码', 'HIS编码', '备注', '规格型号', '27位医保编码', '销售商(自定义)', '结算单价', '库存科室', '记账时间', 'HIS计费时间', '患者名称'],
sortCasePrefix: 'STAT-CHC',
sortCaseStartIndex: 13,
......@@ -142,7 +142,7 @@ export const consumeHighSortColumns = [
'备注',
'规格型号',
'27位医保编码',
'进院供应商',
'销售商(自定义)',
'结算单价',
'库存科室',
'记账时间',
......
import {
buildSortColumnCases,
type CenterReportPageData,
} from './center-report-shared.types';
import type { CenterReportPageData } from './center-report-shared.types';
import type { CenterReportDrawerField } from './center-report-shared.types';
import type { DeptQuickFieldDef } from './dept-report-field-defs';
......@@ -40,24 +37,12 @@ export const costDeptClassifyChargeComboDrawerFields: readonly CenterReportDrawe
export const costDeptClassifyChargeComboAssertKeys = ['deptNameCol'] as const;
/** 主表可排序列(透视表仅「科室名称」列带 sorter)。 */
export const costDeptClassifyChargeSortColumns = ['科室名称'] as const;
export const costDeptClassifyChargeSortCases = buildSortColumnCases(
'STAT-CCC',
costDeptClassifyChargeSortColumns,
4,
);
export const costDeptClassifyChargeData: CenterReportPageData = {
reportPath: '/spd/form/settleCost/report/settel/chargeSummary',
iframeSrc: 'settel/chargeSummary',
productNameSelector: '#form_item_goodsName',
columns: costDeptClassifyChargeColumns,
coreTableHeaders: ['科室类型', '科室名称', '科室小计'],
sortColumns: [...costDeptClassifyChargeSortColumns],
sortCasePrefix: 'STAT-CCC',
sortCaseStartIndex: 4,
mockNotExistProduct: '__NOT_EXIST_DEPT__99999',
mockFieldLabel: '入账日期',
mockNotExistDept: '__NOT_EXIST_DEPT__99999',
......
......@@ -7,7 +7,7 @@ export const costDeptNetInColumns = {
deptName: '科室名称',
consumableType: '耗材类型',
statDate: '统计日期',
supplierName: '进院供应商',
supplierName: '销售商(自定义)',
productCode: '物料编码',
productName: '物料名称',
batchNo: '批号',
......@@ -33,7 +33,7 @@ export const costDeptNetInData: CenterReportPageData = {
iframeSrc: 'settel/costSearch',
productNameSelector: '#form_item_goodsName',
columns: costDeptNetInColumns,
coreTableHeaders: ['物料名称', '科室名称', '进院供应商', '统计日期', '统计月份', '耗材类型'],
coreTableHeaders: ['物料名称', '科室名称', '销售商(自定义)', '统计日期', '统计月份', '耗材类型'],
mockNotExistProduct: '__NOT_EXIST_PRODUCT__99999',
mockFieldLabel: '物料名称',
mockFieldInDrawer: true,
......
......@@ -12,7 +12,7 @@ export const costEntryDetailColumns = {
unit: '单位',
registrationNo: '注册证号',
insuranceCode27: '27位医保编码',
supplierName: '进院供应商',
supplierName: '销售商(自定义)',
settlementPrice: '结算单价',
consumableCategory: '耗材分类',
financeClassify: '财务分类',
......@@ -68,7 +68,7 @@ export const costEntryDetailData: CenterReportPageData = {
columns: costEntryDetailColumns,
columnAliases: {
manufacturer: ['器械注册人-术语配置', '生产厂家', '器械注册人'],
supplierName: ['进院供应商', '进院供应商名称'],
supplierName: ['销售商(自定义)', '销售商(自定义)名称'],
},
coreTableHeaders: ['物料名称', '规格型号', '入账数量', '入账金额', '入账日期'],
summaryKeywords: ['入账数量', '入账金额'],
......
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