dsh-lh-data 0.1.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.
Files changed (89) hide show
  1. package/README.md +332 -0
  2. package/cordis.patch.yml +7 -0
  3. package/lib/admin-contract.d.ts +274 -0
  4. package/lib/admin-http.d.ts +31 -0
  5. package/lib/admin-validate.d.ts +87 -0
  6. package/lib/admin.d.ts +60 -0
  7. package/lib/client.js +2969 -0
  8. package/lib/client.js.map +1 -0
  9. package/lib/datasource/columns.d.ts +13 -0
  10. package/lib/datasource/connection.d.ts +33 -0
  11. package/lib/datasource/connector/base.d.ts +21 -0
  12. package/lib/datasource/connector/mysql.d.ts +23 -0
  13. package/lib/datasource/connector/postgresql.d.ts +23 -0
  14. package/lib/datasource/crypto.d.ts +13 -0
  15. package/lib/datasource/driver.d.ts +39 -0
  16. package/lib/datasource/errors.d.ts +25 -0
  17. package/lib/datasource/importer.d.ts +38 -0
  18. package/lib/datasource/source-sql.d.ts +11 -0
  19. package/lib/datasource/source-store.d.ts +28 -0
  20. package/lib/datasource/types.d.ts +119 -0
  21. package/lib/db.d.ts +91 -0
  22. package/lib/http-common.d.ts +31 -0
  23. package/lib/http.d.ts +13 -0
  24. package/lib/index.d.ts +50 -0
  25. package/lib/index.js +5735 -0
  26. package/lib/parse.d.ts +41 -0
  27. package/lib/preview.d.ts +75 -0
  28. package/lib/render.d.ts +100 -0
  29. package/lib/scope-registry.d.ts +47 -0
  30. package/lib/scope.d.ts +56 -0
  31. package/lib/sql.d.ts +133 -0
  32. package/lib/store.d.ts +174 -0
  33. package/lib/table.d.ts +38 -0
  34. package/lib/tooling.d.ts +267 -0
  35. package/lib/tools/datasource.d.ts +16 -0
  36. package/lib/tools/import.d.ts +21 -0
  37. package/lib/tools/read.d.ts +77 -0
  38. package/lib/tools/registry.d.ts +12 -0
  39. package/lib/tools/write.d.ts +37 -0
  40. package/lib/view.d.ts +138 -0
  41. package/package.json +61 -0
  42. package/src/admin-contract.ts +351 -0
  43. package/src/admin-http.ts +254 -0
  44. package/src/admin-validate.ts +393 -0
  45. package/src/admin.ts +558 -0
  46. package/src/client/index.ts +403 -0
  47. package/src/client/settings/CreateForm.tsx +186 -0
  48. package/src/client/settings/DataSourceForm.tsx +278 -0
  49. package/src/client/settings/DataSourcesPanel.tsx +301 -0
  50. package/src/client/settings/DatasetEditor.tsx +245 -0
  51. package/src/client/settings/DatasetTable.tsx +110 -0
  52. package/src/client/settings/DatasetsPanel.tsx +207 -0
  53. package/src/client/settings/RowsPanel.tsx +196 -0
  54. package/src/client/settings/Section.tsx +41 -0
  55. package/src/client/settings/SourceTablesPanel.tsx +226 -0
  56. package/src/client/settings/api.ts +154 -0
  57. package/src/client/settings/styles.ts +125 -0
  58. package/src/datasource/columns.ts +56 -0
  59. package/src/datasource/connection.ts +124 -0
  60. package/src/datasource/connector/base.ts +54 -0
  61. package/src/datasource/connector/mysql.ts +187 -0
  62. package/src/datasource/connector/postgresql.ts +212 -0
  63. package/src/datasource/crypto.ts +61 -0
  64. package/src/datasource/driver.ts +108 -0
  65. package/src/datasource/errors.ts +69 -0
  66. package/src/datasource/importer.ts +262 -0
  67. package/src/datasource/index.ts +62 -0
  68. package/src/datasource/source-sql.ts +64 -0
  69. package/src/datasource/source-store.ts +152 -0
  70. package/src/datasource/types.ts +133 -0
  71. package/src/db.ts +277 -0
  72. package/src/http-common.ts +91 -0
  73. package/src/http.ts +130 -0
  74. package/src/index.ts +486 -0
  75. package/src/parse.ts +213 -0
  76. package/src/preview.ts +198 -0
  77. package/src/render.ts +294 -0
  78. package/src/scope-registry.ts +111 -0
  79. package/src/scope.ts +159 -0
  80. package/src/sql.ts +491 -0
  81. package/src/store.ts +412 -0
  82. package/src/table.ts +160 -0
  83. package/src/tooling.ts +551 -0
  84. package/src/tools/datasource.ts +378 -0
  85. package/src/tools/import.ts +282 -0
  86. package/src/tools/read.ts +536 -0
  87. package/src/tools/registry.ts +56 -0
  88. package/src/tools/write.ts +241 -0
  89. package/src/view.ts +371 -0
