@jacksontian/kite-server 0.4.0 → 0.4.1
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/lib/summary.js +1 -0
- package/package.json +1 -1
- package/web/index.html +60 -6
- package/web/pc.html +96 -8
package/lib/summary.js
CHANGED
package/package.json
CHANGED
package/web/index.html
CHANGED
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
6
6
|
<meta name="theme-color" content="#111827">
|
|
7
|
+
<script>
|
|
8
|
+
// 主题初始化(先于样式解析,避免先闪一下另一套配色):本地偏好 > 系统外观
|
|
9
|
+
document.documentElement.dataset.theme = localStorage.getItem('rw_theme')
|
|
10
|
+
|| (matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark');
|
|
11
|
+
</script>
|
|
7
12
|
<title>Kite 控制台</title>
|
|
8
13
|
<!-- xterm 随包自托管(vendor/ 为锁版本的官方产物):不依赖 CDN,
|
|
9
14
|
杜绝 CDN 投毒/HTTP 下被篡改注入窃取密钥的供应链风险 -->
|
|
@@ -13,6 +18,14 @@
|
|
|
13
18
|
--bg: #111827; --panel: #1f2937; --line: #374151;
|
|
14
19
|
--text: #e5e7eb; --dim: #9ca3af; --accent: #3b82f6;
|
|
15
20
|
--ok: #22c55e; --err: #ef4444; --warn: #f59e0b;
|
|
21
|
+
--term-bg: #0b1220; --log-text: #a5f3fc;
|
|
22
|
+
}
|
|
23
|
+
/* light 主题:xterm 是 canvas 绘制读不到 CSS 变量,终端色由 JS 同步切 */
|
|
24
|
+
:root[data-theme='light'] {
|
|
25
|
+
--bg: #f4f6f9; --panel: #fff; --line: #d9dee7;
|
|
26
|
+
--text: #1f2937; --dim: #64748b; --accent: #2563eb;
|
|
27
|
+
--ok: #16a34a; --err: #dc2626; --warn: #d97706;
|
|
28
|
+
--term-bg: #fff; --log-text: #0e7490;
|
|
16
29
|
}
|
|
17
30
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
18
31
|
body {
|
|
@@ -30,6 +43,10 @@
|
|
|
30
43
|
#worker-status { font-size: 12px; padding: 4px 10px; border-radius: 999px; background: var(--panel); color: var(--dim); white-space: nowrap; }
|
|
31
44
|
#worker-status.online { color: var(--ok); }
|
|
32
45
|
#worker-status.offline { color: var(--err); }
|
|
46
|
+
#btn-theme {
|
|
47
|
+
flex: none; font-size: 12px; padding: 4px 10px; border-radius: 8px;
|
|
48
|
+
background: var(--panel); color: var(--dim); border: 1px solid var(--line); white-space: nowrap;
|
|
49
|
+
}
|
|
33
50
|
|
|
34
51
|
/* 明文 HTTP 部署的风险提示(仅非 localhost 的 http: 环境显示) */
|
|
35
52
|
#http-warn {
|
|
@@ -66,10 +83,10 @@
|
|
|
66
83
|
.b-canceled { background: rgba(156,163,175,.15); color: var(--dim); }
|
|
67
84
|
|
|
68
85
|
.logs {
|
|
69
|
-
margin-top: 8px; background:
|
|
86
|
+
margin-top: 8px; background: var(--term-bg); border-radius: 8px; padding: 8px;
|
|
70
87
|
font-family: ui-monospace, Menlo, monospace; font-size: 11px; line-height: 1.6;
|
|
71
88
|
max-height: 300px; overflow-y: auto; white-space: pre-wrap; word-break: break-all;
|
|
72
|
-
color:
|
|
89
|
+
color: var(--log-text); display: none;
|
|
73
90
|
}
|
|
74
91
|
.task.open .logs { display: block; }
|
|
75
92
|
.task .actions { margin-top: 8px; display: flex; gap: 8px; }
|
|
@@ -82,7 +99,7 @@
|
|
|
82
99
|
|
|
83
100
|
/* 交互终端全屏覆盖层(pty 任务) */
|
|
84
101
|
#term-overlay {
|
|
85
|
-
position: fixed; inset: 0; z-index: 50; background:
|
|
102
|
+
position: fixed; inset: 0; z-index: 50; background: var(--term-bg);
|
|
86
103
|
display: flex; flex-direction: column;
|
|
87
104
|
padding-top: env(safe-area-inset-top); padding-bottom: env(safe-area-inset-bottom);
|
|
88
105
|
}
|
|
@@ -147,6 +164,7 @@
|
|
|
147
164
|
<div class="header-right">
|
|
148
165
|
<span id="conn-mode">连接中</span>
|
|
149
166
|
<span id="worker-status">检测中…</span>
|
|
167
|
+
<button id="btn-theme" type="button" title="切换 浅色/深色 主题">浅色</button>
|
|
150
168
|
</div>
|
|
151
169
|
</header>
|
|
152
170
|
|
|
@@ -242,6 +260,32 @@ function toast(msg) {
|
|
|
242
260
|
setTimeout(() => { el.style.opacity = '0'; }, 2200);
|
|
243
261
|
}
|
|
244
262
|
|
|
263
|
+
// ---------- 主题:light/dark 切换,偏好存 localStorage,首访跟随系统外观 ----------
|
|
264
|
+
// xterm 是 canvas 绘制读不到 CSS 变量,终端配色在这里维护并同步给各实例;
|
|
265
|
+
// 浅色下重排 16 色调色板(默认 ANSI 的黄/亮黄在白底上不可见)
|
|
266
|
+
const TERM_THEME = {
|
|
267
|
+
dark: { background: '#0b1220', foreground: '#e5e7eb' },
|
|
268
|
+
light: {
|
|
269
|
+
background: '#ffffff', foreground: '#111827', cursor: '#111827',
|
|
270
|
+
black: '#111827', red: '#dc2626', green: '#16a34a', yellow: '#ca8a04',
|
|
271
|
+
blue: '#2563eb', magenta: '#9333ea', cyan: '#0891b2', white: '#f3f4f6',
|
|
272
|
+
brightBlack: '#6b7280', brightRed: '#ef4444', brightGreen: '#22c55e',
|
|
273
|
+
brightYellow: '#eab308', brightBlue: '#3b82f6', brightMagenta: '#a855f7',
|
|
274
|
+
brightCyan: '#22d3ee', brightWhite: '#ffffff',
|
|
275
|
+
},
|
|
276
|
+
};
|
|
277
|
+
function currentTheme() {
|
|
278
|
+
return document.documentElement.dataset.theme === 'light' ? 'light' : 'dark';
|
|
279
|
+
}
|
|
280
|
+
function applyTheme(theme) {
|
|
281
|
+
document.documentElement.dataset.theme = theme;
|
|
282
|
+
localStorage.setItem('rw_theme', theme);
|
|
283
|
+
$('btn-theme').textContent = theme === 'light' ? '深色' : '浅色';
|
|
284
|
+
document.querySelector('meta[name="theme-color"]').content = theme === 'light' ? '#f4f6f9' : '#111827';
|
|
285
|
+
for (const ent of termEntries.values()) ent.xt.options.theme = { ...TERM_THEME[theme] };
|
|
286
|
+
}
|
|
287
|
+
$('btn-theme').addEventListener('click', () => applyTheme(currentTheme() === 'light' ? 'dark' : 'light'));
|
|
288
|
+
|
|
245
289
|
// ---------- 纯 JS SHA-256 / HMAC-SHA256 ----------
|
|
246
290
|
// 非 HTTPS 环境(http://VPS-IP)下浏览器的 crypto.subtle 不可用,故内联实现。
|
|
247
291
|
// 密钥只参与本地 HMAC 运算,从不随请求发送。
|
|
@@ -847,6 +891,15 @@ const ATTACH_HINTS = {
|
|
|
847
891
|
};
|
|
848
892
|
const KEY_SEQ = { BS: '\x7f', Tab: '\t', Esc: '\x1b', Up: '\x1b[A', Down: '\x1b[B', CtrlC: '\x03', CtrlD: '\x04' };
|
|
849
893
|
|
|
894
|
+
// 终端标题要有区分度:自定义标题照用;默认标题(空或「交互终端…」)
|
|
895
|
+
// 退化为「项目目录 · #任务短号」,同项目开多个终端也不会重名
|
|
896
|
+
function termLabel(t) {
|
|
897
|
+
if (t.title && !/^交互终端/.test(t.title)) return t.title;
|
|
898
|
+
const short = String(t.id).slice(-4);
|
|
899
|
+
const proj = t.project || (t.payload && t.payload.project);
|
|
900
|
+
return proj ? `${proj} · #${short}` : `终端 · #${short}`;
|
|
901
|
+
}
|
|
902
|
+
|
|
850
903
|
function termStatus(ent) {
|
|
851
904
|
const el = $('term-status');
|
|
852
905
|
if (!ent) { el.textContent = ''; return; }
|
|
@@ -864,7 +917,7 @@ function ensureTerm(t) {
|
|
|
864
917
|
}
|
|
865
918
|
const xt = new Terminal({
|
|
866
919
|
fontSize: 13, scrollback: 3000, cursorBlink: true,
|
|
867
|
-
theme: {
|
|
920
|
+
theme: { ...TERM_THEME[currentTheme()] },
|
|
868
921
|
});
|
|
869
922
|
const fit = new FitAddon.FitAddon();
|
|
870
923
|
xt.loadAddon(fit);
|
|
@@ -895,7 +948,7 @@ function openTerm(t) {
|
|
|
895
948
|
const ent = ensureTerm(t);
|
|
896
949
|
if (!ent) return;
|
|
897
950
|
activeTermId = t.id;
|
|
898
|
-
$('term-title').textContent = t
|
|
951
|
+
$('term-title').textContent = termLabel(t);
|
|
899
952
|
$('term-overlay').classList.remove('hidden');
|
|
900
953
|
showTermPane(t.id); // 先亮出本终端的 pane 再 fit(隐藏状态下 fit 拿不到尺寸)
|
|
901
954
|
fitActiveTerm();
|
|
@@ -998,7 +1051,7 @@ $('btn-submit').addEventListener('click', async () => {
|
|
|
998
1051
|
if (!/^[\w.-]+$/.test(payload.project)) return toast('项目目录名只能含字母/数字/._-');
|
|
999
1052
|
try {
|
|
1000
1053
|
$('btn-submit').disabled = true;
|
|
1001
|
-
await api('/api/tasks', { type: 'pty', title: title ||
|
|
1054
|
+
await api('/api/tasks', { type: 'pty', title: title || `交互终端(${payload.project})`, payload, targetWorker });
|
|
1002
1055
|
toast('已提交,任务运行后点「进入终端」');
|
|
1003
1056
|
$('f-title').value = '';
|
|
1004
1057
|
if (!wsReady) await refresh();
|
|
@@ -1009,6 +1062,7 @@ $('btn-submit').addEventListener('click', async () => {
|
|
|
1009
1062
|
}
|
|
1010
1063
|
});
|
|
1011
1064
|
|
|
1065
|
+
applyTheme(currentTheme()); // 主题初始化:同步按钮文案与 theme-color(head 已设好 data-theme)
|
|
1012
1066
|
connectWS();
|
|
1013
1067
|
// 首次加载兜底:WS 握手前的空档先用 REST 铺一次数据(无密钥时不发请求)
|
|
1014
1068
|
if (hasSecret()) refresh();
|
package/web/pc.html
CHANGED
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
6
|
<meta name="theme-color" content="#111827">
|
|
7
|
+
<script>
|
|
8
|
+
// 主题初始化(先于样式解析,避免先闪一下另一套配色):本地偏好 > 系统外观
|
|
9
|
+
document.documentElement.dataset.theme = localStorage.getItem('rw_theme')
|
|
10
|
+
|| (matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark');
|
|
11
|
+
</script>
|
|
7
12
|
<title>Kite 控制台</title>
|
|
8
13
|
<!-- xterm 随包自托管(vendor/ 为锁版本的官方产物):不依赖 CDN,
|
|
9
14
|
杜绝 CDN 投毒/HTTP 下被篡改注入窃取密钥的供应链风险 -->
|
|
@@ -13,6 +18,14 @@
|
|
|
13
18
|
--bg: #111827; --panel: #1f2937; --line: #374151;
|
|
14
19
|
--text: #e5e7eb; --dim: #9ca3af; --accent: #3b82f6;
|
|
15
20
|
--ok: #22c55e; --err: #ef4444; --warn: #f59e0b;
|
|
21
|
+
--term-bg: #0b1220; --log-text: #a5f3fc;
|
|
22
|
+
}
|
|
23
|
+
/* light 主题:xterm 是 canvas 绘制读不到 CSS 变量,终端色由 JS 同步切 */
|
|
24
|
+
:root[data-theme='light'] {
|
|
25
|
+
--bg: #f4f6f9; --panel: #fff; --line: #d9dee7;
|
|
26
|
+
--text: #1f2937; --dim: #64748b; --accent: #2563eb;
|
|
27
|
+
--ok: #16a34a; --err: #dc2626; --warn: #d97706;
|
|
28
|
+
--term-bg: #fff; --log-text: #0e7490;
|
|
16
29
|
}
|
|
17
30
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
18
31
|
body {
|
|
@@ -30,6 +43,11 @@
|
|
|
30
43
|
#worker-status { font-size: 13px; padding: 4px 12px; border-radius: 999px; background: var(--panel); color: var(--dim); white-space: nowrap; }
|
|
31
44
|
#worker-status.online { color: var(--ok); }
|
|
32
45
|
#worker-status.offline { color: var(--err); }
|
|
46
|
+
#btn-fullscreen, #btn-theme {
|
|
47
|
+
font-size: 13px; padding: 4px 12px; border-radius: 8px; cursor: pointer;
|
|
48
|
+
background: var(--panel); color: var(--dim); border: 1px solid var(--line); white-space: nowrap;
|
|
49
|
+
}
|
|
50
|
+
#btn-fullscreen:hover, #btn-theme:hover { border-color: var(--accent); color: var(--accent); }
|
|
33
51
|
|
|
34
52
|
/* 明文 HTTP 部署的风险提示(仅非 localhost 的 http: 环境显示) */
|
|
35
53
|
#http-warn {
|
|
@@ -75,10 +93,10 @@
|
|
|
75
93
|
.b-canceled { background: rgba(156,163,175,.15); color: var(--dim); }
|
|
76
94
|
|
|
77
95
|
.logs {
|
|
78
|
-
margin-top: 10px; background:
|
|
96
|
+
margin-top: 10px; background: var(--term-bg); border-radius: 8px; padding: 10px;
|
|
79
97
|
font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 12px; line-height: 1.6;
|
|
80
98
|
max-height: min(60vh, 640px); overflow-y: auto; white-space: pre-wrap; word-break: break-all;
|
|
81
|
-
color:
|
|
99
|
+
color: var(--log-text); display: none;
|
|
82
100
|
}
|
|
83
101
|
.task.open .logs { display: block; }
|
|
84
102
|
.task .actions { margin-top: 10px; display: flex; gap: 8px; }
|
|
@@ -91,7 +109,7 @@
|
|
|
91
109
|
|
|
92
110
|
/* 交互终端全屏覆盖层(pty 任务);PC 有物理键盘,输入直接由 xterm 承接 */
|
|
93
111
|
#term-overlay {
|
|
94
|
-
position: fixed; inset: 0; z-index: 50; background:
|
|
112
|
+
position: fixed; inset: 0; z-index: 50; background: var(--term-bg);
|
|
95
113
|
display: flex; flex-direction: column;
|
|
96
114
|
}
|
|
97
115
|
#term-overlay.hidden { display: none; }
|
|
@@ -113,7 +131,8 @@
|
|
|
113
131
|
background: var(--panel); color: var(--dim); border: 1px solid var(--line);
|
|
114
132
|
}
|
|
115
133
|
#term-layout-btns button.active { border-color: var(--accent); color: var(--accent); background: rgba(59,130,246,.12); }
|
|
116
|
-
#term-close { font-size: 13px; padding: 5px 16px; border-radius: 6px; background: var(--panel); color: var(--text); border: 1px solid var(--line); cursor: pointer; }
|
|
134
|
+
#term-fullscreen, #term-close { font-size: 13px; padding: 5px 16px; border-radius: 6px; background: var(--panel); color: var(--text); border: 1px solid var(--line); cursor: pointer; white-space: nowrap; }
|
|
135
|
+
#term-fullscreen:hover { border-color: var(--accent); color: var(--accent); }
|
|
117
136
|
/* 单屏模式:pane 绝对定位叠层,只显示 active 的一个;
|
|
118
137
|
并排(grid)模式:所有终端按 CSS grid 平铺,同时盯多个 pty */
|
|
119
138
|
#term-body { flex: 1; margin: 0 16px 16px; min-height: 0; position: relative; }
|
|
@@ -183,6 +202,8 @@
|
|
|
183
202
|
<div class="header-right">
|
|
184
203
|
<span id="conn-mode">连接中</span>
|
|
185
204
|
<span id="worker-status">检测中…</span>
|
|
205
|
+
<button id="btn-theme" type="button" title="切换 浅色/深色 主题">浅色</button>
|
|
206
|
+
<button id="btn-fullscreen" type="button">全屏</button>
|
|
186
207
|
</div>
|
|
187
208
|
</header>
|
|
188
209
|
|
|
@@ -253,6 +274,7 @@
|
|
|
253
274
|
<button type="button" data-layout="single">单屏</button>
|
|
254
275
|
<button type="button" data-layout="grid">并排</button>
|
|
255
276
|
</div>
|
|
277
|
+
<button id="term-fullscreen" type="button">全屏</button>
|
|
256
278
|
<button id="term-close" type="button">收起</button>
|
|
257
279
|
</div>
|
|
258
280
|
<div id="term-body"></div>
|
|
@@ -278,6 +300,32 @@ function toast(msg) {
|
|
|
278
300
|
setTimeout(() => { el.style.opacity = '0'; }, 2200);
|
|
279
301
|
}
|
|
280
302
|
|
|
303
|
+
// ---------- 主题:light/dark 切换,偏好存 localStorage,首访跟随系统外观 ----------
|
|
304
|
+
// xterm 是 canvas 绘制读不到 CSS 变量,终端配色在这里维护并同步给各实例;
|
|
305
|
+
// 浅色下重排 16 色调色板(默认 ANSI 的黄/亮黄在白底上不可见)
|
|
306
|
+
const TERM_THEME = {
|
|
307
|
+
dark: { background: '#0b1220', foreground: '#e5e7eb' },
|
|
308
|
+
light: {
|
|
309
|
+
background: '#ffffff', foreground: '#111827', cursor: '#111827',
|
|
310
|
+
black: '#111827', red: '#dc2626', green: '#16a34a', yellow: '#ca8a04',
|
|
311
|
+
blue: '#2563eb', magenta: '#9333ea', cyan: '#0891b2', white: '#f3f4f6',
|
|
312
|
+
brightBlack: '#6b7280', brightRed: '#ef4444', brightGreen: '#22c55e',
|
|
313
|
+
brightYellow: '#eab308', brightBlue: '#3b82f6', brightMagenta: '#a855f7',
|
|
314
|
+
brightCyan: '#22d3ee', brightWhite: '#ffffff',
|
|
315
|
+
},
|
|
316
|
+
};
|
|
317
|
+
function currentTheme() {
|
|
318
|
+
return document.documentElement.dataset.theme === 'light' ? 'light' : 'dark';
|
|
319
|
+
}
|
|
320
|
+
function applyTheme(theme) {
|
|
321
|
+
document.documentElement.dataset.theme = theme;
|
|
322
|
+
localStorage.setItem('rw_theme', theme);
|
|
323
|
+
$('btn-theme').textContent = theme === 'light' ? '深色' : '浅色';
|
|
324
|
+
document.querySelector('meta[name="theme-color"]').content = theme === 'light' ? '#f4f6f9' : '#111827';
|
|
325
|
+
for (const ent of termEntries.values()) ent.xt.options.theme = { ...TERM_THEME[theme] };
|
|
326
|
+
}
|
|
327
|
+
$('btn-theme').addEventListener('click', () => applyTheme(currentTheme() === 'light' ? 'dark' : 'light'));
|
|
328
|
+
|
|
281
329
|
// ---------- 纯 JS SHA-256 / HMAC-SHA256 ----------
|
|
282
330
|
// 非 HTTPS 环境(http://VPS-IP)下浏览器的 crypto.subtle 不可用,故内联实现。
|
|
283
331
|
// 密钥只参与本地 HMAC 运算,从不随请求发送。
|
|
@@ -889,6 +937,15 @@ const ATTACH_HINTS = {
|
|
|
889
937
|
unknown: '任务尚未被领取或 Worker 离线,请稍候…',
|
|
890
938
|
};
|
|
891
939
|
|
|
940
|
+
// 终端 tab/pane 的显示名要有区分度:自定义标题照用;默认标题(空或「交互终端…」)
|
|
941
|
+
// 退化为「项目目录 · #任务短号」,同项目开多个终端也不会重名
|
|
942
|
+
function termLabel(t) {
|
|
943
|
+
if (t.title && !/^交互终端/.test(t.title)) return t.title;
|
|
944
|
+
const short = String(t.id).slice(-4);
|
|
945
|
+
const proj = t.project || (t.payload && t.payload.project);
|
|
946
|
+
return proj ? `${proj} · #${short}` : `终端 · #${short}`;
|
|
947
|
+
}
|
|
948
|
+
|
|
892
949
|
// 状态条挂在各终端自己的 pane 头部(并排时各有各的状态)
|
|
893
950
|
function termStatus(ent) {
|
|
894
951
|
if (!ent || !ent.statusEl) return;
|
|
@@ -906,7 +963,7 @@ function ensureTerm(t) {
|
|
|
906
963
|
}
|
|
907
964
|
const xt = new Terminal({
|
|
908
965
|
fontSize: 14, scrollback: 5000, cursorBlink: true,
|
|
909
|
-
theme: {
|
|
966
|
+
theme: { ...TERM_THEME[currentTheme()] },
|
|
910
967
|
});
|
|
911
968
|
const fit = new FitAddon.FitAddon();
|
|
912
969
|
xt.loadAddon(fit);
|
|
@@ -920,7 +977,7 @@ function ensureTerm(t) {
|
|
|
920
977
|
<button class="tp-close" type="button" title="关闭该终端">✕</button>
|
|
921
978
|
</div>
|
|
922
979
|
<div class="tp-term"></div>`;
|
|
923
|
-
pane.querySelector('.tp-title').textContent = t
|
|
980
|
+
pane.querySelector('.tp-title').textContent = termLabel(t); // textContent 天然防注入
|
|
924
981
|
pane.querySelector('.tp-close').addEventListener('click', (ev) => {
|
|
925
982
|
ev.stopPropagation();
|
|
926
983
|
disposeTerm(t.id); // 只释放本地实例,任务卡上「进入终端」可随时重进(pty.attach 回补缓冲)
|
|
@@ -934,7 +991,7 @@ function ensureTerm(t) {
|
|
|
934
991
|
const tab = document.createElement('div');
|
|
935
992
|
tab.className = 'term-tab';
|
|
936
993
|
tab.innerHTML = '<span class="tt-dot"></span><span class="tt-title"></span>';
|
|
937
|
-
tab.querySelector('.tt-title').textContent = t
|
|
994
|
+
tab.querySelector('.tt-title').textContent = termLabel(t);
|
|
938
995
|
tab.addEventListener('click', () => activateTerm(t.id));
|
|
939
996
|
$('term-tabs').appendChild(tab);
|
|
940
997
|
ent = {
|
|
@@ -1010,6 +1067,8 @@ function openTerm(t) {
|
|
|
1010
1067
|
}
|
|
1011
1068
|
|
|
1012
1069
|
function closeTermOverlay() {
|
|
1070
|
+
// 覆盖层自身处于全屏时先退出全屏,否则会留一个隐藏的黑屏全屏层
|
|
1071
|
+
if (fsElement() === $('term-overlay')) exitFs();
|
|
1013
1072
|
$('term-overlay').classList.add('hidden');
|
|
1014
1073
|
activeTermId = null;
|
|
1015
1074
|
if (document.activeElement && document.activeElement.blur) document.activeElement.blur();
|
|
@@ -1044,9 +1103,37 @@ document.querySelectorAll('#term-layout-btns button').forEach((btn) => {
|
|
|
1044
1103
|
applyTermLayout();
|
|
1045
1104
|
});
|
|
1046
1105
|
});
|
|
1106
|
+
// ---------- 全屏(Fullscreen API:页头整页全屏 / 终端覆盖层单独全屏)----------
|
|
1107
|
+
function fsElement() { return document.fullscreenElement || document.webkitFullscreenElement; }
|
|
1108
|
+
function exitFs() {
|
|
1109
|
+
if (!fsElement()) return;
|
|
1110
|
+
(document.exitFullscreen || document.webkitExitFullscreen).call(document);
|
|
1111
|
+
}
|
|
1112
|
+
function toggleFs(el) {
|
|
1113
|
+
if (fsElement()) { exitFs(); return; }
|
|
1114
|
+
try {
|
|
1115
|
+
const p = el.requestFullscreen ? el.requestFullscreen()
|
|
1116
|
+
: (el.webkitRequestFullscreen ? el.webkitRequestFullscreen() : null);
|
|
1117
|
+
if (p && p.catch) p.catch(() => toast('进入全屏失败(浏览器限制,请重试)'));
|
|
1118
|
+
else if (!p) toast('当前浏览器不支持全屏');
|
|
1119
|
+
} catch { toast('当前浏览器不支持全屏'); }
|
|
1120
|
+
}
|
|
1121
|
+
function syncFsButtons() {
|
|
1122
|
+
const text = fsElement() ? '退出全屏' : '全屏';
|
|
1123
|
+
$('btn-fullscreen').textContent = text;
|
|
1124
|
+
$('term-fullscreen').textContent = text;
|
|
1125
|
+
}
|
|
1126
|
+
document.addEventListener('fullscreenchange', () => {
|
|
1127
|
+
syncFsButtons();
|
|
1128
|
+
setTimeout(fitVisibleTerms, 120); // 全屏进出后视口尺寸变了,终端重 fit
|
|
1129
|
+
});
|
|
1130
|
+
document.addEventListener('webkitfullscreenchange', syncFsButtons);
|
|
1131
|
+
$('btn-fullscreen').addEventListener('click', () => toggleFs(document.documentElement));
|
|
1132
|
+
$('term-fullscreen').addEventListener('click', () => toggleFs($('term-overlay')));
|
|
1047
1133
|
// Esc 收起终端覆盖层(终端聚焦时 Esc 会先被 xterm 吃掉,此处兜底非聚焦场景)
|
|
1048
1134
|
document.addEventListener('keydown', (ev) => {
|
|
1049
1135
|
if (ev.key === 'Escape' && activeTermId && !$('term-overlay').classList.contains('hidden')) {
|
|
1136
|
+
if (fsElement()) return; // 全屏态 Esc 由浏览器用于退出全屏,不再联动收起
|
|
1050
1137
|
const ent = termEntries.get(activeTermId);
|
|
1051
1138
|
// 终端聚焦时 Esc 是有效按键(如退出 less),双击 Esc 才收起,避免误触
|
|
1052
1139
|
if (ent && document.activeElement && document.activeElement.closest
|
|
@@ -1084,7 +1171,7 @@ $('btn-submit').addEventListener('click', async () => {
|
|
|
1084
1171
|
if (!/^[\w.-]+$/.test(payload.project)) return toast('项目目录名只能含字母/数字/._-');
|
|
1085
1172
|
try {
|
|
1086
1173
|
$('btn-submit').disabled = true;
|
|
1087
|
-
await api('/api/tasks', { type: 'pty', title: title ||
|
|
1174
|
+
await api('/api/tasks', { type: 'pty', title: title || `交互终端(${payload.project})`, payload, targetWorker });
|
|
1088
1175
|
toast('已提交,任务运行后点「进入终端」');
|
|
1089
1176
|
$('f-title').value = '';
|
|
1090
1177
|
if (!wsReady) await refresh();
|
|
@@ -1095,6 +1182,7 @@ $('btn-submit').addEventListener('click', async () => {
|
|
|
1095
1182
|
}
|
|
1096
1183
|
});
|
|
1097
1184
|
|
|
1185
|
+
applyTheme(currentTheme()); // 主题初始化:同步按钮文案与 theme-color(head 已设好 data-theme)
|
|
1098
1186
|
connectWS();
|
|
1099
1187
|
// 首次加载兜底:WS 握手前的空档先用 REST 铺一次数据(无密钥时不发请求)
|
|
1100
1188
|
if (hasSecret()) refresh();
|