fix: align market selection checkboxes with rows

- Fix checkbox column misalignment with creator list items
- Add syncContentCellHeight to keep selection cells aligned with native rows
- Add MarketAlignmentDiagnostic for debugging alignment issues
- Update documentation to clarify dist-release folder usage
- Add npm run build:release step to colleague setup guide
This commit is contained in:
admin123 2026-05-08 00:39:15 +08:00
parent ecfe136628
commit 02be56888a
7 changed files with 276 additions and 3 deletions

View File

@ -43,6 +43,8 @@ git clone https://git.internal.intelligrow.cn/wangshaoqing/star-chart-search-enh
star-chart-search-enhancer/dist-release/
```
⚠️ **重要**:必须选择 `dist-release` 这个子文件夹,不要选外层文件夹
✅ 安装成功!你会看到插件卡片。
---
@ -74,10 +76,16 @@ git clone https://git.internal.intelligrow.cn/wangshaoqing/star-chart-search-enh
```bash
cd Desktop/star-chart-search-enhancer
git pull
npm run build:release
```
然后到 `chrome://extensions` 页面点击插件卡片的 **"重新加载"** 🔄
⚠️ **如果重新加载后还是旧版本**
- 先点击插件卡片的 **"移除"** 删除旧版本
- 然后重新点击 **"加载已解压的扩展程序"**
- 再次选择 `dist-release` 文件夹
---
## ❓ 常见问题

View File

@ -37,7 +37,9 @@
4. 点击左上角出现的 **"加载已解压的扩展程序"**
5. 选择刚才解压出来的那个文件夹
5. 选择刚才解压出来的文件夹里的 **`dist-release`** 文件夹
⚠️ **重要**:必须选择 `dist-release` 这个子文件夹,不要选外层文件夹
6. 看到绿色的插件卡片出现,就装好了!
@ -95,6 +97,11 @@ https://xingtu.cn/ad/creator/market
3. 打开 `chrome://extensions`
4. 找到插件卡片,点击 **"重新加载"** 🔄
⚠️ **如果重新加载后还是旧版本**
- 先点击插件卡片的 **"移除"** 删除旧版本
- 然后重新点击 **"加载已解压的扩展程序"**
- 再次选择 `dist-release` 文件夹
---
## ❓ 常见问题

64
package-lock.json generated
View File