package/lib/client.js ADDED
@@ -0,0 +1,2969 @@
1
+ window.__ModuleLoader__.load({ id: "dsh-lh-data", factory: (require) => {
2
+ "use strict";
3
+ var module = { exports: {} }; var exports = module.exports;
4
+ //#region rolldown:runtime
5
+ var __create = Object.create;
6
+ var __defProp = Object.defineProperty;
7
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
8
+ var __getOwnPropNames = Object.getOwnPropertyNames;
9
+ var __getProtoOf = Object.getPrototypeOf;
10
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
13
+ key = keys[i];
14
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
15
+ get: ((k) => from[k]).bind(null, key),
16
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
17
+ });
18
+ }
19
+ return to;
20
+ };
21
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
22
+ value: mod,
23
+ enumerable: true
24
+ }) : target, mod));
25
+
26
+ //#endregion
27
+ const react = __toESM(require("react"));
28
+ const react_jsx_runtime = __toESM(require("react/jsx-runtime"));
29
+
30
+ //#region src/admin-contract.ts
31
+ const ADMIN_ROUTE_PREFIX = "/api/lh-data";
32
+ const ADMIN_API_BASE = `${ADMIN_ROUTE_PREFIX}/admin`;
33
+ const ADMIN_SECTION_ID = "lh-data";
34
+ const ADMIN_SECTION_ORDER = 100;
35
+ const ADMIN_SECTION_LABEL = "数据集";
36
+ const ADMIN_PAGE_SIZE = 20;
37
+ const ADMIN_ROW_PAGE_SIZE = 20;
38
+ const ADMIN_ROW_MAX_PAGE_SIZE = 100;
39
+ const ADMIN_ROW_ID_COLUMN = "_row_id";
40
+ const ADMIN_MAX_COLUMNS = 64;
41
+ const ADMIN_MAX_SAMPLE_VALUES = 5;
42
+ const ADMIN_SOURCE_TYPES = ["mysql", "postgresql"];
43
+ const ADMIN_SOURCE_TYPE_LABELS = {
44
+ mysql: "MySQL",
45
+ postgresql: "PostgreSQL"
46
+ };
47
+ const ADMIN_SOURCE_DEFAULT_PORTS = {
48
+ mysql: 3306,
49
+ postgresql: 5432
50
+ };
51
+ const ADMIN_COLUMN_TYPE_LABELS = {
52
+ text: "文本",
53
+ numeric: "数值",
54
+ boolean: "布尔",
55
+ date: "日期"
56
+ };
57
+ const ADMIN_COLUMN_TYPES = [
58
+ "text",
59
+ "numeric",
60
+ "boolean",
61
+ "date"
62
+ ];
63
+
64
+ //#endregion
65
+ //#region src/client/settings/api.ts
66
+ var AdminApiError = class extends Error {
67
+ constructor(status, code, message) {
68
+ super(message);
69
+ this.status = status;
70
+ this.code = code;
71
+ this.name = "AdminApiError";
72
+ }
73
+ };
74
+ async function request(path, init) {
75
+ const response = await fetch(`${ADMIN_API_BASE}${path}`, {
76
+ ...init,
77
+ credentials: "same-origin",
78
+ headers: {
79
+ "content-type": "application/json",
80
+ ...init?.headers ?? {}
81
+ }
82
+ });
83
+ if (!response.ok) {
84
+ let code = "UNKNOWN";
85
+ let message = `请求失败(HTTP ${response.status})`;
86
+ try {
87
+ const body = await response.json();
88
+ if (body.error) {
89
+ code = body.error.code ?? code;
90
+ message = body.error.message ?? message;
91
+ }
92
+ } catch {}
93
+ throw new AdminApiError(response.status, code, message);
94
+ }
95
+ return await response.json();
96
+ }
97
+ function listScopes() {
98
+ return request("/scopes");
99
+ }
100
+ function listDatasets(query, page, pageSize) {
101
+ const params = new URLSearchParams();
102
+ if (query.trim().length > 0) params.set("q", query);
103
+ params.set("page", String(page));
104
+ params.set("pageSize", String(pageSize));
105
+ return request(`/datasets?${params.toString()}`);
106
+ }
107
+ function getDataset(id, scope) {
108
+ return request(`/datasets/${encodeURIComponent(id)}?scope=${encodeURIComponent(scope)}`);
109
+ }
110
+ function listDatasetRows(id, scope, page, pageSize) {
111
+ const params = new URLSearchParams();
112
+ params.set("scope", scope);
113
+ params.set("page", String(page));
114
+ params.set("pageSize", String(pageSize));
115
+ return request(`/datasets/${encodeURIComponent(id)}/rows?${params.toString()}`);
116
+ }
117
+ function createDataset(body) {
118
+ return request("/datasets", {
119
+ method: "POST",
120
+ body: JSON.stringify(body)
121
+ });
122
+ }
123
+ function patchDataset(id, scope, body) {
124
+ return request(`/datasets/${encodeURIComponent(id)}?scope=${encodeURIComponent(scope)}`, {
125
+ method: "PATCH",
126
+ body: JSON.stringify(body)
127
+ });
128
+ }
129
+ function deleteDataset(id, scope) {
130
+ return request(`/datasets/${encodeURIComponent(id)}?scope=${encodeURIComponent(scope)}`, { method: "DELETE" });
131
+ }
132
+ function listDataSources() {
133
+ return request("/sources");
134
+ }
135
+ function createDataSource(body) {
136
+ return request("/sources", {
137
+ method: "POST",
138
+ body: JSON.stringify(body)
139
+ });
140
+ }
141
+ function patchDataSource(id, body) {
142
+ return request(`/sources/${encodeURIComponent(id)}`, {
143
+ method: "PATCH",
144
+ body: JSON.stringify(body)
145
+ });
146
+ }
147
+ function deleteDataSource(id) {
148
+ return request(`/sources/${encodeURIComponent(id)}`, { method: "DELETE" });
149
+ }
150
+ function testDataSource(body) {
151
+ return request("/sources/test", {
152
+ method: "POST",
153
+ body: JSON.stringify(body)
154
+ });
155
+ }
156
+ function listSourceTables(id, schema, query) {
157
+ const params = new URLSearchParams();
158
+ if (schema !== undefined && schema.trim().length > 0) params.set("schema", schema);
159
+ if (query !== undefined && query.trim().length > 0) params.set("q", query);
160
+ const suffix = params.toString().length > 0 ? `?${params.toString()}` : "";
161
+ return request(`/sources/${encodeURIComponent(id)}/tables${suffix}`);
162
+ }
163
+ function importSourceTable(id, body) {
164
+ return request(`/sources/${encodeURIComponent(id)}/import`, {
165
+ method: "POST",
166
+ body: JSON.stringify(body)
167
+ });
168
+ }
169
+
170
+ //#endregion
171
+ //#region src/client/settings/styles.ts
172
+ const c = {
173
+ bg: "var(--dsw-bg, #ffffff)",
174
+ subtle: "var(--dsw-bg-subtle, #fafafa)",
175
+ fg: "var(--dsw-fg, #1f2328)",
176
+ fg2: "var(--dsw-fg-secondary, #656d76)",
177
+ border: "var(--dsw-border, #e5e5e5)",
178
+ primary: "var(--dsw-primary, #2f6feb)",
179
+ danger: "var(--dsw-danger, #c0392b)",
180
+ ok: "var(--dsw-ok, #1a7f37)",
181
+ warn: "var(--dsw-warn, #9a6700)"
182
+ };
183
+ const s = {
184
+ page: {
185
+ fontSize: 12,
186
+ color: c.fg,
187
+ lineHeight: 1.6
188
+ },
189
+ toolbar: {
190
+ display: "flex",
191
+ alignItems: "center",
192
+ gap: 8,
193
+ marginBottom: 10,
194
+ paddingBottom: 10,
195
+ borderBottom: `1px solid ${c.border}`
196
+ },
197
+ search: {
198
+ flex: 1,
199
+ minWidth: 0,
200
+ padding: "5px 8px",
201
+ fontSize: 12,
202
+ border: `1px solid ${c.border}`,
203
+ borderRadius: 6,
204
+ background: c.bg,
205
+ color: c.fg,
206
+ outline: "none"
207
+ },
208
+ button: {
209
+ border: `1px solid ${c.border}`,
210
+ background: c.bg,
211
+ color: c.fg,
212
+ borderRadius: 6,
213
+ padding: "5px 10px",
214
+ cursor: "pointer",
215
+ fontSize: 12,
216
+ whiteSpace: "nowrap"
217
+ },
218
+ primaryButton: {
219
+ border: `1px solid ${c.primary}`,
220
+ background: c.primary,
221
+ color: "#fff",
222
+ borderRadius: 6,
223
+ padding: "5px 12px",
224
+ cursor: "pointer",
225
+ fontSize: 12,
226
+ whiteSpace: "nowrap"
227
+ },
228
+ dangerButton: {
229
+ border: `1px solid ${c.danger}`,
230
+ background: "transparent",
231
+ color: c.danger,
232
+ borderRadius: 6,
233
+ padding: "5px 10px",
234
+ cursor: "pointer",
235
+ fontSize: 12
236
+ },
237
+ disabledButton: {
238
+ opacity: .5,
239
+ cursor: "not-allowed"
240
+ },
241
+ banner: {
242
+ marginBottom: 10,
243
+ padding: "6px 10px",
244
+ borderRadius: 6,
245
+ border: `1px solid ${c.danger}`,
246
+ color: c.danger,
247
+ fontSize: 12,
248
+ display: "flex",
249
+ alignItems: "center",
250
+ gap: 8
251
+ },
252
+ scroll: {
253
+ maxHeight: 360,
254
+ overflow: "auto",
255
+ border: `1px solid ${c.border}`,
256
+ borderRadius: 6
257
+ },
258
+ table: {
259
+ borderCollapse: "collapse",
260
+ width: "100%",
261
+ fontSize: 12
262
+ },
263
+ th: {
264
+ position: "sticky",
265
+ top: 0,
266
+ background: c.subtle,
267
+ textAlign: "left",
268
+ padding: "6px 8px",
269
+ borderBottom: `1px solid ${c.border}`,
270
+ color: c.fg2,
271
+ fontWeight: 600,
272
+ whiteSpace: "nowrap"
273
+ },
274
+ td: {
275
+ padding: "5px 8px",
276
+ borderBottom: `1px solid ${c.border}`,
277
+ maxWidth: 280,
278
+ overflow: "hidden",
279
+ textOverflow: "ellipsis",
280
+ whiteSpace: "nowrap"
281
+ },
282
+ row: { cursor: "pointer" },
283
+ selectedRow: { boxShadow: `inset 2px 0 0 ${c.primary}` },
284
+ rowHover: { background: c.subtle },
285
+ dot: {
286
+ display: "inline-block",
287
+ width: 7,
288
+ height: 7,
289
+ borderRadius: "50%",
290
+ marginRight: 5
291
+ },
292
+ panel: {
293
+ marginTop: 12,
294
+ padding: 12,
295
+ border: `1px solid ${c.border}`,
296
+ borderRadius: 6,
297
+ background: c.subtle
298
+ },
299
+ field: { marginBottom: 10 },
300
+ label: {
301
+ display: "block",
302
+ fontSize: 12,
303
+ color: c.fg2,
304
+ marginBottom: 4
305
+ },
306
+ input: {
307
+ width: "100%",
308
+ boxSizing: "border-box",
309
+ padding: "5px 8px",
310
+ fontSize: 12,
311
+ border: `1px solid ${c.border}`,
312
+ borderRadius: 6,
313
+ background: c.bg,
314
+ color: c.fg,
315
+ outline: "none"
316
+ },
317
+ select: {
318
+ padding: "4px 6px",
319
+ fontSize: 12,
320
+ border: `1px solid ${c.border}`,
321
+ borderRadius: 6,
322
+ background: c.bg,
323
+ color: c.fg,
324
+ outline: "none"
325
+ },
326
+ textarea: {
327
+ width: "100%",
328
+ boxSizing: "border-box",
329
+ padding: "5px 8px",
330
+ fontSize: 12,
331
+ border: `1px solid ${c.border}`,
332
+ borderRadius: 6,
333
+ background: c.bg,
334
+ color: c.fg,
335
+ outline: "none",
336
+ resize: "vertical",
337
+ minHeight: 54,
338
+ fontFamily: "inherit"
339
+ },
340
+ grid2: {
341
+ display: "grid",
342
+ gridTemplateColumns: "1fr 1fr",
343
+ gap: 10
344
+ },
345
+ grid3: {
346
+ display: "grid",
347
+ gridTemplateColumns: "1fr 1fr 1fr",
348
+ gap: 8,
349
+ alignItems: "end"
350
+ },
351
+ head: {
352
+ display: "flex",
353
+ justifyContent: "space-between",
354
+ alignItems: "center",
355
+ marginBottom: 10
356
+ },
357
+ bar: {
358
+ display: "flex",
359
+ alignItems: "center",
360
+ gap: 8,
361
+ marginTop: 8,
362
+ flexWrap: "wrap"
363
+ },
364
+ muted: { color: c.fg2 },
365
+ link: {
366
+ color: c.primary,
367
+ cursor: "pointer"
368
+ },
369
+ status: { fontWeight: 600 },
370
+ tabs: {
371
+ display: "flex",
372
+ gap: 18,
373
+ marginBottom: 12,
374
+ borderBottom: `1px solid ${c.border}`
375
+ },
376
+ tab: {
377
+ padding: "0 0 6px",
378
+ fontSize: 13,
379
+ fontWeight: 600,
380
+ color: c.fg2,
381
+ background: "none",
382
+ border: "none",
383
+ borderBottom: "2px solid transparent",
384
+ cursor: "pointer",
385
+ transition: "color 120ms ease, border-color 120ms ease"
386
+ },
387
+ activeTab: {
388
+ color: c.fg,
389
+ borderBottom: `2px solid ${c.primary}`
390
+ }
391
+ };
392
+ const STATUS_COLOR = {
393
+ ready: c.ok,
394
+ importing: c.warn,
395
+ failed: c.danger
396
+ };
397
+ const SOURCE_STATUS_COLOR = {
398
+ connected: c.ok,
399
+ error: c.danger,
400
+ unknown: c.fg2
401
+ };
402
+ const SOURCE_STATUS_TEXT = {
403
+ connected: "已连通",
404
+ error: "连接失败",
405
+ unknown: "未检测"
406
+ };
407
+ const TYPE_BADGE = {
408
+ mysql: c.primary,
409
+ postgresql: c.ok
410
+ };
411
+
412
+ //#endregion
413
+ //#region src/client/settings/DatasetTable.tsx
414
+ const STATUS_TEXT = {
415
+ ready: "就绪",
416
+ importing: "导入中",
417
+ failed: "失败"
418
+ };
419
+ function fmtAgo(ts) {
420
+ if (!Number.isFinite(ts) || ts <= 0) return "—";
421
+ const diff = Date.now() - ts;
422
+ if (diff < 6e4) return "刚刚";
423
+ if (diff < 36e5) return `${Math.floor(diff / 6e4)} 分钟前`;
424
+ if (diff < 864e5) return `${Math.floor(diff / 36e5)} 小时前`;
425
+ return `${Math.floor(diff / 864e5)} 天前`;
426
+ }
427
+ function ellipsis(value, max = 32) {
428
+ if (value === null) return "—";
429
+ return value.length > max ? `${value.slice(0, max)}…` : value;
430
+ }
431
+ function DatasetTable(props) {
432
+ const { items, page, pageSize, total, selectedId, loading, onSelect, onPageChange } = props;
433
+ const totalPages = Math.max(1, Math.ceil(total / pageSize));
434
+ const rows = items.length === 0 && !loading ? (0, react_jsx_runtime.jsx)("tr", { children: (0, react_jsx_runtime.jsx)("td", {
435
+ style: {
436
+ ...s.td,
437
+ color: c.fg2,
438
+ padding: "18px 8px",
439
+ textAlign: "center"
440
+ },
441
+ colSpan: 7,
442
+ children: "还没有数据集,用 agent 的 dataset_import 导入,或直接点击「新建数据集」"
443
+ }) }) : items.map((item) => (0, react_jsx_runtime.jsxs)("tr", {
444
+ style: {
445
+ ...s.row,
446
+ ...item.id === selectedId ? s.selectedRow : null
447
+ },
448
+ onClick: () => onSelect(item),
449
+ children: [
450
+ (0, react_jsx_runtime.jsx)("td", {
451
+ style: s.td,
452
+ title: item.name,
453
+ children: ellipsis(item.name, 40)
454
+ }),
455
+ (0, react_jsx_runtime.jsx)("td", {
456
+ style: s.td,
457
+ title: item.scopeKey,
458
+ children: ellipsis(item.scopeKey, 36)
459
+ }),
460
+ (0, react_jsx_runtime.jsx)("td", {
461
+ style: s.td,
462
+ children: item.rowCount
463
+ }),
464
+ (0, react_jsx_runtime.jsx)("td", {
465
+ style: s.td,
466
+ children: item.columnCount
467
+ }),
468
+ (0, react_jsx_runtime.jsxs)("td", {
469
+ style: s.td,
470
+ children: [(0, react_jsx_runtime.jsx)("span", { style: {
471
+ ...s.dot,
472
+ background: STATUS_COLOR[item.status] ?? c.fg2
473
+ } }), STATUS_TEXT[item.status] ?? item.status]
474
+ }),
475
+ (0, react_jsx_runtime.jsx)("td", {
476
+ style: s.td,
477
+ title: item.sourcePath ?? "",
478
+ children: ellipsis(item.sourcePath, 40)
479
+ }),
480
+ (0, react_jsx_runtime.jsx)("td", {
481
+ style: {
482
+ ...s.td,
483
+ color: c.fg2
484
+ },
485
+ children: fmtAgo(item.updatedAt)
486
+ })
487
+ ]
488
+ }, `${item.scopeKey}:${item.id}`));
489
+ return (0, react_jsx_runtime.jsxs)("div", { children: [(0, react_jsx_runtime.jsx)("div", {
490
+ style: s.scroll,
491
+ children: (0, react_jsx_runtime.jsxs)("table", {
492
+ style: s.table,
493
+ children: [(0, react_jsx_runtime.jsx)("thead", { children: (0, react_jsx_runtime.jsxs)("tr", { children: [
494
+ (0, react_jsx_runtime.jsx)("th", {
495
+ style: s.th,
496
+ children: "名称"
497
+ }),
498
+ (0, react_jsx_runtime.jsx)("th", {
499
+ style: s.th,
500
+ children: "工作区"
501
+ }),
502
+ (0, react_jsx_runtime.jsx)("th", {
503
+ style: s.th,
504
+ children: "行数"
505
+ }),
506
+ (0, react_jsx_runtime.jsx)("th", {
507
+ style: s.th,
508
+ children: "列数"
509
+ }),
510
+ (0, react_jsx_runtime.jsx)("th", {
511
+ style: s.th,
512
+ children: "状态"
513
+ }),
514
+ (0, react_jsx_runtime.jsx)("th", {
515
+ style: s.th,
516
+ children: "来源"
517
+ }),
518
+ (0, react_jsx_runtime.jsx)("th", {
519
+ style: s.th,
520
+ children: "更新时间"
521
+ })
522
+ ] }) }), (0, react_jsx_runtime.jsx)("tbody", { children: rows })]
523
+ })
524
+ }), (0, react_jsx_runtime.jsxs)("div", {
525
+ style: {
526
+ ...s.bar,
527
+ display: "flex",
528
+ alignItems: "center",
529
+ gap: 8,
530
+ marginTop: 8
531
+ },
532
+ children: [
533
+ (0, react_jsx_runtime.jsx)("button", {
534
+ type: "button",
535
+ style: {
536
+ ...s.button,
537
+ ...page <= 1 ? s.disabledButton : null
538
+ },
539
+ disabled: page <= 1,
540
+ onClick: () => onPageChange(1),
541
+ children: "« 首页"
542
+ }),
543
+ (0, react_jsx_runtime.jsx)("button", {
544
+ type: "button",
545
+ style: {
546
+ ...s.button,
547
+ ...page <= 1 ? s.disabledButton : null
548
+ },
549
+ disabled: page <= 1,
550
+ onClick: () => onPageChange(page - 1),
551
+ children: "上一页"
552
+ }),
553
+ (0, react_jsx_runtime.jsxs)("span", { children: [
554
+ "第 ",
555
+ Math.min(page, totalPages),
556
+ " / ",
557
+ totalPages,
558
+ " 页 · 共 ",
559
+ total,
560
+ " 个"
561
+ ] }),
562
+ (0, react_jsx_runtime.jsx)("button", {
563
+ type: "button",
564
+ style: {
565
+ ...s.button,
566
+ ...page >= totalPages ? s.disabledButton : null
567
+ },
568
+ disabled: page >= totalPages,
569
+ onClick: () => onPageChange(page + 1),
570
+ children: "下一页"
571
+ }),
572
+ loading ? (0, react_jsx_runtime.jsx)("span", {
573
+ style: s.muted,
574
+ children: "加载中…"
575
+ }) : null
576
+ ]
577
+ })] });
578
+ }
579
+
580
+ //#endregion
581
+ //#region src/client/settings/DatasetEditor.tsx
582
+ /** 样例分隔符:展示用全角顿号,输入同时接受半角与全角逗号。 */
583
+ const SAMPLE_SEPARATORS = /[、,,]/;
584
+ /** 空值的展示记号,与 `sampleToText` 互逆。 */
585
+ const NULL_MARK = "∅";
586
+ function sampleToText(sample) {
587
+ if (!Array.isArray(sample)) return "";
588
+ return sample.slice(0, ADMIN_MAX_SAMPLE_VALUES).map((value) => value === null ? NULL_MARK : String(value)).join("、");
589
+ }
590
+ function textToSample(text) {
591
+ return text.split(SAMPLE_SEPARATORS).map((value) => value.trim()).filter((value) => value.length > 0).map((value) => value === NULL_MARK ? null : value);
592
+ }
593
+ function initialDrafts(columns) {
594
+ const drafts = {};
595
+ for (const column of columns) drafts[column.sanitizedName] = {
596
+ description: column.description ?? "",
597
+ sampleText: sampleToText(column.sample)
598
+ };
599
+ return drafts;
600
+ }
601
+ function DatasetEditor(props) {
602
+ const { detail, writable, onChanged, onDeleted, onError } = props;
603
+ const [name$1, setName] = (0, react.useState)(detail.name);
604
+ const [description, setDescription] = (0, react.useState)(detail.description ?? "");
605
+ const [sourcePath, setSourcePath] = (0, react.useState)(detail.sourcePath ?? "");
606
+ const [columnDrafts, setColumnDrafts] = (0, react.useState)(() => initialDrafts(detail.columns));
607
+ const [saving, setSaving] = (0, react.useState)(false);
608
+ const [deleting, setDeleting] = (0, react.useState)(false);
609
+ const [confirming, setConfirming] = (0, react.useState)(false);
610
+ function updateColumnDraft(key, patch) {
611
+ setColumnDrafts((previous) => ({
612
+ ...previous,
613
+ [key]: {
614
+ ...previous[key],
615
+ ...patch
616
+ }
617
+ }));
618
+ }
619
+ const columnDirty = detail.columns.some((column) => {
620
+ const draft = columnDrafts[column.sanitizedName];
621
+ if (draft === undefined) return false;
622
+ return draft.description !== (column.description ?? "") || draft.sampleText !== sampleToText(column.sample);
623
+ });
624
+ const dirty = name$1 !== detail.name || description !== (detail.description ?? "") || sourcePath !== (detail.sourcePath ?? "") || columnDirty;
625
+ /** 只提交改动的列;未改动的列连同样例一并原样保留,不参与往返。 */
626
+ function buildColumnPatches() {
627
+ const patches = [];
628
+ for (const column of detail.columns) {
629
+ const draft = columnDrafts[column.sanitizedName];
630
+ if (draft === undefined) continue;
631
+ const patch = { sanitizedName: column.sanitizedName };
632
+ let changed = false;
633
+ if (draft.description !== (column.description ?? "")) {
634
+ patch.description = draft.description;
635
+ changed = true;
636
+ }
637
+ if (draft.sampleText !== sampleToText(column.sample)) {
638
+ patch.sample = textToSample(draft.sampleText);
639
+ changed = true;
640
+ }
641
+ if (changed) patches.push(patch);
642
+ }
643
+ return patches;
644
+ }
645
+ async function handleSave() {
646
+ if (!dirty || saving) return;
647
+ const columnPatches = buildColumnPatches();
648
+ const body = {
649
+ name: name$1,
650
+ description,
651
+ sourcePath
652
+ };
653
+ if (columnPatches.length > 0) body.columns = columnPatches;
654
+ setSaving(true);
655
+ try {
656
+ const updated = await patchDataset(detail.id, detail.scopeKey, body);
657
+ onChanged(updated);
658
+ } catch (error) {
659
+ onError(error instanceof Error ? error : new Error("保存失败"));
660
+ } finally {
661
+ setSaving(false);
662
+ }
663
+ }
664
+ async function handleDelete() {
665
+ if (deleting) return;
666
+ setDeleting(true);
667
+ try {
668
+ await deleteDataset(detail.id, detail.scopeKey);
669
+ onDeleted(detail.id);
670
+ } catch (error) {
671
+ onError(error instanceof Error ? error : new Error("删除失败"));
672
+ setConfirming(false);
673
+ } finally {
674
+ setDeleting(false);
675
+ }
676
+ }
677
+ const disabled = !writable || saving;
678
+ return (0, react_jsx_runtime.jsxs)("div", {
679
+ style: s.panel,
680
+ children: [
681
+ (0, react_jsx_runtime.jsxs)("div", {
682
+ style: {
683
+ ...s.head,
684
+ display: "flex",
685
+ justifyContent: "space-between",
686
+ marginBottom: 10
687
+ },
688
+ children: [(0, react_jsx_runtime.jsx)("strong", {
689
+ style: { fontSize: 13 },
690
+ children: "数据集详情"
691
+ }), (0, react_jsx_runtime.jsxs)("span", {
692
+ style: s.muted,
693
+ children: ["工作区:", detail.scopeKey]
694
+ })]
695
+ }),
696
+ (0, react_jsx_runtime.jsxs)("div", {
697
+ style: s.grid2,
698
+ children: [(0, react_jsx_runtime.jsxs)("div", {
699
+ style: s.field,
700
+ children: [(0, react_jsx_runtime.jsx)("label", {
701
+ style: s.label,
702
+ children: "名称"
703
+ }), (0, react_jsx_runtime.jsx)("input", {
704
+ style: s.input,
705
+ value: name$1,
706
+ disabled,
707
+ onChange: (e) => setName(e.target.value)
708
+ })]
709
+ }), (0, react_jsx_runtime.jsxs)("div", {
710
+ style: s.field,
711
+ children: [(0, react_jsx_runtime.jsx)("label", {
712
+ style: s.label,
713
+ children: "来源路径"
714
+ }), (0, react_jsx_runtime.jsx)("input", {
715
+ style: s.input,
716
+ value: sourcePath,
717
+ disabled,
718
+ onChange: (e) => setSourcePath(e.target.value)
719
+ })]
720
+ })]
721
+ }),
722
+ (0, react_jsx_runtime.jsxs)("div", {
723
+ style: s.field,
724
+ children: [(0, react_jsx_runtime.jsx)("label", {
725
+ style: s.label,
726
+ children: "描述"
727
+ }), (0, react_jsx_runtime.jsx)("textarea", {
728
+ style: s.textarea,
729
+ value: description,
730
+ disabled,
731
+ onChange: (e) => setDescription(e.target.value)
732
+ })]
733
+ }),
734
+ (0, react_jsx_runtime.jsxs)("div", {
735
+ style: {
736
+ display: "flex",
737
+ gap: 8,
738
+ alignItems: "center"
739
+ },
740
+ children: [
741
+ (0, react_jsx_runtime.jsx)("button", {
742
+ type: "button",
743
+ style: {
744
+ ...s.primaryButton,
745
+ ...disabled || !dirty ? s.disabledButton : null
746
+ },
747
+ disabled: disabled || !dirty,
748
+ onClick: () => void handleSave(),
749
+ children: saving ? "保存中…" : "保存修改"
750
+ }),
751
+ !confirming ? (0, react_jsx_runtime.jsx)("button", {
752
+ type: "button",
753
+ style: {
754
+ ...s.dangerButton,
755
+ ...!writable ? s.disabledButton : null
756
+ },
757
+ disabled: !writable,
758
+ onClick: () => setConfirming(true),
759
+ children: "删除数据集"
760
+ }) : (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
761
+ (0, react_jsx_runtime.jsxs)("span", {
762
+ style: { color: c.danger },
763
+ children: [
764
+ "确认删除「",
765
+ detail.name,
766
+ "」及其全部数据?"
767
+ ]
768
+ }),
769
+ (0, react_jsx_runtime.jsx)("button", {
770
+ type: "button",
771
+ style: {
772
+ ...s.dangerButton,
773
+ ...deleting ? s.disabledButton : null
774
+ },
775
+ disabled: deleting,
776
+ onClick: () => void handleDelete(),
777
+ children: deleting ? "删除中…" : "确认删除"
778
+ }),
779
+ (0, react_jsx_runtime.jsx)("button", {
780
+ type: "button",
781
+ style: s.button,
782
+ disabled: deleting,
783
+ onClick: () => setConfirming(false),
784
+ children: "取消"
785
+ })
786
+ ] }),
787
+ !writable ? (0, react_jsx_runtime.jsx)("span", {
788
+ style: s.muted,
789
+ children: "(只读模式或接口已关闭,不可修改)"
790
+ }) : null
791
+ ]
792
+ }),
793
+ (0, react_jsx_runtime.jsxs)("div", {
794
+ style: { marginTop: 14 },
795
+ children: [(0, react_jsx_runtime.jsxs)("div", {
796
+ style: {
797
+ ...s.label,
798
+ marginBottom: 6
799
+ },
800
+ children: [
801
+ "列结构 · 共 ",
802
+ detail.columns.length,
803
+ " 列 (列名 / 类型 / 可空性由导入时推断,不可修改;说明与样例可编辑)"
804
+ ]
805
+ }), (0, react_jsx_runtime.jsx)("div", {
806
+ style: {
807
+ ...s.scroll,
808
+ maxHeight: 240
809
+ },
810
+ children: (0, react_jsx_runtime.jsxs)("table", {
811
+ style: s.table,
812
+ children: [(0, react_jsx_runtime.jsx)("thead", { children: (0, react_jsx_runtime.jsxs)("tr", { children: [
813
+ (0, react_jsx_runtime.jsx)("th", {
814
+ style: s.th,
815
+ children: "列名"
816
+ }),
817
+ (0, react_jsx_runtime.jsx)("th", {
818
+ style: s.th,
819
+ children: "类型"
820
+ }),
821
+ (0, react_jsx_runtime.jsx)("th", {
822
+ style: s.th,
823
+ children: "可空"
824
+ }),
825
+ (0, react_jsx_runtime.jsx)("th", {
826
+ style: s.th,
827
+ children: "说明"
828
+ }),
829
+ (0, react_jsx_runtime.jsx)("th", {
830
+ style: s.th,
831
+ children: "样例"
832
+ })
833
+ ] }) }), (0, react_jsx_runtime.jsx)("tbody", { children: detail.columns.map((column) => (0, react_jsx_runtime.jsxs)("tr", { children: [
834
+ (0, react_jsx_runtime.jsx)("td", {
835
+ style: s.td,
836
+ title: column.name,
837
+ children: column.name
838
+ }),
839
+ (0, react_jsx_runtime.jsx)("td", {
840
+ style: s.td,
841
+ children: ADMIN_COLUMN_TYPE_LABELS[column.type] ?? column.type
842
+ }),
843
+ (0, react_jsx_runtime.jsx)("td", {
844
+ style: s.td,
845
+ children: column.nullable ? "是" : "否"
846
+ }),
847
+ (0, react_jsx_runtime.jsx)("td", {
848
+ style: {
849
+ ...s.td,
850
+ overflow: "visible"
851
+ },
852
+ children: (0, react_jsx_runtime.jsx)("input", {
853
+ style: s.input,
854
+ value: columnDrafts[column.sanitizedName]?.description ?? "",
855
+ disabled,
856
+ placeholder: "补充说明,便于 agent 理解该列",
857
+ onChange: (event) => updateColumnDraft(column.sanitizedName, { description: event.target.value })
858
+ })
859
+ }),
860
+ (0, react_jsx_runtime.jsx)("td", {
861
+ style: {
862
+ ...s.td,
863
+ overflow: "visible"
864
+ },
865
+ children: (0, react_jsx_runtime.jsx)("input", {
866
+ style: s.input,
867
+ value: columnDrafts[column.sanitizedName]?.sampleText ?? "",
868
+ disabled,
869
+ placeholder: "多个值用 、 分隔",
870
+ onChange: (event) => updateColumnDraft(column.sanitizedName, { sampleText: event.target.value })
871
+ })
872
+ })
873
+ ] }, column.sanitizedName)) })]
874
+ })
875
+ })]
876
+ })
877
+ ]
878
+ });
879
+ }
880
+
881
+ //#endregion
882
+ //#region src/client/settings/RowsPanel.tsx
883
+ /** 每页条数可选项:默认 20,最大取契约上限。 */
884
+ const PAGE_SIZE_OPTIONS = [...new Set([
885
+ ADMIN_ROW_PAGE_SIZE,
886
+ 50,
887
+ ADMIN_ROW_MAX_PAGE_SIZE
888
+ ])].sort((a, b) => a - b);
889
+ /** 单个单元格的展示字符上限(超长省略,完整值放 title)。 */
890
+ const CELL_MAX_LENGTH = 80;
891
+ function cellText(value) {
892
+ if (typeof value === "string") return value;
893
+ if (typeof value === "object" && value !== null) try {
894
+ return JSON.stringify(value);
895
+ } catch {
896
+ return "[object]";
897
+ }
898
+ return String(value);
899
+ }
900
+ function Cell(props) {
901
+ const { value } = props;
902
+ if (value === null || value === undefined) return (0, react_jsx_runtime.jsx)("span", {
903
+ style: { color: c.fg2 },
904
+ children: "—"
905
+ });
906
+ const text = cellText(value);
907
+ return (0, react_jsx_runtime.jsx)("span", {
908
+ title: text,
909
+ children: text.length > CELL_MAX_LENGTH ? `${text.slice(0, CELL_MAX_LENGTH)}…` : text
910
+ });
911
+ }
912
+ function RowsPanel(props) {
913
+ const { datasetId, scopeKey, recordedRows, status } = props;
914
+ const [data, setData] = (0, react.useState)(null);
915
+ const [page, setPage] = (0, react.useState)(1);
916
+ const [pageSize, setPageSize] = (0, react.useState)(ADMIN_ROW_PAGE_SIZE);
917
+ const [loading, setLoading] = (0, react.useState)(false);
918
+ const [error, setError] = (0, react.useState)(null);
919
+ const requestId = (0, react.useRef)(0);
920
+ const load = (0, react.useCallback)(async (target, size) => {
921
+ const id = requestId.current + 1;
922
+ requestId.current = id;
923
+ setLoading(true);
924
+ setError(null);
925
+ try {
926
+ const result = await listDatasetRows(datasetId, scopeKey, target, size);
927
+ if (id !== requestId.current) return;
928
+ setData(result);
929
+ setPage(result.page);
930
+ } catch (failure) {
931
+ if (id !== requestId.current) return;
932
+ setError(failure instanceof Error ? failure.message : "加载数据失败");
933
+ } finally {
934
+ if (id === requestId.current) setLoading(false);
935
+ }
936
+ }, [datasetId, scopeKey]);
937
+ (0, react.useEffect)(() => {
938
+ setData(null);
939
+ void load(1, pageSize);
940
+ }, [load, pageSize]);
941
+ const total = data?.total ?? 0;
942
+ const totalPages = data?.totalPages ?? 1;
943
+ const columns = data?.columns ?? [];
944
+ const rows = data?.rows ?? [];
945
+ return (0, react_jsx_runtime.jsxs)("div", {
946
+ style: s.panel,
947
+ children: [
948
+ (0, react_jsx_runtime.jsxs)("div", {
949
+ style: {
950
+ ...s.head,
951
+ display: "flex",
952
+ justifyContent: "space-between",
953
+ marginBottom: 10
954
+ },
955
+ children: [(0, react_jsx_runtime.jsx)("strong", {
956
+ style: { fontSize: 13 },
957
+ children: "表数据"
958
+ }), (0, react_jsx_runtime.jsx)("span", {
959
+ style: s.muted,
960
+ children: loading ? "加载中…" : `共 ${total} 行`
961
+ })]
962
+ }),
963
+ status !== "ready" ? (0, react_jsx_runtime.jsxs)("div", {
964
+ style: {
965
+ ...s.muted,
966
+ marginBottom: 8
967
+ },
968
+ children: [
969
+ "数据集当前状态为 ",
970
+ status,
971
+ ",数据可能不完整。"
972
+ ]
973
+ }) : null,
974
+ error !== null ? (0, react_jsx_runtime.jsx)("div", {
975
+ style: {
976
+ ...s.banner,
977
+ marginTop: 0
978
+ },
979
+ children: (0, react_jsx_runtime.jsx)("span", { children: error })
980
+ }) : null,
981
+ recordedRows !== total && error === null && data !== null ? (0, react_jsx_runtime.jsxs)("div", {
982
+ style: {
983
+ ...s.muted,
984
+ marginBottom: 8
985
+ },
986
+ children: [
987
+ "元数据登记 ",
988
+ recordedRows,
989
+ " 行,实际 ",
990
+ total,
991
+ " 行。"
992
+ ]
993
+ }) : null,
994
+ columns.length > 0 ? (0, react_jsx_runtime.jsx)("div", {
995
+ style: {
996
+ ...s.scroll,
997
+ maxHeight: 320
998
+ },
999
+ children: (0, react_jsx_runtime.jsxs)("table", {
1000
+ style: s.table,
1001
+ children: [(0, react_jsx_runtime.jsx)("thead", { children: (0, react_jsx_runtime.jsxs)("tr", { children: [(0, react_jsx_runtime.jsx)("th", {
1002
+ style: s.th,
1003
+ children: "#"
1004
+ }), columns.map((column) => (0, react_jsx_runtime.jsx)("th", {
1005
+ style: s.th,
1006
+ title: column,
1007
+ children: column
1008
+ }, column))] }) }), (0, react_jsx_runtime.jsxs)("tbody", { children: [rows.map((row) => (0, react_jsx_runtime.jsxs)("tr", { children: [(0, react_jsx_runtime.jsx)("td", {
1009
+ style: {
1010
+ ...s.td,
1011
+ color: c.fg2,
1012
+ maxWidth: 60
1013
+ },
1014
+ children: String(row[ADMIN_ROW_ID_COLUMN] ?? "")
1015
+ }), columns.map((column) => (0, react_jsx_runtime.jsx)("td", {
1016
+ style: s.td,
1017
+ children: (0, react_jsx_runtime.jsx)(Cell, { value: row[column] })
1018
+ }, column))] }, String(row[ADMIN_ROW_ID_COLUMN] ?? ""))), rows.length === 0 && !loading ? (0, react_jsx_runtime.jsx)("tr", { children: (0, react_jsx_runtime.jsx)("td", {
1019
+ style: {
1020
+ ...s.td,
1021
+ color: c.fg2,
1022
+ padding: "16px 8px",
1023
+ textAlign: "center"
1024
+ },
1025
+ colSpan: columns.length + 1,
1026
+ children: total === 0 ? "该数据集还没有数据行" : "这一页没有数据"
1027
+ }) }) : null] })]
1028
+ })
1029
+ }) : loading ? (0, react_jsx_runtime.jsx)("div", {
1030
+ style: s.muted,
1031
+ children: "加载中…"
1032
+ }) : error === null ? (0, react_jsx_runtime.jsx)("div", {
1033
+ style: s.muted,
1034
+ children: "该数据集没有已登记的列,无法展示数据。"
1035
+ }) : null,
1036
+ (0, react_jsx_runtime.jsxs)("div", {
1037
+ style: {
1038
+ ...s.bar,
1039
+ display: "flex",
1040
+ alignItems: "center",
1041
+ gap: 8,
1042
+ marginTop: 8
1043
+ },
1044
+ children: [
1045
+ (0, react_jsx_runtime.jsx)("button", {
1046
+ type: "button",
1047
+ style: {
1048
+ ...s.button,
1049
+ ...page <= 1 || loading ? s.disabledButton : null
1050
+ },
1051
+ disabled: page <= 1 || loading,
1052
+ onClick: () => void load(1, pageSize),
1053
+ children: "« 首页"
1054
+ }),
1055
+ (0, react_jsx_runtime.jsx)("button", {
1056
+ type: "button",
1057
+ style: {
1058
+ ...s.button,
1059
+ ...page <= 1 || loading ? s.disabledButton : null
1060
+ },
1061
+ disabled: page <= 1 || loading,
1062
+ onClick: () => void load(page - 1, pageSize),
1063
+ children: "上一页"
1064
+ }),
1065
+ (0, react_jsx_runtime.jsxs)("span", { children: [
1066
+ "第 ",
1067
+ page,
1068
+ " / ",
1069
+ totalPages,
1070
+ " 页"
1071
+ ] }),
1072
+ (0, react_jsx_runtime.jsx)("button", {
1073
+ type: "button",
1074
+ style: {
1075
+ ...s.button,
1076
+ ...page >= totalPages || loading ? s.disabledButton : null
1077
+ },
1078
+ disabled: page >= totalPages || loading,
1079
+ onClick: () => void load(page + 1, pageSize),
1080
+ children: "下一页"
1081
+ }),
1082
+ (0, react_jsx_runtime.jsx)("button", {
1083
+ type: "button",
1084
+ style: {
1085
+ ...s.button,
1086
+ ...page >= totalPages || loading ? s.disabledButton : null
1087
+ },
1088
+ disabled: page >= totalPages || loading,
1089
+ onClick: () => void load(totalPages, pageSize),
1090
+ children: "末页 »"
1091
+ }),
1092
+ (0, react_jsx_runtime.jsxs)("label", {
1093
+ style: {
1094
+ ...s.muted,
1095
+ display: "flex",
1096
+ alignItems: "center",
1097
+ gap: 4
1098
+ },
1099
+ children: [
1100
+ "每页",
1101
+ (0, react_jsx_runtime.jsx)("select", {
1102
+ style: s.select,
1103
+ value: pageSize,
1104
+ disabled: loading,
1105
+ onChange: (event) => setPageSize(Number(event.target.value)),
1106
+ children: PAGE_SIZE_OPTIONS.map((size) => (0, react_jsx_runtime.jsx)("option", {
1107
+ value: size,
1108
+ children: size
1109
+ }, size))
1110
+ }),
1111
+ "行"
1112
+ ]
1113
+ }),
1114
+ (0, react_jsx_runtime.jsx)("button", {
1115
+ type: "button",
1116
+ style: {
1117
+ ...s.button,
1118
+ ...loading ? s.disabledButton : null
1119
+ },
1120
+ disabled: loading,
1121
+ onClick: () => void load(page, pageSize),
1122
+ children: "刷新"
1123
+ })
1124
+ ]
1125
+ })
1126
+ ]
1127
+ });
1128
+ }
1129
+
1130
+ //#endregion
1131
+ //#region src/client/settings/CreateForm.tsx
1132
+ const EMPTY_COLUMN = {
1133
+ name: "",
1134
+ type: "text",
1135
+ nullable: false,
1136
+ description: ""
1137
+ };
1138
+ function CreateForm(props) {
1139
+ const { scopes, onCreated, onCancel, onError } = props;
1140
+ const [scopeKey, setScopeKey] = (0, react.useState)(scopes[0]?.scopeKey ?? "");
1141
+ const [name$1, setName] = (0, react.useState)("");
1142
+ const [description, setDescription] = (0, react.useState)("");
1143
+ const [sourcePath, setSourcePath] = (0, react.useState)("");
1144
+ const [columns, setColumns] = (0, react.useState)([{ ...EMPTY_COLUMN }]);
1145
+ const [submitting, setSubmitting] = (0, react.useState)(false);
1146
+ const canSubmit = scopeKey.length > 0 && name$1.trim().length > 0 && columns.length > 0 && columns.length <= ADMIN_MAX_COLUMNS && columns.every((column) => column.name.trim().length > 0);
1147
+ function updateColumn(index, patch) {
1148
+ setColumns((previous) => previous.map((column, i) => i === index ? {
1149
+ ...column,
1150
+ ...patch
1151
+ } : column));
1152
+ }
1153
+ async function handleSubmit() {
1154
+ if (!canSubmit || submitting) return;
1155
+ const body = {
1156
+ scopeKey,
1157
+ name: name$1.trim(),
1158
+ description: description.trim().length > 0 ? description.trim() : undefined,
1159
+ sourcePath: sourcePath.trim().length > 0 ? sourcePath.trim() : null,
1160
+ columns: columns.map((column) => ({
1161
+ name: column.name.trim(),
1162
+ type: column.type,
1163
+ nullable: column.nullable,
1164
+ description: column.description.trim().length > 0 ? column.description.trim() : undefined
1165
+ }))
1166
+ };
1167
+ setSubmitting(true);
1168
+ try {
1169
+ const created = await createDataset(body);
1170
+ onCreated(created);
1171
+ } catch (error) {
1172
+ onError(error instanceof Error ? error : new Error("新建失败"));
1173
+ } finally {
1174
+ setSubmitting(false);
1175
+ }
1176
+ }
1177
+ return (0, react_jsx_runtime.jsxs)("div", {
1178
+ style: s.panel,
1179
+ children: [
1180
+ (0, react_jsx_runtime.jsxs)("div", {
1181
+ style: {
1182
+ display: "flex",
1183
+ justifyContent: "space-between",
1184
+ marginBottom: 10
1185
+ },
1186
+ children: [(0, react_jsx_runtime.jsx)("strong", {
1187
+ style: { fontSize: 13 },
1188
+ children: "新建数据集"
1189
+ }), (0, react_jsx_runtime.jsx)("span", {
1190
+ style: s.muted,
1191
+ children: "空表,建好后用 agent 工具灌入数据"
1192
+ })]
1193
+ }),
1194
+ (0, react_jsx_runtime.jsxs)("div", {
1195
+ style: s.grid2,
1196
+ children: [(0, react_jsx_runtime.jsxs)("div", {
1197
+ style: s.field,
1198
+ children: [(0, react_jsx_runtime.jsx)("label", {
1199
+ style: s.label,
1200
+ children: "工作区"
1201
+ }), (0, react_jsx_runtime.jsx)("select", {
1202
+ style: s.input,
1203
+ value: scopeKey,
1204
+ disabled: submitting,
1205
+ onChange: (e) => setScopeKey(e.target.value),
1206
+ children: scopes.length === 0 ? (0, react_jsx_runtime.jsx)("option", {
1207
+ value: "",
1208
+ children: "(暂无可用工作区)"
1209
+ }) : scopes.map((scope) => (0, react_jsx_runtime.jsx)("option", {
1210
+ value: scope.scopeKey,
1211
+ children: scope.scopeKey
1212
+ }, scope.scopeKey))
1213
+ })]
1214
+ }), (0, react_jsx_runtime.jsxs)("div", {
1215
+ style: s.field,
1216
+ children: [(0, react_jsx_runtime.jsx)("label", {
1217
+ style: s.label,
1218
+ children: "名称"
1219
+ }), (0, react_jsx_runtime.jsx)("input", {
1220
+ style: s.input,
1221
+ value: name$1,
1222
+ disabled: submitting,
1223
+ onChange: (e) => setName(e.target.value)
1224
+ })]
1225
+ })]
1226
+ }),
1227
+ (0, react_jsx_runtime.jsxs)("div", {
1228
+ style: s.grid2,
1229
+ children: [(0, react_jsx_runtime.jsxs)("div", {
1230
+ style: s.field,
1231
+ children: [(0, react_jsx_runtime.jsx)("label", {
1232
+ style: s.label,
1233
+ children: "描述"
1234
+ }), (0, react_jsx_runtime.jsx)("input", {
1235
+ style: s.input,
1236
+ value: description,
1237
+ disabled: submitting,
1238
+ onChange: (e) => setDescription(e.target.value)
1239
+ })]
1240
+ }), (0, react_jsx_runtime.jsxs)("div", {
1241
+ style: s.field,
1242
+ children: [(0, react_jsx_runtime.jsx)("label", {
1243
+ style: s.label,
1244
+ children: "来源路径(可选)"
1245
+ }), (0, react_jsx_runtime.jsx)("input", {
1246
+ style: s.input,
1247
+ value: sourcePath,
1248
+ disabled: submitting,
1249
+ onChange: (e) => setSourcePath(e.target.value)
1250
+ })]
1251
+ })]
1252
+ }),
1253
+ (0, react_jsx_runtime.jsxs)("div", {
1254
+ style: s.label,
1255
+ children: [
1256
+ "列定义(",
1257
+ columns.length,
1258
+ " / ",
1259
+ ADMIN_MAX_COLUMNS,
1260
+ ")"
1261
+ ]
1262
+ }),
1263
+ (0, react_jsx_runtime.jsx)("div", {
1264
+ style: {
1265
+ ...s.scroll,
1266
+ maxHeight: 220,
1267
+ marginBottom: 10
1268
+ },
1269
+ children: (0, react_jsx_runtime.jsxs)("table", {
1270
+ style: s.table,
1271
+ children: [(0, react_jsx_runtime.jsx)("thead", { children: (0, react_jsx_runtime.jsxs)("tr", { children: [
1272
+ (0, react_jsx_runtime.jsx)("th", {
1273
+ style: s.th,
1274
+ children: "列名"
1275
+ }),
1276
+ (0, react_jsx_runtime.jsx)("th", {
1277
+ style: s.th,
1278
+ children: "类型"
1279
+ }),
1280
+ (0, react_jsx_runtime.jsx)("th", {
1281
+ style: s.th,
1282
+ children: "可空"
1283
+ }),
1284
+ (0, react_jsx_runtime.jsx)("th", {
1285
+ style: s.th,
1286
+ children: "说明"
1287
+ }),
1288
+ (0, react_jsx_runtime.jsx)("th", { style: s.th })
1289
+ ] }) }), (0, react_jsx_runtime.jsx)("tbody", { children: columns.map((column, index) => (0, react_jsx_runtime.jsxs)("tr", { children: [
1290
+ (0, react_jsx_runtime.jsx)("td", {
1291
+ style: s.td,
1292
+ children: (0, react_jsx_runtime.jsx)("input", {
1293
+ style: {
1294
+ ...s.input,
1295
+ border: column.name.trim().length === 0 ? `1px solid ${c.danger}` : s.input.border
1296
+ },
1297
+ value: column.name,
1298
+ disabled: submitting,
1299
+ onChange: (e) => updateColumn(index, { name: e.target.value })
1300
+ })
1301
+ }),
1302
+ (0, react_jsx_runtime.jsx)("td", {
1303
+ style: s.td,
1304
+ children: (0, react_jsx_runtime.jsx)("select", {
1305
+ style: s.input,
1306
+ value: column.type,
1307
+ disabled: submitting,
1308
+ onChange: (e) => updateColumn(index, { type: e.target.value }),
1309
+ children: ADMIN_COLUMN_TYPES.map((type) => (0, react_jsx_runtime.jsx)("option", {
1310
+ value: type,
1311
+ children: ADMIN_COLUMN_TYPE_LABELS[type]
1312
+ }, type))
1313
+ })
1314
+ }),
1315
+ (0, react_jsx_runtime.jsx)("td", {
1316
+ style: s.td,
1317
+ children: (0, react_jsx_runtime.jsx)("input", {
1318
+ type: "checkbox",
1319
+ checked: column.nullable,
1320
+ disabled: submitting,
1321
+ onChange: (e) => updateColumn(index, { nullable: e.target.checked })
1322
+ })
1323
+ }),
1324
+ (0, react_jsx_runtime.jsx)("td", {
1325
+ style: s.td,
1326
+ children: (0, react_jsx_runtime.jsx)("input", {
1327
+ style: s.input,
1328
+ value: column.description,
1329
+ disabled: submitting,
1330
+ onChange: (e) => updateColumn(index, { description: e.target.value })
1331
+ })
1332
+ }),
1333
+ (0, react_jsx_runtime.jsx)("td", {
1334
+ style: s.td,
1335
+ children: (0, react_jsx_runtime.jsx)("span", {
1336
+ style: {
1337
+ ...s.link,
1338
+ ...columns.length <= 1 || submitting ? s.disabledButton : null
1339
+ },
1340
+ onClick: () => {
1341
+ if (columns.length > 1 && !submitting) setColumns((previous) => previous.filter((_, i) => i !== index));
1342
+ },
1343
+ children: "删除"
1344
+ })
1345
+ })
1346
+ ] }, index)) })]
1347
+ })
1348
+ }),
1349
+ (0, react_jsx_runtime.jsx)("button", {
1350
+ type: "button",
1351
+ style: {
1352
+ ...s.button,
1353
+ ...columns.length >= ADMIN_MAX_COLUMNS || submitting ? s.disabledButton : null
1354
+ },
1355
+ disabled: columns.length >= ADMIN_MAX_COLUMNS || submitting,
1356
+ onClick: () => setColumns((previous) => [...previous, { ...EMPTY_COLUMN }]),
1357
+ children: "添加一列"
1358
+ }),
1359
+ (0, react_jsx_runtime.jsxs)("div", {
1360
+ style: {
1361
+ display: "flex",
1362
+ gap: 8,
1363
+ marginTop: 12
1364
+ },
1365
+ children: [(0, react_jsx_runtime.jsx)("button", {
1366
+ type: "button",
1367
+ style: {
1368
+ ...s.primaryButton,
1369
+ ...!canSubmit || submitting ? s.disabledButton : null
1370
+ },
1371
+ disabled: !canSubmit || submitting,
1372
+ onClick: () => void handleSubmit(),
1373
+ children: submitting ? "提交中…" : "提交"
1374
+ }), (0, react_jsx_runtime.jsx)("button", {
1375
+ type: "button",
1376
+ style: s.button,
1377
+ disabled: submitting,
1378
+ onClick: onCancel,
1379
+ children: "取消"
1380
+ })]
1381
+ })
1382
+ ]
1383
+ });
1384
+ }
1385
+
1386
+ //#endregion
1387
+ //#region src/client/settings/DatasetsPanel.tsx
1388
+ function DatasetsPanel() {
1389
+ const [scopes, setScopes] = (0, react.useState)([]);
1390
+ const [items, setItems] = (0, react.useState)([]);
1391
+ const [total, setTotal] = (0, react.useState)(0);
1392
+ const [query, setQuery] = (0, react.useState)("");
1393
+ const [page, setPage] = (0, react.useState)(1);
1394
+ const [mode, setMode] = (0, react.useState)("browse");
1395
+ const [selectedId, setSelectedId] = (0, react.useState)(null);
1396
+ const [detail, setDetail] = (0, react.useState)(null);
1397
+ const [loadingList, setLoadingList] = (0, react.useState)(false);
1398
+ const [loadingDetail, setLoadingDetail] = (0, react.useState)(false);
1399
+ const [writable, setWritable] = (0, react.useState)(true);
1400
+ const [error, setError] = (0, react.useState)(null);
1401
+ const requestId = (0, react.useRef)(0);
1402
+ const loadScopes = (0, react.useCallback)(async () => {
1403
+ try {
1404
+ const result = await listScopes();
1405
+ setScopes(result.scopes);
1406
+ } catch (failure) {
1407
+ if (failure instanceof AdminApiError && (failure.code === "READ_ONLY" || failure.code === "ADMIN_DISABLED")) setWritable(false);
1408
+ }
1409
+ }, []);
1410
+ const loadList = (0, react.useCallback)(async (q, p) => {
1411
+ const id = requestId.current + 1;
1412
+ requestId.current = id;
1413
+ setLoadingList(true);
1414
+ try {
1415
+ const result = await listDatasets(q, p, ADMIN_PAGE_SIZE);
1416
+ if (id !== requestId.current) return;
1417
+ setItems(result.items);
1418
+ setTotal(result.total);
1419
+ const warning = result.warnings.map((w) => `${w.scopeKey}: ${w.message}`).join(";");
1420
+ setError(warning.length > 0 ? warning : null);
1421
+ } catch (failure) {
1422
+ if (id !== requestId.current) return;
1423
+ setError(failure instanceof Error ? failure.message : "加载失败");
1424
+ } finally {
1425
+ if (id === requestId.current) setLoadingList(false);
1426
+ }
1427
+ }, []);
1428
+ (0, react.useEffect)(() => {
1429
+ void loadScopes();
1430
+ void loadList("", 1);
1431
+ }, [loadScopes, loadList]);
1432
+ (0, react.useEffect)(() => {
1433
+ const timer = setTimeout(() => {
1434
+ setPage(1);
1435
+ void loadList(query, 1);
1436
+ }, 300);
1437
+ return () => clearTimeout(timer);
1438
+ }, [query, loadList]);
1439
+ const openDetail = (0, react.useCallback)(async (item) => {
1440
+ setSelectedId(item.id);
1441
+ setMode("browse");
1442
+ setDetail(null);
1443
+ setLoadingDetail(true);
1444
+ try {
1445
+ const fetched = await getDataset(item.id, item.scopeKey);
1446
+ setDetail(fetched);
1447
+ } catch (failure) {
1448
+ setError(failure instanceof Error ? failure.message : "加载详情失败");
1449
+ } finally {
1450
+ setLoadingDetail(false);
1451
+ }
1452
+ }, []);
1453
+ const handleWriteError = (0, react.useCallback)((failure) => {
1454
+ setError(failure.message);
1455
+ if (failure instanceof AdminApiError && (failure.code === "READ_ONLY" || failure.code === "ADMIN_DISABLED")) setWritable(false);
1456
+ }, []);
1457
+ const handleChanged = (0, react.useCallback)((updated) => {
1458
+ setDetail(updated);
1459
+ void loadList(query, page);
1460
+ }, [
1461
+ query,
1462
+ page,
1463
+ loadList
1464
+ ]);
1465
+ const handleDeleted = (0, react.useCallback)(async (id) => {
1466
+ setSelectedId(null);
1467
+ setDetail(null);
1468
+ await loadScopes();
1469
+ await loadList(query, page);
1470
+ void id;
1471
+ }, [
1472
+ query,
1473
+ page,
1474
+ loadList,
1475
+ loadScopes
1476
+ ]);
1477
+ const handleCreated = (0, react.useCallback)(async (created) => {
1478
+ setMode("browse");
1479
+ await loadScopes();
1480
+ await loadList(query, page);
1481
+ const record = created;
1482
+ setSelectedId(record.id);
1483
+ setDetail(record);
1484
+ }, [
1485
+ query,
1486
+ page,
1487
+ loadList,
1488
+ loadScopes
1489
+ ]);
1490
+ return (0, react_jsx_runtime.jsxs)("div", { children: [
1491
+ (0, react_jsx_runtime.jsxs)("div", {
1492
+ style: s.toolbar,
1493
+ children: [
1494
+ (0, react_jsx_runtime.jsx)("input", {
1495
+ style: s.search,
1496
+ placeholder: "按名称 / 来源 / 工作区搜索…",
1497
+ value: query,
1498
+ onChange: (e) => setQuery(e.target.value)
1499
+ }),
1500
+ (0, react_jsx_runtime.jsx)("button", {
1501
+ type: "button",
1502
+ style: {
1503
+ ...s.primaryButton,
1504
+ ...!writable ? s.disabledButton : null
1505
+ },
1506
+ disabled: !writable,
1507
+ onClick: () => setMode("create"),
1508
+ title: writable ? "新建空数据集" : "只读模式或接口已关闭",
1509
+ children: "+ 新建数据集"
1510
+ }),
1511
+ (0, react_jsx_runtime.jsx)("button", {
1512
+ type: "button",
1513
+ style: s.button,
1514
+ disabled: loadingList,
1515
+ onClick: () => void loadList(query, page),
1516
+ children: "刷新"
1517
+ })
1518
+ ]
1519
+ }),
1520
+ error !== null ? (0, react_jsx_runtime.jsxs)("div", {
1521
+ style: s.banner,
1522
+ children: [(0, react_jsx_runtime.jsx)("span", { children: error }), (0, react_jsx_runtime.jsx)("span", {
1523
+ style: {
1524
+ ...s.link,
1525
+ marginLeft: "auto"
1526
+ },
1527
+ onClick: () => setError(null),
1528
+ children: "忽略"
1529
+ })]
1530
+ }) : null,
1531
+ (0, react_jsx_runtime.jsx)(DatasetTable, {
1532
+ items,
1533
+ page,
1534
+ pageSize: ADMIN_PAGE_SIZE,
1535
+ total,
1536
+ selectedId,
1537
+ loading: loadingList,
1538
+ onSelect: (item) => void openDetail(item),
1539
+ onPageChange: (p) => {
1540
+ setPage(p);
1541
+ void loadList(query, p);
1542
+ }
1543
+ }),
1544
+ mode === "create" ? (0, react_jsx_runtime.jsx)(CreateForm, {
1545
+ scopes,
1546
+ onCreated: (detail$1) => void handleCreated(detail$1),
1547
+ onCancel: () => setMode("browse"),
1548
+ onError: handleWriteError
1549
+ }) : detail !== null ? (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsx)(DatasetEditor, {
1550
+ detail,
1551
+ writable,
1552
+ onChanged: handleChanged,
1553
+ onDeleted: (id) => void handleDeleted(id),
1554
+ onError: handleWriteError
1555
+ }), (0, react_jsx_runtime.jsx)(RowsPanel, {
1556
+ datasetId: detail.id,
1557
+ scopeKey: detail.scopeKey,
1558
+ recordedRows: detail.rowCount,
1559
+ status: detail.status
1560
+ }, `${detail.scopeKey}:${detail.id}`)] }) : (0, react_jsx_runtime.jsx)("div", {
1561
+ style: {
1562
+ ...s.muted,
1563
+ marginTop: 12
1564
+ },
1565
+ children: loadingDetail ? "加载详情中…" : "选择一个数据集查看详情,或点击「新建数据集」创建空表。"
1566
+ })
1567
+ ] });
1568
+ }
1569
+
1570
+ //#endregion
1571
+ //#region src/client/settings/DataSourceForm.tsx
1572
+ function DataSourceForm(props) {
1573
+ const { initial, onSaved, onCancel, onError } = props;
1574
+ const editing = initial !== null && initial !== undefined;
1575
+ const defaultType = initial?.type ?? "mysql";
1576
+ const [name$1, setName] = (0, react.useState)(initial?.name ?? "");
1577
+ const [type, setType] = (0, react.useState)(defaultType);
1578
+ const [host, setHost] = (0, react.useState)(initial?.host ?? "127.0.0.1");
1579
+ const [port, setPort] = (0, react.useState)(String(initial?.port ?? ADMIN_SOURCE_DEFAULT_PORTS[defaultType]));
1580
+ const [database, setDatabase] = (0, react.useState)(initial?.database ?? "");
1581
+ const [username, setUsername] = (0, react.useState)(initial?.username ?? "");
1582
+ const [password, setPassword] = (0, react.useState)("");
1583
+ const [sslMode, setSslMode] = (0, react.useState)(initial?.sslMode ?? "");
1584
+ const [poolMax, setPoolMax] = (0, react.useState)(initial?.poolMax === null ? "" : String(initial?.poolMax ?? ""));
1585
+ const [description, setDescription] = (0, react.useState)(initial?.description ?? "");
1586
+ const [testState, setTestState] = (0, react.useState)("idle");
1587
+ const [testResult, setTestResult] = (0, react.useState)(null);
1588
+ const [saving, setSaving] = (0, react.useState)(false);
1589
+ const requiredFilled = name$1.trim().length > 0 && host.trim().length > 0 && database.trim().length > 0 && username.trim().length > 0;
1590
+ /** 连接参数变了,之前的测试结论就作废。 */
1591
+ function changeConnection(next) {
1592
+ next();
1593
+ setTestState("idle");
1594
+ setTestResult(null);
1595
+ }
1596
+ function draft() {
1597
+ const parsedPort = Number.parseInt(port, 10);
1598
+ const parsedPool = Number.parseInt(poolMax, 10);
1599
+ return {
1600
+ type,
1601
+ host: host.trim(),
1602
+ port: Number.isFinite(parsedPort) ? parsedPort : ADMIN_SOURCE_DEFAULT_PORTS[type],
1603
+ database: database.trim(),
1604
+ username: username.trim(),
1605
+ password,
1606
+ sslMode: sslMode.trim().length > 0 ? sslMode.trim() : null,
1607
+ poolMax: Number.isFinite(parsedPool) ? parsedPool : null
1608
+ };
1609
+ }
1610
+ async function handleTest() {
1611
+ if (!requiredFilled || testState === "testing") return;
1612
+ setTestState("testing");
1613
+ try {
1614
+ const result = await testDataSource(draft());
1615
+ setTestResult(result);
1616
+ setTestState(result.success ? "ok" : "fail");
1617
+ } catch (error) {
1618
+ setTestState("fail");
1619
+ onError(error instanceof Error ? error : new Error("测试连接失败"));
1620
+ }
1621
+ }
1622
+ async function handleSave() {
1623
+ if (testState !== "ok" || saving) return;
1624
+ setSaving(true);
1625
+ try {
1626
+ const connection = draft();
1627
+ const saved = editing ? await patchDataSource(initial.id, {
1628
+ name: name$1.trim(),
1629
+ ...connection,
1630
+ ...password.length > 0 ? {} : { password: undefined },
1631
+ description: description.trim().length > 0 ? description.trim() : null,
1632
+ test: true
1633
+ }) : await createDataSource({
1634
+ ...connection,
1635
+ name: name$1.trim(),
1636
+ description: description.trim().length > 0 ? description.trim() : null,
1637
+ test: true
1638
+ });
1639
+ onSaved(saved);
1640
+ } catch (error) {
1641
+ onError(error instanceof Error ? error : new Error("保存失败"));
1642
+ } finally {
1643
+ setSaving(false);
1644
+ }
1645
+ }
1646
+ const testHint = testState === "ok" ? `连接成功(${testResult?.latency ?? 0}ms${testResult?.version === null ? "" : ` · ${testResult?.version}`})` : testState === "fail" ? testResult?.error ?? "连接失败" : "保存前请先测试连接";
1647
+ return (0, react_jsx_runtime.jsxs)("div", {
1648
+ style: s.panel,
1649
+ children: [
1650
+ (0, react_jsx_runtime.jsxs)("div", {
1651
+ style: {
1652
+ display: "flex",
1653
+ justifyContent: "space-between",
1654
+ marginBottom: 10
1655
+ },
1656
+ children: [(0, react_jsx_runtime.jsx)("strong", {
1657
+ style: { fontSize: 13 },
1658
+ children: editing ? "编辑数据源" : "新建数据源"
1659
+ }), (0, react_jsx_runtime.jsx)("span", {
1660
+ style: s.muted,
1661
+ children: editing && initial.hasPassword ? "已保存过密码,留空即保留" : "密码加密存储,不会回显"
1662
+ })]
1663
+ }),
1664
+ (0, react_jsx_runtime.jsxs)("div", {
1665
+ style: s.grid2,
1666
+ children: [(0, react_jsx_runtime.jsxs)("div", {
1667
+ style: s.field,
1668
+ children: [(0, react_jsx_runtime.jsx)("label", {
1669
+ style: s.label,
1670
+ children: "名称"
1671
+ }), (0, react_jsx_runtime.jsx)("input", {
1672
+ style: s.input,
1673
+ value: name$1,
1674
+ disabled: saving,
1675
+ onChange: (e) => setName(e.target.value)
1676
+ })]
1677
+ }), (0, react_jsx_runtime.jsxs)("div", {
1678
+ style: s.field,
1679
+ children: [(0, react_jsx_runtime.jsx)("label", {
1680
+ style: s.label,
1681
+ children: "类型"
1682
+ }), (0, react_jsx_runtime.jsx)("select", {
1683
+ style: s.input,
1684
+ value: type,
1685
+ disabled: saving,
1686
+ onChange: (e) => changeConnection(() => {
1687
+ setType(e.target.value);
1688
+ setPort(String(ADMIN_SOURCE_DEFAULT_PORTS[e.target.value]));
1689
+ }),
1690
+ children: ADMIN_SOURCE_TYPES.map((item) => (0, react_jsx_runtime.jsx)("option", {
1691
+ value: item,
1692
+ children: ADMIN_SOURCE_TYPE_LABELS[item]
1693
+ }, item))
1694
+ })]
1695
+ })]
1696
+ }),
1697
+ (0, react_jsx_runtime.jsxs)("div", {
1698
+ style: s.grid2,
1699
+ children: [(0, react_jsx_runtime.jsxs)("div", {
1700
+ style: s.field,
1701
+ children: [(0, react_jsx_runtime.jsx)("label", {
1702
+ style: s.label,
1703
+ children: "主机"
1704
+ }), (0, react_jsx_runtime.jsx)("input", {
1705
+ style: s.input,
1706
+ value: host,
1707
+ disabled: saving,
1708
+ onChange: (e) => changeConnection(() => setHost(e.target.value))
1709
+ })]
1710
+ }), (0, react_jsx_runtime.jsxs)("div", {
1711
+ style: s.field,
1712
+ children: [(0, react_jsx_runtime.jsx)("label", {
1713
+ style: s.label,
1714
+ children: "端口"
1715
+ }), (0, react_jsx_runtime.jsx)("input", {
1716
+ style: s.input,
1717
+ value: port,
1718
+ inputMode: "numeric",
1719
+ disabled: saving,
1720
+ onChange: (e) => changeConnection(() => setPort(e.target.value))
1721
+ })]
1722
+ })]
1723
+ }),
1724
+ (0, react_jsx_runtime.jsxs)("div", {
1725
+ style: s.grid2,
1726
+ children: [(0, react_jsx_runtime.jsxs)("div", {
1727
+ style: s.field,
1728
+ children: [(0, react_jsx_runtime.jsx)("label", {
1729
+ style: s.label,
1730
+ children: "数据库名"
1731
+ }), (0, react_jsx_runtime.jsx)("input", {
1732
+ style: s.input,
1733
+ value: database,
1734
+ disabled: saving,
1735
+ onChange: (e) => changeConnection(() => setDatabase(e.target.value))
1736
+ })]
1737
+ }), (0, react_jsx_runtime.jsxs)("div", {
1738
+ style: s.field,
1739
+ children: [(0, react_jsx_runtime.jsx)("label", {
1740
+ style: s.label,
1741
+ children: "用户名"
1742
+ }), (0, react_jsx_runtime.jsx)("input", {
1743
+ style: s.input,
1744
+ value: username,
1745
+ disabled: saving,
1746
+ onChange: (e) => changeConnection(() => setUsername(e.target.value))
1747
+ })]
1748
+ })]
1749
+ }),
1750
+ (0, react_jsx_runtime.jsxs)("div", {
1751
+ style: s.grid2,
1752
+ children: [(0, react_jsx_runtime.jsxs)("div", {
1753
+ style: s.field,
1754
+ children: [(0, react_jsx_runtime.jsx)("label", {
1755
+ style: s.label,
1756
+ children: editing ? "重设密码(留空保留)" : "密码"
1757
+ }), (0, react_jsx_runtime.jsx)("input", {
1758
+ style: s.input,
1759
+ type: "password",
1760
+ value: password,
1761
+ disabled: saving,
1762
+ onChange: (e) => changeConnection(() => setPassword(e.target.value))
1763
+ })]
1764
+ }), (0, react_jsx_runtime.jsxs)("div", {
1765
+ style: s.field,
1766
+ children: [(0, react_jsx_runtime.jsx)("label", {
1767
+ style: s.label,
1768
+ children: "SSL 模式(可选)"
1769
+ }), (0, react_jsx_runtime.jsx)("input", {
1770
+ style: s.input,
1771
+ value: sslMode,
1772
+ disabled: saving,
1773
+ placeholder: type === "mysql" ? "disabled / preferred / required" : "disable / require / verify-full",
1774
+ onChange: (e) => changeConnection(() => setSslMode(e.target.value))
1775
+ })]
1776
+ })]
1777
+ }),
1778
+ (0, react_jsx_runtime.jsxs)("div", {
1779
+ style: s.grid2,
1780
+ children: [(0, react_jsx_runtime.jsxs)("div", {
1781
+ style: s.field,
1782
+ children: [(0, react_jsx_runtime.jsx)("label", {
1783
+ style: s.label,
1784
+ children: "连接池上限(可选)"
1785
+ }), (0, react_jsx_runtime.jsx)("input", {
1786
+ style: s.input,
1787
+ value: poolMax,
1788
+ inputMode: "numeric",
1789
+ disabled: saving,
1790
+ onChange: (e) => changeConnection(() => setPoolMax(e.target.value))
1791
+ })]
1792
+ }), (0, react_jsx_runtime.jsxs)("div", {
1793
+ style: s.field,
1794
+ children: [(0, react_jsx_runtime.jsx)("label", {
1795
+ style: s.label,
1796
+ children: "描述(可选)"
1797
+ }), (0, react_jsx_runtime.jsx)("input", {
1798
+ style: s.input,
1799
+ value: description,
1800
+ disabled: saving,
1801
+ onChange: (e) => setDescription(e.target.value)
1802
+ })]
1803
+ })]
1804
+ }),
1805
+ (0, react_jsx_runtime.jsx)("div", {
1806
+ style: {
1807
+ ...s.banner,
1808
+ marginBottom: 10,
1809
+ borderColor: testState === "ok" ? c.ok : testState === "fail" ? c.danger : c.border,
1810
+ color: testState === "ok" ? c.ok : testState === "fail" ? c.danger : c.fg2
1811
+ },
1812
+ children: (0, react_jsx_runtime.jsx)("span", { children: testHint })
1813
+ }),
1814
+ (0, react_jsx_runtime.jsxs)("div", {
1815
+ style: {
1816
+ display: "flex",
1817
+ gap: 8
1818
+ },
1819
+ children: [
1820
+ (0, react_jsx_runtime.jsx)("button", {
1821
+ type: "button",
1822
+ style: {
1823
+ ...s.button,
1824
+ ...!requiredFilled || testState === "testing" ? s.disabledButton : null
1825
+ },
1826
+ disabled: !requiredFilled || testState === "testing",
1827
+ onClick: () => void handleTest(),
1828
+ children: testState === "testing" ? "测试中…" : "测试连接"
1829
+ }),
1830
+ (0, react_jsx_runtime.jsx)("button", {
1831
+ type: "button",
1832
+ style: {
1833
+ ...s.primaryButton,
1834
+ ...testState !== "ok" || saving ? s.disabledButton : null
1835
+ },
1836
+ disabled: testState !== "ok" || saving,
1837
+ onClick: () => void handleSave(),
1838
+ children: saving ? "保存中…" : "保存"
1839
+ }),
1840
+ (0, react_jsx_runtime.jsx)("button", {
1841
+ type: "button",
1842
+ style: s.button,
1843
+ disabled: saving,
1844
+ onClick: onCancel,
1845
+ children: "取消"
1846
+ })
1847
+ ]
1848
+ })
1849
+ ]
1850
+ });
1851
+ }
1852
+
1853
+ //#endregion
1854
+ //#region src/client/settings/SourceTablesPanel.tsx
1855
+ function formatRows(count) {
1856
+ if (count < 0) return "未知";
1857
+ if (count >= 1e6) return `${(count / 1e6).toFixed(1)}M`;
1858
+ if (count >= 1e3) return `${(count / 1e3).toFixed(1)}k`;
1859
+ return String(count);
1860
+ }
1861
+ function SourceTablesPanel(props) {
1862
+ const { source, scopes, onError } = props;
1863
+ const [schemas, setSchemas] = (0, react.useState)([]);
1864
+ const [schema, setSchema] = (0, react.useState)("");
1865
+ const [keyword, setKeyword] = (0, react.useState)("");
1866
+ const [tables, setTables] = (0, react.useState)([]);
1867
+ const [loading, setLoading] = (0, react.useState)(false);
1868
+ const [selected, setSelected] = (0, react.useState)(null);
1869
+ const [scopeKey, setScopeKey] = (0, react.useState)("");
1870
+ const [name$1, setName] = (0, react.useState)("");
1871
+ const [importing, setImporting] = (0, react.useState)(false);
1872
+ const [result, setResult] = (0, react.useState)(null);
1873
+ const requestId = (0, react.useRef)(0);
1874
+ const load = (0, react.useCallback)(async (schemaArg, q) => {
1875
+ const id = requestId.current + 1;
1876
+ requestId.current = id;
1877
+ setLoading(true);
1878
+ try {
1879
+ const loaded = await listSourceTables(source.id, schemaArg, q);
1880
+ if (id !== requestId.current) return;
1881
+ setSchemas(loaded.schemas);
1882
+ setTables(loaded.tables);
1883
+ setSelected(null);
1884
+ } catch (failure) {
1885
+ if (id !== requestId.current) return;
1886
+ onError(failure instanceof Error ? failure : new Error("加载远端表失败"));
1887
+ } finally {
1888
+ if (id === requestId.current) setLoading(false);
1889
+ }
1890
+ }, [source.id, onError]);
1891
+ (0, react.useEffect)(() => {
1892
+ const timer = setTimeout(() => void load(schema, keyword), keyword.trim().length > 0 ? 300 : 0);
1893
+ return () => clearTimeout(timer);
1894
+ }, [
1895
+ load,
1896
+ schema,
1897
+ keyword
1898
+ ]);
1899
+ (0, react.useEffect)(() => {
1900
+ if (scopeKey.length === 0 && scopes.length > 0) setScopeKey(scopes[0].scopeKey);
1901
+ }, [scopes, scopeKey]);
1902
+ const current = tables.find((table) => table.tableName === selected) ?? null;
1903
+ async function handleImport() {
1904
+ if (selected === null || scopeKey.length === 0 || importing) return;
1905
+ setImporting(true);
1906
+ setResult(null);
1907
+ try {
1908
+ const imported = await importSourceTable(source.id, {
1909
+ scopeKey,
1910
+ tableName: selected,
1911
+ schemaName: schema.trim().length > 0 ? schema.trim() : null,
1912
+ name: name$1.trim().length > 0 ? name$1.trim() : null
1913
+ });
1914
+ setResult(imported);
1915
+ } catch (failure) {
1916
+ onError(failure instanceof Error ? failure : new Error("导入失败"));
1917
+ } finally {
1918
+ setImporting(false);
1919
+ }
1920
+ }
1921
+ return (0, react_jsx_runtime.jsxs)("div", {
1922
+ style: {
1923
+ ...s.panel,
1924
+ background: "transparent",
1925
+ borderTop: `1px solid ${c.border}`,
1926
+ borderRadius: 0
1927
+ },
1928
+ children: [
1929
+ (0, react_jsx_runtime.jsxs)("div", {
1930
+ style: {
1931
+ display: "flex",
1932
+ justifyContent: "space-between",
1933
+ alignItems: "center",
1934
+ marginBottom: 8
1935
+ },
1936
+ children: [(0, react_jsx_runtime.jsx)("strong", {
1937
+ style: { fontSize: 13 },
1938
+ children: "远端表"
1939
+ }), (0, react_jsx_runtime.jsx)("span", {
1940
+ style: s.muted,
1941
+ children: loading ? "加载中…" : `共 ${tables.length} 张`
1942
+ })]
1943
+ }),
1944
+ (0, react_jsx_runtime.jsxs)("div", {
1945
+ style: {
1946
+ display: "flex",
1947
+ gap: 8,
1948
+ marginBottom: 10,
1949
+ flexWrap: "wrap"
1950
+ },
1951
+ children: [
1952
+ (0, react_jsx_runtime.jsx)("select", {
1953
+ style: s.select,
1954
+ value: schema,
1955
+ disabled: loading || schemas.length <= 1,
1956
+ onChange: (e) => setSchema(e.target.value),
1957
+ children: schemas.length === 0 ? (0, react_jsx_runtime.jsx)("option", {
1958
+ value: "",
1959
+ children: "(默认)"
1960
+ }) : schemas.map((item) => (0, react_jsx_runtime.jsx)("option", {
1961
+ value: item,
1962
+ children: item
1963
+ }, item))
1964
+ }),
1965
+ (0, react_jsx_runtime.jsx)("input", {
1966
+ style: {
1967
+ ...s.search,
1968
+ flex: 1,
1969
+ minWidth: 140
1970
+ },
1971
+ placeholder: "按表名过滤…",
1972
+ value: keyword,
1973
+ onChange: (e) => setKeyword(e.target.value)
1974
+ }),
1975
+ (0, react_jsx_runtime.jsx)("button", {
1976
+ type: "button",
1977
+ style: s.button,
1978
+ disabled: loading,
1979
+ onClick: () => void load(schema, keyword),
1980
+ children: "刷新"
1981
+ })
1982
+ ]
1983
+ }),
1984
+ (0, react_jsx_runtime.jsx)("div", {
1985
+ style: {
1986
+ ...s.scroll,
1987
+ maxHeight: 240
1988
+ },
1989
+ children: (0, react_jsx_runtime.jsxs)("table", {
1990
+ style: s.table,
1991
+ children: [(0, react_jsx_runtime.jsx)("thead", { children: (0, react_jsx_runtime.jsxs)("tr", { children: [
1992
+ (0, react_jsx_runtime.jsx)("th", {
1993
+ style: s.th,
1994
+ children: "表名"
1995
+ }),
1996
+ (0, react_jsx_runtime.jsx)("th", {
1997
+ style: s.th,
1998
+ children: "行数"
1999
+ }),
2000
+ (0, react_jsx_runtime.jsx)("th", {
2001
+ style: s.th,
2002
+ children: "列数"
2003
+ }),
2004
+ (0, react_jsx_runtime.jsx)("th", {
2005
+ style: s.th,
2006
+ children: "主键"
2007
+ })
2008
+ ] }) }), (0, react_jsx_runtime.jsx)("tbody", { children: tables.length === 0 && !loading ? (0, react_jsx_runtime.jsx)("tr", { children: (0, react_jsx_runtime.jsx)("td", {
2009
+ style: {
2010
+ ...s.td,
2011
+ color: c.fg2,
2012
+ textAlign: "center",
2013
+ padding: "16px 8px"
2014
+ },
2015
+ colSpan: 4,
2016
+ children: "没有匹配的表"
2017
+ }) }) : tables.map((table) => (0, react_jsx_runtime.jsxs)("tr", {
2018
+ style: {
2019
+ ...s.row,
2020
+ ...table.tableName === selected ? s.selectedRow : null
2021
+ },
2022
+ onClick: () => setSelected(table.tableName === selected ? null : table.tableName),
2023
+ children: [
2024
+ (0, react_jsx_runtime.jsx)("td", {
2025
+ style: s.td,
2026
+ title: table.tableName,
2027
+ children: table.tableName
2028
+ }),
2029
+ (0, react_jsx_runtime.jsx)("td", {
2030
+ style: s.td,
2031
+ children: formatRows(table.rowCount)
2032
+ }),
2033
+ (0, react_jsx_runtime.jsx)("td", {
2034
+ style: s.td,
2035
+ children: table.columns.length
2036
+ }),
2037
+ (0, react_jsx_runtime.jsx)("td", {
2038
+ style: s.td,
2039
+ children: table.primaryKey ?? "—"
2040
+ })
2041
+ ]
2042
+ }, table.tableName)) })]
2043
+ })
2044
+ }),
2045
+ current !== null ? (0, react_jsx_runtime.jsxs)("div", {
2046
+ style: { marginTop: 10 },
2047
+ children: [(0, react_jsx_runtime.jsxs)("div", {
2048
+ style: s.label,
2049
+ children: [
2050
+ current.tableName,
2051
+ " 的列结构(",
2052
+ current.columns.length,
2053
+ ")"
2054
+ ]
2055
+ }), (0, react_jsx_runtime.jsx)("div", {
2056
+ style: {
2057
+ ...s.scroll,
2058
+ maxHeight: 180
2059
+ },
2060
+ children: (0, react_jsx_runtime.jsxs)("table", {
2061
+ style: s.table,
2062
+ children: [(0, react_jsx_runtime.jsx)("thead", { children: (0, react_jsx_runtime.jsxs)("tr", { children: [
2063
+ (0, react_jsx_runtime.jsx)("th", {
2064
+ style: s.th,
2065
+ children: "列名"
2066
+ }),
2067
+ (0, react_jsx_runtime.jsx)("th", {
2068
+ style: s.th,
2069
+ children: "类型"
2070
+ }),
2071
+ (0, react_jsx_runtime.jsx)("th", {
2072
+ style: s.th,
2073
+ children: "可空"
2074
+ }),
2075
+ (0, react_jsx_runtime.jsx)("th", {
2076
+ style: s.th,
2077
+ children: "注释"
2078
+ })
2079
+ ] }) }), (0, react_jsx_runtime.jsx)("tbody", { children: current.columns.map((column) => (0, react_jsx_runtime.jsxs)("tr", { children: [
2080
+ (0, react_jsx_runtime.jsx)("td", {
2081
+ style: s.td,
2082
+ title: column.name,
2083
+ children: column.name
2084
+ }),
2085
+ (0, react_jsx_runtime.jsx)("td", {
2086
+ style: s.td,
2087
+ children: ADMIN_COLUMN_TYPE_LABELS[column.type] ?? column.type
2088
+ }),
2089
+ (0, react_jsx_runtime.jsx)("td", {
2090
+ style: s.td,
2091
+ children: column.nullable ? "是" : "否"
2092
+ }),
2093
+ (0, react_jsx_runtime.jsx)("td", {
2094
+ style: {
2095
+ ...s.td,
2096
+ color: c.fg2
2097
+ },
2098
+ title: column.description ?? "",
2099
+ children: column.description ?? "—"
2100
+ })
2101
+ ] }, column.name)) })]
2102
+ })
2103
+ })]
2104
+ }) : null,
2105
+ (0, react_jsx_runtime.jsxs)("div", {
2106
+ style: {
2107
+ ...s.bar,
2108
+ marginTop: 12
2109
+ },
2110
+ children: [
2111
+ (0, react_jsx_runtime.jsx)("select", {
2112
+ style: s.select,
2113
+ value: scopeKey,
2114
+ disabled: importing || selected === null,
2115
+ onChange: (e) => setScopeKey(e.target.value),
2116
+ children: scopes.length === 0 ? (0, react_jsx_runtime.jsx)("option", {
2117
+ value: "",
2118
+ children: "(暂无可用工作区)"
2119
+ }) : scopes.map((scope) => (0, react_jsx_runtime.jsx)("option", {
2120
+ value: scope.scopeKey,
2121
+ children: scope.scopeKey
2122
+ }, scope.scopeKey))
2123
+ }),
2124
+ (0, react_jsx_runtime.jsx)("input", {
2125
+ style: {
2126
+ ...s.search,
2127
+ flex: 1,
2128
+ minWidth: 120
2129
+ },
2130
+ placeholder: "数据集名称(默认取表名)",
2131
+ value: name$1,
2132
+ disabled: importing,
2133
+ onChange: (e) => setName(e.target.value)
2134
+ }),
2135
+ (0, react_jsx_runtime.jsx)("button", {
2136
+ type: "button",
2137
+ style: {
2138
+ ...s.primaryButton,
2139
+ ...selected === null || scopeKey.length === 0 || importing ? s.disabledButton : null
2140
+ },
2141
+ disabled: selected === null || scopeKey.length === 0 || importing,
2142
+ onClick: () => void handleImport(),
2143
+ children: importing ? "导入中…" : "导入到工作区"
2144
+ })
2145
+ ]
2146
+ }),
2147
+ result !== null ? (0, react_jsx_runtime.jsx)("div", {
2148
+ style: {
2149
+ ...s.banner,
2150
+ marginTop: 8,
2151
+ marginBottom: 0,
2152
+ borderColor: c.ok,
2153
+ color: c.ok
2154
+ },
2155
+ children: (0, react_jsx_runtime.jsxs)("span", { children: [
2156
+ "已导入 ",
2157
+ result.name,
2158
+ "(datasetId: ",
2159
+ result.datasetId,
2160
+ "):",
2161
+ result.rowCount,
2162
+ " 行 / ",
2163
+ result.columnCount,
2164
+ " 列",
2165
+ result.status === "running" ? "(后台导入中,完成后会通知)" : ""
2166
+ ] })
2167
+ }) : null
2168
+ ]
2169
+ });
2170
+ }
2171
+
2172
+ //#endregion
2173
+ //#region src/client/settings/DataSourcesPanel.tsx
2174
+ const READ_ONLY_CODES = [
2175
+ "READ_ONLY",
2176
+ "ADMIN_DISABLED",
2177
+ "SOURCE_DISABLED"
2178
+ ];
2179
+ function DataSourcesPanel() {
2180
+ const [items, setItems] = (0, react.useState)([]);
2181
+ const [scopes, setScopes] = (0, react.useState)([]);
2182
+ const [query, setQuery] = (0, react.useState)("");
2183
+ const [selectedId, setSelectedId] = (0, react.useState)(null);
2184
+ const [mode, setMode] = (0, react.useState)("browse");
2185
+ const [loading, setLoading] = (0, react.useState)(false);
2186
+ const [writable, setWritable] = (0, react.useState)(true);
2187
+ const [error, setError] = (0, react.useState)(null);
2188
+ const [notice, setNotice] = (0, react.useState)(null);
2189
+ const [testingId, setTestingId] = (0, react.useState)(null);
2190
+ const [confirmId, setConfirmId] = (0, react.useState)(null);
2191
+ const loadScopes = (0, react.useCallback)(async () => {
2192
+ try {
2193
+ setScopes((await listScopes()).scopes);
2194
+ } catch {}
2195
+ }, []);
2196
+ const loadList = (0, react.useCallback)(async () => {
2197
+ setLoading(true);
2198
+ try {
2199
+ setItems((await listDataSources()).sources);
2200
+ setError(null);
2201
+ } catch (failure) {
2202
+ setError(failure instanceof Error ? failure.message : "加载数据源失败");
2203
+ if (failure instanceof AdminApiError && READ_ONLY_CODES.includes(failure.code)) setWritable(false);
2204
+ } finally {
2205
+ setLoading(false);
2206
+ }
2207
+ }, []);
2208
+ (0, react.useEffect)(() => {
2209
+ void loadScopes();
2210
+ void loadList();
2211
+ }, [loadScopes, loadList]);
2212
+ const selected = items.find((item) => item.id === selectedId) ?? null;
2213
+ const needle = query.trim().toLowerCase();
2214
+ const visible = needle.length === 0 ? items : items.filter((item) => item.name.toLowerCase().includes(needle) || item.type.toLowerCase().includes(needle) || item.host.toLowerCase().includes(needle) || item.database.toLowerCase().includes(needle));
2215
+ async function handleTest(id) {
2216
+ if (testingId !== null) return;
2217
+ setTestingId(id);
2218
+ setNotice(null);
2219
+ try {
2220
+ const result = await testDataSource({ source: id });
2221
+ setNotice(result.success ? `连接成功(${result.latency}ms${result.version === null ? "" : ` · ${result.version}`})` : `连接失败:${result.error ?? "未知错误"}`);
2222
+ await loadList();
2223
+ } catch (failure) {
2224
+ setError(failure instanceof Error ? failure.message : "测试连接失败");
2225
+ } finally {
2226
+ setTestingId(null);
2227
+ }
2228
+ }
2229
+ async function handleDelete(id) {
2230
+ setConfirmId(null);
2231
+ try {
2232
+ await deleteDataSource(id);
2233
+ if (selectedId === id) {
2234
+ setSelectedId(null);
2235
+ setMode("browse");
2236
+ }
2237
+ await loadList();
2238
+ } catch (failure) {
2239
+ setError(failure instanceof Error ? failure.message : "删除失败");
2240
+ }
2241
+ }
2242
+ async function handleSaved(view) {
2243
+ setMode("browse");
2244
+ setNotice(`已保存数据源「${view.name}」`);
2245
+ await loadList();
2246
+ setSelectedId(view.id);
2247
+ }
2248
+ function handleError(failure) {
2249
+ setError(failure.message);
2250
+ if (failure instanceof AdminApiError && READ_ONLY_CODES.includes(failure.code)) setWritable(false);
2251
+ }
2252
+ return (0, react_jsx_runtime.jsxs)("div", { children: [
2253
+ (0, react_jsx_runtime.jsxs)("div", {
2254
+ style: s.toolbar,
2255
+ children: [
2256
+ (0, react_jsx_runtime.jsx)("input", {
2257
+ style: s.search,
2258
+ placeholder: "按名称 / 类型 / 主机 / 库名搜索…",
2259
+ value: query,
2260
+ onChange: (e) => setQuery(e.target.value)
2261
+ }),
2262
+ (0, react_jsx_runtime.jsx)("button", {
2263
+ type: "button",
2264
+ style: {
2265
+ ...s.primaryButton,
2266
+ ...!writable ? s.disabledButton : null
2267
+ },
2268
+ disabled: !writable,
2269
+ onClick: () => {
2270
+ setMode("create");
2271
+ setSelectedId(null);
2272
+ },
2273
+ title: writable ? "新建数据源" : "只读模式、接口已关闭或数据源功能未启用",
2274
+ children: "+ 新建数据源"
2275
+ }),
2276
+ (0, react_jsx_runtime.jsx)("button", {
2277
+ type: "button",
2278
+ style: s.button,
2279
+ disabled: loading,
2280
+ onClick: () => void loadList(),
2281
+ children: "刷新"
2282
+ })
2283
+ ]
2284
+ }),
2285
+ error !== null ? (0, react_jsx_runtime.jsxs)("div", {
2286
+ style: s.banner,
2287
+ children: [(0, react_jsx_runtime.jsx)("span", { children: error }), (0, react_jsx_runtime.jsx)("span", {
2288
+ style: {
2289
+ ...s.link,
2290
+ marginLeft: "auto"
2291
+ },
2292
+ onClick: () => setError(null),
2293
+ children: "忽略"
2294
+ })]
2295
+ }) : null,
2296
+ notice !== null ? (0, react_jsx_runtime.jsxs)("div", {
2297
+ style: {
2298
+ ...s.banner,
2299
+ borderColor: c.ok,
2300
+ color: c.ok
2301
+ },
2302
+ children: [(0, react_jsx_runtime.jsx)("span", { children: notice }), (0, react_jsx_runtime.jsx)("span", {
2303
+ style: {
2304
+ ...s.link,
2305
+ marginLeft: "auto"
2306
+ },
2307
+ onClick: () => setNotice(null),
2308
+ children: "忽略"
2309
+ })]
2310
+ }) : null,
2311
+ confirmId !== null ? (0, react_jsx_runtime.jsxs)("div", {
2312
+ style: s.banner,
2313
+ children: [
2314
+ (0, react_jsx_runtime.jsx)("span", { children: "确认删除这个数据源?该操作不可恢复。" }),
2315
+ (0, react_jsx_runtime.jsx)("button", {
2316
+ type: "button",
2317
+ style: {
2318
+ ...s.dangerButton,
2319
+ marginLeft: "auto"
2320
+ },
2321
+ onClick: () => void handleDelete(confirmId),
2322
+ children: "删除"
2323
+ }),
2324
+ (0, react_jsx_runtime.jsx)("button", {
2325
+ type: "button",
2326
+ style: s.button,
2327
+ onClick: () => setConfirmId(null),
2328
+ children: "取消"
2329
+ })
2330
+ ]
2331
+ }) : null,
2332
+ (0, react_jsx_runtime.jsx)("div", {
2333
+ style: s.scroll,
2334
+ children: (0, react_jsx_runtime.jsxs)("table", {
2335
+ style: s.table,
2336
+ children: [(0, react_jsx_runtime.jsx)("thead", { children: (0, react_jsx_runtime.jsxs)("tr", { children: [
2337
+ (0, react_jsx_runtime.jsx)("th", {
2338
+ style: s.th,
2339
+ children: "名称"
2340
+ }),
2341
+ (0, react_jsx_runtime.jsx)("th", {
2342
+ style: s.th,
2343
+ children: "类型"
2344
+ }),
2345
+ (0, react_jsx_runtime.jsx)("th", {
2346
+ style: s.th,
2347
+ children: "地址"
2348
+ }),
2349
+ (0, react_jsx_runtime.jsx)("th", {
2350
+ style: s.th,
2351
+ children: "库名"
2352
+ }),
2353
+ (0, react_jsx_runtime.jsx)("th", {
2354
+ style: s.th,
2355
+ children: "状态"
2356
+ }),
2357
+ (0, react_jsx_runtime.jsx)("th", {
2358
+ style: s.th,
2359
+ children: "最近检测"
2360
+ }),
2361
+ (0, react_jsx_runtime.jsx)("th", {
2362
+ style: s.th,
2363
+ children: "操作"
2364
+ })
2365
+ ] }) }), (0, react_jsx_runtime.jsx)("tbody", { children: visible.length === 0 && !loading ? (0, react_jsx_runtime.jsx)("tr", { children: (0, react_jsx_runtime.jsx)("td", {
2366
+ style: {
2367
+ ...s.td,
2368
+ color: c.fg2,
2369
+ textAlign: "center",
2370
+ padding: "18px 8px"
2371
+ },
2372
+ colSpan: 7,
2373
+ children: "还没有数据源,点击「新建数据源」登记一个 MySQL / PostgreSQL 连接"
2374
+ }) }) : visible.map((item) => (0, react_jsx_runtime.jsxs)("tr", {
2375
+ style: {
2376
+ ...s.row,
2377
+ ...item.id === selectedId ? s.selectedRow : null
2378
+ },
2379
+ onClick: () => {
2380
+ setSelectedId(item.id);
2381
+ setMode("browse");
2382
+ },
2383
+ children: [
2384
+ (0, react_jsx_runtime.jsx)("td", {
2385
+ style: s.td,
2386
+ title: item.description ?? "",
2387
+ children: item.name
2388
+ }),
2389
+ (0, react_jsx_runtime.jsx)("td", {
2390
+ style: s.td,
2391
+ children: (0, react_jsx_runtime.jsx)("span", {
2392
+ style: {
2393
+ display: "inline-block",
2394
+ padding: "0 6px",
2395
+ borderRadius: 4,
2396
+ fontSize: 11,
2397
+ color: TYPE_BADGE[item.type] ?? c.fg2,
2398
+ border: `1px solid ${TYPE_BADGE[item.type] ?? c.border}`
2399
+ },
2400
+ children: ADMIN_SOURCE_TYPE_LABELS[item.type] ?? item.type
2401
+ })
2402
+ }),
2403
+ (0, react_jsx_runtime.jsxs)("td", {
2404
+ style: s.td,
2405
+ title: `${item.host}:${item.port}`,
2406
+ children: [
2407
+ item.host,
2408
+ ":",
2409
+ item.port
2410
+ ]
2411
+ }),
2412
+ (0, react_jsx_runtime.jsx)("td", {
2413
+ style: s.td,
2414
+ children: item.database
2415
+ }),
2416
+ (0, react_jsx_runtime.jsxs)("td", {
2417
+ style: s.td,
2418
+ children: [(0, react_jsx_runtime.jsx)("span", { style: {
2419
+ ...s.dot,
2420
+ background: SOURCE_STATUS_COLOR[item.status] ?? c.fg2
2421
+ } }), SOURCE_STATUS_TEXT[item.status] ?? item.status]
2422
+ }),
2423
+ (0, react_jsx_runtime.jsx)("td", {
2424
+ style: {
2425
+ ...s.td,
2426
+ color: c.fg2
2427
+ },
2428
+ children: item.lastCheckedAt === null ? "—" : fmtAgo(item.lastCheckedAt)
2429
+ }),
2430
+ (0, react_jsx_runtime.jsxs)("td", {
2431
+ style: {
2432
+ ...s.td,
2433
+ whiteSpace: "nowrap"
2434
+ },
2435
+ children: [
2436
+ (0, react_jsx_runtime.jsx)("span", {
2437
+ style: {
2438
+ ...s.link,
2439
+ marginRight: 8,
2440
+ ...testingId !== null ? s.disabledButton : null
2441
+ },
2442
+ onClick: (event) => {
2443
+ event.stopPropagation();
2444
+ if (testingId === null) void handleTest(item.id);
2445
+ },
2446
+ children: testingId === item.id ? "测试中…" : "测试"
2447
+ }),
2448
+ (0, react_jsx_runtime.jsx)("span", {
2449
+ style: {
2450
+ ...s.link,
2451
+ marginRight: 8,
2452
+ ...!writable ? s.disabledButton : null
2453
+ },
2454
+ onClick: (event) => {
2455
+ event.stopPropagation();
2456
+ if (!writable) return;
2457
+ setSelectedId(item.id);
2458
+ setMode("edit");
2459
+ },
2460
+ children: "编辑"
2461
+ }),
2462
+ (0, react_jsx_runtime.jsx)("span", {
2463
+ style: {
2464
+ ...s.link,
2465
+ color: c.danger,
2466
+ ...!writable ? s.disabledButton : null
2467
+ },
2468
+ onClick: (event) => {
2469
+ event.stopPropagation();
2470
+ if (writable) setConfirmId(item.id);
2471
+ },
2472
+ children: "删除"
2473
+ })
2474
+ ]
2475
+ })
2476
+ ]
2477
+ }, item.id)) })]
2478
+ })
2479
+ }),
2480
+ mode === "create" ? (0, react_jsx_runtime.jsx)(DataSourceForm, {
2481
+ onSaved: (view) => void handleSaved(view),
2482
+ onCancel: () => setMode("browse"),
2483
+ onError: handleError
2484
+ }) : mode === "edit" && selected !== null ? (0, react_jsx_runtime.jsx)(DataSourceForm, {
2485
+ initial: selected,
2486
+ onSaved: (view) => void handleSaved(view),
2487
+ onCancel: () => setMode("browse"),
2488
+ onError: handleError
2489
+ }) : selected !== null ? (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsxs)("div", {
2490
+ style: s.panel,
2491
+ children: [
2492
+ (0, react_jsx_runtime.jsxs)("div", {
2493
+ style: {
2494
+ display: "flex",
2495
+ justifyContent: "space-between",
2496
+ marginBottom: 8
2497
+ },
2498
+ children: [(0, react_jsx_runtime.jsx)("strong", {
2499
+ style: { fontSize: 13 },
2500
+ children: selected.name
2501
+ }), (0, react_jsx_runtime.jsxs)("span", {
2502
+ style: s.muted,
2503
+ children: [
2504
+ ADMIN_SOURCE_TYPE_LABELS[selected.type] ?? selected.type,
2505
+ " · 最近更新 ",
2506
+ fmtAgo(selected.updatedAt)
2507
+ ]
2508
+ })]
2509
+ }),
2510
+ (0, react_jsx_runtime.jsxs)("div", {
2511
+ style: s.grid2,
2512
+ children: [
2513
+ (0, react_jsx_runtime.jsxs)("div", { children: [
2514
+ (0, react_jsx_runtime.jsx)("span", {
2515
+ style: s.muted,
2516
+ children: "地址:"
2517
+ }),
2518
+ selected.host,
2519
+ ":",
2520
+ selected.port
2521
+ ] }),
2522
+ (0, react_jsx_runtime.jsxs)("div", { children: [(0, react_jsx_runtime.jsx)("span", {
2523
+ style: s.muted,
2524
+ children: "库名:"
2525
+ }), selected.database] }),
2526
+ (0, react_jsx_runtime.jsxs)("div", { children: [(0, react_jsx_runtime.jsx)("span", {
2527
+ style: s.muted,
2528
+ children: "用户名:"
2529
+ }), selected.username] }),
2530
+ (0, react_jsx_runtime.jsxs)("div", { children: [(0, react_jsx_runtime.jsx)("span", {
2531
+ style: s.muted,
2532
+ children: "密码:"
2533
+ }), selected.hasPassword ? "已设置 · 不可查看" : "未设置"] }),
2534
+ (0, react_jsx_runtime.jsxs)("div", { children: [(0, react_jsx_runtime.jsx)("span", {
2535
+ style: s.muted,
2536
+ children: "SSL:"
2537
+ }), selected.sslMode ?? "—"] }),
2538
+ (0, react_jsx_runtime.jsxs)("div", { children: [(0, react_jsx_runtime.jsx)("span", {
2539
+ style: s.muted,
2540
+ children: "连接池上限:"
2541
+ }), selected.poolMax ?? "—"] })
2542
+ ]
2543
+ }),
2544
+ selected.description !== null ? (0, react_jsx_runtime.jsx)("div", {
2545
+ style: {
2546
+ ...s.muted,
2547
+ marginTop: 6
2548
+ },
2549
+ children: selected.description
2550
+ }) : null,
2551
+ selected.lastError !== null ? (0, react_jsx_runtime.jsxs)("div", {
2552
+ style: {
2553
+ ...s.muted,
2554
+ marginTop: 6,
2555
+ color: c.danger
2556
+ },
2557
+ children: ["最近错误:", selected.lastError]
2558
+ }) : null
2559
+ ]
2560
+ }), (0, react_jsx_runtime.jsx)(SourceTablesPanel, {
2561
+ source: selected,
2562
+ scopes,
2563
+ onError: handleError
2564
+ })] }) : (0, react_jsx_runtime.jsx)("div", {
2565
+ style: {
2566
+ ...s.muted,
2567
+ marginTop: 12
2568
+ },
2569
+ children: loading ? "加载中…" : "选择一个数据源查看详情与远端表,或点击「新建数据源」。"
2570
+ })
2571
+ ] });
2572
+ }
2573
+
2574
+ //#endregion
2575
+ //#region src/client/settings/Section.tsx
2576
+ const TABS = [{
2577
+ key: "datasets",
2578
+ label: "数据集"
2579
+ }, {
2580
+ key: "sources",
2581
+ label: "数据源"
2582
+ }];
2583
+ function DatasetSettingsSection() {
2584
+ const [tab, setTab] = (0, react.useState)("datasets");
2585
+ return (0, react_jsx_runtime.jsxs)("div", {
2586
+ style: s.page,
2587
+ children: [(0, react_jsx_runtime.jsx)("div", {
2588
+ style: s.tabs,
2589
+ children: TABS.map((entry) => (0, react_jsx_runtime.jsx)("button", {
2590
+ type: "button",
2591
+ style: {
2592
+ ...s.tab,
2593
+ ...tab === entry.key ? s.activeTab : null
2594
+ },
2595
+ onClick: () => setTab(entry.key),
2596
+ children: entry.label
2597
+ }, entry.key))
2598
+ }), tab === "datasets" ? (0, react_jsx_runtime.jsx)(DatasetsPanel, {}) : (0, react_jsx_runtime.jsx)(DataSourcesPanel, {})]
2599
+ });
2600
+ }
2601
+
2602
+ //#endregion
2603
+ //#region src/client/index.ts
2604
+ /** 本插件注册的工具名,也是 `tool.call.toolview` 的 keyed 派发键。 */
2605
+ const TOOL_KEY = "dataset_query";
2606
+ const TOOLVIEW_SLOT = "tool.call.toolview";
2607
+ function asViewMeta(value) {
2608
+ if (typeof value !== "object" || value === null) return undefined;
2609
+ const meta = value;
2610
+ if (meta.kind !== "dataset-view") return undefined;
2611
+ if (typeof meta.viewId !== "string" || typeof meta.endpoint !== "string") return undefined;
2612
+ return meta;
2613
+ }
2614
+ const styles = {
2615
+ wrap: {
2616
+ margin: "8px 0",
2617
+ fontSize: 12,
2618
+ color: "var(--dsw-fg-secondary, #666)"
2619
+ },
2620
+ head: {
2621
+ display: "flex",
2622
+ alignItems: "center",
2623
+ gap: 8,
2624
+ marginBottom: 6
2625
+ },
2626
+ title: {
2627
+ fontWeight: 600,
2628
+ color: "var(--dsw-fg, inherit)"
2629
+ },
2630
+ scroll: {
2631
+ maxHeight: 420,
2632
+ overflow: "auto",
2633
+ border: "1px solid var(--dsw-border, #e5e5e5)",
2634
+ borderRadius: 6
2635
+ },
2636
+ table: {
2637
+ borderCollapse: "collapse",
2638
+ width: "100%",
2639
+ fontSize: 12
2640
+ },
2641
+ th: {
2642
+ position: "sticky",
2643
+ top: 0,
2644
+ background: "var(--dsw-bg-subtle, #fafafa)",
2645
+ textAlign: "left",
2646
+ padding: "6px 8px",
2647
+ borderBottom: "1px solid var(--dsw-border, #e5e5e5)",
2648
+ whiteSpace: "nowrap"
2649
+ },
2650
+ thButton: {
2651
+ background: "none",
2652
+ border: "none",
2653
+ cursor: "pointer",
2654
+ font: "inherit",
2655
+ padding: 0,
2656
+ color: "inherit"
2657
+ },
2658
+ td: {
2659
+ padding: "4px 8px",
2660
+ borderBottom: "1px solid var(--dsw-border, #eee)",
2661
+ maxWidth: 320,
2662
+ overflow: "hidden",
2663
+ textOverflow: "ellipsis",
2664
+ whiteSpace: "nowrap"
2665
+ },
2666
+ bar: {
2667
+ display: "flex",
2668
+ alignItems: "center",
2669
+ gap: 8,
2670
+ marginTop: 6,
2671
+ flexWrap: "wrap"
2672
+ },
2673
+ button: {
2674
+ border: "1px solid var(--dsw-border, #ddd)",
2675
+ background: "transparent",
2676
+ borderRadius: 4,
2677
+ padding: "2px 8px",
2678
+ cursor: "pointer",
2679
+ fontSize: 12
2680
+ },
2681
+ muted: { opacity: .7 },
2682
+ error: { color: "var(--dsw-danger, #c0392b)" },
2683
+ warn: {
2684
+ background: "#FEF3C7",
2685
+ color: "#92400E",
2686
+ padding: "6px 10px",
2687
+ borderRadius: 6,
2688
+ fontSize: 12,
2689
+ marginBottom: 6
2690
+ }
2691
+ };
2692
+ function formatCell(value) {
2693
+ if (value === null || value === undefined) return "";
2694
+ if (typeof value === "object") try {
2695
+ return JSON.stringify(value);
2696
+ } catch {
2697
+ return "[object]";
2698
+ }
2699
+ return String(value);
2700
+ }
2701
+ /** 命中即需要加引号转义的字符:逗号、双引号、回车、换行。 */
2702
+ const CSV_NEEDS_QUOTE = /[",\r\n]/;
2703
+ /** Excel 公式注入的高危前缀;只对文本值生效,负数等数值原样导出。 */
2704
+ const CSV_RISKY_PREFIX = /^[=+\-@\t\r]/;
2705
+ function toCsvCell(value) {
2706
+ let text = formatCell(value);
2707
+ if (text.length === 0) return "";
2708
+ if (typeof value === "string" && CSV_RISKY_PREFIX.test(text)) text = `'${text}`;
2709
+ if (CSV_NEEDS_QUOTE.test(text)) text = `"${text.replace(/"/g, "\"\"")}"`;
2710
+ return text;
2711
+ }
2712
+ function toCsvRow(cells) {
2713
+ return cells.map((cell) => toCsvCell(cell)).join(",");
2714
+ }
2715
+ /** 表头 + 数据行;行分隔用 `\r\n` 且末行收尾,Excel / Numbers 都不会串列。 */
2716
+ function toCsv(columns, rows) {
2717
+ const lines = [toCsvRow(columns)];
2718
+ for (const row of rows) lines.push(toCsvRow(columns.map((column) => row[column])));
2719
+ return `${lines.join("\r\n")}\r\n`;
2720
+ }
2721
+ /** 只拼数据行:导出逐页追加时用(表头由 `toCsv` 的首页分片带出)。 */
2722
+ function toCsvRows(columns, rows) {
2723
+ if (rows.length === 0) return "";
2724
+ return `${rows.map((row) => toCsvRow(columns.map((column) => row[column]))).join("\r\n")}\r\n`;
2725
+ }
2726
+ /** 文件名 `名称-YYYYMMDD-HHmmss.csv`,剔除文件系统不接受与控制字符。 */
2727
+ function buildCsvFileName(name$1, now = new Date()) {
2728
+ const pad = (value) => String(value).padStart(2, "0");
2729
+ const stamp = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}` + `-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
2730
+ const safe = name$1.replace(/[\\/:*?"<>|\r\n\t]/g, "_").trim();
2731
+ return `${safe.length > 0 ? safe : "dataset"}-${stamp}.csv`;
2732
+ }
2733
+ /** Blob + 临时 `<a download>` 触发下载;前置 BOM 让 Excel 认成 UTF-8。 */
2734
+ function downloadCsv(filename, text) {
2735
+ const url = URL.createObjectURL(new Blob([`\uFEFF${text}`], { type: "text/csv;charset=utf-8" }));
2736
+ const link = document.createElement("a");
2737
+ link.href = url;
2738
+ link.download = filename;
2739
+ link.style.display = "none";
2740
+ document.body.appendChild(link);
2741
+ link.click();
2742
+ link.remove();
2743
+ setTimeout(() => URL.revokeObjectURL(url), 0);
2744
+ }
2745
+ function DatasetViewCard(props) {
2746
+ const meta = (0, react.useMemo)(() => asViewMeta(props.block?.meta), [props.block]);
2747
+ const [page, setPage] = (0, react.useState)(1);
2748
+ const [sort, setSort] = (0, react.useState)(undefined);
2749
+ const [state, setState] = (0, react.useState)({ status: "loading" });
2750
+ const requestId = (0, react.useRef)(0);
2751
+ const exportAbort = (0, react.useRef)(undefined);
2752
+ const [exportState, setExportState] = (0, react.useState)({ running: false });
2753
+ (0, react.useEffect)(() => () => exportAbort.current?.abort(), []);
2754
+ (0, react.useEffect)(() => {
2755
+ if (meta === undefined) return;
2756
+ const controller = new AbortController();
2757
+ const id = requestId.current + 1;
2758
+ requestId.current = id;
2759
+ setState((previous) => previous.status === "ready" ? {
2760
+ ...previous,
2761
+ status: "loading"
2762
+ } : previous);
2763
+ const url = new URL(meta.endpoint, window.location.origin);
2764
+ url.searchParams.set("page", String(page));
2765
+ url.searchParams.set("pageSize", String(meta.pageSize));
2766
+ if (sort !== undefined) {
2767
+ url.searchParams.set("sort", sort.column);
2768
+ url.searchParams.set("order", sort.order);
2769
+ }
2770
+ void (async () => {
2771
+ try {
2772
+ const response = await fetch(url.toString(), {
2773
+ signal: controller.signal,
2774
+ credentials: "same-origin"
2775
+ });
2776
+ if (!response.ok) {
2777
+ const body$1 = await response.json().catch(() => null);
2778
+ if (id === requestId.current) setState((previous) => ({
2779
+ status: "error",
2780
+ payload: previous.payload,
2781
+ message: body$1?.error?.code === "VIEW_NOT_FOUND" || response.status === 404 ? "数据接口不可用(请检查视图路由前缀配置或插件是否已启用视图)" : `取数失败(HTTP ${response.status})`
2782
+ }));
2783
+ return;
2784
+ }
2785
+ const payload$1 = await response.json();
2786
+ if (id === requestId.current) setState({
2787
+ status: "ready",
2788
+ payload: payload$1
2789
+ });
2790
+ } catch (error) {
2791
+ if (controller.signal.aborted) return;
2792
+ if (id === requestId.current) setState({
2793
+ status: "error",
2794
+ message: `取数失败:${String(error)}`
2795
+ });
2796
+ }
2797
+ })();
2798
+ return () => controller.abort();
2799
+ }, [
2800
+ meta,
2801
+ page,
2802
+ sort
2803
+ ]);
2804
+ const toggleSort = (0, react.useCallback)((column) => {
2805
+ if (meta === undefined || !meta.sortable.includes(column)) return;
2806
+ if (exportAbort.current !== undefined) {
2807
+ exportAbort.current.abort();
2808
+ exportAbort.current = undefined;
2809
+ setExportState({ running: false });
2810
+ }
2811
+ setPage(1);
2812
+ setSort((previous) => previous?.column === column ? {
2813
+ column,
2814
+ order: previous.order === "asc" ? "desc" : "asc"
2815
+ } : {
2816
+ column,
2817
+ order: "asc"
2818
+ });
2819
+ }, [meta]);
2820
+ /**
2821
+
2822
+ * 导出全部可浏览行为 CSV:把 `pageSize` 顶到 `maxPageSize`,从第 1 页顺序翻到
2823
+
2824
+ * 末页,不受当前页码/每页行数限制。每页转成字符串分片后即丢掉行对象,
2825
+
2826
+ * 避免几万行对象同时常驻;最后拼一次 Blob 触发下载。
2827
+
2828
+ */
2829
+ const exportCsv = (0, react.useCallback)(() => {
2830
+ if (meta === undefined) return;
2831
+ exportAbort.current?.abort();
2832
+ const controller = new AbortController();
2833
+ exportAbort.current = controller;
2834
+ setExportState({
2835
+ running: true,
2836
+ progress: {
2837
+ done: 0,
2838
+ total: meta.totalRows
2839
+ }
2840
+ });
2841
+ void (async () => {
2842
+ try {
2843
+ const base = new URL(meta.endpoint, window.location.origin);
2844
+ base.searchParams.set("pageSize", String(Math.max(1, Math.floor(meta.maxPageSize > 0 ? meta.maxPageSize : meta.pageSize))));
2845
+ if (sort !== undefined) {
2846
+ base.searchParams.set("sort", sort.column);
2847
+ base.searchParams.set("order", sort.order);
2848
+ }
2849
+ const chunks = [];
2850
+ let exportColumns = meta.columns.map((column) => column.name);
2851
+ let totalRows = meta.totalRows;
2852
+ let totalPages$1 = 1;
2853
+ let done = 0;
2854
+ for (let index = 1; index <= totalPages$1; index += 1) {
2855
+ if (controller.signal.aborted) return;
2856
+ const url = new URL(base.toString());
2857
+ url.searchParams.set("page", String(index));
2858
+ const response = await fetch(url.toString(), {
2859
+ signal: controller.signal,
2860
+ credentials: "same-origin"
2861
+ });
2862
+ if (!response.ok) {
2863
+ const body$1 = await response.json().catch(() => null);
2864
+ if (controller.signal.aborted) return;
2865
+ throw new Error(body$1?.error?.code === "VIEW_NOT_FOUND" || response.status === 404 ? "数据接口不可用(请检查视图路由前缀配置或插件是否已启用视图)" : `导出失败(HTTP ${response.status})`);
2866
+ }
2867
+ const current = await response.json();
2868
+ if (controller.signal.aborted) return;
2869
+ if (index === 1) {
2870
+ exportColumns = current.columns.length > 0 ? current.columns : exportColumns;
2871
+ totalRows = current.totalRows;
2872
+ totalPages$1 = Math.max(1, current.totalPages);
2873
+ chunks.push(toCsv(exportColumns, current.rows));
2874
+ } else chunks.push(toCsvRows(exportColumns, current.rows));
2875
+ done += current.rows.length;
2876
+ setExportState({
2877
+ running: true,
2878
+ progress: {
2879
+ done,
2880
+ total: totalRows
2881
+ }
2882
+ });
2883
+ }
2884
+ downloadCsv(buildCsvFileName(meta.name), chunks.join(""));
2885
+ if (exportAbort.current === controller) exportAbort.current = undefined;
2886
+ setExportState({ running: false });
2887
+ } catch (error) {
2888
+ if (controller.signal.aborted) return;
2889
+ if (exportAbort.current === controller) exportAbort.current = undefined;
2890
+ setExportState({
2891
+ running: false,
2892
+ error: error instanceof Error ? error.message : `导出失败:${String(error)}`
2893
+ });
2894
+ }
2895
+ })();
2896
+ }, [meta, sort]);
2897
+ if (meta === undefined) {
2898
+ const text = (props.block?.content ?? []).filter((block) => block.type === "text").map((block) => block.text ?? "").join("\n");
2899
+ return react.default.createElement("div", { style: styles.wrap }, react.default.createElement("pre", { style: {
2900
+ margin: 0,
2901
+ whiteSpace: "pre-wrap"
2902
+ } }, text));
2903
+ }
2904
+ const payload = state.payload;
2905
+ const columns = payload?.columns ?? meta.columns.map((column) => column.name);
2906
+ const rows = payload?.rows ?? [];
2907
+ const totalPages = payload?.totalPages ?? Math.max(1, Math.ceil(meta.totalRows / meta.pageSize));
2908
+ const busy = state.status === "loading" && payload === undefined;
2909
+ const servableRows = payload === undefined ? meta.totalRows : payload.totalPages * payload.pageSize;
2910
+ const head = react.default.createElement("tr", null, ...columns.map((column) => react.default.createElement("th", {
2911
+ key: column,
2912
+ style: styles.th
2913
+ }, meta.sortable.includes(column) ? react.default.createElement("button", {
2914
+ type: "button",
2915
+ style: styles.thButton,
2916
+ onClick: () => toggleSort(column),
2917
+ title: "按该列排序"
2918
+ }, `${column}${sort?.column === column ? sort.order === "asc" ? " ↑" : " ↓" : ""}`) : column)));
2919
+ const body = rows.map((row, index) => react.default.createElement("tr", { key: String(row._row_id ?? index) }, ...columns.map((column) => react.default.createElement("td", {
2920
+ key: column,
2921
+ style: styles.td,
2922
+ title: formatCell(row[column])
2923
+ }, formatCell(row[column])))));
2924
+ return react.default.createElement("div", { style: styles.wrap }, react.default.createElement("div", { style: styles.warn }, "数据可能已发生变化,以最新查询结果为准"), react.default.createElement("div", { style: styles.head }, react.default.createElement("span", { style: styles.title }, `数据集 ${meta.name}`), react.default.createElement("span", null, `共 ${meta.totalRows} 行 · ${meta.columns.length} 列`), servableRows < meta.totalRows ? react.default.createElement("span", { style: styles.muted }, `(仅可浏览前 ${servableRows} 行)`) : null, state.status === "error" ? react.default.createElement("span", { style: styles.error }, state.message ?? "") : null, busy ? react.default.createElement("span", { style: styles.muted }, "加载中…") : null), react.default.createElement("div", { style: styles.scroll }, react.default.createElement("table", { style: styles.table }, react.default.createElement("thead", null, head), react.default.createElement("tbody", null, ...body))), react.default.createElement("div", { style: styles.bar }, react.default.createElement("button", {
2925
+ type: "button",
2926
+ style: styles.button,
2927
+ disabled: page <= 1,
2928
+ onClick: () => setPage(1)
2929
+ }, "« 首页"), react.default.createElement("button", {
2930
+ type: "button",
2931
+ style: styles.button,
2932
+ disabled: page <= 1,
2933
+ onClick: () => setPage((current) => Math.max(1, current - 1))
2934
+ }, "上一页"), react.default.createElement("span", null, `第 ${payload?.page ?? page} / ${totalPages} 页`), react.default.createElement("button", {
2935
+ type: "button",
2936
+ style: styles.button,
2937
+ disabled: !meta.stable || page >= totalPages,
2938
+ onClick: () => setPage((current) => Math.min(totalPages, current + 1))
2939
+ }, "下一页"), !meta.stable ? react.default.createElement("span", { style: styles.muted }, "(该查询未声明稳定排序,仅展示首页)") : null, react.default.createElement("button", {
2940
+ type: "button",
2941
+ style: styles.button,
2942
+ disabled: exportState.running || state.status === "error",
2943
+ title: `按当前排序导出全部可浏览行(${servableRows} 行)为 CSV`,
2944
+ onClick: exportCsv
2945
+ }, exportState.running ? `导出中…(${exportState.progress?.done ?? 0} / ${exportState.progress?.total ?? servableRows} 行)` : "导出 CSV"), exportState.error === undefined ? null : react.default.createElement("span", { style: styles.error }, exportState.error)));
2946
+ }
2947
+ const name = "dsh-lh-data";
2948
+ const inject = ["slots"];
2949
+ function apply(ctx) {
2950
+ const slots = ctx.slots;
2951
+ if (slots === undefined) return;
2952
+ slots.inject(TOOLVIEW_SLOT, () => slots.register({
2953
+ name: TOOLVIEW_SLOT,
2954
+ key: TOOL_KEY
2955
+ }, DatasetViewCard));
2956
+ slots.inject("settings.section", () => slots.register({
2957
+ name: "settings.section",
2958
+ id: ADMIN_SECTION_ID,
2959
+ order: ADMIN_SECTION_ORDER,
2960
+ label: ADMIN_SECTION_LABEL
2961
+ }, DatasetSettingsSection));
2962
+ }
2963
+
2964
+ //#endregion
2965
+ exports.apply = apply
2966
+ exports.inject = inject
2967
+ exports.name = name
2968
+ return module.exports; } });
2969
+ //# sourceMappingURL=client.js.map