@feiyang666/dsh-mobile-remote 1.0.1 → 1.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.
- package/CHANGELOG.md +17 -16
- package/LICENSE +1 -1
- package/README.md +152 -63
- package/README.zh.md +153 -64
- package/RELEASE_NOTES.md +49 -0
- package/cordis.patch.yml +12 -16
- package/lib/client.js +256 -467
- package/lib/index.js +380 -189
- package/lib/qrcode.js +2300 -0
- package/package.json +29 -23
package/lib/client.js
CHANGED
|
@@ -1,503 +1,292 @@
|
|
|
1
|
-
// dsh-mobile-remote —
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
1
|
+
// dsh-mobile-remote — DSH 移动端远程控制插件(client 侧)。
|
|
2
|
+
//
|
|
3
|
+
// 在 Web UI 的「设置」中注册一个「远程控制」section,提供:
|
|
4
|
+
// 1. 开启 / 关闭开关(写入 / 移除 profile patch,经 HMR 热重载生效);
|
|
5
|
+
// 2. 连接二维码(通过 host 的 /__dsh_remote/qr 接口返回 SVG);
|
|
6
|
+
// 3. 当前连接的设备数量(host 侧心跳统计)。
|
|
7
|
+
//
|
|
8
|
+
// 打包格式:浏览器 module loader(window.__ModuleLoader__.load)标准 bundle,
|
|
9
|
+
// 与官方 client 插件一致。纯 React.createElement,无 JSX 构建依赖。
|
|
10
10
|
|
|
11
11
|
window.__ModuleLoader__.load({
|
|
12
|
-
id: '
|
|
12
|
+
id: 'dsh-mobile-remote',
|
|
13
13
|
factory: (require) => {
|
|
14
|
-
var module = { exports: {} }
|
|
15
|
-
var exports = module.exports
|
|
16
|
-
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
|
|
17
|
-
var React = require('react')
|
|
18
|
-
var el = React.createElement
|
|
14
|
+
var module = { exports: {} };
|
|
15
|
+
var exports = module.exports;
|
|
16
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
19
17
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
var MOBILE_CSS = [
|
|
26
|
-
'@media (max-width: 768px) {',
|
|
27
|
-
'/* 1) 三列 → 单列:隐藏侧栏 rail 与 details 列 */',
|
|
28
|
-
'div:has(> [data-shell-overlay]) {',
|
|
29
|
-
' grid-template-columns: 0px minmax(0, 1fr) 0px !important;',
|
|
30
|
-
'}',
|
|
31
|
-
'/* 2) 侧栏改为抽屉:默认滑出屏幕,打开时滑入(JS 维护 data-mr-open) */',
|
|
32
|
-
'div:has(> [data-shell-overlay]) > div:first-child {',
|
|
33
|
-
' position: fixed !important;',
|
|
34
|
-
' top: 0 !important;',
|
|
35
|
-
' left: 0 !important;',
|
|
36
|
-
' bottom: 0 !important;',
|
|
37
|
-
' height: 100% !important;',
|
|
38
|
-
' height: 100dvh !important;',
|
|
39
|
-
' width: min(84vw, 280px) !important;',
|
|
40
|
-
' margin: 0 !important;',
|
|
41
|
-
' z-index: 120 !important;',
|
|
42
|
-
' background: var(--dsw-alias-bg-base, #ffffff) !important;',
|
|
43
|
-
' border-right: 1px solid rgba(128,128,128,.2) !important;',
|
|
44
|
-
' box-shadow: 0 0 36px rgba(0,0,0,.35) !important;',
|
|
45
|
-
' transform: translateX(-106%);',
|
|
46
|
-
' transition: transform .22s ease !important;',
|
|
47
|
-
' -webkit-overflow-scrolling: touch;',
|
|
48
|
-
'}',
|
|
49
|
-
'div:has(> [data-shell-overlay]) > div:first-child[data-mr-open="true"] {',
|
|
50
|
-
' transform: translateX(0) !important;',
|
|
51
|
-
'}',
|
|
52
|
-
'/* 3) 抽屉遮罩(JS 创建) */',
|
|
53
|
-
'body > [data-mr-mask] {',
|
|
54
|
-
' position: fixed !important;',
|
|
55
|
-
' inset: 0 !important;',
|
|
56
|
-
' z-index: 110 !important;',
|
|
57
|
-
' background: rgba(0,0,0,.35) !important;',
|
|
58
|
-
'}',
|
|
59
|
-
'/* 4) 触控优化 */',
|
|
60
|
-
'button, [role="button"], input, select, textarea {',
|
|
61
|
-
' touch-action: manipulation;',
|
|
62
|
-
' -webkit-tap-highlight-color: transparent;',
|
|
63
|
-
'}',
|
|
64
|
-
'body { -webkit-text-size-adjust: 100%; }',
|
|
65
|
-
'}'
|
|
66
|
-
].join('\n')
|
|
18
|
+
var React = require('react');
|
|
19
|
+
var el = React.createElement;
|
|
20
|
+
var useState = React.useState;
|
|
21
|
+
var useEffect = React.useEffect;
|
|
22
|
+
var useCallback = React.useCallback;
|
|
67
23
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (existing) return
|
|
72
|
-
var tag = document.createElement('style')
|
|
73
|
-
tag.setAttribute('data-plugin', 'dsh-mobile-remote')
|
|
74
|
-
tag.setAttribute('data-plugin-css', 'dsh-mobile-remote/mobile')
|
|
75
|
-
tag.textContent = MOBILE_CSS
|
|
76
|
-
document.head.appendChild(tag)
|
|
77
|
-
})()
|
|
24
|
+
/* ------------------------------------------------------------------ */
|
|
25
|
+
/* 内联样式(跟随宿主主题 token,深色/浅色自动适配) */
|
|
26
|
+
/* ------------------------------------------------------------------ */
|
|
78
27
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
28
|
+
var st = {
|
|
29
|
+
root: {
|
|
30
|
+
display: 'flex', flexDirection: 'column', gap: 16,
|
|
31
|
+
width: '100%', maxWidth: 560, minWidth: 0, boxSizing: 'border-box',
|
|
32
|
+
padding: '8px 0',
|
|
33
|
+
},
|
|
34
|
+
card: {
|
|
35
|
+
display: 'flex', flexDirection: 'column', gap: 12,
|
|
36
|
+
padding: '16px 18px', borderRadius: 12,
|
|
37
|
+
background: 'var(--dsw-alias-bg-overlay, #f6f6f7)',
|
|
38
|
+
border: '1px solid var(--dsw-alias-border-l2, rgba(127,127,127,.18))',
|
|
39
|
+
boxSizing: 'border-box',
|
|
40
|
+
},
|
|
41
|
+
row: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12 },
|
|
42
|
+
title: { fontSize: 15, fontWeight: 600, margin: 0, lineHeight: 1.4 },
|
|
43
|
+
sub: { fontSize: 13, color: 'var(--dsw-alias-label-tertiary, #767676)', lineHeight: 1.6, margin: 0 },
|
|
44
|
+
hint: { fontSize: 13, color: 'var(--dsw-alias-label-tertiary, #767676)', lineHeight: 1.6, margin: 0 },
|
|
45
|
+
badge: {
|
|
46
|
+
display: 'inline-flex', alignItems: 'center', gap: 6,
|
|
47
|
+
fontSize: 13, fontWeight: 600, padding: '2px 10px', borderRadius: 999,
|
|
48
|
+
whiteSpace: 'nowrap',
|
|
49
|
+
},
|
|
50
|
+
badgeOn: { color: '#0d7a3f', background: 'rgba(18,150,78,.14)' },
|
|
51
|
+
badgeOff: { color: '#8a8a8a', background: 'rgba(127,127,127,.12)' },
|
|
52
|
+
switch: {
|
|
53
|
+
position: 'relative', width: 46, height: 26, flex: 'none',
|
|
54
|
+
borderRadius: 999, border: 'none', cursor: 'pointer',
|
|
55
|
+
transition: 'background .18s ease', padding: 0,
|
|
56
|
+
background: 'var(--dsw-alias-state-disabled-bg, #d1d1d1)',
|
|
57
|
+
},
|
|
58
|
+
switchOn: { background: 'var(--dsw-alias-state-business-primary, #2a7de1)' },
|
|
59
|
+
knob: {
|
|
60
|
+
position: 'absolute', top: 3, left: 3, width: 20, height: 20,
|
|
61
|
+
borderRadius: 999, background: '#fff', boxShadow: '0 1px 3px rgba(0,0,0,.25)',
|
|
62
|
+
transition: 'transform .18s ease',
|
|
63
|
+
},
|
|
64
|
+
knobOn: { transform: 'translateX(20px)' },
|
|
65
|
+
qrBox: {
|
|
66
|
+
display: 'flex', justifyContent: 'center', alignItems: 'center',
|
|
67
|
+
padding: 12, borderRadius: 10,
|
|
68
|
+
background: '#fff',
|
|
69
|
+
border: '1px solid var(--dsw-alias-border-l2, rgba(127,127,127,.18))',
|
|
70
|
+
},
|
|
71
|
+
qrImg: { width: 180, height: 180, display: 'block' },
|
|
72
|
+
qrFallback: {
|
|
73
|
+
fontSize: 12, color: 'var(--dsw-alias-label-tertiary, #767676)',
|
|
74
|
+
textAlign: 'center', lineHeight: 1.6, wordBreak: 'break-all', padding: 8,
|
|
75
|
+
},
|
|
76
|
+
grid: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 },
|
|
77
|
+
stat: {
|
|
78
|
+
display: 'flex', flexDirection: 'column', gap: 4,
|
|
79
|
+
padding: '12px 14px', borderRadius: 10,
|
|
80
|
+
background: 'var(--dsw-alias-bg-overlay, #f6f6f7)',
|
|
81
|
+
border: '1px solid var(--dsw-alias-border-l2, rgba(127,127,127,.18))',
|
|
82
|
+
boxSizing: 'border-box',
|
|
83
|
+
},
|
|
84
|
+
statValue: { fontSize: 20, fontWeight: 700, lineHeight: 1.2 },
|
|
85
|
+
statLabel: { fontSize: 12, color: 'var(--dsw-alias-label-tertiary, #767676)' },
|
|
86
|
+
link: {
|
|
87
|
+
fontSize: 13, color: 'var(--dsw-alias-state-business-primary, #2a7de1)',
|
|
88
|
+
wordBreak: 'break-all', textDecoration: 'none', lineHeight: 1.6,
|
|
89
|
+
},
|
|
90
|
+
code: {
|
|
91
|
+
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Consolas, monospace',
|
|
92
|
+
fontSize: 12, padding: '8px 10px', borderRadius: 8,
|
|
93
|
+
background: 'var(--dsw-alias-bg-base, #f0f0f1)',
|
|
94
|
+
border: '1px solid var(--dsw-alias-border-l2, rgba(127,127,127,.18))',
|
|
95
|
+
overflowX: 'auto', whiteSpace: 'pre-wrap', wordBreak: 'break-all', lineHeight: 1.5,
|
|
96
|
+
color: 'var(--dsw-alias-label-primary, #1f1f1f)',
|
|
97
|
+
},
|
|
98
|
+
button: {
|
|
99
|
+
alignSelf: 'flex-start', padding: '8px 16px', borderRadius: 8,
|
|
100
|
+
border: '1px solid var(--dsw-alias-border-l2, rgba(127,127,127,.18))',
|
|
101
|
+
background: 'transparent', color: 'var(--dsw-alias-label-primary, #1f1f1f)',
|
|
102
|
+
fontSize: 13, fontWeight: 500, cursor: 'pointer',
|
|
103
|
+
},
|
|
104
|
+
divider: { height: 1, background: 'var(--dsw-alias-border-l2, rgba(127,127,127,.18))', border: 'none' },
|
|
105
|
+
disabled: { opacity: 0.55 },
|
|
106
|
+
};
|
|
84
107
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
headers: { 'Content-Type': 'application/json' },
|
|
89
|
-
body: JSON.stringify(args || {}),
|
|
90
|
-
}).then(function (r) { return r.json() })
|
|
91
|
-
}
|
|
108
|
+
/* ------------------------------------------------------------------ */
|
|
109
|
+
/* 远程控制面板 */
|
|
110
|
+
/* ------------------------------------------------------------------ */
|
|
92
111
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
var w = window.innerWidth || (document.documentElement && document.documentElement.clientWidth) || 0
|
|
96
|
-
var coarse = window.matchMedia('(pointer: coarse)').matches
|
|
97
|
-
return w <= 768 || (coarse && w <= 1024)
|
|
98
|
-
}
|
|
112
|
+
var STATUS_URL = '/__dsh_remote/status';
|
|
113
|
+
var TOGGLE_URL = '/__dsh_remote/toggle';
|
|
99
114
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (typeof location === 'undefined') return false
|
|
103
|
-
var h = String(location.hostname || '').toLowerCase()
|
|
104
|
-
return !(h === '' || h === 'localhost' || h === '127.0.0.1' || h === '::1' || h === '[::1]')
|
|
115
|
+
function fmtUrl(url) {
|
|
116
|
+
return url || '未检测到局域网地址';
|
|
105
117
|
}
|
|
106
118
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
var ok = document.execCommand('copy')
|
|
118
|
-
document.body.removeChild(ta)
|
|
119
|
-
cb(ok)
|
|
120
|
-
} catch (e) { cb(false) }
|
|
121
|
-
}
|
|
122
|
-
if (typeof navigator !== 'undefined' && navigator.clipboard && navigator.clipboard.writeText) {
|
|
123
|
-
navigator.clipboard.writeText(text).then(function () { cb(true) }).catch(fallback)
|
|
124
|
-
} else {
|
|
125
|
-
fallback()
|
|
126
|
-
}
|
|
127
|
-
}
|
|
119
|
+
function RemoteControlPanel() {
|
|
120
|
+
var _s = useState(null);
|
|
121
|
+
var status = _s[0];
|
|
122
|
+
var setStatus = _s[1];
|
|
123
|
+
var _t = useState(false);
|
|
124
|
+
var busy = _t[0];
|
|
125
|
+
var setBusy = _t[1];
|
|
126
|
+
var _e = useState(null);
|
|
127
|
+
var error = _e[0];
|
|
128
|
+
var setError = _e[1];
|
|
128
129
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if (typeof MutationObserver === 'undefined' || typeof document === 'undefined') return
|
|
136
|
-
var frame = null
|
|
137
|
-
var sidebar = null
|
|
138
|
-
var mask = null
|
|
139
|
-
var styleObserver = null
|
|
140
|
-
var enabled = false // 仅在移动视口下启用抽屉逻辑(桌面端绝不干预)
|
|
130
|
+
var refresh = useCallback(function () {
|
|
131
|
+
fetch(STATUS_URL)
|
|
132
|
+
.then(function (r) { return r.json(); })
|
|
133
|
+
.then(function (data) { setStatus(data); setError(null); })
|
|
134
|
+
.catch(function (e) { setError(String(e && e.message || e)); });
|
|
135
|
+
}, []);
|
|
141
136
|
|
|
142
|
-
function
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
if (layout) layout.toggleSidebar()
|
|
148
|
-
})
|
|
149
|
-
document.body.appendChild(mask)
|
|
150
|
-
}
|
|
151
|
-
function removeMask() {
|
|
152
|
-
if (mask) { mask.remove(); mask = null }
|
|
153
|
-
}
|
|
154
|
-
function applyOpen(open) {
|
|
155
|
-
if (!sidebar) return
|
|
156
|
-
if (open) {
|
|
157
|
-
sidebar.setAttribute('data-mr-open', 'true')
|
|
158
|
-
ensureMask()
|
|
159
|
-
} else {
|
|
160
|
-
sidebar.removeAttribute('data-mr-open')
|
|
161
|
-
removeMask()
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
function syncFromFrame() {
|
|
165
|
-
if (!frame || !enabled) { applyOpen(false); return }
|
|
166
|
-
var cols = String(frame.style.gridTemplateColumns || '').trim()
|
|
167
|
-
var m = /^(-?\d+(?:\.\d+)?)/.exec(cols)
|
|
168
|
-
var first = m ? parseFloat(m[1]) : 0
|
|
169
|
-
applyOpen(first > 0)
|
|
170
|
-
}
|
|
171
|
-
function setup() {
|
|
172
|
-
var overlay = document.querySelector('[data-shell-overlay]')
|
|
173
|
-
if (!overlay) return
|
|
174
|
-
var f = overlay.parentElement
|
|
175
|
-
if (!f || f === frame) return
|
|
176
|
-
frame = f
|
|
177
|
-
sidebar = f.firstElementChild
|
|
178
|
-
if (styleObserver) styleObserver.disconnect()
|
|
179
|
-
styleObserver = new MutationObserver(syncFromFrame)
|
|
180
|
-
styleObserver.observe(frame, { attributes: true, attributeFilter: ['style'] })
|
|
181
|
-
syncFromFrame()
|
|
182
|
-
}
|
|
183
|
-
// 视口变化时切换启用/停用抽屉逻辑
|
|
184
|
-
function syncEnabled() {
|
|
185
|
-
var on = isMobileViewport()
|
|
186
|
-
if (on === enabled) return
|
|
187
|
-
enabled = on
|
|
188
|
-
if (!on) applyOpen(false)
|
|
189
|
-
else syncFromFrame()
|
|
190
|
-
}
|
|
191
|
-
function watchViewport() {
|
|
192
|
-
if (typeof window === 'undefined') return
|
|
193
|
-
var mq = window.matchMedia('(max-width: 768px)')
|
|
194
|
-
var mq2 = window.matchMedia('(pointer: coarse)')
|
|
195
|
-
function update() { syncEnabled() }
|
|
196
|
-
if (mq.addEventListener) mq.addEventListener('change', update)
|
|
197
|
-
if (mq2.addEventListener) mq2.addEventListener('change', update)
|
|
198
|
-
window.addEventListener('resize', update)
|
|
199
|
-
}
|
|
200
|
-
var wait = new MutationObserver(function () { setup() })
|
|
201
|
-
if (document.body) wait.observe(document.body, { childList: true, subtree: true })
|
|
202
|
-
syncEnabled()
|
|
203
|
-
watchViewport()
|
|
204
|
-
setup()
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// ---------------------------------------------------------------
|
|
208
|
-
// 浮动控制条(shell.overlay):只在移动端渲染。
|
|
209
|
-
// - ☰ 打开会话列表(侧栏抽屉)
|
|
210
|
-
// - ● 远程控制状态指示(绿=局域网已连,黄=已开启本机访问,灰=未开启)
|
|
211
|
-
// - 点击状态点弹出信息卡:当前访问方式 / 手机地址 / 复制
|
|
212
|
-
// ---------------------------------------------------------------
|
|
213
|
-
var DOCK_STYLE = {
|
|
214
|
-
position: 'fixed',
|
|
215
|
-
top: 'calc(10px + env(safe-area-inset-top, 0px))',
|
|
216
|
-
right: 'calc(10px + env(safe-area-inset-right, 0px))',
|
|
217
|
-
display: 'flex',
|
|
218
|
-
flexDirection: 'column',
|
|
219
|
-
gap: 8,
|
|
220
|
-
alignItems: 'center',
|
|
221
|
-
zIndex: 200,
|
|
222
|
-
pointerEvents: 'none',
|
|
223
|
-
}
|
|
224
|
-
var DOCK_BTN = {
|
|
225
|
-
width: 44,
|
|
226
|
-
height: 44,
|
|
227
|
-
borderRadius: 22,
|
|
228
|
-
border: '1px solid rgba(128,128,128,.3)',
|
|
229
|
-
background: 'var(--dsw-alias-bg-base, #ffffff)',
|
|
230
|
-
color: 'var(--dsw-alias-label-primary, #111111)',
|
|
231
|
-
fontSize: 20,
|
|
232
|
-
lineHeight: 1,
|
|
233
|
-
display: 'flex',
|
|
234
|
-
alignItems: 'center',
|
|
235
|
-
justifyContent: 'center',
|
|
236
|
-
cursor: 'pointer',
|
|
237
|
-
boxShadow: '0 2px 10px rgba(0,0,0,.18)',
|
|
238
|
-
pointerEvents: 'auto',
|
|
239
|
-
padding: 0,
|
|
240
|
-
boxSizing: 'border-box',
|
|
241
|
-
}
|
|
242
|
-
var INFO_CARD = {
|
|
243
|
-
position: 'absolute',
|
|
244
|
-
top: 104,
|
|
245
|
-
right: 0,
|
|
246
|
-
width: 240,
|
|
247
|
-
background: 'var(--dsw-alias-bg-base, #ffffff)',
|
|
248
|
-
border: '1px solid rgba(128,128,128,.25)',
|
|
249
|
-
borderRadius: 12,
|
|
250
|
-
boxShadow: '0 6px 24px rgba(0,0,0,.2)',
|
|
251
|
-
padding: '10px 12px',
|
|
252
|
-
fontSize: 12,
|
|
253
|
-
color: 'var(--dsw-alias-label-primary, #111111)',
|
|
254
|
-
pointerEvents: 'auto',
|
|
255
|
-
boxSizing: 'border-box',
|
|
256
|
-
}
|
|
137
|
+
useEffect(function () {
|
|
138
|
+
refresh();
|
|
139
|
+
var t = setInterval(refresh, 5000);
|
|
140
|
+
return function () { clearInterval(t); };
|
|
141
|
+
}, [refresh]);
|
|
257
142
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
143
|
+
var toggle = useCallback(function () {
|
|
144
|
+
if (busy || !status) return;
|
|
145
|
+
setBusy(true);
|
|
146
|
+
setError(null);
|
|
147
|
+
fetch(TOGGLE_URL, {
|
|
148
|
+
method: 'POST',
|
|
149
|
+
headers: { 'Content-Type': 'application/json' },
|
|
150
|
+
body: JSON.stringify({ enabled: !status.remoteEnabled }),
|
|
151
|
+
})
|
|
152
|
+
.then(function (r) { return r.json(); })
|
|
153
|
+
.then(function (data) {
|
|
154
|
+
if (!data.ok) throw new Error(data.error || 'toggle failed');
|
|
155
|
+
setBusy(false);
|
|
156
|
+
setTimeout(refresh, 1500); // 等待 HMR 重载后再刷新
|
|
157
|
+
})
|
|
158
|
+
.catch(function (e) {
|
|
159
|
+
setBusy(false);
|
|
160
|
+
setError(String(e && e.message || e));
|
|
161
|
+
});
|
|
162
|
+
}, [busy, status, refresh]);
|
|
264
163
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
if (mq.addEventListener) mq.addEventListener('change', update)
|
|
270
|
-
if (mq2.addEventListener) mq2.addEventListener('change', update)
|
|
271
|
-
return function () {
|
|
272
|
-
if (mq.removeEventListener) mq.removeEventListener('change', update)
|
|
273
|
-
if (mq2.removeEventListener) mq2.removeEventListener('change', update)
|
|
274
|
-
}
|
|
275
|
-
}, [])
|
|
164
|
+
// 状态徽标
|
|
165
|
+
var on = !!(status && status.remoteEnabled);
|
|
166
|
+
var badge = el('span', { style: Object.assign({}, st.badge, on ? st.badgeOn : st.badgeOff) },
|
|
167
|
+
on ? '已开启' : '未开启');
|
|
276
168
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
169
|
+
// 开关
|
|
170
|
+
var sw = el('button', {
|
|
171
|
+
type: 'button',
|
|
172
|
+
role: 'switch',
|
|
173
|
+
'aria-checked': String(on),
|
|
174
|
+
'aria-label': on ? '关闭远程控制' : '开启远程控制',
|
|
175
|
+
onClick: toggle,
|
|
176
|
+
disabled: busy || !status,
|
|
177
|
+
style: Object.assign({}, st.switch, on ? st.switchOn : null),
|
|
178
|
+
}, el('span', { style: Object.assign({}, st.knob, on ? st.knobOn : null) }));
|
|
285
179
|
|
|
286
|
-
|
|
180
|
+
// 二维码
|
|
181
|
+
var qrEl;
|
|
182
|
+
if (on && status && status.url) {
|
|
183
|
+
var qrSrc = '/__dsh_remote/qr?url=' + encodeURIComponent(status.url);
|
|
184
|
+
qrEl = el('div', { style: st.qrBox },
|
|
185
|
+
el('img', { src: qrSrc, alt: '连接二维码', style: st.qrImg }));
|
|
186
|
+
} else if (status) {
|
|
187
|
+
qrEl = el('div', { style: st.qrFallback },
|
|
188
|
+
on
|
|
189
|
+
? '开启中…请稍候刷新'
|
|
190
|
+
: '开启远程控制后,这里会显示手机扫码连接二维码');
|
|
191
|
+
} else {
|
|
192
|
+
qrEl = el('div', { style: st.qrFallback }, '加载中…');
|
|
193
|
+
}
|
|
287
194
|
|
|
288
|
-
|
|
289
|
-
var
|
|
290
|
-
var urls = (status && status.urls) || []
|
|
291
|
-
var dotColor = (enabled && remote) ? '#22c55e' : (enabled ? '#eab308' : '#9ca3af')
|
|
292
|
-
var statusTitle = (enabled && remote)
|
|
293
|
-
? '已通过局域网连接电脑'
|
|
294
|
-
: (enabled ? '远程控制已开启(当前为本机访问)' : '远程控制未开启(见设置页「远程控制」)')
|
|
195
|
+
// 设备数
|
|
196
|
+
var count = status ? status.deviceCount : 0;
|
|
295
197
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
'
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
style:
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
el('span', { style: { opacity: .65 } }, '连接状态'),
|
|
314
|
-
el('span', {}, (enabled && remote) ? '局域网已连接' : (enabled ? '已开启(本机访问)' : '未开启'))),
|
|
315
|
-
el('div', { style: { display: 'flex', justifyContent: 'space-between', gap: 8, padding: '2px 0' } },
|
|
316
|
-
el('span', { style: { opacity: .65 } }, '当前访问'),
|
|
317
|
-
el('span', {}, remote ? '局域网' : '本机')),
|
|
318
|
-
urls.length
|
|
319
|
-
? el('div', { style: { padding: '6px 0 2px', wordBreak: 'break-all', opacity: .85 } }, urls[0])
|
|
320
|
-
: el('div', { style: { padding: '6px 0 2px', opacity: .65 } }, '未检测到局域网地址'),
|
|
321
|
-
el('div', { style: { display: 'flex', gap: 8, marginTop: 8, flexWrap: 'wrap' } },
|
|
322
|
-
urls.length
|
|
323
|
-
? el('button', {
|
|
324
|
-
style: { border: '1px solid rgba(128,128,128,.4)', background: 'transparent', borderRadius: 6, padding: '4px 10px', fontSize: 12, cursor: 'pointer', color: 'inherit' },
|
|
325
|
-
onClick: function () { copyText(urls[0], function (ok) { setCopied(ok) }) },
|
|
326
|
-
}, copied ? '已复制' : '复制地址')
|
|
327
|
-
: null,
|
|
328
|
-
),
|
|
329
|
-
)
|
|
330
|
-
: null,
|
|
331
|
-
)
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
// ---------------------------------------------------------------
|
|
335
|
-
// 设置页(settings.section):远程控制状态 / 访问地址 / 二维码 / 使用说明
|
|
336
|
-
// ---------------------------------------------------------------
|
|
337
|
-
var BASE_FS = 13
|
|
338
|
-
function fs(n) { return (Math.round(n / BASE_FS * 100) / 100).toFixed(2) + 'em' }
|
|
198
|
+
// 命令提示(未开启时展示 CLI 启动方式)
|
|
199
|
+
var cliHint = null;
|
|
200
|
+
if (status && !status.remoteEnabled) {
|
|
201
|
+
cliHint = el('div', { style: st.card },
|
|
202
|
+
el('p', { style: st.title }, '命令行启动方式'),
|
|
203
|
+
el('p', { style: st.sub },
|
|
204
|
+
'开启开关后,本插件会把 webserver.host 写入当前 profile 的 cordis.patch.yml,' +
|
|
205
|
+
'由 dsh 的 HMR 热重载生效。如果你希望手动启动(无需桌面端),可预先创建 overlay:'),
|
|
206
|
+
el('pre', { style: st.code },
|
|
207
|
+
'# remote-control.patch.yml\n' +
|
|
208
|
+
'- id: webserver\n' +
|
|
209
|
+
' config:\n' +
|
|
210
|
+
" host: '0.0.0.0'\n"),
|
|
211
|
+
el('p', { style: st.hint },
|
|
212
|
+
'然后以 dsh --profile web --patch remote-control.patch.yml 启动。'),
|
|
213
|
+
);
|
|
214
|
+
}
|
|
339
215
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
216
|
+
return el('div', { style: st.root },
|
|
217
|
+
// 状态行
|
|
218
|
+
el('div', { style: st.row },
|
|
219
|
+
el('div', { style: { display: 'flex', flexDirection: 'column', gap: 4 } },
|
|
220
|
+
el('p', { style: st.title }, '远程控制'),
|
|
221
|
+
el('p', { style: st.sub }, '允许手机通过局域网访问并操控本电脑上的 DeepSeek Harness')),
|
|
222
|
+
el('div', { style: { display: 'flex', alignItems: 'center', gap: 10 } },
|
|
223
|
+
badge, sw)),
|
|
345
224
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
225
|
+
// 二维码卡片
|
|
226
|
+
el('div', { style: st.card },
|
|
227
|
+
el('div', { style: { display: 'flex', flexDirection: 'column', gap: 6 } },
|
|
228
|
+
el('p', { style: st.title }, '手机连接'),
|
|
229
|
+
el('p', { style: st.sub }, '手机与电脑连接同一 Wi-Fi,用相机或浏览器扫码访问')),
|
|
230
|
+
qrEl,
|
|
231
|
+
status && status.url
|
|
232
|
+
? el('a', { href: status.url, style: st.link, target: '_blank', rel: 'noreferrer' }, status.url)
|
|
233
|
+
: el('p', { style: st.hint }, fmtUrl(status && status.url))),
|
|
350
234
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
setPingResult('✗ ' + ((r && r.error) || '无响应'))
|
|
360
|
-
}
|
|
361
|
-
}).catch(function (e) { setPingResult('✗ ' + String(e)) }).finally(function () { setPinging(false) })
|
|
362
|
-
}
|
|
235
|
+
// 统计
|
|
236
|
+
el('div', { style: st.grid },
|
|
237
|
+
el('div', { style: st.stat },
|
|
238
|
+
el('span', { style: st.statValue }, String(count)),
|
|
239
|
+
el('span', { style: st.statLabel }, '当前连接设备')),
|
|
240
|
+
el('div', { style: st.stat },
|
|
241
|
+
el('span', { style: st.statValue }, on ? String(status ? status.lanAddresses.length : 0) : '—'),
|
|
242
|
+
el('span', { style: st.statLabel }, '局域网地址'))),
|
|
363
243
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
var sessionCount = status && typeof status.sessionCount === 'number' ? status.sessionCount : null
|
|
371
|
-
var qrUrl = urls.length
|
|
372
|
-
? ('https://api.qrserver.com/v1/create-qr-code/?size=220x220&margin=8&data=' + encodeURIComponent(urls[0]))
|
|
373
|
-
: ''
|
|
244
|
+
status && status.lanAddresses.length
|
|
245
|
+
? el('div', { style: st.card },
|
|
246
|
+
el('p', { style: st.title }, '手机访问地址'),
|
|
247
|
+
el('pre', { style: st.code },
|
|
248
|
+
status.lanAddresses.map(function (ip) { return 'http://' + ip + ':' + status.port; }).join('\n')))
|
|
249
|
+
: null,
|
|
374
250
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
row: { display: 'flex', justifyContent: 'space-between', gap: 10, padding: '2px 0', fontSize: fs(12), flexWrap: 'wrap' },
|
|
381
|
-
rowKey: { opacity: .65 },
|
|
382
|
-
statusLine: { padding: '9px 11px', borderRadius: 8, fontSize: fs(12), fontWeight: 600 },
|
|
383
|
-
urlRow: { display: 'flex', alignItems: 'center', gap: 8, padding: '4px 0', flexWrap: 'wrap' },
|
|
384
|
-
urlText: { fontSize: fs(12), wordBreak: 'break-all', flex: '1 1 180px', minWidth: 0 },
|
|
385
|
-
btn: { border: '1px solid rgba(128,128,128,.4)', background: 'transparent', borderRadius: 6, padding: '5px 12px', fontSize: fs(12), cursor: 'pointer', color: 'inherit', whiteSpace: 'nowrap' },
|
|
386
|
-
btnPrimary: { border: '1px solid rgba(90,140,255,.6)', background: 'rgba(90,140,255,.22)', borderRadius: 6, padding: '5px 12px', fontSize: fs(12), cursor: 'pointer', color: 'inherit', fontWeight: 600 },
|
|
387
|
-
section: { fontWeight: 600, fontSize: fs(13), marginTop: 4 },
|
|
388
|
-
qr: { display: 'flex', justifyContent: 'center', margin: '8px 0' },
|
|
389
|
-
qrImg: { width: 220, height: 220, borderRadius: 8, border: '1px solid rgba(128,128,128,.2)' },
|
|
390
|
-
qrFallback: { fontSize: fs(11), opacity: .6, textAlign: 'center', padding: '12px 0' },
|
|
391
|
-
msg: { fontSize: fs(11), opacity: .8, wordBreak: 'break-all' },
|
|
392
|
-
note: { fontSize: fs(11), opacity: .55, lineHeight: 1.6 },
|
|
393
|
-
}
|
|
251
|
+
status && status.patchFile
|
|
252
|
+
? el('div', { style: st.card },
|
|
253
|
+
el('p', { style: st.title }, '配置位置'),
|
|
254
|
+
el('pre', { style: st.code }, status.patchFile))
|
|
255
|
+
: null,
|
|
394
256
|
|
|
395
|
-
|
|
396
|
-
el('div', { style: st.title }, '手机端远程控制'),
|
|
397
|
-
el('div', { style: st.sub }, '手机与电脑连接同一 Wi-Fi 时,用手机浏览器打开下方地址,即可实时同步对话记录、会话列表与设置,并直接发送消息远程操控电脑端 DeepSeek Harness。'),
|
|
257
|
+
cliHint,
|
|
398
258
|
|
|
399
|
-
|
|
400
|
-
? el('div', { style:
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
el('div', { style: st.card },
|
|
404
|
-
el('div', { style: st.statusLine, background: enabled ? 'rgba(34,197,94,.16)' : 'rgba(156,163,175,.14)', color: enabled ? '#16a34a' : 'inherit', borderRadius: 8 },
|
|
405
|
-
enabled ? '✓ 远程控制已开启:服务正在监听局域网' : '○ 远程控制未开启:当前仅本机可访问'),
|
|
406
|
-
el('div', { style: { height: 8 } }),
|
|
407
|
-
el('div', { style: st.row }, el('span', { style: st.rowKey }, '监听地址'), el('span', {}, host + ':' + port)),
|
|
408
|
-
el('div', { style: st.row }, el('span', { style: st.rowKey }, '会话数'), el('span', {}, sessionCount === null ? '…' : String(sessionCount))),
|
|
409
|
-
el('div', { style: st.row }, el('span', { style: st.rowKey }, '本机访问'), el('span', {}, 'http://127.0.0.1:' + port)),
|
|
410
|
-
),
|
|
411
|
-
// 手机访问地址
|
|
412
|
-
el('div', { style: st.section }, '手机访问地址'),
|
|
413
|
-
el('div', { style: st.card },
|
|
414
|
-
lanAddresses.length === 0
|
|
415
|
-
? el('div', { style: st.msg }, '未检测到局域网 IPv4 地址。请检查电脑是否连接 Wi-Fi / 有线网络,或在 dsh-desktop 中重新开启「移动端远程控制」。')
|
|
416
|
-
: urls.map(function (u) {
|
|
417
|
-
return el('div', { key: u, style: st.urlRow },
|
|
418
|
-
el('span', { style: st.urlText }, u),
|
|
419
|
-
el('button', { style: st.btn, onClick: function () { copyText(u, function (ok) { setCopied(ok ? u : '') }) } }, copied === u ? '✓ 已复制' : '复制'),
|
|
420
|
-
)
|
|
421
|
-
}),
|
|
422
|
-
el('div', { style: { height: 4 } }),
|
|
423
|
-
el('div', { style: st.note }, '打开 dsh-desktop 设置页的「移动端远程控制」开关后,重启服务即生效(服务监听 0.0.0.0,手机可访问)。'),
|
|
424
|
-
),
|
|
425
|
-
// 二维码
|
|
426
|
-
urls.length
|
|
427
|
-
? el(React.Fragment, null,
|
|
428
|
-
el('div', { style: st.section }, '扫码访问'),
|
|
429
|
-
el('div', { style: st.card },
|
|
430
|
-
el('div', { style: st.qr },
|
|
431
|
-
el('img', {
|
|
432
|
-
src: qrUrl,
|
|
433
|
-
alt: '手机访问二维码',
|
|
434
|
-
style: st.qrImg,
|
|
435
|
-
onError: function (e) {
|
|
436
|
-
// 外部二维码服务不可用时降级为文本
|
|
437
|
-
e.currentTarget.style.display = 'none'
|
|
438
|
-
},
|
|
439
|
-
}),
|
|
440
|
-
),
|
|
441
|
-
el('div', { style: st.qrFallback }, '用手机相机 / 浏览器扫码打开。'),
|
|
442
|
-
),
|
|
443
|
-
)
|
|
444
|
-
: null,
|
|
445
|
-
// 连接测试
|
|
446
|
-
el('div', { style: st.section }, '连接测试'),
|
|
447
|
-
el('div', { style: st.card },
|
|
448
|
-
el('div', { style: { display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' } },
|
|
449
|
-
el('button', { style: st.btnPrimary, disabled: pinging, onClick: doPing }, pinging ? '测试中…' : '发送测试请求'),
|
|
450
|
-
el('span', { style: st.msg }, '用于确认本页面与电脑端 Harness 服务的通信链路'),
|
|
451
|
-
),
|
|
452
|
-
pingResult ? el('div', { style: { fontSize: fs(11), opacity: 1, wordBreak: 'break-all', marginTop: 8 } }, pingResult) : null,
|
|
453
|
-
),
|
|
454
|
-
// 使用说明
|
|
455
|
-
el('div', { style: st.section }, '使用说明'),
|
|
456
|
-
el('div', { style: st.card },
|
|
457
|
-
el('div', { style: st.note }, [
|
|
458
|
-
'1. 电脑与手机连接到同一个 Wi-Fi;',
|
|
459
|
-
'2. 在 dsh-desktop 设置中开启「移动端远程控制」,重启服务;',
|
|
460
|
-
'3. 用手机浏览器打开上方任一地址(或扫码);',
|
|
461
|
-
'4. 手机端与电脑端共用同一份会话数据,对话、列表、设置实时双向同步,任何一端的操作都会即时反映到另一端;',
|
|
462
|
-
'5. 手机端已针对触控优化:侧栏自动变为抽屉,右上角浮动按钮可随时打开会话列表并查看连接状态。',
|
|
463
|
-
].join('\n')),
|
|
464
|
-
),
|
|
465
|
-
),
|
|
466
|
-
)
|
|
259
|
+
error
|
|
260
|
+
? el('div', { style: Object.assign({}, st.card, { borderColor: 'rgba(200,40,40,.35)' }) },
|
|
261
|
+
el('p', { style: Object.assign({}, st.hint, { color: '#c0272b' }) }, '操作失败:' + error))
|
|
262
|
+
: null);
|
|
467
263
|
}
|
|
468
264
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
var inject = ['slots', 'layout']
|
|
265
|
+
/* ------------------------------------------------------------------ */
|
|
266
|
+
/* 注册到设置页 */
|
|
267
|
+
/* ------------------------------------------------------------------ */
|
|
473
268
|
|
|
474
|
-
|
|
475
|
-
var slots = ctx.get('slots')
|
|
476
|
-
if (slots === undefined) return
|
|
477
|
-
var layout = ctx.get('layout')
|
|
478
|
-
|
|
479
|
-
// 移动端适配:抽屉化(内部仅在移动视口下生效,桌面端绝不干预)
|
|
480
|
-
startDrawer(layout)
|
|
269
|
+
var inject = ['slots'];
|
|
481
270
|
|
|
482
|
-
|
|
483
|
-
slots.
|
|
484
|
-
|
|
485
|
-
{ name: 'shell.overlay', id: 'mobile-remote-dock', order: 80 },
|
|
486
|
-
function () { return el(Dock, { layout: layout }) },
|
|
487
|
-
)
|
|
488
|
-
})
|
|
271
|
+
function apply(ctx) {
|
|
272
|
+
var slots = ctx.get('slots');
|
|
273
|
+
if (slots === undefined) return;
|
|
489
274
|
|
|
490
|
-
// 远程控制设置页(settings.section)
|
|
491
275
|
slots.inject('settings.section', function () {
|
|
492
276
|
return slots.register(
|
|
493
|
-
{
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
277
|
+
{
|
|
278
|
+
name: 'settings.section',
|
|
279
|
+
id: 'remote-control',
|
|
280
|
+
order: 40,
|
|
281
|
+
label: '远程控制',
|
|
282
|
+
},
|
|
283
|
+
function () { return el(RemoteControlPanel, null); }
|
|
284
|
+
);
|
|
285
|
+
});
|
|
497
286
|
}
|
|
498
287
|
|
|
499
|
-
exports.
|
|
500
|
-
exports.
|
|
501
|
-
return module.exports
|
|
288
|
+
exports.inject = inject;
|
|
289
|
+
exports.apply = apply;
|
|
290
|
+
return module.exports;
|
|
502
291
|
},
|
|
503
|
-
})
|
|
292
|
+
});
|