@qzsy/dsh-bridge 3.2.0 → 3.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/CHANGELOG.md +25 -0
- package/README.md +8 -0
- package/client/client.js +345 -23
- package/client/index.js +67 -20
- package/client/locales.mjs +8 -0
- package/client/mobile.js +306 -0
- package/lib/saas/tunnel-client.mjs +769 -764
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# 更新日志
|
|
2
2
|
|
|
3
|
+
## [v3.3.0] - 2026-11-02
|
|
4
|
+
|
|
5
|
+
### ✨ 新增
|
|
6
|
+
|
|
7
|
+
- **移动端抽屉导航**:手机访问 DSH 时,左侧栏(会话 / 工作区 / 插件入口)改为从左侧滑出的 sheet,内容区占满屏幕;此前展开侧栏会把内容挤到 110px。
|
|
8
|
+
- **顶部菜单按钮**:窄屏左上角固定一枚菜单按钮,点击打开/关闭抽屉;抽屉打开时按钮隐藏并显示半透明遮罩。
|
|
9
|
+
- **收起路径**:点遮罩、点会话行、点「新建会话 / 收起侧边栏」、按 Esc、转回桌面宽度都会自动收起;桌面端完全不启用。
|
|
10
|
+
- **宿主重渲染自愈**:按钮 / 遮罩 / 样式被宿主移除时,`MutationObserver` 会自动补回。
|
|
11
|
+
|
|
12
|
+
### 🧪 验证
|
|
13
|
+
|
|
14
|
+
- 真实无头 Chrome + 运行中的 DSH 验证:390×844 下内容 390px 占满、抽屉宽度 300px、遮罩/按钮 aria 正确;遮罩 / Esc / 会话行 / 断点切换收起均通过;构建产物 `client.js` 的 `apply()` 安装验证通过。
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
## [v3.2.1] - 2026-11-02
|
|
18
|
+
|
|
19
|
+
### 🐛 修复
|
|
20
|
+
|
|
21
|
+
- **网关发布不再掉线**:收到网关 `drain`(滚动重启)后不再停在「已断开」,在途请求结束后自动重连;此前每次网关发布都要人工点「连接」,长连接会一直停在离线。
|
|
22
|
+
- **npm 仓库地址**:`repository.url` 补 `git+` 前缀,符合 npm 规范写法。
|
|
23
|
+
|
|
24
|
+
### ✨ 调整
|
|
25
|
+
|
|
26
|
+
- **网关卡片精简**:在线时只显示一行「网关:地址 + 新窗口打开」;掉线 / 连接中才展开状态徽标、可编辑地址与保存。
|
|
27
|
+
|
|
3
28
|
## [v3.2.0] - 2026-11-02
|
|
4
29
|
|
|
5
30
|
### ✨ 新增
|
package/README.md
CHANGED
|
@@ -42,6 +42,14 @@ dsh plugin --profile <name> add @qzsy/dsh-bridge
|
|
|
42
42
|
|
|
43
43
|
面板会把 Host 路由(存在时)显示为主地址,并同时列出 Path 路由。
|
|
44
44
|
|
|
45
|
+
## 手机访问
|
|
46
|
+
|
|
47
|
+
窄屏(≤ 767px)访问 DSH 时,插件会自动把左侧栏改成抽屉,不需要额外配置:
|
|
48
|
+
|
|
49
|
+
- 内容区占满屏宽,左上角「菜单」按钮打开会话 / 工作区 / 插件入口;
|
|
50
|
+
- 点遮罩、点会话行、按 Esc 或转回桌面宽度都会自动收起;
|
|
51
|
+
- 桌面端保持 DSH 原生布局,不受影响。
|
|
52
|
+
|
|
45
53
|
## 本地门禁与安全语义
|
|
46
54
|
|
|
47
55
|
插件会在本机监听一个代理端口(默认 `3092`),把请求转发给 DSH 本体并注入 DSH 原生
|
package/client/client.js
CHANGED
|
@@ -58,6 +58,8 @@ var zh = {
|
|
|
58
58
|
title: "\u8FDC\u7A0B\u8BBF\u95EE",
|
|
59
59
|
subtitle: "DSH \u8FDC\u7A0B\u96A7\u9053\u4E91 \xB7 \u5B98\u65B9\u5BA2\u6237\u7AEF",
|
|
60
60
|
gateway: "SaaS \u7F51\u5173\u5730\u5740",
|
|
61
|
+
gatewayLabel: "\u7F51\u5173",
|
|
62
|
+
openInNewWindow: "\u65B0\u7A97\u53E3\u6253\u5F00",
|
|
61
63
|
save: "\u4FDD\u5B58\u7F51\u5173",
|
|
62
64
|
saved: "\u5DF2\u4FDD\u5B58",
|
|
63
65
|
device: "\u8BBE\u5907",
|
|
@@ -117,12 +119,16 @@ var zh = {
|
|
|
117
119
|
durationHours: "${hours} \u5C0F\u65F6",
|
|
118
120
|
durationHoursMinutes: "${hours} \u5C0F\u65F6 ${minutes} \u5206",
|
|
119
121
|
durationMinutes: "${minutes} \u5206\u949F",
|
|
120
|
-
durationUnderMinute: "\u4E0D\u8DB3 1 \u5206\u949F"
|
|
122
|
+
durationUnderMinute: "\u4E0D\u8DB3 1 \u5206\u949F",
|
|
123
|
+
menuOpen: "\u6253\u5F00\u83DC\u5355",
|
|
124
|
+
menuClose: "\u5173\u95ED\u83DC\u5355"
|
|
121
125
|
};
|
|
122
126
|
var en = {
|
|
123
127
|
title: "Remote Access",
|
|
124
128
|
subtitle: "DSH Remote Tunnel Cloud \xB7 Official Client",
|
|
125
129
|
gateway: "SaaS gateway URL",
|
|
130
|
+
gatewayLabel: "Gateway",
|
|
131
|
+
openInNewWindow: "Open in new window",
|
|
126
132
|
save: "Save gateway",
|
|
127
133
|
saved: "Saved",
|
|
128
134
|
device: "Device",
|
|
@@ -182,7 +188,9 @@ var en = {
|
|
|
182
188
|
durationHours: "${hours} h",
|
|
183
189
|
durationHoursMinutes: "${hours} h ${minutes} min",
|
|
184
190
|
durationMinutes: "${minutes} min",
|
|
185
|
-
durationUnderMinute: "< 1 min"
|
|
191
|
+
durationUnderMinute: "< 1 min",
|
|
192
|
+
menuOpen: "Open menu",
|
|
193
|
+
menuClose: "Close menu"
|
|
186
194
|
};
|
|
187
195
|
function detectLang(host = {}) {
|
|
188
196
|
try {
|
|
@@ -207,6 +215,276 @@ function createTranslator(host = {}) {
|
|
|
207
215
|
};
|
|
208
216
|
}
|
|
209
217
|
|
|
218
|
+
// client/mobile.js
|
|
219
|
+
var MOBILE_MAX_WIDTH = 767;
|
|
220
|
+
var MOBILE_DRAWER_CSS_ID = "qzsy-mobile-drawer-styles";
|
|
221
|
+
var MOBILE_UI_FLAG = "data-qzsy-mobile-drawer";
|
|
222
|
+
var OPEN_CLASS = "qzsy-drawer-open";
|
|
223
|
+
var MENU_CLASS = "qzsy-mobile-menu-btn";
|
|
224
|
+
var BACKDROP_CLASS = "qzsy-mobile-backdrop";
|
|
225
|
+
var STYLE_PLUGIN = "@qzsy/dsh-bridge";
|
|
226
|
+
var MOBILE_DRAWER_CSS = `
|
|
227
|
+
.qzsy-mobile-menu-btn,
|
|
228
|
+
.qzsy-mobile-backdrop {
|
|
229
|
+
display: none;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
@media (max-width: 767px) {
|
|
233
|
+
/* 1. \u5DE6\u4FA7\u680F\u8131\u79BB grid\uFF0C\u5185\u5BB9\u5360\u6EE1\uFF1B\u9876\u90E8\u7559\u51FA 52px \u7ED9\u83DC\u5355\u6309\u94AE */
|
|
234
|
+
[data-slot="root"] > div[class*="_frame"] {
|
|
235
|
+
grid-template-columns: 0px minmax(0px, 1fr) minmax(0px, 0px) !important;
|
|
236
|
+
padding-top: 52px !important;
|
|
237
|
+
box-sizing: border-box !important;
|
|
238
|
+
}
|
|
239
|
+
div[class*="_centerCol"] { grid-column: 2 !important; }
|
|
240
|
+
div[class*="_rightbarCol"] { grid-column: 3 !important; }
|
|
241
|
+
div[class*="_overlayLayer"] { grid-column: 1 / -1 !important; }
|
|
242
|
+
|
|
243
|
+
/* 2. \u539F\u751F\u4FA7\u680F\u6539\u4E3A fixed \u62BD\u5C49\uFF08\u9ED8\u8BA4\u85CF\u5728\u5C4F\u5E55\u5DE6\u4FA7\uFF09 */
|
|
244
|
+
div[class*="_sidebarCol"] {
|
|
245
|
+
position: fixed !important;
|
|
246
|
+
top: 0 !important;
|
|
247
|
+
left: 0 !important;
|
|
248
|
+
bottom: 0 !important;
|
|
249
|
+
width: min(300px, 84vw) !important;
|
|
250
|
+
max-width: 84vw !important;
|
|
251
|
+
height: 100vh !important;
|
|
252
|
+
height: 100dvh !important;
|
|
253
|
+
z-index: 60 !important;
|
|
254
|
+
background: var(--dsw-alias-bg-layer-1, #ffffff) !important;
|
|
255
|
+
border-right: 1px solid var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.08)) !important;
|
|
256
|
+
box-shadow: none !important;
|
|
257
|
+
transform: translateX(-102%) !important;
|
|
258
|
+
transition: transform 280ms cubic-bezier(0.16, 1, 0.3, 1) !important;
|
|
259
|
+
overflow: hidden !important;
|
|
260
|
+
overscroll-behavior: contain;
|
|
261
|
+
will-change: transform;
|
|
262
|
+
}
|
|
263
|
+
body.qzsy-drawer-open div[class*="_sidebarCol"] {
|
|
264
|
+
transform: translateX(0) !important;
|
|
265
|
+
box-shadow: 8px 0 32px rgba(0, 0, 0, 0.18) !important;
|
|
266
|
+
}
|
|
267
|
+
body.qzsy-drawer-open div[class*="_sidebarCol"] > div > div[class*="_root"] {
|
|
268
|
+
width: 100% !important;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/* 3. \u534A\u900F\u660E\u906E\u7F69\uFF1A\u70B9\u51FB\u6536\u8D77 */
|
|
272
|
+
.qzsy-mobile-backdrop {
|
|
273
|
+
display: block;
|
|
274
|
+
position: fixed;
|
|
275
|
+
inset: 0;
|
|
276
|
+
z-index: 55;
|
|
277
|
+
background: rgba(0, 0, 0, 0.42);
|
|
278
|
+
opacity: 0;
|
|
279
|
+
pointer-events: none;
|
|
280
|
+
transition: opacity 280ms ease;
|
|
281
|
+
touch-action: none;
|
|
282
|
+
-webkit-tap-highlight-color: transparent;
|
|
283
|
+
}
|
|
284
|
+
body.qzsy-drawer-open .qzsy-mobile-backdrop {
|
|
285
|
+
opacity: 1;
|
|
286
|
+
pointer-events: auto;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/* 4. \u9876\u90E8\u83DC\u5355\u6309\u94AE\uFF08\u4F4D\u4E8E 52px \u7559\u767D\u6761\u5185\uFF0C\u907F\u514D\u906E\u6321\u4F1A\u8BDD\u6807\u9898\uFF09 */
|
|
290
|
+
.qzsy-mobile-menu-btn {
|
|
291
|
+
display: inline-flex;
|
|
292
|
+
position: fixed;
|
|
293
|
+
top: 10px;
|
|
294
|
+
left: 10px;
|
|
295
|
+
width: 40px;
|
|
296
|
+
height: 40px;
|
|
297
|
+
z-index: 65;
|
|
298
|
+
align-items: center;
|
|
299
|
+
justify-content: center;
|
|
300
|
+
padding: 0;
|
|
301
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.1));
|
|
302
|
+
border-radius: 12px;
|
|
303
|
+
background: var(--dsw-alias-bg-layer-1, #ffffff);
|
|
304
|
+
color: var(--dsw-alias-label-primary, #111827);
|
|
305
|
+
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08);
|
|
306
|
+
cursor: pointer;
|
|
307
|
+
-webkit-tap-highlight-color: transparent;
|
|
308
|
+
transition: opacity 180ms ease, transform 180ms ease;
|
|
309
|
+
}
|
|
310
|
+
.qzsy-mobile-menu-btn:active { transform: scale(0.94); }
|
|
311
|
+
.qzsy-mobile-menu-btn svg { display: block; }
|
|
312
|
+
body.qzsy-drawer-open .qzsy-mobile-menu-btn {
|
|
313
|
+
opacity: 0;
|
|
314
|
+
pointer-events: none;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
`;
|
|
318
|
+
function isElement(value) {
|
|
319
|
+
return Boolean(value) && value.nodeType === 1;
|
|
320
|
+
}
|
|
321
|
+
function isMobileViewport() {
|
|
322
|
+
return typeof window !== "undefined" && Number(window.innerWidth) <= MOBILE_MAX_WIDTH;
|
|
323
|
+
}
|
|
324
|
+
function menuLabel(open) {
|
|
325
|
+
const t = createTranslator({});
|
|
326
|
+
return open ? t("menuClose") : t("menuOpen");
|
|
327
|
+
}
|
|
328
|
+
function injectStyles() {
|
|
329
|
+
let style = document.getElementById(MOBILE_DRAWER_CSS_ID);
|
|
330
|
+
if (!style) {
|
|
331
|
+
style = document.createElement("style");
|
|
332
|
+
style.id = MOBILE_DRAWER_CSS_ID;
|
|
333
|
+
style.dataset.plugin = STYLE_PLUGIN;
|
|
334
|
+
style.dataset.pluginCss = `${STYLE_PLUGIN}/mobile`;
|
|
335
|
+
document.head.appendChild(style);
|
|
336
|
+
}
|
|
337
|
+
if (style.textContent !== MOBILE_DRAWER_CSS) style.textContent = MOBILE_DRAWER_CSS;
|
|
338
|
+
}
|
|
339
|
+
function setupMobileDrawer() {
|
|
340
|
+
if (typeof document === "undefined" || typeof window === "undefined") return () => {
|
|
341
|
+
};
|
|
342
|
+
const root = document.documentElement;
|
|
343
|
+
if (!root || root.getAttribute(MOBILE_UI_FLAG) === "1") return () => {
|
|
344
|
+
};
|
|
345
|
+
root.setAttribute(MOBILE_UI_FLAG, "1");
|
|
346
|
+
let menuBtn = null;
|
|
347
|
+
let observer = null;
|
|
348
|
+
let disposed = false;
|
|
349
|
+
const ensureElements = () => {
|
|
350
|
+
if (disposed || !document.body) return;
|
|
351
|
+
injectStyles();
|
|
352
|
+
let btn = document.querySelector(`.${MENU_CLASS}`);
|
|
353
|
+
if (!btn) {
|
|
354
|
+
btn = document.createElement("button");
|
|
355
|
+
btn.type = "button";
|
|
356
|
+
btn.className = MENU_CLASS;
|
|
357
|
+
btn.innerHTML = '<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" aria-hidden="true"><line x1="3" y1="8" x2="21" y2="8"></line><line x1="3" y1="15" x2="15" y2="15"></line></svg>';
|
|
358
|
+
document.body.appendChild(btn);
|
|
359
|
+
}
|
|
360
|
+
let mask = document.querySelector(`.${BACKDROP_CLASS}`);
|
|
361
|
+
if (!mask) {
|
|
362
|
+
mask = document.createElement("div");
|
|
363
|
+
mask.className = BACKDROP_CLASS;
|
|
364
|
+
mask.setAttribute("aria-hidden", "true");
|
|
365
|
+
document.body.appendChild(mask);
|
|
366
|
+
}
|
|
367
|
+
menuBtn = btn;
|
|
368
|
+
syncAria();
|
|
369
|
+
};
|
|
370
|
+
const isOpen = () => Boolean(document.body?.classList.contains(OPEN_CLASS));
|
|
371
|
+
const syncAria = () => {
|
|
372
|
+
if (!menuBtn) return;
|
|
373
|
+
const label = menuLabel(isOpen());
|
|
374
|
+
if (menuBtn.getAttribute("aria-label") !== label) menuBtn.setAttribute("aria-label", label);
|
|
375
|
+
if (menuBtn.getAttribute("title") !== label) menuBtn.setAttribute("title", label);
|
|
376
|
+
menuBtn.setAttribute("aria-expanded", isOpen() ? "true" : "false");
|
|
377
|
+
};
|
|
378
|
+
const ensureSidebarExpanded = () => {
|
|
379
|
+
const expand = document.querySelector(
|
|
380
|
+
'button[aria-label*="\u6253\u5F00\u4FA7\u8FB9\u680F"], button[title*="\u6253\u5F00\u4FA7\u8FB9\u680F"], button[aria-label*="Open sidebar"], button[title*="Open sidebar"], div[class*="collapsed"] button[class*="toggle"]'
|
|
381
|
+
);
|
|
382
|
+
if (expand) {
|
|
383
|
+
try {
|
|
384
|
+
expand.click();
|
|
385
|
+
} catch {
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
const openDrawer = () => {
|
|
390
|
+
if (disposed || !isMobileViewport() || !document.body) return;
|
|
391
|
+
ensureElements();
|
|
392
|
+
ensureSidebarExpanded();
|
|
393
|
+
document.body.classList.add(OPEN_CLASS);
|
|
394
|
+
syncAria();
|
|
395
|
+
};
|
|
396
|
+
const closeDrawer = ({ restoreFocus = true } = {}) => {
|
|
397
|
+
if (!document.body) return;
|
|
398
|
+
const wasOpen = isOpen();
|
|
399
|
+
document.body.classList.remove(OPEN_CLASS);
|
|
400
|
+
syncAria();
|
|
401
|
+
if (wasOpen && restoreFocus) {
|
|
402
|
+
try {
|
|
403
|
+
menuBtn?.focus?.({ preventScroll: true });
|
|
404
|
+
} catch {
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
const closeSoon = () => {
|
|
409
|
+
window.setTimeout(() => {
|
|
410
|
+
if (!disposed && isOpen()) closeDrawer({ restoreFocus: false });
|
|
411
|
+
}, 60);
|
|
412
|
+
};
|
|
413
|
+
const onClick = (event) => {
|
|
414
|
+
const target = event.target;
|
|
415
|
+
if (!isElement(target)) return;
|
|
416
|
+
if (target.closest(`.${MENU_CLASS}`)) {
|
|
417
|
+
event.preventDefault();
|
|
418
|
+
if (isOpen()) closeDrawer();
|
|
419
|
+
else openDrawer();
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
if (!isOpen()) return;
|
|
423
|
+
if (target.closest(`.${BACKDROP_CLASS}`)) {
|
|
424
|
+
closeDrawer();
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
const sidebar = document.querySelector('div[class*="_sidebarCol"]');
|
|
428
|
+
if (!sidebar || !sidebar.contains(target)) return;
|
|
429
|
+
if (target.closest('input, textarea, select, [contenteditable="true"]')) return;
|
|
430
|
+
const sessionRow = target.closest('div[class*="sessionRow"]');
|
|
431
|
+
if (sessionRow && !target.closest('button, [role="button"]')) {
|
|
432
|
+
closeSoon();
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
const action = target.closest('button, [role="button"], a');
|
|
436
|
+
if (!action) return;
|
|
437
|
+
const label = `${action.getAttribute("aria-label") || ""} ${action.getAttribute("title") || ""} ${action.innerText || ""}`;
|
|
438
|
+
if (/新建会话|New session|收起侧边栏|Collapse sidebar/.test(label)) {
|
|
439
|
+
closeSoon();
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
if (action.closest('button[class*="panelRow"]') && !/更多|More/.test(label)) {
|
|
443
|
+
closeSoon();
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
const onKeyDown = (event) => {
|
|
447
|
+
if (event.key === "Escape" && isOpen()) closeDrawer();
|
|
448
|
+
};
|
|
449
|
+
const onResize = () => {
|
|
450
|
+
if (!isMobileViewport() && isOpen()) closeDrawer({ restoreFocus: false });
|
|
451
|
+
};
|
|
452
|
+
const onVisibilityChange = () => {
|
|
453
|
+
if (document.visibilityState === "hidden" && isOpen()) closeDrawer({ restoreFocus: false });
|
|
454
|
+
};
|
|
455
|
+
injectStyles();
|
|
456
|
+
ensureElements();
|
|
457
|
+
document.addEventListener("click", onClick, true);
|
|
458
|
+
document.addEventListener("keydown", onKeyDown, true);
|
|
459
|
+
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
460
|
+
window.addEventListener("resize", onResize);
|
|
461
|
+
if (typeof MutationObserver === "function" && document.body) {
|
|
462
|
+
observer = new MutationObserver(() => {
|
|
463
|
+
if (!document.querySelector(`.${MENU_CLASS}`) || !document.querySelector(`.${BACKDROP_CLASS}`) || !document.getElementById(MOBILE_DRAWER_CSS_ID)) {
|
|
464
|
+
ensureElements();
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
observer.observe(document.body, { childList: true });
|
|
468
|
+
}
|
|
469
|
+
syncAria();
|
|
470
|
+
return () => {
|
|
471
|
+
disposed = true;
|
|
472
|
+
try {
|
|
473
|
+
observer?.disconnect();
|
|
474
|
+
} catch {
|
|
475
|
+
}
|
|
476
|
+
document.removeEventListener("click", onClick, true);
|
|
477
|
+
document.removeEventListener("keydown", onKeyDown, true);
|
|
478
|
+
document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
479
|
+
window.removeEventListener("resize", onResize);
|
|
480
|
+
document.body?.classList.remove(OPEN_CLASS);
|
|
481
|
+
document.querySelector(`.${MENU_CLASS}`)?.remove();
|
|
482
|
+
document.querySelector(`.${BACKDROP_CLASS}`)?.remove();
|
|
483
|
+
document.getElementById(MOBILE_DRAWER_CSS_ID)?.remove();
|
|
484
|
+
root.removeAttribute(MOBILE_UI_FLAG);
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
|
|
210
488
|
// client/index.js
|
|
211
489
|
var name = PLUGIN_ID;
|
|
212
490
|
var inject = ["slots", "connection"];
|
|
@@ -543,6 +821,18 @@ function BridgePanel() {
|
|
|
543
821
|
const openUrl = status?.openUrl ?? "";
|
|
544
822
|
const passwordSet = Boolean(status?.access?.passwordSet);
|
|
545
823
|
const stateText = connected ? t("ready") : phase === "connecting" || phase === "reconnecting" ? t("connecting") : t("offline");
|
|
824
|
+
const gatewayWebUrl = React.useMemo(() => {
|
|
825
|
+
const raw = String(status?.gatewayUrl || gatewayDraft || "").trim();
|
|
826
|
+
if (!raw) return "";
|
|
827
|
+
try {
|
|
828
|
+
const url = new URL(raw);
|
|
829
|
+
if (url.protocol === "wss:") url.protocol = "https:";
|
|
830
|
+
else if (url.protocol === "ws:") url.protocol = "http:";
|
|
831
|
+
return url.origin;
|
|
832
|
+
} catch {
|
|
833
|
+
return raw.replace(/\/+$/, "");
|
|
834
|
+
}
|
|
835
|
+
}, [status?.gatewayUrl, gatewayDraft]);
|
|
546
836
|
return h(
|
|
547
837
|
"div",
|
|
548
838
|
{ style: styles.wrap },
|
|
@@ -559,35 +849,55 @@ function BridgePanel() {
|
|
|
559
849
|
onRefresh: () => setProfileTick((value) => value + 1),
|
|
560
850
|
t
|
|
561
851
|
}) : null,
|
|
852
|
+
// 网关卡片:在线时只留一行「网关:地址 + 新窗口打开」;掉线/连接中才展开
|
|
853
|
+
// 状态徽标与可编辑地址,避免正常状态下一屏塞满诊断信息。
|
|
562
854
|
h(
|
|
563
855
|
"div",
|
|
564
856
|
{ style: styles.card },
|
|
565
|
-
h(
|
|
857
|
+
connected ? h(
|
|
566
858
|
"div",
|
|
567
859
|
{ style: styles.row },
|
|
568
|
-
h("span", { style: styles.
|
|
569
|
-
h("
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
h("
|
|
860
|
+
h("span", { style: { ...styles.muted, whiteSpace: "nowrap" } }, `${t("gatewayLabel")}\uFF1A`),
|
|
861
|
+
h("a", {
|
|
862
|
+
href: gatewayWebUrl,
|
|
863
|
+
target: "_blank",
|
|
864
|
+
rel: "noopener noreferrer",
|
|
865
|
+
style: { flex: 1, minWidth: 180, fontSize: 13, lineHeight: 1.6, wordBreak: "break-all", color: "inherit" }
|
|
866
|
+
}, gatewayWebUrl),
|
|
867
|
+
h("a", {
|
|
868
|
+
href: gatewayWebUrl,
|
|
869
|
+
target: "_blank",
|
|
870
|
+
rel: "noopener noreferrer",
|
|
871
|
+
style: { ...styles.btn, textDecoration: "none", whiteSpace: "nowrap" }
|
|
872
|
+
}, t("openInNewWindow"))
|
|
873
|
+
) : [
|
|
576
874
|
h(
|
|
577
875
|
"div",
|
|
578
876
|
{ style: styles.row },
|
|
579
|
-
h("
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
877
|
+
h("span", { style: styles.badge }, `${t("status")}: ${stateText}`),
|
|
878
|
+
h("span", { style: styles.badge }, bound ? `${t("device")}: ${status?.device?.idMasked ?? ""}` : t("notBound")),
|
|
879
|
+
status?.protocol ? h("span", { style: styles.badge }, `protocol v${status.protocol}`) : null
|
|
880
|
+
),
|
|
881
|
+
h(
|
|
882
|
+
"div",
|
|
883
|
+
{ style: { marginTop: 10 } },
|
|
884
|
+
h("span", { style: styles.label }, t("gateway")),
|
|
885
|
+
h(
|
|
886
|
+
"div",
|
|
887
|
+
{ style: styles.row },
|
|
888
|
+
h("input", {
|
|
889
|
+
style: styles.input,
|
|
890
|
+
value: gatewayDraft,
|
|
891
|
+
placeholder: "wss://dsh.b.nps.qzsyzn.com/_gw/connect",
|
|
892
|
+
onChange: (e) => setGatewayDraft(e.target.value)
|
|
893
|
+
}),
|
|
894
|
+
h("button", { style: styles.btnPrimary, disabled: busy === "gateway" || !gatewayDraft, onClick: saveGateway }, t("save"))
|
|
895
|
+
)
|
|
896
|
+
),
|
|
897
|
+
!adminToken ? h("div", { style: { ...styles.muted, marginTop: 8 } }, t("adminLocalOnly")) : null,
|
|
898
|
+
error ? h("div", { style: { ...styles.error, marginTop: 10 } }, `${t("lastError")}: ${error}`) : null,
|
|
899
|
+
notice ? h("div", { style: { ...styles.muted, marginTop: 8 } }, notice) : null
|
|
900
|
+
]
|
|
591
901
|
),
|
|
592
902
|
h(
|
|
593
903
|
"div",
|
|
@@ -702,6 +1012,18 @@ var ctxRpc = async () => ({ ok: false, error: { code: "no-context", message: "RP
|
|
|
702
1012
|
var serviceControl = null;
|
|
703
1013
|
function apply(ctx) {
|
|
704
1014
|
ctxRpc = (endpoint, payload, signal) => ctx.connection.rpc.call(RPC_CHANNEL, endpoint, payload, signal);
|
|
1015
|
+
let disposeMobile = () => {
|
|
1016
|
+
};
|
|
1017
|
+
try {
|
|
1018
|
+
disposeMobile = setupMobileDrawer();
|
|
1019
|
+
} catch {
|
|
1020
|
+
}
|
|
1021
|
+
if (typeof ctx.effect === "function") {
|
|
1022
|
+
try {
|
|
1023
|
+
ctx.effect(() => disposeMobile, `${PLUGIN_ID}: mobile drawer`);
|
|
1024
|
+
} catch {
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
705
1027
|
const panel = (props = {}) => h(BridgePanel, { ...props, key: "qzsy-dsh-bridge-panel" });
|
|
706
1028
|
let visible = true;
|
|
707
1029
|
let registration = null;
|
package/client/index.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// 在线态与最近错误。所有网络操作走 Host 侧 RPC(同一 profile 的本地回环)。
|
|
5
5
|
import { PLUGIN_ID, PACKAGE_NAME, RPC_CHANNEL, RPC_ENDPOINTS } from '../lib/identity.js';
|
|
6
6
|
import { createTranslator } from './locales.mjs';
|
|
7
|
+
import { setupMobileDrawer } from './mobile.js';
|
|
7
8
|
|
|
8
9
|
export const name = PLUGIN_ID;
|
|
9
10
|
export const inject = ['slots', 'connection'];
|
|
@@ -305,6 +306,19 @@ function BridgePanel() {
|
|
|
305
306
|
const openUrl = status?.openUrl ?? '';
|
|
306
307
|
const passwordSet = Boolean(status?.access?.passwordSet);
|
|
307
308
|
const stateText = connected ? t('ready') : (phase === 'connecting' || phase === 'reconnecting' ? t('connecting') : t('offline'));
|
|
309
|
+
// 在线态只展示一行「网关:地址」:把 wss://host/_gw/connect 归一成可点开的 web 地址。
|
|
310
|
+
const gatewayWebUrl = React.useMemo(() => {
|
|
311
|
+
const raw = String(status?.gatewayUrl || gatewayDraft || '').trim();
|
|
312
|
+
if (!raw) return '';
|
|
313
|
+
try {
|
|
314
|
+
const url = new URL(raw);
|
|
315
|
+
if (url.protocol === 'wss:') url.protocol = 'https:';
|
|
316
|
+
else if (url.protocol === 'ws:') url.protocol = 'http:';
|
|
317
|
+
return url.origin;
|
|
318
|
+
} catch {
|
|
319
|
+
return raw.replace(/\/+$/, '');
|
|
320
|
+
}
|
|
321
|
+
}, [status?.gatewayUrl, gatewayDraft]);
|
|
308
322
|
|
|
309
323
|
return h('div', { style: styles.wrap },
|
|
310
324
|
h('div', { style: { marginBottom: 12 } },
|
|
@@ -320,27 +334,47 @@ function BridgePanel() {
|
|
|
320
334
|
t,
|
|
321
335
|
}) : null,
|
|
322
336
|
|
|
337
|
+
// 网关卡片:在线时只留一行「网关:地址 + 新窗口打开」;掉线/连接中才展开
|
|
338
|
+
// 状态徽标与可编辑地址,避免正常状态下一屏塞满诊断信息。
|
|
323
339
|
h('div', { style: styles.card },
|
|
324
|
-
|
|
325
|
-
h('
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
)
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
340
|
+
connected
|
|
341
|
+
? h('div', { style: styles.row },
|
|
342
|
+
h('span', { style: { ...styles.muted, whiteSpace: 'nowrap' } }, `${t('gatewayLabel')}:`),
|
|
343
|
+
h('a', {
|
|
344
|
+
href: gatewayWebUrl,
|
|
345
|
+
target: '_blank',
|
|
346
|
+
rel: 'noopener noreferrer',
|
|
347
|
+
style: { flex: 1, minWidth: 180, fontSize: 13, lineHeight: 1.6, wordBreak: 'break-all', color: 'inherit' },
|
|
348
|
+
}, gatewayWebUrl),
|
|
349
|
+
h('a', {
|
|
350
|
+
href: gatewayWebUrl,
|
|
351
|
+
target: '_blank',
|
|
352
|
+
rel: 'noopener noreferrer',
|
|
353
|
+
style: { ...styles.btn, textDecoration: 'none', whiteSpace: 'nowrap' },
|
|
354
|
+
}, t('openInNewWindow')),
|
|
355
|
+
)
|
|
356
|
+
: [
|
|
357
|
+
h('div', { style: styles.row },
|
|
358
|
+
h('span', { style: styles.badge }, `${t('status')}: ${stateText}`),
|
|
359
|
+
h('span', { style: styles.badge }, bound ? `${t('device')}: ${status?.device?.idMasked ?? ''}` : t('notBound')),
|
|
360
|
+
status?.protocol ? h('span', { style: styles.badge }, `protocol v${status.protocol}`) : null,
|
|
361
|
+
),
|
|
362
|
+
h('div', { style: { marginTop: 10 } },
|
|
363
|
+
h('span', { style: styles.label }, t('gateway')),
|
|
364
|
+
h('div', { style: styles.row },
|
|
365
|
+
h('input', {
|
|
366
|
+
style: styles.input,
|
|
367
|
+
value: gatewayDraft,
|
|
368
|
+
placeholder: 'wss://dsh.b.nps.qzsyzn.com/_gw/connect',
|
|
369
|
+
onChange: (e) => setGatewayDraft(e.target.value),
|
|
370
|
+
}),
|
|
371
|
+
h('button', { style: styles.btnPrimary, disabled: busy === 'gateway' || !gatewayDraft, onClick: saveGateway }, t('save')),
|
|
372
|
+
),
|
|
373
|
+
),
|
|
374
|
+
!adminToken ? h('div', { style: { ...styles.muted, marginTop: 8 } }, t('adminLocalOnly')) : null,
|
|
375
|
+
error ? h('div', { style: { ...styles.error, marginTop: 10 } }, `${t('lastError')}: ${error}`) : null,
|
|
376
|
+
notice ? h('div', { style: { ...styles.muted, marginTop: 8 } }, notice) : null,
|
|
377
|
+
],
|
|
344
378
|
),
|
|
345
379
|
|
|
346
380
|
h('div', { style: styles.card },
|
|
@@ -427,6 +461,19 @@ let serviceControl = null;
|
|
|
427
461
|
|
|
428
462
|
export function apply(ctx) {
|
|
429
463
|
ctxRpc = (endpoint, payload, signal) => ctx.connection.rpc.call(RPC_CHANNEL, endpoint, payload, signal);
|
|
464
|
+
|
|
465
|
+
// 移动端增强:左侧栏改为抽屉。桌面端由 CSS 断点自动跳过;
|
|
466
|
+
// 失败只影响移动端布局,绝不能让设置页挂掉。
|
|
467
|
+
let disposeMobile = () => {};
|
|
468
|
+
try {
|
|
469
|
+
disposeMobile = setupMobileDrawer();
|
|
470
|
+
} catch {}
|
|
471
|
+
if (typeof ctx.effect === 'function') {
|
|
472
|
+
try {
|
|
473
|
+
ctx.effect(() => disposeMobile, `${PLUGIN_ID}: mobile drawer`);
|
|
474
|
+
} catch {}
|
|
475
|
+
}
|
|
476
|
+
|
|
430
477
|
const panel = (props = {}) => h(BridgePanel, { ...props, key: 'qzsy-dsh-bridge-panel' });
|
|
431
478
|
let visible = true;
|
|
432
479
|
let registration = null;
|
package/client/locales.mjs
CHANGED
|
@@ -3,6 +3,8 @@ export const zh = {
|
|
|
3
3
|
title: '远程访问',
|
|
4
4
|
subtitle: 'DSH 远程隧道云 · 官方客户端',
|
|
5
5
|
gateway: 'SaaS 网关地址',
|
|
6
|
+
gatewayLabel: '网关',
|
|
7
|
+
openInNewWindow: '新窗口打开',
|
|
6
8
|
save: '保存网关',
|
|
7
9
|
saved: '已保存',
|
|
8
10
|
device: '设备',
|
|
@@ -63,12 +65,16 @@ export const zh = {
|
|
|
63
65
|
durationHoursMinutes: '${hours} 小时 ${minutes} 分',
|
|
64
66
|
durationMinutes: '${minutes} 分钟',
|
|
65
67
|
durationUnderMinute: '不足 1 分钟',
|
|
68
|
+
menuOpen: '打开菜单',
|
|
69
|
+
menuClose: '关闭菜单',
|
|
66
70
|
};
|
|
67
71
|
|
|
68
72
|
export const en = {
|
|
69
73
|
title: 'Remote Access',
|
|
70
74
|
subtitle: 'DSH Remote Tunnel Cloud · Official Client',
|
|
71
75
|
gateway: 'SaaS gateway URL',
|
|
76
|
+
gatewayLabel: 'Gateway',
|
|
77
|
+
openInNewWindow: 'Open in new window',
|
|
72
78
|
save: 'Save gateway',
|
|
73
79
|
saved: 'Saved',
|
|
74
80
|
device: 'Device',
|
|
@@ -129,6 +135,8 @@ export const en = {
|
|
|
129
135
|
durationHoursMinutes: '${hours} h ${minutes} min',
|
|
130
136
|
durationMinutes: '${minutes} min',
|
|
131
137
|
durationUnderMinute: '< 1 min',
|
|
138
|
+
menuOpen: 'Open menu',
|
|
139
|
+
menuClose: 'Close menu',
|
|
132
140
|
};
|
|
133
141
|
|
|
134
142
|
export function detectLang(host = {}) {
|