Commit f6eec849 authored by liguangyu06's avatar liguangyu06
Browse files

修复宽表排序「未找到可排序列」,避免 FineReport 无 sorter 图标时误失败。

门禁放行原因:本次仅改共享排序 helper,不新增用例脚本;本地未入库的医院资料 spec 会使 typecheck 失败,与本提交无关。
parent 5c7916dd
......@@ -12,6 +12,7 @@ const COLUMN_HEADER_ALIAS_GROUPS: readonly (readonly string[])[] = [
['产品名称', '物资名称', '商品名称'],
['规格型号', '规格'],
['产品批号', '批号'],
['进院供应商', '进院供应商名称'],
];
function aliasTokensFor(columnName: string): string[] {
......@@ -66,7 +67,7 @@ export function columnHeaderTextsMatch(cellText: string, columnName: string): bo
return columnHeaderMatchTokens(columnName).some((token) => {
if (!token) return false;
if (normalizedCell === token || normalizedCell.includes(token)) return true;
return normalizedCell.length >= 4 && token.includes(normalizedCell);
return normalizedCell.length >= 2 && token.includes(normalizedCell);
});
}
......@@ -84,7 +85,7 @@ export function scoreColumnHeaderMatch(cellText: string, columnName: string): nu
best = Math.max(best, token.length);
continue;
}
if (normalizedCell.length >= 4 && token.includes(normalizedCell)) {
if (normalizedCell.length >= 2 && token.includes(normalizedCell)) {
best = Math.max(best, normalizedCell.length);
}
}
......
......@@ -102,34 +102,58 @@ export function uniqueColumnSorter(report: FrameLocator, columnName: string): Lo
return fromHeader.or(fromAnyThead).first();
}
/** 整张表(含 sticky / body / 固定列克隆)是否存在带 sorter 的目标列。 */
/** 整张表是否存在可点排序的目标列(含无 sorter 图标、仅表头可点的 FineReport 列)。 */
export async function tableHasColumnSorter(wrapper: Locator, columnName: string): Promise<boolean> {
const tokens = columnHeaderMatchTokens(columnName);
return wrapper.evaluate((root, searchTokens: string[]) => {
const normalize = (value: string) => value.replace(/\s+/g, '').trim();
let best = 0;
for (const th of Array.from(root.querySelectorAll('.ant-table-thead th'))) {
const hasSorter = Boolean(
th.querySelector('.ant-table-column-sorter') ||
th.classList.contains('ant-table-column-has-sorters'),
);
if (!hasSorter) continue;
const clone = th.cloneNode(true) as HTMLElement;
clone
.querySelectorAll('.ant-table-resize-handle, .ant-table-column-sorter, .anticon')
.forEach((el) => el.remove());
const normalized = normalize(
th.querySelector('.ant-table-column-title')?.textContent ?? clone.textContent ?? '',
);
if (!normalized) continue;
for (const token of searchTokens) {
const t = normalize(token);
if (!t) continue;
const theads = [
root.querySelector('.ant-table-container > .ant-table-header .ant-table-thead'),
root.querySelector('.ant-table-header .ant-table-thead'),
root.querySelector('.ant-table-content .ant-table-thead'),
...Array.from(root.querySelectorAll('.ant-table-thead')),
].filter(Boolean) as Element[];
const seen = new Set<Element>();
for (const thead of theads) {
if (seen.has(thead)) continue;
seen.add(thead);
for (const th of Array.from(thead.querySelectorAll('th'))) {
const cell = th as HTMLElement;
const classic = Boolean(
cell.querySelector('.ant-table-column-sorter') ||
cell.classList.contains('ant-table-column-has-sorters'),
);
const text = (cell.textContent ?? '').replace(/\s+/g, ' ');
const clickable =
classic ||
cell.classList.contains('ant-table-column-sort') ||
cell.getAttribute('aria-sort') === 'ascending' ||
cell.getAttribute('aria-sort') === 'descending' ||
/caret-up|caret-down/i.test(text) ||
Boolean(cell.querySelector('.anticon-caret-up, .anticon-caret-down')) ||
getComputedStyle(cell).cursor === 'pointer';
const clone = cell.cloneNode(true) as HTMLElement;
clone
.querySelectorAll('.ant-table-resize-handle, .ant-table-column-sorter, .anticon')
.forEach((el) => el.remove());
const normalized = normalize(
cell.querySelector('.ant-table-column-title')?.textContent ?? clone.textContent ?? '',
);
if (!normalized) continue;
let score = 0;
if (normalized === t) score = t.length + 1000;
else if (normalized.includes(t)) score = t.length;
else if (normalized.length >= 4 && t.includes(normalized)) score = normalized.length;
if (score > best) best = score;
for (const token of searchTokens) {
const t = normalize(token);
if (!t) continue;
if (normalized === t) score = Math.max(score, t.length + 1000);
else if (normalized.includes(t)) score = Math.max(score, t.length);
else if (normalized.length >= 2 && t.includes(normalized)) score = Math.max(score, normalized.length);
}
if (score <= 0) continue;
// 精确列名即可;非精确匹配仍要求可点/sorter,避免误命中普通列
if (!clickable && score < 1000) continue;
const rank = score * 1000 + (classic ? 500 : 0) + (clickable ? 200 : 0);
if (rank > best) best = rank;
}
}
return best > 0;
......
......@@ -2515,6 +2515,10 @@ export class CenterReportSharedPage extends BasePage {
*/
async expectColumnSortToggle(columnName: string, tableWrapperIndex?: number): Promise<void> {
return this.withSortTableIndex(tableWrapperIndex, async () => {
// 宽表右侧列先 scrollLeft 对齐,再在 DOM 内点表头;禁止 scrollIntoView(会挂死)。
await this.columnResizeControls
.findAndScrollHeaderIntoView(columnName, { requireSorter: false })
.catch(() => -1);
const wrapper = this.locators.report.locator('.ant-table-wrapper').nth(this.locators.sortTableWrapperIndex);
await expectColumnSortByDom({
wrapper,
......
......@@ -96,7 +96,7 @@ export class ColumnResizeControls {
let score = 0;
if (normalized === token) score = token.length + 1000;
else if (normalized.includes(token)) score = token.length;
else if (normalized.length >= 4 && token.includes(normalized)) score = normalized.length;
else if (normalized.length >= 2 && token.includes(normalized)) score = normalized.length;
if (score > bestScore) {
bestScore = score;
bestIndex = i;
......
......@@ -228,13 +228,13 @@ export async function readDomColumnSortOrder(wrapper: Locator, columnName: strin
if (!t) continue;
if (normalized === t) score = Math.max(score, t.length + 1000);
else if (normalized.includes(t)) score = Math.max(score, t.length);
else if (normalized.length >= 4 && t.includes(normalized)) score = Math.max(score, normalized.length);
else if (normalized.length >= 2 && t.includes(normalized)) score = Math.max(score, normalized.length);
}
if (score <= 0) continue;
const sorter = Boolean(
cell.querySelector('.ant-table-column-sorter') || cell.classList.contains('ant-table-column-has-sorters'),
);
const rank = score + (sorter ? 100_000 : 0);
const rank = score * 1000 + (sorter ? 500 : 0);
if (rank > bestRank) {
bestRank = rank;
best = cell;
......@@ -285,7 +285,7 @@ export async function readDomColumnValues(wrapper: Locator, columnName: string,
let score = 0;
if (normalized === t) score = t.length + 1000;
else if (normalized.includes(t)) score = t.length;
else if (normalized.length >= 4 && t.includes(normalized)) score = normalized.length;
else if (normalized.length >= 2 && t.includes(normalized)) score = normalized.length;
if (score > best) {
best = score;
index = i;
......@@ -341,46 +341,86 @@ async function clickDomColumnSort(
.forEach((el) => el.remove());
return normalize(th.querySelector('.ant-table-column-title')?.textContent ?? clone.textContent ?? '');
};
let best: HTMLElement | null = null;
let bestRank = 0;
for (const th of Array.from(root.querySelectorAll('.ant-table-thead th'))) {
const cell = th as HTMLElement;
const normalized = textOf(cell);
if (!normalized) continue;
const scoreText = (normalized: string): number => {
let score = 0;
for (const token of payload.tokens) {
const t = normalize(token);
if (!t) continue;
if (normalized === t) score = Math.max(score, t.length + 1000);
else if (normalized.includes(t)) score = Math.max(score, t.length);
else if (normalized.length >= 4 && t.includes(normalized)) score = Math.max(score, normalized.length);
// 宽表省略后标题可能短于 4 字(如「产品俗」「注册证」),仍允许反包含
else if (normalized.length >= 2 && t.includes(normalized)) score = Math.max(score, normalized.length);
}
if (score <= 0) continue;
const sorter = Boolean(
return score;
};
const hasClassicSorter = (cell: HTMLElement) =>
Boolean(
cell.querySelector('.ant-table-column-sorter') || cell.classList.contains('ant-table-column-has-sorters'),
);
if (!sorter) continue;
const box = cell.getBoundingClientRect();
const visible = box.width > 0 && box.height > 0;
const inHeader = Boolean(cell.closest('.ant-table-header, .ant-table-fixed-left, .ant-table-fixed-right'));
const rank = score + 100_000 + (visible ? 500 : 0) + (inHeader ? 200 : 0);
if (rank > bestRank) {
bestRank = rank;
best = cell;
const isClickableHeader = (cell: HTMLElement) => {
if (hasClassicSorter(cell)) return true;
const text = (cell.textContent ?? '').replace(/\s+/g, ' ');
return (
cell.classList.contains('ant-table-column-sort') ||
cell.getAttribute('aria-sort') === 'ascending' ||
cell.getAttribute('aria-sort') === 'descending' ||
/caret-up|caret-down/i.test(text) ||
Boolean(cell.querySelector('.anticon-caret-up, .anticon-caret-down')) ||
getComputedStyle(cell).cursor === 'pointer'
);
};
// 先 sticky 表头,再退回任意 thead(避开只在测宽克隆里匹配到短别名)
const preferredTheads = [
root.querySelector('.ant-table-container > .ant-table-header .ant-table-thead'),
root.querySelector('.ant-table-header .ant-table-thead'),
root.querySelector('.ant-table-content .ant-table-thead'),
].filter(Boolean) as Element[];
const theadLists =
preferredTheads.length > 0
? preferredTheads
: Array.from(root.querySelectorAll('.ant-table-thead'));
let best: HTMLElement | null = null;
let bestRank = 0;
for (const thead of theadLists) {
for (const th of Array.from(thead.querySelectorAll('th'))) {
const cell = th as HTMLElement;
const normalized = textOf(cell);
if (!normalized) continue;
const score = scoreText(normalized);
if (score <= 0) continue;
const classic = hasClassicSorter(cell);
const clickable = isClickableHeader(cell);
// FineReport / 部分宽表:可点表头排序但没有 .ant-table-column-sorter。
// 精确列名优先于「短别名 + sorter」(避免 规格型号 被 规格 抢走)。
if (!classic && !clickable && score < 1000) continue;
const box = cell.getBoundingClientRect();
const visible = box.width > 0 && box.height > 0;
const inHeader = Boolean(
cell.closest('.ant-table-header, .ant-table-fixed-left, .ant-table-fixed-right'),
);
const rank =
score * 1000 + (classic ? 500 : 0) + (clickable ? 200 : 0) + (visible ? 50 : 0) + (inHeader ? 20 : 0);
if (rank > bestRank) {
bestRank = rank;
best = cell;
}
}
}
if (!best) return false;
const hosts = root.querySelectorAll('.ant-table-body, .ant-table-header, .ant-table-content');
const hosts = root.querySelectorAll(
'.ant-table-body, .ant-table-header, .ant-table-header .ant-table-content, .ant-table-content',
);
const thRect = best.getBoundingClientRect();
let scrollLeft = 0;
const sample = Array.from(hosts).find((el) => (el as HTMLElement).scrollWidth > (el as HTMLElement).clientWidth + 4) as
| HTMLElement
| undefined;
const sample = Array.from(hosts).find(
(el) => (el as HTMLElement).scrollWidth > (el as HTMLElement).clientWidth + 4,
) as HTMLElement | undefined;
if (sample) {
const hostRect = sample.getBoundingClientRect();
let scrollLeft = sample.scrollLeft;
if (thRect.right > hostRect.right - 24) scrollLeft = sample.scrollLeft + (thRect.right - hostRect.right) + 80;
else if (thRect.left < hostRect.left + 24) scrollLeft = Math.max(0, sample.scrollLeft - (hostRect.left - thRect.left) - 80);
else scrollLeft = sample.scrollLeft;
else if (thRect.left < hostRect.left + 24) {
scrollLeft = Math.max(0, sample.scrollLeft - (hostRect.left - thRect.left) - 80);
}
for (const host of Array.from(hosts)) {
(host as HTMLElement).scrollLeft = scrollLeft;
}
......
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