@ -12,6 +12,7 @@
"@logto/chrome-extension": "^0.1.27"
},
"devDependencies": {
"@playwright/test": "^1.59.1",
"jsdom": "^29.0.2",
"tsup": "^8.5.1",
"typescript": "^6.0.3",
@ -826,6 +827,22 @@
"url": "https://github.com/sponsors/Boshen"
}
},
"node_modules/@playwright/test": {
"version": "1.59.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz",
"integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.59.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@rolldown/binding-android-arm64": {
"version": "1.0.0-rc.16",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.16.tgz",
@ -2502,6 +2519,53 @@
"pathe": "^2.0.1"
}
},
"node_modules/playwright": {
"version": "1.59.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz",
"integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.59.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.59.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz",
"integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/playwright/node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/postcss": {
"version": "8.5.10",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz",

View File

@ -17,6 +17,7 @@
},
"license": "UNLICENSED",
"devDependencies": {
"@playwright/test": "^1.59.1",
"jsdom": "^29.0.2",
"tsup": "^8.5.1",
"typescript": "^6.0.3",

View File

@ -99,10 +99,39 @@ export interface MarketTableDom {
rows: MarketRowDom[];
}
export interface MarketAlignmentDiagnostic {
authorId: string;
authorName: string;
checkboxTop: number;
rowTop: number;
topDelta: number;
}
export function syncMarketTable(root: ParentNode): MarketTableDom | null {
return syncSyntheticMarketTable(root) ?? syncDivGridMarketTable(root);
}
export function readMarketAlignmentDiagnostics(
table: MarketTableDom
): MarketAlignmentDiagnostic[] {
return table.rows.map((rowDom) => {
const selectionCell = rowDom.selectionCheckbox.parentElement;
const selectionRect = selectionCell?.getBoundingClientRect();
const rowRect = rowDom.row.getBoundingClientRect();
const checkboxTop = selectionRect?.top ?? Number.NaN;
const rowTop = rowRect.top ?? Number.NaN;
return {
authorId: rowDom.authorId,
authorName: rowDom.authorName,
checkboxTop,
rowTop,
topDelta: checkboxTop - rowTop
};
});
}
export function readMarketPageSignature(root: ParentNode): string {
const document = getOwnerDocument(root);
const explicitPageIndex =
@ -801,14 +830,15 @@ function syncDivColumnCells(
const templateCells = getDirectContentCells(templateColumn);
for (let index = 0; index < rowCount; index += 1) {
const existingCell = getDirectContentCells(column)[index] ?? null;
const templateCell =
templateCells[index] ?? templateCells[templateCells.length - 1] ?? null;
if (existingCell) {
existingCell.dataset.marketRowCell = field;
applyPluginContentCellStyles(existingCell);
syncContentCellHeight(existingCell, templateCell);
continue;
}
const templateCell =
templateCells[index] ?? templateCells[templateCells.length - 1] ?? null;
const nextCell =
field === SELECTION_COLUMN_KEY
? templateCell
@ -820,11 +850,31 @@ function syncDivColumnCells(
nextCell.dataset.marketRowCell = field;
applyColumnWidth(nextCell, field);
applyPluginContentCellStyles(nextCell);
syncContentCellHeight(nextCell, templateCell);
nextCell.textContent = "";
column.appendChild(nextCell);
}
}
function syncContentCellHeight(
cell: HTMLElement,
templateCell: HTMLElement | null
): void {
if (!templateCell) {
return;
}
const measuredHeight = Math.round(templateCell.getBoundingClientRect().height);
const nextHeight =
measuredHeight > 0 ? `${measuredHeight}px` : templateCell.style.height;
if (nextHeight) {
cell.style.height = nextHeight;
} else {
cell.style.removeProperty("height");
}
}
function applyPluginHeaderCellStyles(cell: HTMLElement): void {
cell.style.display = "flex";
cell.style.alignItems = "center";

View File

@ -4,6 +4,7 @@ import { createBatchPayload, type BatchPayload } from "./batch-payload";
import {
applyRowOrder,
applyRowVisibility,
readMarketAlignmentDiagnostics,
readMarketPageSignature,
renderMarketRowState,
syncPluginSortHeaders,
@ -396,6 +397,7 @@ export function createMarketController(options: CreateMarketControllerOptions) {
applyRowOrder(table, records.map((record) => record.authorId));
bindSelectionControls(table);
syncMarketSelectionState(table, selectedAuthorIds);
logMarketAlignmentDiagnosticsIfEnabled(options.window, table, "applyCurrentView");
lastKnownPageSignature = readMarketPageSignature(options.document);
});
}
@ -662,12 +664,23 @@ export function createMarketController(options: CreateMarketControllerOptions) {
}
const step = Math.max(scrollContainer.clientHeight, 240);
logMarketScrollTraceIfEnabled(options.window, {
event: "start",
maxScrollTop,
originalScrollTop,
step
});
for (
let nextScrollTop = Math.min(originalScrollTop + step, maxScrollTop);
nextScrollTop > originalScrollTop && nextScrollTop <= maxScrollTop;
nextScrollTop = Math.min(nextScrollTop + step, maxScrollTop)
) {
setScrollTop(scrollContainer, nextScrollTop);
logMarketScrollTraceIfEnabled(options.window, {
event: "step",
nextScrollTop,
scrollTop: scrollContainer.scrollTop
});
hydrationSnapshot = await collectCurrentPageSnapshotsUntilSettled();
if (
hydrationSnapshot.missingDefaultFieldCount === 0 &&
@ -683,6 +696,10 @@ export function createMarketController(options: CreateMarketControllerOptions) {
if (scrollContainer.scrollTop !== originalScrollTop) {
setScrollTop(scrollContainer, originalScrollTop);
logMarketScrollTraceIfEnabled(options.window, {
event: "restore",
restoredScrollTop: scrollContainer.scrollTop
});
}
}
@ -1009,6 +1026,45 @@ function setScrollTop(element: HTMLElement, top: number): void {
element.dispatchEvent(new Event("scroll"));
}
function logMarketAlignmentDiagnosticsIfEnabled(
window: Window,
table: ReturnType<typeof syncMarketTable>,
label: string
): void {
if (!isMarketDebugLoggingEnabled(window) || !table) {
return;
}
const diagnostics = readMarketAlignmentDiagnostics(table);
console.info(`[SCES debug] ${label} alignment`, diagnostics);
}
function logMarketScrollTraceIfEnabled(
window: Window,
payload: Record<string, unknown>
): void {
if (!isMarketDebugLoggingEnabled(window)) {
return;
}
console.info("[SCES debug] submit/export scroll", payload);
}
function isMarketDebugLoggingEnabled(window: Window): boolean {
try {
const searchParams = new URL(window.location.href).searchParams;
if (searchParams.get("scesDebug") === "1") {
return true;
}
} catch {}
try {
return window.localStorage.getItem("scesDebugMarket") === "1";
} catch {
return false;
}
}
function readCurrentPageRows(document: Document): MarketRowSnapshot[] {
const table = syncMarketTable(document);
if (!table) {

View File

@ -7,6 +7,7 @@ import {
applyRowVisibility,
findNextPageControl,
isPageControlDisabled,
readMarketAlignmentDiagnostics,
readMarketPageSignature,
renderMarketRowState,
syncMarketTable
@ -329,6 +330,70 @@ describe("market-dom-sync", () => {
expect(readSelectionCellHeights()).toEqual(["120px", "120px"]);
});
test("resyncs selection cell heights when native author row heights change", () => {
document.body.innerHTML = buildRealMarketGridFixture();
expect(syncMarketTable(document)).not.toBeNull();
expect(readSelectionCellHeights()).toEqual(["120px", "120px"]);
const authorCells = Array.from(
document.querySelectorAll('[data-testid="author-section"] .content-column:not([data-market-column-group="selection"]) > .content-cell')
) as HTMLElement[];
const middleCells = Array.from(
document.querySelectorAll('.middle-columns .content-column > .content-cell')
) as HTMLElement[];
const rightCells = Array.from(
document.querySelectorAll('[data-testid="right-section"] .content-column > .content-cell')
) as HTMLElement[];
[...authorCells, ...middleCells, ...rightCells].forEach((cell) => {
cell.style.height = "152px";
});
expect(syncMarketTable(document)).not.toBeNull();
expect(readSelectionCellHeights()).toEqual(["152px", "152px"]);
});
test("reads alignment diagnostics for selection cells against native rows", () => {
document.body.innerHTML = buildRealMarketGridFixture();
const table = syncMarketTable(document);
if (!table) {
throw new Error("Expected market table");
}
const firstSelectionCell = table.rows[0].selectionCheckbox.parentElement as HTMLElement;
const secondSelectionCell = table.rows[1].selectionCheckbox.parentElement as HTMLElement;
vi.spyOn(firstSelectionCell, "getBoundingClientRect").mockReturnValue(
createRect({ height: 24, top: 100 })
);
vi.spyOn(table.rows[0].row, "getBoundingClientRect").mockReturnValue(
createRect({ height: 120, top: 120 })
);
vi.spyOn(secondSelectionCell, "getBoundingClientRect").mockReturnValue(
createRect({ height: 24, top: 260 })
);
vi.spyOn(table.rows[1].row, "getBoundingClientRect").mockReturnValue(
createRect({ height: 120, top: 250 })
);
expect(readMarketAlignmentDiagnostics(table)).toEqual([
expect.objectContaining({
authorId: "111",
checkboxTop: 100,
rowTop: 120,
topDelta: -20
}),
expect.objectContaining({
authorId: "222",
checkboxTop: 260,
rowTop: 250,
topDelta: 10
})
]);
});
test("uses native-like alignment styles for plugin cells", () => {
document.body.innerHTML = buildRealMarketGridFixtureWithScopedAttributes();
@ -1047,3 +1112,25 @@ function readVisualCells(root: Element | null): HTMLElement[] {
return cells.indexOf(left) - cells.indexOf(right);
});
}
function createRect({
height,
top
}: {
height: number;
top: number;
}): DOMRect {
return {
bottom: top + height,
height,
left: 0,
right: 0,
toJSON() {
return {};
},
top,
width: 0,
x: 0,
y: top
} as DOMRect;
}