dsh-api-runner 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +97 -3
- package/client/client.js +518 -26
- package/package.json +4 -4
- package/src/db.js +958 -0
- package/src/main.js +137 -1
- package/src/ops.js +1327 -1257
- package/src/project-store.js +350 -334
- package/test/client.mjs +102 -1
- package/test/db.mjs +346 -0
- package/test/plugin.mjs +123 -0
- package/test/smoke.mjs +114 -0
package/client/client.js
CHANGED
|
@@ -25,7 +25,14 @@ window.__ModuleLoader__.load({
|
|
|
25
25
|
/** 面板 id:既是左栏全局页在 `main` 里的 key,也是 `sidebar.panellist` 的 id */
|
|
26
26
|
const TAB_ID = 'dsh-api-runner';
|
|
27
27
|
const API = '/dsh-api-runner/api';
|
|
28
|
-
|
|
28
|
+
/**
|
|
29
|
+
* 样式元素 id 带版本后缀。
|
|
30
|
+
*
|
|
31
|
+
* 为什么:宿主的 styles.insert 和浏览器都会按 id 去重,如果 id 固定,
|
|
32
|
+
* 插件升级后页面里仍是旧 CSS(表现为「改了样式但刷新没用」)。
|
|
33
|
+
* 每次改样式就把版本号 +1,旧元素自然失效,新样式必定注入。
|
|
34
|
+
*/
|
|
35
|
+
const STYLE_ID = 'dsh-api-runner-style-v3';
|
|
29
36
|
const METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD'];
|
|
30
37
|
|
|
31
38
|
// ---------------------------------------------------------------- 样式
|
|
@@ -36,6 +43,8 @@ window.__ModuleLoader__.load({
|
|
|
36
43
|
--ar-accent: var(--dsw-alias-brand-primary, #0a84ff);
|
|
37
44
|
--ar-surface: rgba(120, 120, 128, .12);
|
|
38
45
|
--ar-surface-strong: rgba(120, 120, 128, .18);
|
|
46
|
+
/* 选中态用的「抬起」底色:深色主题下比背景亮一点,浅色主题下接近纯白 */
|
|
47
|
+
--ar-elevated: var(--dsw-alias-bg-elevated, rgba(255, 255, 255, .14));
|
|
39
48
|
--ar-sep: rgba(120, 120, 128, .22);
|
|
40
49
|
--ar-danger: var(--dsw-alias-state-error-primary, #ff453a);
|
|
41
50
|
--ar-success: var(--dsw-alias-state-success-primary, #30d158);
|
|
@@ -48,25 +57,50 @@ window.__ModuleLoader__.load({
|
|
|
48
57
|
}
|
|
49
58
|
@media (prefers-color-scheme: light) {
|
|
50
59
|
.ar-root { --ar-surface: rgba(120, 120, 128, .09); --ar-surface-strong: rgba(120, 120, 128, .15);
|
|
60
|
+
--ar-elevated: var(--dsw-alias-bg-elevated, #ffffff);
|
|
51
61
|
--ar-sep: rgba(60, 60, 67, .16);
|
|
52
62
|
--ar-danger: var(--dsw-alias-state-error-primary, #ff3b30);
|
|
53
63
|
--ar-success: var(--dsw-alias-state-success-primary, #34c759);
|
|
54
64
|
--ar-warn: var(--dsw-alias-state-warn-primary, #ff9f0a); }
|
|
55
65
|
}
|
|
56
66
|
|
|
57
|
-
/* ----
|
|
67
|
+
/* ---- 顶部:页签胶囊(参照 DSH 插件详情页的分段控件)----
|
|
68
|
+
两层结构刻意做得不一样,一眼能看出层级:
|
|
69
|
+
· 顶部 ar-topbar:整条浅灰底容器,装四个页签,选中项白底 + 阴影
|
|
70
|
+
· 一级标题 ar-segmented:独立小胶囊,不套外层容器,选中项白底
|
|
71
|
+
*/
|
|
58
72
|
.ar-top { flex: 0 0 auto; display: flex; align-items: center; gap: 6px; padding: 10px 12px 8px; }
|
|
59
|
-
|
|
60
|
-
|
|
73
|
+
/* 窄面板(右侧栏 ~300px)里四个页签 + 刷新按钮会挤,允许容器横向滚动兜底 */
|
|
74
|
+
.ar-tabs { flex: 1 1 auto; display: flex; gap: 2px; padding: 3px; min-width: 0;
|
|
75
|
+
background: var(--ar-surface); border-radius: var(--ar-r-ctrl); overflow-x: auto; scrollbar-width: none; }
|
|
76
|
+
.ar-tabs::-webkit-scrollbar { display: none; }
|
|
61
77
|
.ar-tab { flex: 1 1 0; min-width: 0; display: inline-flex; align-items: center; justify-content: center; gap: 5px;
|
|
62
78
|
border: 0; background: transparent; color: inherit; cursor: pointer; padding: 5px 8px; font: inherit; font-size: 12.5px;
|
|
63
|
-
border-radius: var(--ar-r-pill); white-space: nowrap; opacity: .
|
|
79
|
+
border-radius: var(--ar-r-pill); white-space: nowrap; opacity: .7;
|
|
64
80
|
transition: background-color .18s var(--ar-ease), opacity .18s var(--ar-ease), box-shadow .18s var(--ar-ease); }
|
|
65
|
-
.ar-tab:hover { opacity: .
|
|
66
|
-
|
|
81
|
+
.ar-tab:hover { opacity: .92; }
|
|
82
|
+
/* 选中:白底 + 轻阴影 + 深色字,和宿主的分段控件一致 */
|
|
83
|
+
.ar-tab.on { opacity: 1; font-weight: 600; background: var(--ar-elevated); box-shadow: 0 1px 3px rgba(0, 0, 0, .14); }
|
|
67
84
|
.ar-count { font-size: 10px; font-weight: 600; line-height: 1; padding: 2px 4px; border-radius: 4px;
|
|
68
85
|
background: var(--ar-surface-strong); opacity: .85; }
|
|
69
|
-
.ar-tab.on .ar-count { background: var(--ar-
|
|
86
|
+
.ar-tab.on .ar-count { background: var(--ar-surface-strong); color: inherit; opacity: 1; }
|
|
87
|
+
|
|
88
|
+
/* ---- 一级标题:独立小胶囊(参照 DSH 插件详情页的「连接…/项目…/数据…」)---- */
|
|
89
|
+
/* 一级标题胶囊:强制横向一排,绝不换行。
|
|
90
|
+
面板很窄(右侧栏只有 ~300px),一旦允许 wrap 就会被挤成竖排——
|
|
91
|
+
参照宿主插件详情页的做法:横向滚动,胶囊本身不压缩。 */
|
|
92
|
+
.ar-segmented { display: flex; align-items: center; gap: 6px; flex-wrap: nowrap;
|
|
93
|
+
padding: 2px 0 10px; overflow-x: auto; overflow-y: hidden; scrollbar-width: none; }
|
|
94
|
+
.ar-segmented::-webkit-scrollbar { display: none; }
|
|
95
|
+
.ar-seg { flex: 0 0 auto; display: inline-flex; align-items: center; gap: 5px; border: 0; cursor: pointer;
|
|
96
|
+
padding: 5px 12px; font: inherit; font-size: 12.5px; white-space: nowrap;
|
|
97
|
+
background: var(--ar-surface); color: inherit; border-radius: var(--ar-r-pill); opacity: .82;
|
|
98
|
+
transition: background-color .18s var(--ar-ease), opacity .18s var(--ar-ease), box-shadow .18s var(--ar-ease); }
|
|
99
|
+
.ar-seg:hover { opacity: 1; background: var(--ar-surface-strong); }
|
|
100
|
+
.ar-seg.on { opacity: 1; font-weight: 600; background: var(--ar-elevated);
|
|
101
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, .14), 0 0 0 .5px rgba(0, 0, 0, .04); }
|
|
102
|
+
.ar-seg .n { font-size: 10.5px; font-weight: 600; opacity: .55; }
|
|
103
|
+
.ar-seg.on .n { opacity: .8; }
|
|
70
104
|
|
|
71
105
|
.ar-body { flex: 1 1 auto; min-height: 0; min-width: 0; overflow-y: auto; padding: 2px 12px 24px;
|
|
72
106
|
display: flex; flex-direction: column; gap: 10px; }
|
|
@@ -91,7 +125,11 @@ window.__ModuleLoader__.load({
|
|
|
91
125
|
注意:选中态用实心 accent 而不是半透明混合,避免依赖 color-mix,浅色/深色主题都稳。 */
|
|
92
126
|
.ar-tabs.sub { flex: none; background: transparent; padding: 0; gap: 4px; }
|
|
93
127
|
.ar-tabs.sub .ar-tab { padding: 4px 8px; font-size: 12px; background: var(--ar-surface); opacity: .72; }
|
|
94
|
-
|
|
128
|
+
/* 二级页签(已接管/运行中/…、项目分类):改成和一级标题同款的小胶囊,
|
|
129
|
+
但用「品牌色实心」区分层级,不和顶部那排白底选中块混淆。 */
|
|
130
|
+
.ar-tabs.sub { flex: none; background: transparent; padding: 0; gap: 6px; }
|
|
131
|
+
.ar-tabs.sub .ar-tab { flex: 0 0 auto; padding: 5px 11px; font-size: 12px; background: var(--ar-surface); opacity: .82; }
|
|
132
|
+
.ar-tabs.sub .ar-tab:hover { opacity: 1; }
|
|
95
133
|
.ar-tabs.sub .ar-tab.on {
|
|
96
134
|
background: var(--ar-accent); color: #fff; opacity: 1; font-weight: 600;
|
|
97
135
|
box-shadow: 0 1px 3px rgba(0, 0, 0, .18);
|
|
@@ -207,6 +245,17 @@ window.__ModuleLoader__.load({
|
|
|
207
245
|
.ar-empty-title { font-size: 13px; font-weight: 600; }
|
|
208
246
|
.ar-empty-hint { font-size: 12px; opacity: .55; line-height: 1.6; max-width: 230px; }
|
|
209
247
|
|
|
248
|
+
/* ---- 数据库:结果表格 ---- */
|
|
249
|
+
.ar-item-on { box-shadow: inset 2px 0 0 var(--ar-accent); }
|
|
250
|
+
.ar-table-wrap { width: 100%; overflow-x: auto; border-radius: var(--ar-r-ctrl); background: var(--ar-surface); }
|
|
251
|
+
.ar-table { border-collapse: collapse; width: 100%; font-size: 12px; font-family: var(--ar-mono); }
|
|
252
|
+
.ar-table th { text-align: left; font-weight: 600; padding: 6px 9px; white-space: nowrap;
|
|
253
|
+
border-bottom: 1px solid var(--ar-sep); opacity: .7; position: sticky; top: 0; background: var(--ar-surface-strong); }
|
|
254
|
+
.ar-table td { padding: 5px 9px; border-bottom: 1px solid var(--ar-sep); white-space: nowrap; max-width: 320px;
|
|
255
|
+
overflow: hidden; text-overflow: ellipsis; }
|
|
256
|
+
.ar-table tbody tr:last-child td { border-bottom: none; }
|
|
257
|
+
.ar-table tbody tr:hover td { background: var(--ar-surface-strong); }
|
|
258
|
+
|
|
210
259
|
/* ---- 一键启动进度 ---- */
|
|
211
260
|
.ar-batch { display: flex; flex-direction: column; gap: 6px; padding: 9px 11px; border-radius: var(--ar-r-card);
|
|
212
261
|
background: var(--ar-surface); }
|
|
@@ -248,22 +297,25 @@ window.__ModuleLoader__.load({
|
|
|
248
297
|
|
|
249
298
|
/**
|
|
250
299
|
* 插入样式表。
|
|
251
|
-
*
|
|
252
|
-
*
|
|
300
|
+
*
|
|
301
|
+
* 不再依赖宿主的 styles.insert:它按插件 id 去重,插件升级后页面里
|
|
302
|
+
* 仍是旧 CSS(胶囊退化成原生 button、竖排),刷新也无效。
|
|
303
|
+
* 现在自己维护 <style>,并**先移除所有旧版本元素**再插入,
|
|
304
|
+
* 保证每次加载都是当前这份样式。
|
|
253
305
|
*/
|
|
254
|
-
function installStyles(
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
306
|
+
function installStyles() {
|
|
307
|
+
const head = document.head || document.documentElement;
|
|
308
|
+
// 清掉历史版本(含旧的无版本号 id),避免旧规则覆盖新规则
|
|
309
|
+
for (const node of document.querySelectorAll('style[id^="dsh-api-runner-style"]')) {
|
|
310
|
+
if (node.id !== STYLE_ID) node.remove();
|
|
311
|
+
}
|
|
312
|
+
let el = document.getElementById(STYLE_ID);
|
|
313
|
+
if (el === null) {
|
|
314
|
+
el = document.createElement('style');
|
|
315
|
+
el.id = STYLE_ID;
|
|
316
|
+
head.appendChild(el);
|
|
261
317
|
}
|
|
262
|
-
if (
|
|
263
|
-
const el = document.createElement('style');
|
|
264
|
-
el.id = STYLE_ID;
|
|
265
|
-
el.textContent = CSS;
|
|
266
|
-
(document.head || document.documentElement).appendChild(el);
|
|
318
|
+
if (el.textContent !== CSS) el.textContent = CSS;
|
|
267
319
|
}
|
|
268
320
|
|
|
269
321
|
// ---------------------------------------------------------------- 工具
|
|
@@ -326,7 +378,7 @@ window.__ModuleLoader__.load({
|
|
|
326
378
|
function emptyForm(baseUrl) {
|
|
327
379
|
return {
|
|
328
380
|
id: '', name: '', method: 'GET', url: '', baseUrl: baseUrl || '', description: '',
|
|
329
|
-
headersText: '', bodyText: '', sampleText: '', useToken: '',
|
|
381
|
+
headersText: '', queryText: '', bodyText: '', sampleText: '', useToken: '',
|
|
330
382
|
isLogin: false, tokenPath: 'token', authHeader: 'Authorization', prefix: 'Bearer ',
|
|
331
383
|
timeoutMs: 30000,
|
|
332
384
|
};
|
|
@@ -341,6 +393,7 @@ window.__ModuleLoader__.load({
|
|
|
341
393
|
baseUrl: api.baseUrl || '',
|
|
342
394
|
description: api.description || '',
|
|
343
395
|
headersText: api.headersText || '',
|
|
396
|
+
queryText: api.queryText || '',
|
|
344
397
|
bodyText: api.bodyText || '',
|
|
345
398
|
sampleText: api.sampleText || '',
|
|
346
399
|
useToken: api.useToken || '',
|
|
@@ -372,6 +425,9 @@ window.__ModuleLoader__.load({
|
|
|
372
425
|
else if (value && typeof value === 'object') Object.values(value).forEach(scan);
|
|
373
426
|
};
|
|
374
427
|
scan(api.url);
|
|
428
|
+
// query 里的占位符也要算:只写在 query 里的键,提示行不显示的话用户会以为漏了
|
|
429
|
+
scan(api.queryText);
|
|
430
|
+
if (api.query && typeof api.query === 'object') scan(api.query);
|
|
375
431
|
scan(api.bodyText);
|
|
376
432
|
return [...out];
|
|
377
433
|
}
|
|
@@ -471,9 +527,15 @@ window.__ModuleLoader__.load({
|
|
|
471
527
|
h(Field, { label: '名称' }, h('input', { className: 'ar-in', value: value.name, placeholder: '显示名,留空则用 id', onChange: (e) => set('name', e.target.value) })),
|
|
472
528
|
h(Field, { label: '方法' }, h('select', { className: 'ar-sel', value: value.method, onChange: (e) => set('method', e.target.value) },
|
|
473
529
|
METHODS.map((m) => h('option', { key: m, value: m }, m)))),
|
|
474
|
-
h(Field, { label: '路径' }, h('input', { className: 'ar-in', value: value.url, placeholder: '/
|
|
530
|
+
h(Field, { label: '路径' }, h('input', { className: 'ar-in', value: value.url, placeholder: '/system/library/matter/billList,支持 {id}', onChange: (e) => set('url', e.target.value) })),
|
|
475
531
|
h(Field, { label: 'baseUrl' }, h('input', { className: 'ar-in', value: value.baseUrl, placeholder: '留空则用「设置」里的 baseUrl', onChange: (e) => set('baseUrl', e.target.value) })),
|
|
476
532
|
h(Field, { label: '说明' }, h('input', { className: 'ar-in', value: value.description, onChange: (e) => set('description', e.target.value) })),
|
|
533
|
+
h(Field, { label: 'query', top: true }, h('textarea', {
|
|
534
|
+
className: 'ar-ta',
|
|
535
|
+
value: value.queryText,
|
|
536
|
+
placeholder: '查询参数(JSON),如 {"matterCd":"{matterCd}"};不用把 ?a=b 写进「路径」',
|
|
537
|
+
onChange: (e) => set('queryText', e.target.value),
|
|
538
|
+
})),
|
|
477
539
|
h(Field, { label: '请求头', top: true }, h('textarea', { className: 'ar-ta', value: value.headersText, placeholder: '{"X-Token":"${DEMO_TOKEN}"}', onChange: (e) => set('headersText', e.target.value) })),
|
|
478
540
|
h(Field, { label: 'body', top: true }, h('textarea', { className: 'ar-ta', value: value.bodyText, placeholder: '{"username":"{user}","password":"{pwd}"}', onChange: (e) => set('bodyText', e.target.value) })),
|
|
479
541
|
h(Field, { label: '示例参数', top: true }, h('textarea', { className: 'ar-ta', value: value.sampleText, placeholder: '「测试」按钮用它填 {占位},如 {"user":"alice"}', onChange: (e) => set('sampleText', e.target.value) })),
|
|
@@ -601,6 +663,174 @@ window.__ModuleLoader__.load({
|
|
|
601
663
|
: null);
|
|
602
664
|
}
|
|
603
665
|
|
|
666
|
+
/**
|
|
667
|
+
* 数据库连接编辑表单。
|
|
668
|
+
*
|
|
669
|
+
* 密码框用 type=password 且编辑时不回填:服务端只回传掩码,
|
|
670
|
+
* 留空表示「不修改密码」,避免编辑别处字段时把密码清掉。
|
|
671
|
+
*/
|
|
672
|
+
function EditDbForm(props) {
|
|
673
|
+
const { value, onChange, onSave, onCancel, busy } = props;
|
|
674
|
+
const set = (key, next) => onChange({ ...value, [key]: next });
|
|
675
|
+
const isSqlite = value.type === 'sqlite';
|
|
676
|
+
const needsHost = !isSqlite;
|
|
677
|
+
return h('div', { className: 'ar-card' },
|
|
678
|
+
h('div', { className: 'ar-name' }, value.id ? `编辑连接 ${value.name}` : '新增数据库连接'),
|
|
679
|
+
h(Field, { label: '名称' }, h('input', {
|
|
680
|
+
className: 'ar-in', value: value.name, placeholder: '如 本地 MySQL',
|
|
681
|
+
onChange: (e) => set('name', e.target.value),
|
|
682
|
+
})),
|
|
683
|
+
h(Field, { label: '类型' }, h('select', {
|
|
684
|
+
className: 'ar-sel', value: value.type,
|
|
685
|
+
onChange: (e) => set('type', e.target.value),
|
|
686
|
+
}, ['sqlite', 'mysql', 'postgres', 'oracle', 'dm'].map((t) => h('option', { key: t, value: t }, t)))),
|
|
687
|
+
isSqlite
|
|
688
|
+
? h(Field, { label: '文件路径' }, h('input', {
|
|
689
|
+
className: 'ar-in ar-mono', value: value.database,
|
|
690
|
+
placeholder: ':memory: 或 D:\\data\\app.db',
|
|
691
|
+
onChange: (e) => set('database', e.target.value),
|
|
692
|
+
}))
|
|
693
|
+
: h('div', null,
|
|
694
|
+
h('div', { className: 'ar-row' },
|
|
695
|
+
h('div', { style: { flex: '1 1 auto', minWidth: 0 } },
|
|
696
|
+
h(Field, { label: 'host' }, h('input', {
|
|
697
|
+
className: 'ar-in ar-mono', value: value.host, placeholder: '127.0.0.1',
|
|
698
|
+
onChange: (e) => set('host', e.target.value),
|
|
699
|
+
}))),
|
|
700
|
+
h('div', { style: { flex: '0 0 90px' } },
|
|
701
|
+
h(Field, { label: '端口' }, h('input', {
|
|
702
|
+
className: 'ar-in ar-mono', value: value.port, placeholder: '默认',
|
|
703
|
+
onChange: (e) => set('port', e.target.value),
|
|
704
|
+
})))),
|
|
705
|
+
h(Field, { label: '数据库' }, h('input', {
|
|
706
|
+
className: 'ar-in ar-mono', value: value.database, placeholder: '库名',
|
|
707
|
+
onChange: (e) => set('database', e.target.value),
|
|
708
|
+
})),
|
|
709
|
+
h(Field, { label: '用户名' }, h('input', {
|
|
710
|
+
className: 'ar-in', value: value.username, placeholder: 'root',
|
|
711
|
+
onChange: (e) => set('username', e.target.value),
|
|
712
|
+
})),
|
|
713
|
+
h(Field, { label: '密码' }, h('input', {
|
|
714
|
+
className: 'ar-in', type: 'password', value: value.password,
|
|
715
|
+
placeholder: value.id ? '留空表示不修改' : '',
|
|
716
|
+
onChange: (e) => set('password', e.target.value),
|
|
717
|
+
}))),
|
|
718
|
+
h(Field, { label: '额外参数', top: true }, h('textarea', {
|
|
719
|
+
className: 'ar-ta ar-mono', value: value.extraText,
|
|
720
|
+
placeholder: '可选,JSON:{"ssl":true}',
|
|
721
|
+
onChange: (e) => set('extraText', e.target.value),
|
|
722
|
+
})),
|
|
723
|
+
h('div', { className: 'ar-hint' }, '密码会加密后保存,列表与工具输出只显示掩码。'),
|
|
724
|
+
h('div', { className: 'ar-row' },
|
|
725
|
+
h('button', {
|
|
726
|
+
className: 'ar-btn primary',
|
|
727
|
+
disabled: Boolean(busy),
|
|
728
|
+
onClick: () => {
|
|
729
|
+
// 额外参数是 JSON,解析失败就拦在这里,别把坏值发到后端
|
|
730
|
+
let extra;
|
|
731
|
+
const text = String(value.extraText || '').trim();
|
|
732
|
+
if (text !== '') {
|
|
733
|
+
try {
|
|
734
|
+
extra = JSON.parse(text);
|
|
735
|
+
} catch (error) {
|
|
736
|
+
window.alert(`额外参数不是合法 JSON:${error.message}`);
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
onSave({
|
|
741
|
+
id: value.id || undefined,
|
|
742
|
+
name: value.name,
|
|
743
|
+
type: value.type,
|
|
744
|
+
host: value.host,
|
|
745
|
+
port: value.port === '' ? undefined : Number(value.port),
|
|
746
|
+
database: value.database,
|
|
747
|
+
username: value.username,
|
|
748
|
+
// 空密码不传,让后端保留原值
|
|
749
|
+
password: value.password === '' ? undefined : value.password,
|
|
750
|
+
extra,
|
|
751
|
+
});
|
|
752
|
+
},
|
|
753
|
+
}, '保存'),
|
|
754
|
+
h('button', { className: 'ar-btn ghost', onClick: onCancel }, '取消')));
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/** SQL 执行结果:表格 + 行数/耗时 */
|
|
758
|
+
function DbResultCard(props) {
|
|
759
|
+
const { result, onClose } = props;
|
|
760
|
+
if (!result) return null;
|
|
761
|
+
if (result.ok === false) {
|
|
762
|
+
return h('div', { className: 'ar-card' },
|
|
763
|
+
h('div', { className: 'ar-row' },
|
|
764
|
+
h('span', { className: 'ar-badge', style: { background: 'var(--ar-danger)', color: '#fff' } }, result.code || 'ERROR'),
|
|
765
|
+
h('span', { className: 'ar-spacer' }),
|
|
766
|
+
h('button', { className: 'ar-btn ghost icon', title: '关闭', onClick: onClose }, '×')),
|
|
767
|
+
h('div', { className: 'ar-hint ar-mono', style: { overflowWrap: 'anywhere' } }, result.message || '执行失败'));
|
|
768
|
+
}
|
|
769
|
+
const columns = Array.isArray(result.columns) ? result.columns : [];
|
|
770
|
+
const rows = Array.isArray(result.rows) ? result.rows : [];
|
|
771
|
+
const affected = result.affectedRows;
|
|
772
|
+
return h('div', { className: 'ar-card' },
|
|
773
|
+
h('div', { className: 'ar-row' },
|
|
774
|
+
h('span', { className: 'ar-badge ok' }, 'OK'),
|
|
775
|
+
h('span', { className: 'ar-hint' }, `${result.rowCount} 行 · ${result.durationMs}ms`),
|
|
776
|
+
result.truncated ? h('span', { className: 'ar-badge', style: { background: 'var(--ar-warn)' } }, `已截断至 ${result.maxRows}`) : null,
|
|
777
|
+
h('span', { className: 'ar-spacer' }),
|
|
778
|
+
h('button', { className: 'ar-btn ghost icon', title: '关闭', onClick: onClose }, '×')),
|
|
779
|
+
typeof affected === 'number' && affected > 0
|
|
780
|
+
? h('div', { className: 'ar-hint' }, `影响 ${affected} 行`)
|
|
781
|
+
: null,
|
|
782
|
+
columns.length > 0 && rows.length > 0
|
|
783
|
+
? h('div', { className: 'ar-table-wrap' },
|
|
784
|
+
h('table', { className: 'ar-table' },
|
|
785
|
+
h('thead', null, h('tr', null, columns.map((c) => h('th', { key: c }, c)))),
|
|
786
|
+
h('tbody', null, rows.map((row, index) => h('tr', { key: index },
|
|
787
|
+
columns.map((c) => h('td', { key: c }, formatCell(row[c]))))))))
|
|
788
|
+
: h('div', { className: 'ar-hint' }, rows.length === 0 ? '查询成功,没有数据行。' : ''));
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/** 单元格渲染:对象/数组转 JSON,null 显示为灰色占位 */
|
|
792
|
+
function formatCell(value) {
|
|
793
|
+
if (value === null) return h('span', { className: 'ar-faint' }, 'NULL');
|
|
794
|
+
if (value === undefined) return '';
|
|
795
|
+
if (typeof value === 'object') {
|
|
796
|
+
try {
|
|
797
|
+
return JSON.stringify(value);
|
|
798
|
+
} catch {
|
|
799
|
+
return String(value);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
if (typeof value === 'boolean') return value ? 'true' : 'false';
|
|
803
|
+
return String(value);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
/** 表结构:字段表 + 索引 */
|
|
807
|
+
function TableDetail(props) {
|
|
808
|
+
const { detail, onClose } = props;
|
|
809
|
+
const columns = Array.isArray(detail.columns) ? detail.columns : [];
|
|
810
|
+
const indexes = Array.isArray(detail.indexes) ? detail.indexes : [];
|
|
811
|
+
return h('div', { className: 'ar-card' },
|
|
812
|
+
h('div', { className: 'ar-row' },
|
|
813
|
+
h('span', { className: 'ar-name ar-mono' }, detail.table),
|
|
814
|
+
h('span', { className: 'ar-spacer' }),
|
|
815
|
+
h('button', { className: 'ar-btn ghost icon', title: '关闭', onClick: onClose }, '×')),
|
|
816
|
+
h('div', { className: 'ar-table-wrap' },
|
|
817
|
+
h('table', { className: 'ar-table' },
|
|
818
|
+
h('thead', null, h('tr', null,
|
|
819
|
+
h('th', null, '字段'), h('th', null, '类型'), h('th', null, '可空'), h('th', null, '默认'), h('th', null, '主键'))),
|
|
820
|
+
h('tbody', null, columns.map((c) => h('tr', { key: c.name },
|
|
821
|
+
h('td', { className: 'ar-mono' }, c.name),
|
|
822
|
+
h('td', { className: 'ar-mono ar-muted' }, c.type || ''),
|
|
823
|
+
h('td', null, c.nullable ? 'Y' : ''),
|
|
824
|
+
h('td', { className: 'ar-mono ar-muted' }, c.defaultValue === null || c.defaultValue === undefined ? '' : String(c.defaultValue)),
|
|
825
|
+
h('td', null, c.primaryKey ? 'PK' : '')))))),
|
|
826
|
+
indexes.length > 0
|
|
827
|
+
? h('div', null,
|
|
828
|
+
h('div', { className: 'ar-sec' }, h('span', { className: 't' }, `索引 · ${indexes.length}`)),
|
|
829
|
+
indexes.map((i) => h('div', { key: i.name, className: 'ar-hint ar-mono ar-cut' },
|
|
830
|
+
`${i.name}${i.unique ? ' (unique)' : ''}${Array.isArray(i.columns) ? ` · ${i.columns.join(', ')}` : ''}${i.definition ? ` · ${i.definition}` : ''}`)))
|
|
831
|
+
: null);
|
|
832
|
+
}
|
|
833
|
+
|
|
604
834
|
function SavedRow(props) {
|
|
605
835
|
const { item, busy, onStart, onDelete } = props;
|
|
606
836
|
return h('div', { className: 'ar-item' },
|
|
@@ -878,6 +1108,19 @@ window.__ModuleLoader__.load({
|
|
|
878
1108
|
const [mavenHomeDraft, setMavenHomeDraft] = React.useState('');
|
|
879
1109
|
/** 一键启动的后台批次进度(null = 没有正在进行的批次) */
|
|
880
1110
|
const [batch, setBatch] = React.useState(null);
|
|
1111
|
+
/** 数据库:连接列表 / 选中的连接 / 执行结果 / 表清单 */
|
|
1112
|
+
const [dbConnections, setDbConnections] = React.useState([]);
|
|
1113
|
+
const [dbError, setDbError] = React.useState('');
|
|
1114
|
+
const [dbActive, setDbActive] = React.useState('');
|
|
1115
|
+
const [dbForm, setDbForm] = React.useState(null);
|
|
1116
|
+
const [dbSql, setDbSql] = React.useState('');
|
|
1117
|
+
const [dbResult, setDbResult] = React.useState(null);
|
|
1118
|
+
const [dbReadonly, setDbReadonly] = React.useState(true);
|
|
1119
|
+
const [dbTables, setDbTables] = React.useState([]);
|
|
1120
|
+
const [dbTableDetail, setDbTableDetail] = React.useState(null);
|
|
1121
|
+
const [dbDbBusy, setDbDbBusy] = React.useState('');
|
|
1122
|
+
/** 数据库子页的一级标题:conn(连接)/ sql / schema(表结构) */
|
|
1123
|
+
const [dbSection, setDbSection] = React.useState('conn');
|
|
881
1124
|
|
|
882
1125
|
const headers = React.useMemo(() => ({ 'Content-Type': 'application/json', 'x-dsh-session-id': sessionId }), [sessionId]);
|
|
883
1126
|
const get = React.useCallback(async (path) => {
|
|
@@ -1003,6 +1246,12 @@ window.__ModuleLoader__.load({
|
|
|
1003
1246
|
baseUrl: value.baseUrl.trim(),
|
|
1004
1247
|
description: value.description.trim(),
|
|
1005
1248
|
headers: parseJsonField(value.headersText, '请求头') || {},
|
|
1249
|
+
/**
|
|
1250
|
+
* query / sampleParams 用 `|| {}` 兜底:表单里清空时 parseJsonField 返回
|
|
1251
|
+
* undefined,这里显式给 {},表示「用户明确清空了」,后端才会覆盖旧值;
|
|
1252
|
+
* 若传 undefined 会被当成「本次没提交」,旧值又会被沿用回来。
|
|
1253
|
+
*/
|
|
1254
|
+
query: parseJsonField(value.queryText, 'query') || {},
|
|
1006
1255
|
body: parseJsonField(value.bodyText, 'body'),
|
|
1007
1256
|
sampleParams: parseJsonField(value.sampleText, '示例参数') || {},
|
|
1008
1257
|
useToken: value.useToken,
|
|
@@ -1302,9 +1551,142 @@ window.__ModuleLoader__.load({
|
|
|
1302
1551
|
}
|
|
1303
1552
|
};
|
|
1304
1553
|
|
|
1554
|
+
// ---------------------------------------------------------------- 数据库
|
|
1555
|
+
/**
|
|
1556
|
+
* 数据库连接是跨项目的(同一套库常被多个项目共用),
|
|
1557
|
+
* 所以这里不跟着「接口」页的项目页签走,始终操作全局连接列表。
|
|
1558
|
+
*/
|
|
1559
|
+
const refreshDb = React.useCallback(async () => {
|
|
1560
|
+
try {
|
|
1561
|
+
const data = await request('db', { headers });
|
|
1562
|
+
if (data.ok === false) {
|
|
1563
|
+
setDbError(data.message || '读取数据库连接失败');
|
|
1564
|
+
setDbConnections([]);
|
|
1565
|
+
return;
|
|
1566
|
+
}
|
|
1567
|
+
const list = Array.isArray(data.connections) ? data.connections : [];
|
|
1568
|
+
setDbConnections(list);
|
|
1569
|
+
setDbError('');
|
|
1570
|
+
// 选中的连接被删了就退回第一个
|
|
1571
|
+
setDbActive((current) => (list.some((item) => item.id === current) ? current : (list[0] ? list[0].id : '')));
|
|
1572
|
+
} catch (e) {
|
|
1573
|
+
setDbError(e.message);
|
|
1574
|
+
}
|
|
1575
|
+
}, [headers]);
|
|
1576
|
+
|
|
1577
|
+
/**
|
|
1578
|
+
* 数据库连接只在面板可见时加载一次。
|
|
1579
|
+
*
|
|
1580
|
+
* 注意:这个 effect 必须放在 refreshDb 声明**之后**。
|
|
1581
|
+
* 之前把它并进上面那个主 effect 的依赖数组,而那个 effect 在 refreshDb
|
|
1582
|
+
* 之前执行,const 的暂时性死区直接抛 `Cannot access 'refreshDb' before
|
|
1583
|
+
* initialization`,整个 Panel 崩掉 -> 面板白屏。
|
|
1584
|
+
*/
|
|
1585
|
+
React.useEffect(() => {
|
|
1586
|
+
if (!visible) return undefined;
|
|
1587
|
+
refreshDb();
|
|
1588
|
+
return undefined;
|
|
1589
|
+
}, [visible, refreshDb]);
|
|
1590
|
+
|
|
1591
|
+
const loadDbTables = React.useCallback(async (connectionId) => {
|
|
1592
|
+
if (!connectionId) { setDbTables([]); return; }
|
|
1593
|
+
try {
|
|
1594
|
+
const data = await request(`db/tables?connection_id=${encodeURIComponent(connectionId)}`, { headers });
|
|
1595
|
+
setDbTables(data.ok === false ? [] : (Array.isArray(data.tables) ? data.tables : []));
|
|
1596
|
+
if (data.ok === false) setDbError(data.message || '读取表清单失败');
|
|
1597
|
+
} catch (e) {
|
|
1598
|
+
setDbError(e.message);
|
|
1599
|
+
}
|
|
1600
|
+
}, [headers]);
|
|
1601
|
+
|
|
1602
|
+
const dbSave = async (value) => {
|
|
1603
|
+
setDbDbBusy('save');
|
|
1604
|
+
setDbError('');
|
|
1605
|
+
try {
|
|
1606
|
+
const payload = await post('db/save', { connection: value });
|
|
1607
|
+
if (payload.ok === false) throw new Error(payload.message || '保存失败');
|
|
1608
|
+
setDbForm(null);
|
|
1609
|
+
setNotice(payload.message || '连接已保存');
|
|
1610
|
+
await refreshDb();
|
|
1611
|
+
} catch (e) {
|
|
1612
|
+
setDbError(e.message);
|
|
1613
|
+
} finally {
|
|
1614
|
+
setDbDbBusy('');
|
|
1615
|
+
}
|
|
1616
|
+
};
|
|
1617
|
+
|
|
1618
|
+
const dbDelete = async (item) => {
|
|
1619
|
+
if (!window.confirm(`删除连接「${item.name}」?(只删除配置,不影响数据库本身)`)) return;
|
|
1620
|
+
setDbDbBusy('delete');
|
|
1621
|
+
try {
|
|
1622
|
+
const payload = await post('db/delete', { connection_id: item.id });
|
|
1623
|
+
if (payload.ok === false) throw new Error(payload.message || '删除失败');
|
|
1624
|
+
setNotice(payload.message || '已删除');
|
|
1625
|
+
setDbResult(null);
|
|
1626
|
+
setDbTables([]);
|
|
1627
|
+
await refreshDb();
|
|
1628
|
+
} catch (e) {
|
|
1629
|
+
setDbError(e.message);
|
|
1630
|
+
} finally {
|
|
1631
|
+
setDbDbBusy('');
|
|
1632
|
+
}
|
|
1633
|
+
};
|
|
1634
|
+
|
|
1635
|
+
const dbTest = async (item) => {
|
|
1636
|
+
setDbDbBusy('test');
|
|
1637
|
+
setDbError('');
|
|
1638
|
+
try {
|
|
1639
|
+
const payload = await post('db/test', { connection_id: item.id });
|
|
1640
|
+
if (payload.ok) setNotice(`连接「${item.name}」正常(${payload.durationMs}ms)`);
|
|
1641
|
+
else setDbError(payload.message || '连接失败');
|
|
1642
|
+
} catch (e) {
|
|
1643
|
+
setDbError(e.message);
|
|
1644
|
+
} finally {
|
|
1645
|
+
setDbDbBusy('');
|
|
1646
|
+
}
|
|
1647
|
+
};
|
|
1648
|
+
|
|
1649
|
+
const dbRun = async () => {
|
|
1650
|
+
if (!dbActive) { setDbError('请先选择一个连接'); return; }
|
|
1651
|
+
if (String(dbSql).trim() === '') { setDbError('SQL 不能为空'); return; }
|
|
1652
|
+
setDbDbBusy('query');
|
|
1653
|
+
setDbError('');
|
|
1654
|
+
setDbResult(null);
|
|
1655
|
+
try {
|
|
1656
|
+
const payload = await post('db/query', {
|
|
1657
|
+
connection_id: dbActive,
|
|
1658
|
+
sql: dbSql,
|
|
1659
|
+
readonly: dbReadonly,
|
|
1660
|
+
});
|
|
1661
|
+
setDbResult(payload);
|
|
1662
|
+
if (payload.ok === false) setDbError(payload.message || '执行失败');
|
|
1663
|
+
} catch (e) {
|
|
1664
|
+
setDbError(e.message);
|
|
1665
|
+
} finally {
|
|
1666
|
+
setDbDbBusy('');
|
|
1667
|
+
}
|
|
1668
|
+
};
|
|
1669
|
+
|
|
1670
|
+
const dbDescribe = async (tableName) => {
|
|
1671
|
+
if (!dbActive || !tableName) return;
|
|
1672
|
+
setDbDbBusy('describe');
|
|
1673
|
+
try {
|
|
1674
|
+
const data = await request(`db/table?connection_id=${encodeURIComponent(dbActive)}&name=${encodeURIComponent(tableName)}`, { headers });
|
|
1675
|
+
if (data.ok === false) { setDbError(data.message || '读取表结构失败'); return; }
|
|
1676
|
+
setDbTableDetail(data);
|
|
1677
|
+
// 点表名顺手把查询填好,省得手打
|
|
1678
|
+
setDbSql(`SELECT * FROM ${tableName} LIMIT 100`);
|
|
1679
|
+
} catch (e) {
|
|
1680
|
+
setDbError(e.message);
|
|
1681
|
+
} finally {
|
|
1682
|
+
setDbDbBusy('');
|
|
1683
|
+
}
|
|
1684
|
+
};
|
|
1685
|
+
|
|
1305
1686
|
const tabs = [
|
|
1306
1687
|
{ id: 'apis', label: '接口', count: apis.length },
|
|
1307
1688
|
{ id: 'services', label: '服务', count: runningList.length },
|
|
1689
|
+
{ id: 'db', label: '数据库', count: dbConnections.length },
|
|
1308
1690
|
{ id: 'settings', label: '设置', count: 0 },
|
|
1309
1691
|
];
|
|
1310
1692
|
|
|
@@ -1562,6 +1944,115 @@ window.__ModuleLoader__.load({
|
|
|
1562
1944
|
? state.sources.map((s) => h('div', { key: s, className: 'ar-kv' }, h('span', { className: 'k' }, '来源'), h('span', { className: 'v ar-mono ar-muted ar-cut', title: s }, s)))
|
|
1563
1945
|
: h('div', { className: 'ar-kv' }, h('span', { className: 'k' }, '来源'), h('span', { className: 'v ar-muted' }, '暂无 YAML 文件,接口全部来自面板')))));
|
|
1564
1946
|
|
|
1947
|
+
// ---------------------------------------------------------------- 数据库面板
|
|
1948
|
+
const activeConn = dbConnections.find((item) => item.id === dbActive) || null;
|
|
1949
|
+
/** 数据库子页的一级标题:独立小胶囊,一次只看一块(和参考图的「连接…/SQL…/数据…」一致) */
|
|
1950
|
+
const dbSections = [
|
|
1951
|
+
{ id: 'conn', label: '连接', count: dbConnections.length },
|
|
1952
|
+
{ id: 'sql', label: 'SQL', count: 0 },
|
|
1953
|
+
{ id: 'schema', label: '表结构', count: dbTables.length },
|
|
1954
|
+
];
|
|
1955
|
+
const dbPane = h('div', { className: 'ar-pane' },
|
|
1956
|
+
h(Banner, { kind: 'err', text: dbError, onClose: () => setDbError('') }),
|
|
1957
|
+
|
|
1958
|
+
h('div', { className: 'ar-segmented' }, dbSections.map((item) => h('button', {
|
|
1959
|
+
key: item.id,
|
|
1960
|
+
className: item.id === dbSection ? 'ar-seg on' : 'ar-seg',
|
|
1961
|
+
onClick: () => setDbSection(item.id),
|
|
1962
|
+
ref: (node) => {
|
|
1963
|
+
// 窄面板里胶囊会横向滚动,选中的那个必须自己滚进视野
|
|
1964
|
+
if (node && item.id === dbSection) node.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
|
1965
|
+
},
|
|
1966
|
+
}, item.label, item.count > 0 ? h('span', { className: 'n' }, item.count) : null))),
|
|
1967
|
+
|
|
1968
|
+
// ---- 连接 ----
|
|
1969
|
+
dbSection === 'conn' ? h('div', { className: 'ar-pane' },
|
|
1970
|
+
h('div', { className: 'ar-row' },
|
|
1971
|
+
h('button', {
|
|
1972
|
+
className: 'ar-btn tonal',
|
|
1973
|
+
disabled: dbDbBusy !== '',
|
|
1974
|
+
onClick: () => setDbForm({
|
|
1975
|
+
id: '', name: '', type: 'sqlite', host: '', port: '',
|
|
1976
|
+
database: '', username: '', password: '', extraText: '',
|
|
1977
|
+
}),
|
|
1978
|
+
}, '新增连接'),
|
|
1979
|
+
h('span', { className: 'ar-spacer' }),
|
|
1980
|
+
h('button', { className: 'ar-btn ghost icon', disabled: dbDbBusy !== '', title: '重新读取连接列表', onClick: refreshDb }, '↻')),
|
|
1981
|
+
|
|
1982
|
+
dbConnections.length === 0
|
|
1983
|
+
? h(Empty, {
|
|
1984
|
+
icon: '🗄',
|
|
1985
|
+
title: '还没有数据库连接',
|
|
1986
|
+
hint: '点「新增连接」配一个。SQLite 直接填文件路径(:memory: 表示内存库);MySQL / PostgreSQL / Oracle / 达梦填 host、端口、账号密码。',
|
|
1987
|
+
})
|
|
1988
|
+
: h('div', { className: 'ar-group' }, dbConnections.map((item) => h('div', {
|
|
1989
|
+
key: item.id,
|
|
1990
|
+
className: item.id === dbActive ? 'ar-item ar-item-on' : 'ar-item',
|
|
1991
|
+
onClick: () => { setDbActive(item.id); setDbResult(null); setDbTableDetail(null); loadDbTables(item.id); },
|
|
1992
|
+
},
|
|
1993
|
+
h('div', { className: 'ar-row', style: { flexWrap: 'wrap', gap: '5px' } },
|
|
1994
|
+
h('span', { className: 'ar-badge brand' }, item.type),
|
|
1995
|
+
h('span', { className: 'ar-name ar-cut', style: { flex: '0 1 auto' } }, item.name),
|
|
1996
|
+
item.id === dbActive ? h('span', { className: 'ar-badge' }, '当前') : null),
|
|
1997
|
+
h('div', { className: 'ar-hint ar-mono ar-cut', title: item.type === 'sqlite' ? item.file : `${item.host}:${item.port}/${item.database}` },
|
|
1998
|
+
item.type === 'sqlite' ? (item.file || ':memory:') : `${item.host}:${item.port}/${item.database}`),
|
|
1999
|
+
h('div', { className: 'ar-row', style: { flexWrap: 'wrap' } },
|
|
2000
|
+
h('button', { className: 'ar-btn ghost', disabled: dbDbBusy !== '', onClick: () => dbTest(item) }, '测试'),
|
|
2001
|
+
h('button', { className: 'ar-btn ghost', disabled: dbDbBusy !== '', onClick: () => setDbForm({
|
|
2002
|
+
id: item.id, name: item.name, type: item.type, host: item.host,
|
|
2003
|
+
port: item.port ? String(item.port) : '', database: item.database,
|
|
2004
|
+
username: item.username, password: '', extraText: Object.keys(item.extra || {}).length ? JSON.stringify(item.extra, null, 2) : '',
|
|
2005
|
+
}) }, '编辑'),
|
|
2006
|
+
h('span', { className: 'ar-spacer' }),
|
|
2007
|
+
h('button', { className: 'ar-btn ghost danger', disabled: dbDbBusy !== '', onClick: () => dbDelete(item) }, '删除'))))),
|
|
2008
|
+
|
|
2009
|
+
dbForm ? h(EditDbForm, { value: dbForm, onChange: setDbForm, onSave: dbSave, onCancel: () => setDbForm(null), busy: dbDbBusy }) : null)
|
|
2010
|
+
: null,
|
|
2011
|
+
|
|
2012
|
+
// ---- SQL ----
|
|
2013
|
+
dbSection === 'sql' ? h('div', { className: 'ar-pane' },
|
|
2014
|
+
h('div', { className: 'ar-card' },
|
|
2015
|
+
h('div', { className: 'ar-hint' }, activeConn
|
|
2016
|
+
? `当前连接:${activeConn.name}(${activeConn.type})`
|
|
2017
|
+
: '先到「连接」里选一个连接'),
|
|
2018
|
+
h('textarea', {
|
|
2019
|
+
className: 'ar-ta ar-mono',
|
|
2020
|
+
value: dbSql,
|
|
2021
|
+
placeholder: 'SELECT * FROM table LIMIT 100',
|
|
2022
|
+
spellCheck: false,
|
|
2023
|
+
onChange: (e) => setDbSql(e.target.value),
|
|
2024
|
+
style: { minHeight: '90px' },
|
|
2025
|
+
}),
|
|
2026
|
+
h('div', { className: 'ar-row', style: { flexWrap: 'wrap' } },
|
|
2027
|
+
h('label', { className: 'ar-check' },
|
|
2028
|
+
h('input', { type: 'checkbox', checked: dbReadonly, onChange: (e) => setDbReadonly(e.target.checked) }),
|
|
2029
|
+
h('span', {}, '只读模式(拒绝写操作)')),
|
|
2030
|
+
h('span', { className: 'ar-spacer' }),
|
|
2031
|
+
h('button', { className: 'ar-btn primary', disabled: dbDbBusy !== '' || !dbActive, onClick: dbRun },
|
|
2032
|
+
dbDbBusy === 'query' ? '执行中…' : '执行')),
|
|
2033
|
+
dbReadonly ? null : h('div', { className: 'ar-hint', style: { color: 'var(--ar-warn)' } }, '只读模式已关闭:写操作会真实生效。')),
|
|
2034
|
+
|
|
2035
|
+
dbResult ? h(DbResultCard, { result: dbResult, onClose: () => setDbResult(null) }) : null)
|
|
2036
|
+
: null,
|
|
2037
|
+
|
|
2038
|
+
// ---- 表结构 ----
|
|
2039
|
+
dbSection === 'schema' ? h('div', { className: 'ar-pane' },
|
|
2040
|
+
h('div', { className: 'ar-row' },
|
|
2041
|
+
h('button', { className: 'ar-btn ghost', disabled: dbDbBusy !== '' || !dbActive, onClick: () => loadDbTables(dbActive) }, '刷新表清单'),
|
|
2042
|
+
h('span', { className: 'ar-hint', style: { flex: '1 1 auto', minWidth: 0 } },
|
|
2043
|
+
dbTables.length > 0 ? `${dbTables.length} 个表` : '(未加载)')),
|
|
2044
|
+
dbTables.length === 0
|
|
2045
|
+
? h('div', { className: 'ar-hint' }, dbActive ? '点「刷新表清单」查看这个库里的表。' : '先到「连接」里选一个连接。')
|
|
2046
|
+
: h('div', { className: 'ar-group' }, dbTables.map((item) => h('button', {
|
|
2047
|
+
key: item.name,
|
|
2048
|
+
className: 'ar-btn ghost ar-mono',
|
|
2049
|
+
style: { justifyContent: 'flex-start', width: '100%' },
|
|
2050
|
+
disabled: dbDbBusy !== '',
|
|
2051
|
+
onClick: () => dbDescribe(item.name),
|
|
2052
|
+
}, item.name))),
|
|
2053
|
+
dbTableDetail ? h(TableDetail, { detail: dbTableDetail, onClose: () => setDbTableDetail(null) }) : null)
|
|
2054
|
+
: null);
|
|
2055
|
+
|
|
1565
2056
|
return h('div', { className: 'ar-root' },
|
|
1566
2057
|
h('div', { className: 'ar-top' },
|
|
1567
2058
|
h('div', { className: 'ar-tabs' }, tabs.map((item) => h('button', {
|
|
@@ -1575,6 +2066,7 @@ window.__ModuleLoader__.load({
|
|
|
1575
2066
|
h(Banner, { kind: 'ok', text: error ? '' : notice, onClose: () => setNotice('') }),
|
|
1576
2067
|
tab === 'apis' ? apisPane : null,
|
|
1577
2068
|
tab === 'services' ? servicesPane : null,
|
|
2069
|
+
tab === 'db' ? dbPane : null,
|
|
1578
2070
|
tab === 'settings' ? settingsPane : null));
|
|
1579
2071
|
}
|
|
1580
2072
|
|
|
@@ -1757,7 +2249,7 @@ window.__ModuleLoader__.load({
|
|
|
1757
2249
|
}
|
|
1758
2250
|
|
|
1759
2251
|
function apply(ctx) {
|
|
1760
|
-
installStyles(
|
|
2252
|
+
installStyles();
|
|
1761
2253
|
const slots = typeof ctx.get === 'function' ? ctx.get('slots') : undefined;
|
|
1762
2254
|
if (!slots || typeof slots.register !== 'function') return;
|
|
1763
2255
|
|