@icen.ai/ui 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/behaviors/charts.mjs +188 -0
- package/dist/behaviors/datatable.mjs +244 -24
- package/dist/components/chart-area.css +15 -0
- package/dist/components/chart-area.mjs +4 -0
- package/dist/components/chart-bar.css +79 -0
- package/dist/components/chart-bar.mjs +4 -0
- package/dist/components/chart-gauge.css +38 -0
- package/dist/components/chart-gauge.mjs +4 -0
- package/dist/components/chart-heatmap.css +52 -0
- package/dist/components/chart-heatmap.mjs +4 -0
- package/dist/components/chart-line.css +15 -0
- package/dist/components/chart-line.mjs +4 -0
- package/dist/components/chart-pie.css +26 -0
- package/dist/components/chart-pie.mjs +4 -0
- package/dist/components/chart-radar.css +19 -0
- package/dist/components/chart-radar.mjs +4 -0
- package/dist/components/chart-sparkline.css +14 -0
- package/dist/components/chart-sparkline.mjs +4 -0
- package/dist/components/chart-stack.css +30 -0
- package/dist/components/chart-stack.mjs +4 -0
- package/dist/components/charts.css +7 -207
- package/dist/components/charts.mjs +9 -0
- package/dist/components/datatable.css +86 -0
- package/dist/index.mjs +189 -1
- package/dist/registry.json +137 -24
- package/dist/ui.css +334 -133
- package/package.json +1 -1
|
@@ -258,6 +258,192 @@ function renderLine(el, opts) {
|
|
|
258
258
|
wrap.append(svg, labelsRow(labels.slice(-7)));
|
|
259
259
|
el.appendChild(wrap);
|
|
260
260
|
}
|
|
261
|
+
function renderArea(el, opts) {
|
|
262
|
+
if (typeof document === "undefined") return;
|
|
263
|
+
el.textContent = "";
|
|
264
|
+
const n = Math.min(opts.labels.length, opts.values.length);
|
|
265
|
+
if (n === 0) {
|
|
266
|
+
renderEmpty(el, opts.emptyLabel);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
const labels = opts.labels.slice(0, n);
|
|
270
|
+
const values = opts.values.slice(0, n);
|
|
271
|
+
const width = 360;
|
|
272
|
+
const height = 150;
|
|
273
|
+
const pad = 14;
|
|
274
|
+
const max = Math.max(...values, 1);
|
|
275
|
+
const tone = toneVar(opts.tone);
|
|
276
|
+
const points = values.map((value, i) => ({
|
|
277
|
+
x: values.length <= 1 ? width / 2 : pad + i / (values.length - 1) * (width - pad * 2),
|
|
278
|
+
y: height - pad - value / max * (height - pad * 2),
|
|
279
|
+
value,
|
|
280
|
+
label: labels[i] ?? ""
|
|
281
|
+
}));
|
|
282
|
+
const line = points.map((p, i) => `${i === 0 ? "M" : "L"}${p.x.toFixed(1)},${p.y.toFixed(1)}`).join(" ");
|
|
283
|
+
const last = points[points.length - 1];
|
|
284
|
+
const first = points[0];
|
|
285
|
+
const area = `${line} L${last.x.toFixed(1)},${height - pad} L${first.x.toFixed(1)},${height - pad} Z`;
|
|
286
|
+
const gradientId = `icen-chart-area-${++lineGradientSeq}`;
|
|
287
|
+
const wrap = document.createElement("div");
|
|
288
|
+
wrap.className = "chart-area";
|
|
289
|
+
const svg = svgEl("svg", { viewBox: `0 0 ${width} ${height}`, role: "img" });
|
|
290
|
+
const defs = svgEl("defs", {});
|
|
291
|
+
const gradient = svgEl("linearGradient", { id: gradientId, x1: "0", x2: "0", y1: "0", y2: "1" });
|
|
292
|
+
gradient.appendChild(svgEl("stop", { offset: "0%", "stop-color": tone, "stop-opacity": "0.5" }));
|
|
293
|
+
gradient.appendChild(svgEl("stop", { offset: "100%", "stop-color": tone, "stop-opacity": "0.05" }));
|
|
294
|
+
defs.appendChild(gradient);
|
|
295
|
+
svg.appendChild(defs);
|
|
296
|
+
for (let i = 0; i < 4; i++) {
|
|
297
|
+
const y = pad + i * (height - pad * 2) / 3;
|
|
298
|
+
svg.appendChild(svgEl("line", {
|
|
299
|
+
x1: String(pad),
|
|
300
|
+
x2: String(width - pad),
|
|
301
|
+
y1: String(y),
|
|
302
|
+
y2: String(y),
|
|
303
|
+
stroke: "var(--token-line-soft)",
|
|
304
|
+
"stroke-width": "1"
|
|
305
|
+
}));
|
|
306
|
+
}
|
|
307
|
+
svg.appendChild(svgEl("path", { d: area, fill: `url(#${gradientId})` }));
|
|
308
|
+
svg.appendChild(svgEl("path", {
|
|
309
|
+
d: line,
|
|
310
|
+
fill: "none",
|
|
311
|
+
stroke: tone,
|
|
312
|
+
"stroke-width": "1.5",
|
|
313
|
+
"stroke-linecap": "round",
|
|
314
|
+
"stroke-linejoin": "round"
|
|
315
|
+
}));
|
|
316
|
+
for (const p of points) {
|
|
317
|
+
const title = svgEl("title", {});
|
|
318
|
+
title.textContent = `${p.label}: ${fmt(opts, p.value)}`;
|
|
319
|
+
const inv = svgEl("circle", {
|
|
320
|
+
cx: p.x.toFixed(1),
|
|
321
|
+
cy: p.y.toFixed(1),
|
|
322
|
+
r: "2",
|
|
323
|
+
fill: "transparent"
|
|
324
|
+
});
|
|
325
|
+
inv.appendChild(title);
|
|
326
|
+
svg.appendChild(inv);
|
|
327
|
+
}
|
|
328
|
+
wrap.append(svg, labelsRow(labels.slice(-7)));
|
|
329
|
+
el.appendChild(wrap);
|
|
330
|
+
}
|
|
331
|
+
function renderRadar(el, opts) {
|
|
332
|
+
if (typeof document === "undefined") return;
|
|
333
|
+
el.textContent = "";
|
|
334
|
+
const axes = opts.axes;
|
|
335
|
+
const n = axes.length;
|
|
336
|
+
if (n < 3 || opts.series.length === 0) {
|
|
337
|
+
renderEmpty(el, opts.emptyLabel);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
let maxVal = opts.max ?? 0;
|
|
341
|
+
if (opts.max == null) {
|
|
342
|
+
for (const s of opts.series) {
|
|
343
|
+
for (let i = 0; i < n; i++) maxVal = Math.max(maxVal, s.values[i] ?? 0);
|
|
344
|
+
}
|
|
345
|
+
maxVal = niceCeil(maxVal);
|
|
346
|
+
}
|
|
347
|
+
if (maxVal <= 0) maxVal = 1;
|
|
348
|
+
const size = 200;
|
|
349
|
+
const cx = size / 2;
|
|
350
|
+
const cy = size / 2;
|
|
351
|
+
const radius = 78;
|
|
352
|
+
const levels = Math.max(2, opts.levels ?? 4);
|
|
353
|
+
const angleOf = (i) => -Math.PI / 2 + i * 2 * Math.PI / n;
|
|
354
|
+
const pointAt = (i, r) => ({
|
|
355
|
+
x: cx + r * Math.cos(angleOf(i)),
|
|
356
|
+
y: cy + r * Math.sin(angleOf(i))
|
|
357
|
+
});
|
|
358
|
+
const wrap = document.createElement("div");
|
|
359
|
+
wrap.className = "chart-radar";
|
|
360
|
+
const svg = svgEl("svg", { viewBox: `0 0 ${size} ${size}`, role: "img" });
|
|
361
|
+
for (let lv = 1; lv <= levels; lv++) {
|
|
362
|
+
const r = radius * lv / levels;
|
|
363
|
+
const pts = Array.from({ length: n }, (_, i) => {
|
|
364
|
+
const p = pointAt(i, r);
|
|
365
|
+
return `${p.x.toFixed(1)},${p.y.toFixed(1)}`;
|
|
366
|
+
}).join(" ");
|
|
367
|
+
svg.appendChild(svgEl("polygon", {
|
|
368
|
+
points: pts,
|
|
369
|
+
fill: "none",
|
|
370
|
+
stroke: "var(--token-line-soft)",
|
|
371
|
+
"stroke-width": "1",
|
|
372
|
+
"stroke-dasharray": lv === levels ? "0" : "3 2"
|
|
373
|
+
}));
|
|
374
|
+
}
|
|
375
|
+
for (let i = 0; i < n; i++) {
|
|
376
|
+
const edge = pointAt(i, radius);
|
|
377
|
+
svg.appendChild(svgEl("line", {
|
|
378
|
+
x1: String(cx),
|
|
379
|
+
y1: String(cy),
|
|
380
|
+
x2: edge.x.toFixed(1),
|
|
381
|
+
y2: edge.y.toFixed(1),
|
|
382
|
+
stroke: "var(--token-line-soft)",
|
|
383
|
+
"stroke-width": "1"
|
|
384
|
+
}));
|
|
385
|
+
const labelP = pointAt(i, radius + 14);
|
|
386
|
+
const txt = svgEl("text", {
|
|
387
|
+
x: labelP.x.toFixed(1),
|
|
388
|
+
y: labelP.y.toFixed(1),
|
|
389
|
+
"text-anchor": "middle",
|
|
390
|
+
"dominant-baseline": "middle",
|
|
391
|
+
"font-size": "9",
|
|
392
|
+
fill: "var(--token-text-muted)"
|
|
393
|
+
});
|
|
394
|
+
txt.textContent = axes[i] ?? "";
|
|
395
|
+
svg.appendChild(txt);
|
|
396
|
+
}
|
|
397
|
+
const legend = document.createElement("div");
|
|
398
|
+
legend.className = "chart-legend";
|
|
399
|
+
for (const s of opts.series) {
|
|
400
|
+
const tone = toneVar(s.tone);
|
|
401
|
+
const pts = Array.from({ length: n }, (_, i) => {
|
|
402
|
+
const v = Math.max(0, Math.min(maxVal, s.values[i] ?? 0));
|
|
403
|
+
const p = pointAt(i, v / maxVal * radius);
|
|
404
|
+
return `${p.x.toFixed(1)},${p.y.toFixed(1)}`;
|
|
405
|
+
}).join(" ");
|
|
406
|
+
const polyFill = svgEl("polygon", {
|
|
407
|
+
points: pts,
|
|
408
|
+
fill: tone,
|
|
409
|
+
"fill-opacity": "0.16",
|
|
410
|
+
stroke: tone,
|
|
411
|
+
"stroke-width": "2",
|
|
412
|
+
"stroke-linejoin": "round"
|
|
413
|
+
});
|
|
414
|
+
const title = svgEl("title", {});
|
|
415
|
+
title.textContent = `${s.name}: ${s.values.map((v) => fmt(opts, v)).join(" / ")}`;
|
|
416
|
+
polyFill.appendChild(title);
|
|
417
|
+
svg.appendChild(polyFill);
|
|
418
|
+
for (let i = 0; i < n; i++) {
|
|
419
|
+
const v = Math.max(0, Math.min(maxVal, s.values[i] ?? 0));
|
|
420
|
+
const p = pointAt(i, v / maxVal * radius);
|
|
421
|
+
svg.appendChild(svgEl("circle", {
|
|
422
|
+
cx: p.x.toFixed(1),
|
|
423
|
+
cy: p.y.toFixed(1),
|
|
424
|
+
r: "2.5",
|
|
425
|
+
fill: "var(--token-card)",
|
|
426
|
+
stroke: tone,
|
|
427
|
+
"stroke-width": "1.5"
|
|
428
|
+
}));
|
|
429
|
+
}
|
|
430
|
+
legend.appendChild(legendItem(s.name, "", s.tone));
|
|
431
|
+
}
|
|
432
|
+
wrap.append(svg, legend);
|
|
433
|
+
el.appendChild(wrap);
|
|
434
|
+
}
|
|
435
|
+
function niceCeil(v) {
|
|
436
|
+
if (v <= 0) return 1;
|
|
437
|
+
const exp = Math.floor(Math.log10(v));
|
|
438
|
+
const base = Math.pow(10, exp);
|
|
439
|
+
const frac = v / base;
|
|
440
|
+
let nice;
|
|
441
|
+
if (frac <= 1) nice = 1;
|
|
442
|
+
else if (frac <= 2) nice = 2;
|
|
443
|
+
else if (frac <= 5) nice = 5;
|
|
444
|
+
else nice = 10;
|
|
445
|
+
return nice * base;
|
|
446
|
+
}
|
|
261
447
|
function isoDate(d) {
|
|
262
448
|
const m = String(d.getMonth() + 1).padStart(2, "0");
|
|
263
449
|
const day = String(d.getDate()).padStart(2, "0");
|
|
@@ -424,11 +610,13 @@ function renderGauge(el, opts) {
|
|
|
424
610
|
el.appendChild(wrap);
|
|
425
611
|
}
|
|
426
612
|
export {
|
|
613
|
+
renderArea,
|
|
427
614
|
renderDonut,
|
|
428
615
|
renderGauge,
|
|
429
616
|
renderHBar,
|
|
430
617
|
renderHeatmap,
|
|
431
618
|
renderLine,
|
|
619
|
+
renderRadar,
|
|
432
620
|
renderSparkline,
|
|
433
621
|
renderStack,
|
|
434
622
|
renderVBar
|
|
@@ -155,6 +155,8 @@ function svgIcon(d, size = 12) {
|
|
|
155
155
|
var ICON_SEARCH = "M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16Zm10 2-4.3-4.3";
|
|
156
156
|
var ICON_FUNNEL = "M22 3H2l8 9.46V19l4 2v-8.54L22 3Z";
|
|
157
157
|
var ICON_CHEVRON = "m6 9 6 6 6-6";
|
|
158
|
+
var ICON_COLUMNS = "M3 3h7v18H3zM14 3h7v18h-7z";
|
|
159
|
+
var ICON_DOWNLOAD = "M12 3v12m0 0-4-4m4 4 4-4M5 21h14";
|
|
158
160
|
function defaultCompare(a, b) {
|
|
159
161
|
if (typeof a === "number" && typeof b === "number") return a - b;
|
|
160
162
|
return String(a ?? "").localeCompare(String(b ?? ""), "zh-CN", { numeric: true });
|
|
@@ -169,7 +171,11 @@ function noopHandle() {
|
|
|
169
171
|
setFilter: noop,
|
|
170
172
|
setSort: noop,
|
|
171
173
|
refresh: noop,
|
|
172
|
-
destroy: noop
|
|
174
|
+
destroy: noop,
|
|
175
|
+
getData: () => [],
|
|
176
|
+
exportCSV: noop,
|
|
177
|
+
showColumn: noop,
|
|
178
|
+
hideColumn: noop
|
|
173
179
|
};
|
|
174
180
|
}
|
|
175
181
|
function createTable(el, opts) {
|
|
@@ -184,13 +190,18 @@ function createTable(el, opts) {
|
|
|
184
190
|
const filters = /* @__PURE__ */ new Map();
|
|
185
191
|
const selected = /* @__PURE__ */ new Set();
|
|
186
192
|
const expanded = /* @__PURE__ */ new Set();
|
|
193
|
+
const colWidths = /* @__PURE__ */ new Map();
|
|
194
|
+
const colHidden = /* @__PURE__ */ new Map();
|
|
195
|
+
let lastClickedIndex = -1;
|
|
187
196
|
let destroyed = false;
|
|
188
197
|
const virtual = opts.virtual === true;
|
|
189
|
-
const
|
|
198
|
+
const compact = opts.density === "compact";
|
|
199
|
+
const rowH = opts.rowHeight ?? (compact ? 32 : 44);
|
|
190
200
|
const pageSizes = opts.pageSizes ?? [10, 20, 50];
|
|
191
201
|
const selectable = opts.selectable === true;
|
|
192
202
|
const expandable = !virtual ? opts.expandable : void 0;
|
|
193
|
-
const
|
|
203
|
+
const resizable = opts.resizable !== false;
|
|
204
|
+
const cols = () => opts.columns.filter((c) => !(c.hidden || colHidden.get(c.key)));
|
|
194
205
|
const keyOf = (row, index) => {
|
|
195
206
|
if (typeof opts.rowKey === "function") return opts.rowKey(row);
|
|
196
207
|
if (typeof opts.rowKey === "string") return String(row[opts.rowKey]);
|
|
@@ -202,7 +213,12 @@ function createTable(el, opts) {
|
|
|
202
213
|
keyCache = /* @__PURE__ */ new WeakMap();
|
|
203
214
|
data.forEach((r, i) => keyCache.set(r, keyOf(r, i)));
|
|
204
215
|
}
|
|
216
|
+
let pipelineCache = null;
|
|
217
|
+
function invalidate() {
|
|
218
|
+
pipelineCache = null;
|
|
219
|
+
}
|
|
205
220
|
function pipeline() {
|
|
221
|
+
if (pipelineCache) return pipelineCache;
|
|
206
222
|
let rows = data;
|
|
207
223
|
if (search) {
|
|
208
224
|
const q = search.toLowerCase();
|
|
@@ -223,6 +239,7 @@ function createTable(el, opts) {
|
|
|
223
239
|
const cmp = col?.sortFn ? (a, b) => col.sortFn(a, b) : (a, b) => defaultCompare(a[sortKey], b[sortKey]);
|
|
224
240
|
rows = [...rows].sort((a, b) => sortDir === "asc" ? cmp(a, b) : -cmp(a, b));
|
|
225
241
|
}
|
|
242
|
+
pipelineCache = rows;
|
|
226
243
|
return rows;
|
|
227
244
|
}
|
|
228
245
|
el.textContent = "";
|
|
@@ -230,21 +247,55 @@ function createTable(el, opts) {
|
|
|
230
247
|
root.className = "datatable";
|
|
231
248
|
if (opts.striped) root.classList.add("datatable--striped");
|
|
232
249
|
if (virtual) root.classList.add("datatable--virtual");
|
|
250
|
+
if (compact) root.classList.add("datatable--compact");
|
|
251
|
+
if (opts.columns.some((c) => c.frozen)) root.classList.add("datatable--frozen");
|
|
233
252
|
let searchInput = null;
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
253
|
+
const needsToolbar = opts.searchable || opts.columnVisibility || opts.exportable;
|
|
254
|
+
let toolbar = null;
|
|
255
|
+
if (needsToolbar) {
|
|
256
|
+
toolbar = document.createElement("div");
|
|
257
|
+
toolbar.className = "dt-toolbar";
|
|
258
|
+
if (opts.searchable) {
|
|
259
|
+
const box = document.createElement("div");
|
|
260
|
+
box.className = "dt-search";
|
|
261
|
+
box.appendChild(svgIcon(ICON_SEARCH, 14));
|
|
262
|
+
searchInput = document.createElement("input");
|
|
263
|
+
searchInput.className = "dt-search-input";
|
|
264
|
+
searchInput.type = "search";
|
|
265
|
+
searchInput.placeholder = opts.searchPlaceholder ?? "\u641C\u7D22\u2026";
|
|
266
|
+
searchInput.setAttribute("aria-label", "\u641C\u7D22\u8868\u683C");
|
|
267
|
+
box.appendChild(searchInput);
|
|
268
|
+
toolbar.appendChild(box);
|
|
269
|
+
}
|
|
270
|
+
const actions = document.createElement("div");
|
|
271
|
+
actions.className = "dt-toolbar-actions";
|
|
272
|
+
if (opts.columnVisibility) {
|
|
273
|
+
const colBtn = document.createElement("button");
|
|
274
|
+
colBtn.type = "button";
|
|
275
|
+
colBtn.className = "dt-tool-btn";
|
|
276
|
+
colBtn.setAttribute("aria-label", "\u5217\u8BBE\u7F6E");
|
|
277
|
+
colBtn.appendChild(svgIcon(ICON_COLUMNS, 14));
|
|
278
|
+
colBtn.addEventListener("click", (ev) => {
|
|
279
|
+
ev.stopPropagation();
|
|
280
|
+
if (openPanel?.key === "__cols") closeFilterPanel();
|
|
281
|
+
else {
|
|
282
|
+
closeFilterPanel();
|
|
283
|
+
openColumnPanel(colBtn);
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
actions.appendChild(colBtn);
|
|
287
|
+
}
|
|
288
|
+
if (opts.exportable) {
|
|
289
|
+
const expBtn = document.createElement("button");
|
|
290
|
+
expBtn.type = "button";
|
|
291
|
+
expBtn.className = "dt-tool-btn";
|
|
292
|
+
expBtn.setAttribute("aria-label", "\u5BFC\u51FA CSV");
|
|
293
|
+
expBtn.appendChild(svgIcon(ICON_DOWNLOAD, 14));
|
|
294
|
+
expBtn.addEventListener("click", () => exportCSV());
|
|
295
|
+
actions.appendChild(expBtn);
|
|
296
|
+
}
|
|
297
|
+
if (actions.children.length > 0) toolbar.appendChild(actions);
|
|
298
|
+
root.appendChild(toolbar);
|
|
248
299
|
}
|
|
249
300
|
const scroll = document.createElement("div");
|
|
250
301
|
scroll.className = "dt-scroll";
|
|
@@ -269,11 +320,29 @@ function createTable(el, opts) {
|
|
|
269
320
|
foot.className = "dt-foot";
|
|
270
321
|
root.appendChild(foot);
|
|
271
322
|
el.appendChild(root);
|
|
323
|
+
function frozenOffsets() {
|
|
324
|
+
const offsets = /* @__PURE__ */ new Map();
|
|
325
|
+
if (!opts.columns.some((c) => c.frozen)) return offsets;
|
|
326
|
+
const visible = cols();
|
|
327
|
+
let acc = 0;
|
|
328
|
+
if (selectable) acc += 40;
|
|
329
|
+
if (expandable) acc += 34;
|
|
330
|
+
for (const c of visible) {
|
|
331
|
+
if (c.frozen) offsets.set(c.key, acc);
|
|
332
|
+
const w = colWidths.get(c.key) ?? c.width ?? "minmax(0,1fr)";
|
|
333
|
+
acc += parsePx(w);
|
|
334
|
+
}
|
|
335
|
+
return offsets;
|
|
336
|
+
}
|
|
337
|
+
function parsePx(w) {
|
|
338
|
+
const m = /^(\d+(?:\.\d+)?)px/.exec(w);
|
|
339
|
+
return m ? Number(m[1]) : 0;
|
|
340
|
+
}
|
|
272
341
|
function template() {
|
|
273
342
|
const parts = [];
|
|
274
343
|
if (selectable) parts.push("40px");
|
|
275
344
|
if (expandable) parts.push("34px");
|
|
276
|
-
for (const c of cols()) parts.push(c.width ?? "minmax(0,1fr)");
|
|
345
|
+
for (const c of cols()) parts.push(colWidths.get(c.key) ?? c.width ?? "minmax(0,1fr)");
|
|
277
346
|
return parts.join(" ");
|
|
278
347
|
}
|
|
279
348
|
function fillCell(cell, col, row, rowIndex) {
|
|
@@ -355,6 +424,7 @@ function createTable(el, opts) {
|
|
|
355
424
|
const cell = document.createElement("div");
|
|
356
425
|
cell.className = "dt-cell dt-cell--head";
|
|
357
426
|
if (col.align) cell.dataset.align = col.align;
|
|
427
|
+
if (col.frozen) cell.classList.add("dt-cell--frozen");
|
|
358
428
|
const title = document.createElement("span");
|
|
359
429
|
title.className = "dt-head-title";
|
|
360
430
|
title.textContent = col.title;
|
|
@@ -376,10 +446,12 @@ function createTable(el, opts) {
|
|
|
376
446
|
sortDir = "asc";
|
|
377
447
|
} else if (sortDir === "asc") sortDir = "desc";
|
|
378
448
|
else sortKey = null;
|
|
449
|
+
invalidate();
|
|
450
|
+
opts.onSortChange?.(sortKey, sortDir);
|
|
379
451
|
renderAll();
|
|
380
452
|
};
|
|
381
453
|
cell.addEventListener("click", (ev) => {
|
|
382
|
-
if (ev.target.closest(".dt-filter-btn")) return;
|
|
454
|
+
if (ev.target.closest(".dt-filter-btn, .dt-resize")) return;
|
|
383
455
|
toggleSort();
|
|
384
456
|
});
|
|
385
457
|
cell.addEventListener("keydown", (ev) => {
|
|
@@ -407,9 +479,102 @@ function createTable(el, opts) {
|
|
|
407
479
|
});
|
|
408
480
|
cell.appendChild(btn);
|
|
409
481
|
}
|
|
482
|
+
if (resizable && col !== cols()[cols().length - 1]) {
|
|
483
|
+
const handle = document.createElement("div");
|
|
484
|
+
handle.className = "dt-resize";
|
|
485
|
+
handle.setAttribute("role", "separator");
|
|
486
|
+
handle.setAttribute("aria-orientation", "vertical");
|
|
487
|
+
handle.setAttribute("aria-label", `\u8C03\u6574\u300C${col.title}\u300D\u5217\u5BBD`);
|
|
488
|
+
attachResize(handle, col);
|
|
489
|
+
cell.appendChild(handle);
|
|
490
|
+
}
|
|
410
491
|
headRow.appendChild(cell);
|
|
411
492
|
}
|
|
412
493
|
}
|
|
494
|
+
function attachResize(handle, col) {
|
|
495
|
+
handle.addEventListener("pointerdown", (ev) => {
|
|
496
|
+
ev.preventDefault();
|
|
497
|
+
ev.stopPropagation();
|
|
498
|
+
const startX = ev.clientX;
|
|
499
|
+
const startW = handle.closest(".dt-cell").getBoundingClientRect().width;
|
|
500
|
+
handle.setPointerCapture(ev.pointerId);
|
|
501
|
+
const onMove = (e) => {
|
|
502
|
+
const delta = e.clientX - startX;
|
|
503
|
+
const next = Math.max(48, Math.round(startW + delta));
|
|
504
|
+
colWidths.set(col.key, `${next}px`);
|
|
505
|
+
headRow.style.gridTemplateColumns = template();
|
|
506
|
+
body.querySelectorAll(".dt-row").forEach((r) => {
|
|
507
|
+
r.style.gridTemplateColumns = template();
|
|
508
|
+
});
|
|
509
|
+
};
|
|
510
|
+
const onUp = (e) => {
|
|
511
|
+
handle.releasePointerCapture(e.pointerId);
|
|
512
|
+
handle.removeEventListener("pointermove", onMove);
|
|
513
|
+
handle.removeEventListener("pointerup", onUp);
|
|
514
|
+
};
|
|
515
|
+
handle.addEventListener("pointermove", onMove);
|
|
516
|
+
handle.addEventListener("pointerup", onUp);
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
function openColumnPanel(btn) {
|
|
520
|
+
const panel2 = document.createElement("div");
|
|
521
|
+
panel2.className = "dt-filter-panel dt-col-panel";
|
|
522
|
+
panel2.hidden = true;
|
|
523
|
+
panel2.addEventListener("click", (ev) => ev.stopPropagation());
|
|
524
|
+
const list = document.createElement("div");
|
|
525
|
+
list.className = "dt-filter-list";
|
|
526
|
+
for (const col of opts.columns) {
|
|
527
|
+
if (col.key === "__select" || col.key === "__expand") continue;
|
|
528
|
+
const row = document.createElement("label");
|
|
529
|
+
row.className = "dt-filter-item";
|
|
530
|
+
const isHidden = colHidden.get(col.key) ?? col.hidden ?? false;
|
|
531
|
+
const box = makeCheck(!isHidden, col.title);
|
|
532
|
+
box.addEventListener("change", () => {
|
|
533
|
+
if (box.checked) colHidden.delete(col.key);
|
|
534
|
+
else colHidden.set(col.key, true);
|
|
535
|
+
invalidate();
|
|
536
|
+
renderAll();
|
|
537
|
+
});
|
|
538
|
+
const text = document.createElement("span");
|
|
539
|
+
text.textContent = col.title;
|
|
540
|
+
row.append(box, text);
|
|
541
|
+
list.appendChild(row);
|
|
542
|
+
}
|
|
543
|
+
panel2.appendChild(list);
|
|
544
|
+
document.body.appendChild(panel2);
|
|
545
|
+
openPanel = { key: "__cols", el: panel2 };
|
|
546
|
+
const rect = btn.getBoundingClientRect();
|
|
547
|
+
const pr = panel2.getBoundingClientRect();
|
|
548
|
+
let x = rect.right - pr.width;
|
|
549
|
+
let y = rect.bottom + 4;
|
|
550
|
+
if (x < 8) x = 8;
|
|
551
|
+
if (y + pr.height > window.innerHeight - 8) y = rect.top - pr.height - 4;
|
|
552
|
+
panel2.style.left = `${x}px`;
|
|
553
|
+
panel2.style.top = `${y}px`;
|
|
554
|
+
panel2.hidden = false;
|
|
555
|
+
window.addEventListener("scroll", onPanelScroll, true);
|
|
556
|
+
window.addEventListener("resize", onPanelScroll);
|
|
557
|
+
}
|
|
558
|
+
function exportCSV(filename) {
|
|
559
|
+
const rows = pipeline();
|
|
560
|
+
const visible = cols();
|
|
561
|
+
const header = visible.map((c) => csvEscape(c.title)).join(",");
|
|
562
|
+
const lines = rows.map(
|
|
563
|
+
(row) => visible.map((c) => csvEscape(String(row[c.key] ?? ""))).join(",")
|
|
564
|
+
);
|
|
565
|
+
const csv = "\uFEFF" + [header, ...lines].join("\r\n");
|
|
566
|
+
const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
|
|
567
|
+
const url = URL.createObjectURL(blob);
|
|
568
|
+
const a = document.createElement("a");
|
|
569
|
+
a.href = url;
|
|
570
|
+
a.download = filename ?? "export.csv";
|
|
571
|
+
a.click();
|
|
572
|
+
URL.revokeObjectURL(url);
|
|
573
|
+
}
|
|
574
|
+
function csvEscape(s) {
|
|
575
|
+
if (/[",\r\n]/.test(s)) return `"${s.replace(/"/g, '""')}"`;
|
|
576
|
+
return s;
|
|
577
|
+
}
|
|
413
578
|
function buildFilterPanel(col) {
|
|
414
579
|
const panel2 = document.createElement("div");
|
|
415
580
|
panel2.className = "dt-filter-panel";
|
|
@@ -428,6 +593,7 @@ function createTable(el, opts) {
|
|
|
428
593
|
else cur.delete(v);
|
|
429
594
|
if (cur.size === values.length) filters.delete(col.key);
|
|
430
595
|
else filters.set(col.key, cur);
|
|
596
|
+
invalidate();
|
|
431
597
|
page = 1;
|
|
432
598
|
renderAll();
|
|
433
599
|
});
|
|
@@ -444,6 +610,7 @@ function createTable(el, opts) {
|
|
|
444
610
|
reset.textContent = "\u91CD\u7F6E";
|
|
445
611
|
reset.addEventListener("click", () => {
|
|
446
612
|
filters.delete(col.key);
|
|
613
|
+
invalidate();
|
|
447
614
|
page = 1;
|
|
448
615
|
closeFilterPanel();
|
|
449
616
|
renderAll();
|
|
@@ -459,15 +626,40 @@ function createTable(el, opts) {
|
|
|
459
626
|
tr.style.gridTemplateColumns = template();
|
|
460
627
|
if (virtual) tr.style.height = `${rowH}px`;
|
|
461
628
|
if (selected.has(key)) tr.classList.add("is-selected");
|
|
629
|
+
if (opts.rowClassName) {
|
|
630
|
+
const extra = opts.rowClassName(row, rowIndex);
|
|
631
|
+
if (extra) tr.classList.add(...extra.split(/\s+/).filter(Boolean));
|
|
632
|
+
}
|
|
462
633
|
if (selectable) {
|
|
463
634
|
const cell = document.createElement("div");
|
|
464
635
|
cell.className = "dt-cell dt-cell--check";
|
|
636
|
+
if (frozenOffsets().size > 0 && selectable) {
|
|
637
|
+
cell.classList.add("dt-cell--frozen");
|
|
638
|
+
cell.style.left = "0px";
|
|
639
|
+
}
|
|
465
640
|
const box = makeCheck(selected.has(key), "\u9009\u62E9\u8BE5\u884C");
|
|
466
|
-
box.addEventListener("click", (ev) =>
|
|
641
|
+
box.addEventListener("click", (ev) => {
|
|
642
|
+
box._shift = ev.shiftKey;
|
|
643
|
+
ev.stopPropagation();
|
|
644
|
+
});
|
|
467
645
|
box.addEventListener("change", () => {
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
646
|
+
const shift = box._shift === true;
|
|
647
|
+
if (shift && lastClickedIndex >= 0 && lastClickedIndex !== rowIndex) {
|
|
648
|
+
const rows = pipeline();
|
|
649
|
+
const [lo, hi] = lastClickedIndex < rowIndex ? [lastClickedIndex, rowIndex] : [rowIndex, lastClickedIndex];
|
|
650
|
+
const willSelect = box.checked;
|
|
651
|
+
for (let i = lo; i <= hi; i++) {
|
|
652
|
+
const k = keyOfRow(rows[i]);
|
|
653
|
+
if (willSelect) selected.add(k);
|
|
654
|
+
else selected.delete(k);
|
|
655
|
+
}
|
|
656
|
+
renderBody();
|
|
657
|
+
} else {
|
|
658
|
+
if (box.checked) selected.add(key);
|
|
659
|
+
else selected.delete(key);
|
|
660
|
+
tr.classList.toggle("is-selected", box.checked);
|
|
661
|
+
}
|
|
662
|
+
lastClickedIndex = rowIndex;
|
|
471
663
|
emitSelection();
|
|
472
664
|
renderHead();
|
|
473
665
|
renderFoot(pipeline().length);
|
|
@@ -493,10 +685,16 @@ function createTable(el, opts) {
|
|
|
493
685
|
cell.appendChild(btn);
|
|
494
686
|
tr.appendChild(cell);
|
|
495
687
|
}
|
|
688
|
+
const offsets = frozenOffsets();
|
|
496
689
|
for (const col of cols()) {
|
|
497
690
|
const cell = document.createElement("div");
|
|
498
691
|
cell.className = "dt-cell";
|
|
499
692
|
if (col.align) cell.dataset.align = col.align;
|
|
693
|
+
if (col.frozen) {
|
|
694
|
+
cell.classList.add("dt-cell--frozen");
|
|
695
|
+
const off = offsets.get(col.key);
|
|
696
|
+
if (off != null) cell.style.left = `${off}px`;
|
|
697
|
+
}
|
|
500
698
|
fillCell(cell, col, row, rowIndex);
|
|
501
699
|
tr.appendChild(cell);
|
|
502
700
|
}
|
|
@@ -663,6 +861,7 @@ function createTable(el, opts) {
|
|
|
663
861
|
window.clearTimeout(searchTimer);
|
|
664
862
|
searchTimer = window.setTimeout(() => {
|
|
665
863
|
search = searchInput?.value.trim() ?? "";
|
|
864
|
+
invalidate();
|
|
666
865
|
page = 1;
|
|
667
866
|
renderAll();
|
|
668
867
|
}, 160);
|
|
@@ -676,7 +875,7 @@ function createTable(el, opts) {
|
|
|
676
875
|
function onDocPointerDown(ev) {
|
|
677
876
|
if (!openPanel) return;
|
|
678
877
|
const t = ev.target;
|
|
679
|
-
if (t instanceof Node && (openPanel.el.contains(t) || headRow.contains(t))) return;
|
|
878
|
+
if (t instanceof Node && (openPanel.el.contains(t) || headRow.contains(t) || toolbar?.contains(t))) return;
|
|
680
879
|
closeFilterPanel();
|
|
681
880
|
}
|
|
682
881
|
document.addEventListener("pointerdown", onDocPointerDown, true);
|
|
@@ -700,6 +899,7 @@ function createTable(el, opts) {
|
|
|
700
899
|
setData(next) {
|
|
701
900
|
data = next;
|
|
702
901
|
rekey();
|
|
902
|
+
invalidate();
|
|
703
903
|
page = 1;
|
|
704
904
|
renderAll();
|
|
705
905
|
},
|
|
@@ -714,21 +914,41 @@ function createTable(el, opts) {
|
|
|
714
914
|
setSearch(q) {
|
|
715
915
|
search = q;
|
|
716
916
|
if (searchInput) searchInput.value = q;
|
|
917
|
+
invalidate();
|
|
717
918
|
page = 1;
|
|
718
919
|
renderAll();
|
|
719
920
|
},
|
|
720
921
|
setFilter(key, values) {
|
|
721
922
|
if (values === null) filters.delete(key);
|
|
722
923
|
else filters.set(key, new Set(values));
|
|
924
|
+
invalidate();
|
|
723
925
|
page = 1;
|
|
724
926
|
renderAll();
|
|
725
927
|
},
|
|
726
928
|
setSort(key, dir = "asc") {
|
|
727
929
|
sortKey = key;
|
|
728
930
|
sortDir = dir;
|
|
931
|
+
invalidate();
|
|
932
|
+
opts.onSortChange?.(sortKey, sortDir);
|
|
933
|
+
renderAll();
|
|
934
|
+
},
|
|
935
|
+
getData() {
|
|
936
|
+
return pipeline();
|
|
937
|
+
},
|
|
938
|
+
exportCSV,
|
|
939
|
+
showColumn(key) {
|
|
940
|
+
colHidden.delete(key);
|
|
941
|
+
opts.columns.find((c) => c.key === key).hidden = false;
|
|
942
|
+
invalidate();
|
|
943
|
+
renderAll();
|
|
944
|
+
},
|
|
945
|
+
hideColumn(key) {
|
|
946
|
+
colHidden.set(key, true);
|
|
947
|
+
invalidate();
|
|
729
948
|
renderAll();
|
|
730
949
|
},
|
|
731
950
|
refresh() {
|
|
951
|
+
invalidate();
|
|
732
952
|
renderAll();
|
|
733
953
|
},
|
|
734
954
|
destroy() {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* @icen.ai/ui — Component: chart-area(面积图专属样式)
|
|
3
|
+
* 依赖:charts.css(基座)
|
|
4
|
+
*
|
|
5
|
+
* DOM 由 behaviors/charts.ts 的 renderArea 构建:
|
|
6
|
+
* .chart-area > svg[viewBox="0 0 360 150"]
|
|
7
|
+
* 与折线同族,但以面积填充为视觉主体(渐变更浓、描边更细)。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
.chart-area > svg {
|
|
11
|
+
display: block;
|
|
12
|
+
width: 100%;
|
|
13
|
+
height: 150px;
|
|
14
|
+
overflow: visible;
|
|
15
|
+
}
|