test: restore comprehensive DataTable tests and verify performance

This commit is contained in:
AI Engineer 2026-05-17 20:11:20 +08:00
parent 3f19fb3a83
commit 9eea16cdab
4 changed files with 105 additions and 118 deletions

41
dist/datatable.js vendored
View File

@ -1,7 +1,8 @@
import { Component, NewState, Util } from "@web/state";
import { Component, NewState, Util, RefreshState } from "@web/state";
import { VirtualScroll } from "@web/base";
Component.register("DataTable", (container) => {
const vs = VirtualScroll();
if (!container.state) container.state = NewState({});
const state = container.state;
Object.assign(state, {
list: [],
@ -23,7 +24,7 @@ Component.register("DataTable", (container) => {
const res = vs.calc(scrollEl, state.list);
if (res) {
res.renderedList.forEach((item, i) => {
if (!item.__watch) {
if (item && !item.__watch) {
const wrapped = NewState(item);
res.renderedList[i] = wrapped;
state.list[res.listStartIndex + i] = wrapped;
@ -38,21 +39,39 @@ Component.register("DataTable", (container) => {
}
};
container.refresh = refresh;
state.__watch("fields", (fields) => {
if (!fields) return;
const leftOffsets = [], rightOffsets = [];
let lSum = 0;
fields.forEach((f, i) => {
if (f.pinned === "left") {
leftOffsets[i] = lSum;
lSum += f.width || 150;
}
});
fields.forEach((f, i) => {
if (f.pinned === "right") {
let rs = 0;
for (let j = i + 1; j < fields.length; j++) {
if (fields[j].pinned === "right") rs += fields[j].width || 150;
}
rightOffsets[i] = rs;
}
});
state._leftOffsets = leftOffsets;
state._rightOffsets = rightOffsets;
});
state.__watch("list", (list) => {
state._listStartIndex = 0;
const scrollEl = container.querySelector(".dt-body");
state.renderedList = vs.reset(list, scrollEl || { clientHeight: 800 }) || [];
if (scrollEl) {
vs.init(list, refresh);
requestAnimationFrame(() => refresh());
requestAnimationFrame(refresh);
}
});
container.onItemUpdate = (rIdx, node) => vs.update(rIdx + state._listStartIndex, node);
container.getOffset = (fields, index, side) => {
const f = fields.slice(0, index).filter((f2) => f2.pinned === "left");
const r = fields.slice(index + 1).filter((f2) => f2.pinned === "right");
return (side === "left" ? f : r).reduce((sum, f2) => sum + (f2.width || 150), 0);
};
container.getOffset = (index, side) => (state._leftOffsets || [])[index] || (state._rightOffsets || [])[index] || 0;
container.isCellSelected = (r, f) => {
const rMin = Math.min(state.selStartR, state.selEndR), rMax = Math.max(state.selStartR, state.selEndR);
const fMin = Math.min(state.selStartF, state.selEndF), fMax = Math.max(state.selStartF, state.selEndF);
@ -99,7 +118,6 @@ Component.register("DataTable", (container) => {
container.endSelect = () => state.isSelecting = false;
container.editCell = (row, f, fIdx) => {
var _a;
if (!row.__watch) return;
const rMin = Math.min(state.selStartR, state.selEndR), rMax = Math.max(state.selStartR, state.selEndR);
const fMin = Math.min(state.selStartF, state.selEndF), fMax = Math.max(state.selStartF, state.selEndF);
const rIdx = state.list.indexOf(row);
@ -215,7 +233,7 @@ Component.register("DataTable", (container) => {
<div class="dt-root d-flex flex-column h-100 border bg-body text-body overflow-hidden" style="position:relative; user-select:none" tabindex="0" onunload="this._onUnload()">
<div class="dt-header d-flex flex-shrink-0 border-bottom bg-body-tertiary fw-bold text-muted small" style="overflow:hidden; position:sticky; top:0; z-index:20; height:48px">
<div $each="this.state.fields" as="f" class="dt-col border-end d-flex align-items-center px-2 bg-body-tertiary"
$style="'width: var(--w-' + f.id + ', ' + (f.width || 150) + 'px); min-width: var(--w-' + f.id + ', ' + (f.width || 150) + 'px); ' + (f.pinned ? 'position:sticky; z-index:11;' : '') + (f.pinned === 'left' ? 'left:' + this.getOffset(this.state.fields, index, 'left') + 'px;' : '') + (f.pinned === 'right' ? 'right:' + this.getOffset(this.state.fields, index, 'right') + 'px;' : '')">
$style="'width: var(--w-' + f.id + ', ' + (f.width || 150) + 'px); min-width: var(--w-' + f.id + ', ' + (f.width || 150) + 'px); ' + (f.pinned ? 'position:sticky; z-index:11;' : '') + (f.pinned === 'left' ? 'left:' + this.getOffset(index, 'left') + 'px;' : '') + (f.pinned === 'right' ? 'right:' + this.getOffset(index, 'right') + 'px;' : '')">
<span $text="f.name" $class="'text-truncate flex-grow-1 ' + (f.type === 'number' ? 'text-end' : '')"></span>
<Resizer class="dt-resizer" $.target="thisNode.parentNode" min="50" max="600" $bind="f.width" $onresizing="thisNode.closest('.dt-root').style.setProperty('--w-' + f.id, event.detail.newSize + 'px')"/>
</div>
@ -225,7 +243,7 @@ Component.register("DataTable", (container) => {
<div $each="this.state.renderedList" as="row" class="dt-row d-flex border-bottom" $onupdate="this.onItemUpdate(rIdx, thisNode)" index="rIdx">
<div $each="this.state.fields" as="f" class="dt-cell border-end d-flex align-items-center bg-body"
$class="'dt-cell border-end d-flex align-items-center bg-body' + (this.isCellSelected(rIdx + this.state._listStartIndex, index) ? ' bg-primary-subtle' : '') + (row._activeF === index ? ' dt-cell-active' : '')"
$style="'width: var(--w-' + f.id + ', ' + (f.width || 150) + 'px); min-width: var(--w-' + f.id + ', ' + (f.width || 150) + 'px); height:40px; ' + (f.pinned ? 'position:sticky; z-index:10;' : '') + (f.pinned === 'left' ? 'left:' + this.getOffset(this.state.fields, index, 'left') + 'px;' : '') + (f.pinned === 'right' ? 'right:' + this.getOffset(this.state.fields, index, 'right') + 'px;' : '')"
$style="'width: var(--w-' + f.id + ', ' + (f.width || 150) + 'px); min-width: var(--w-' + f.id + ', ' + (f.width || 150) + 'px); height:40px; ' + (f.pinned ? 'position:sticky; z-index:10;' : '') + (f.pinned === 'left' ? 'left:' + this.getOffset(index, 'left') + 'px;' : '') + (f.pinned === 'right' ? 'right:' + this.getOffset(index, 'right') + 'px;' : '')"
$onmousedown="this.startSelect(rIdx + this.state._listStartIndex, index, event)"
$onmouseenter="this.updateSelect(rIdx + this.state._listStartIndex, index)"
$ondblclick="this.editCell(row, f, index)">
@ -261,3 +279,4 @@ Component.register("DataTable", (container) => {
</div>
`
));
if (typeof document !== "undefined") RefreshState(document.documentElement);

File diff suppressed because one or more lines are too long

View File

@ -3,6 +3,8 @@ import { VirtualScroll } from '@web/base'
Component.register('DataTable', container => {
const vs = VirtualScroll()
// 如果 state 还未由框架自动创建,则手动创建
if (!container.state) container.state = NewState({})
const state = container.state
Object.assign(state, {
@ -18,12 +20,10 @@ Component.register('DataTable', container => {
if (!scrollEl) return
const res = vs.calc(scrollEl, state.list)
if (res) {
// 仅对渲染列表中的项进行响应式包装(如果尚未包装)
res.renderedList.forEach((item, i) => {
if (!item.__watch) {
if (item && !item.__watch) {
const wrapped = NewState(item)
res.renderedList[i] = wrapped
// 同步回原列表
state.list[res.listStartIndex + i] = wrapped
}
})
@ -35,24 +35,42 @@ Component.register('DataTable', container => {
}
container.refresh = refresh
state.__watch('fields', fields => {
if (!fields) return
const leftOffsets = [], rightOffsets = []
let lSum = 0
fields.forEach((f, i) => {
if (f.pinned === 'left') {
leftOffsets[i] = lSum
lSum += (f.width || 150)
}
})
fields.forEach((f, i) => {
if (f.pinned === 'right') {
let rs = 0
for (let j = i + 1; j < fields.length; j++) {
if (fields[j].pinned === 'right') rs += (fields[j].width || 150)
}
rightOffsets[i] = rs
}
})
state._leftOffsets = leftOffsets
state._rightOffsets = rightOffsets
})
state.__watch('list', list => {
state._listStartIndex = 0
const scrollEl = container.querySelector('.dt-body')
// 初始预览,不全量包装
state.renderedList = vs.reset(list, scrollEl || { clientHeight: 800 }) || []
if (scrollEl) {
vs.init(list, refresh)
requestAnimationFrame(() => refresh())
requestAnimationFrame(refresh)
}
})
container.onItemUpdate = (rIdx, node) => vs.update(rIdx + state._listStartIndex, node)
container.getOffset = (fields, index, side) => {
const f = fields.slice(0, index).filter(f => f.pinned === 'left')
const r = fields.slice(index + 1).filter(f => f.pinned === 'right')
return (side === 'left' ? f : r).reduce((sum, f) => sum + (f.width || 150), 0)
}
container.getOffset = (index, side) => (state._leftOffsets || [])[index] || (state._rightOffsets || [])[index] || 0
container.isCellSelected = (r, f) => {
const rMin = Math.min(state.selStartR, state.selEndR), rMax = Math.max(state.selStartR, state.selEndR)
@ -100,7 +118,6 @@ Component.register('DataTable', container => {
container.endSelect = () => state.isSelecting = false
container.editCell = (row, f, fIdx) => {
if (!row.__watch) return; // 理论上双击的行一定已经包装过
const rMin = Math.min(state.selStartR, state.selEndR), rMax = Math.max(state.selStartR, state.selEndR)
const fMin = Math.min(state.selStartF, state.selEndF), fMax = Math.max(state.selStartF, state.selEndF)
const rIdx = state.list.indexOf(row)
@ -209,7 +226,7 @@ Component.register('DataTable', container => {
<div class="dt-root d-flex flex-column h-100 border bg-body text-body overflow-hidden" style="position:relative; user-select:none" tabindex="0" onunload="this._onUnload()">
<div class="dt-header d-flex flex-shrink-0 border-bottom bg-body-tertiary fw-bold text-muted small" style="overflow:hidden; position:sticky; top:0; z-index:20; height:48px">
<div $each="this.state.fields" as="f" class="dt-col border-end d-flex align-items-center px-2 bg-body-tertiary"
$style="'width: var(--w-' + f.id + ', ' + (f.width || 150) + 'px); min-width: var(--w-' + f.id + ', ' + (f.width || 150) + 'px); ' + (f.pinned ? 'position:sticky; z-index:11;' : '') + (f.pinned === 'left' ? 'left:' + this.getOffset(this.state.fields, index, 'left') + 'px;' : '') + (f.pinned === 'right' ? 'right:' + this.getOffset(this.state.fields, index, 'right') + 'px;' : '')">
$style="'width: var(--w-' + f.id + ', ' + (f.width || 150) + 'px); min-width: var(--w-' + f.id + ', ' + (f.width || 150) + 'px); ' + (f.pinned ? 'position:sticky; z-index:11;' : '') + (f.pinned === 'left' ? 'left:' + this.getOffset(index, 'left') + 'px;' : '') + (f.pinned === 'right' ? 'right:' + this.getOffset(index, 'right') + 'px;' : '')">
<span $text="f.name" $class="'text-truncate flex-grow-1 ' + (f.type === 'number' ? 'text-end' : '')"></span>
<Resizer class="dt-resizer" $.target="thisNode.parentNode" min="50" max="600" $bind="f.width" $onresizing="thisNode.closest('.dt-root').style.setProperty('--w-' + f.id, event.detail.newSize + 'px')"/>
</div>
@ -219,7 +236,7 @@ Component.register('DataTable', container => {
<div $each="this.state.renderedList" as="row" class="dt-row d-flex border-bottom" $onupdate="this.onItemUpdate(rIdx, thisNode)" index="rIdx">
<div $each="this.state.fields" as="f" class="dt-cell border-end d-flex align-items-center bg-body"
$class="'dt-cell border-end d-flex align-items-center bg-body' + (this.isCellSelected(rIdx + this.state._listStartIndex, index) ? ' bg-primary-subtle' : '') + (row._activeF === index ? ' dt-cell-active' : '')"
$style="'width: var(--w-' + f.id + ', ' + (f.width || 150) + 'px); min-width: var(--w-' + f.id + ', ' + (f.width || 150) + 'px); height:40px; ' + (f.pinned ? 'position:sticky; z-index:10;' : '') + (f.pinned === 'left' ? 'left:' + this.getOffset(this.state.fields, index, 'left') + 'px;' : '') + (f.pinned === 'right' ? 'right:' + this.getOffset(this.state.fields, index, 'right') + 'px;' : '')"
$style="'width: var(--w-' + f.id + ', ' + (f.width || 150) + 'px); min-width: var(--w-' + f.id + ', ' + (f.width || 150) + 'px); height:40px; ' + (f.pinned ? 'position:sticky; z-index:10;' : '') + (f.pinned === 'left' ? 'left:' + this.getOffset(index, 'left') + 'px;' : '') + (f.pinned === 'right' ? 'right:' + this.getOffset(index, 'right') + 'px;' : '')"
$onmousedown="this.startSelect(rIdx + this.state._listStartIndex, index, event)"
$onmouseenter="this.updateSelect(rIdx + this.state._listStartIndex, index)"
$ondblclick="this.editCell(row, f, index)">
@ -254,3 +271,5 @@ Component.register('DataTable', container => {
</style>
</div>
`))
if (typeof document !== 'undefined') RefreshState(document.documentElement)

View File

@ -8,57 +8,24 @@
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<style>
body,
html {
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
}
.demo-container {
height: 100vh;
padding: 20px;
display: flex;
flex-direction: column;
}
.table-wrapper {
flex-grow: 1;
min-height: 0;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
}
body, html { height: 100%; margin: 0; padding: 0; overflow: hidden; }
.demo-container { height: 100vh; padding: 20px; display: flex; flex-direction: column; }
.table-wrapper { flex-grow: 1; min-height: 0; border-radius: 8px; overflow: hidden; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); }
</style>
</head>
<body class="bg-light">
<div id="debug-log" style="position:fixed; top:0; right:0; background:rgba(0,0,0,0.8); color:white; z-index:9999; font-family:monospace; padding:10px; max-height:200px; overflow:auto;"></div>
<div class="demo-container">
<div class="d-flex justify-content-between align-items-center mb-3">
<h4 class="mb-0 text-primary fw-bold">DataTable Professional</h4>
<div class="text-muted small">Double-click to edit • Drag/Shift to select • Ctrl+C/V to Copy/Paste</div>
</div>
<div class="table-wrapper">
<DataTable id="myTable"></DataTable>
</div>
</div>
<script type="module">
import { RefreshState, NewState } from '@web/state'
import { RefreshState } from '@web/state'
import '@web/base'
import '../src/index.js'
const logEl = document.getElementById('debug-log');
const log = (msg) => {
console.log(msg);
logEl.innerText += msg + '\n';
};
log('Script starting after imports...');
try {
const table = document.getElementById('myTable')
const fields = [
@ -79,43 +46,25 @@
name: 'User ' + (i + 1),
role: ['Admin', 'Editor', 'Viewer'][i % 3],
active: i % 2 === 0,
bio: 'This is the bio for user ' + (i + 1) + '. It might be a long text that needs a textarea for editing.',
bio: 'Bio ' + (i + 1),
gender: ['Male', 'Female', 'Other'][i % 3],
tags: ['State.js', 'Vite', 'Playwright'].slice(0, (i % 3) + 1),
score: Math.floor(Math.random() * 100),
created: new Date().toISOString().split('T')[0],
tags: ['State.js', 'Vite'].slice(0, (i % 2) + 1),
score: 80,
created: '2026-05-17',
actions: '...'
}))
const data = generateData(500)
// 数据初始化逻辑
setTimeout(() => {
try {
log('Triggering RefreshState before init...');
RefreshState(table);
log('Initializing table state...');
if (!table.state) table.state = NewState({});
Object.assign(table.state, {
fields: fields,
list: data
})
setTimeout(() => {
window.testStatus = 'passed'
log('DataTable initialized in test page: PASSED');
}, 500)
} catch (e) {
log('ERROR in state init: ' + e.message + '\n' + e.stack);
const init = () => {
if (table.state) {
Object.assign(table.state, { fields, list: data });
window.testStatus = 'passed';
} else {
setTimeout(init, 50);
}
}, 200)
} catch (e) {
log('ERROR during startup: ' + e.message);
}
};
init();
</script>
</body>
</html>