dsh-plugin-mobile-gateway 0.8.1 → 0.10.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/PROTOCOL.md +37 -2
- package/README.md +16 -4
- package/docs/dsh-0.2.0-rc.2-plugin-management-mobile-integration.md +109 -0
- package/lib/client.js +424 -130
- package/lib/cloudflare-tunnel.mjs +43 -2
- package/lib/devices.js +23 -7
- package/lib/dsh-host-adapter.mjs +28 -1
- package/lib/gateway-probe.mjs +6 -0
- package/lib/index.mjs +72 -11
- package/lib/plugin-management.mjs +194 -0
- package/lib/public-endpoint-check.mjs +61 -0
- package/package.json +3 -3
package/lib/client.js
CHANGED
|
@@ -20,46 +20,55 @@ window.__ModuleLoader__.load({
|
|
|
20
20
|
}
|
|
21
21
|
const getOpen = () => open
|
|
22
22
|
|
|
23
|
+
// Shared Web UI / Desktop semantic tokens update with the host theme.
|
|
24
|
+
const restartNoticeStorageKey = 'mobile-gateway-dismissed-restart-notice'
|
|
25
|
+
let dismissedRestartInstance = null
|
|
26
|
+
try { dismissedRestartInstance = window.localStorage.getItem(restartNoticeStorageKey) } catch { /* storage unavailable */ }
|
|
27
|
+
|
|
23
28
|
const colors = {
|
|
24
|
-
panel: 'var(--color-bg,
|
|
25
|
-
card: 'var(--color-bg-elevated,
|
|
26
|
-
text: 'var(--color-text,
|
|
27
|
-
muted: 'var(--color-text-muted,
|
|
28
|
-
border: 'var(--color-border, rgba(127,127,127,.24))',
|
|
29
|
-
accent: 'var(--color-primary, #4f7cff)',
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
panel: 'var(--dsw-alias-bg-layer-2, var(--color-bg, Canvas))',
|
|
30
|
+
card: 'var(--dsw-alias-bg-module-platform, var(--color-bg-elevated, Canvas))',
|
|
31
|
+
text: 'var(--dsw-alias-label-primary, var(--color-text, CanvasText))',
|
|
32
|
+
muted: 'var(--dsw-alias-label-secondary, var(--color-text-muted, GrayText))',
|
|
33
|
+
border: 'var(--dsw-alias-border-l3, var(--color-border, rgba(127,127,127,.24)))',
|
|
34
|
+
accent: 'var(--dsw-alias-button-primary-fill, var(--color-primary, #4f7cff))',
|
|
35
|
+
accentText: 'var(--dsw-alias-label-primary-foreground, #fff)',
|
|
36
|
+
danger: 'var(--dsw-alias-state-error-primary, #dc4c64)',
|
|
37
|
+
success: 'var(--dsw-alias-state-success-primary, #2fb171)',
|
|
32
38
|
}
|
|
33
39
|
const styles = {
|
|
34
40
|
backdrop: {
|
|
35
41
|
position: 'fixed', inset: 0, zIndex: 1200, pointerEvents: 'auto',
|
|
36
|
-
background: 'rgba(0,0,0,.32)', display: 'flex',
|
|
42
|
+
background: 'var(--dsw-alias-bg-mask-1, rgba(0,0,0,.32))', display: 'flex',
|
|
43
|
+
justifyContent: 'center', alignItems: 'center',
|
|
44
|
+
padding: 'max(16px, var(--dsh-frame-overlay-top, 16px)) 16px', boxSizing: 'border-box',
|
|
37
45
|
},
|
|
38
46
|
panel: {
|
|
39
|
-
width:
|
|
47
|
+
width: 'clamp(640px, 84vw, 1120px)', maxWidth: '100%', maxHeight: '100%', minHeight: 0, height: 'min(780px, calc(100dvh - 48px))', overflow: 'hidden',
|
|
48
|
+
scrollbarWidth: 'none', msOverflowStyle: 'none',
|
|
40
49
|
display: 'flex', flexDirection: 'column',
|
|
41
|
-
background: colors.panel, color: colors.text,
|
|
42
|
-
boxShadow: '-
|
|
43
|
-
fontFamily: 'system-ui, sans-serif', fontSize: 13, boxSizing: 'border-box',
|
|
50
|
+
background: colors.panel, color: colors.text, border: `1px solid ${colors.border}`, borderRadius: 'var(--dsw-radius-panel, 16px)',
|
|
51
|
+
boxShadow: 'var(--dsw-elevation-prominent, 0 20px 60px rgba(0,0,0,.22))', padding: 24,
|
|
52
|
+
fontFamily: 'var(--dsw-font-family, system-ui, sans-serif)', fontSize: 13, boxSizing: 'border-box',
|
|
44
53
|
},
|
|
45
54
|
header: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 18, flexShrink: 0 },
|
|
46
|
-
title: { margin: 0, fontSize:
|
|
47
|
-
card: { background:
|
|
55
|
+
title: { margin: 0, fontSize: 22, fontWeight: 650 },
|
|
56
|
+
card: { background: 'transparent', border: 0, borderBottom: `1px solid ${colors.border}`, borderRadius: 0, padding: '16px 0', marginBottom: 0, flexShrink: 0 },
|
|
48
57
|
label: { display: 'block', color: colors.muted, fontSize: 12, marginBottom: 6 },
|
|
49
|
-
input: { width: '100%', boxSizing: 'border-box', border: `1px solid ${colors.border}`, borderRadius:
|
|
58
|
+
input: { width: '100%', boxSizing: 'border-box', border: `1px solid ${colors.border}`, borderRadius: 10, padding: '8px 12px', marginBottom: 8, background: colors.card, color: 'inherit', outline: 'none' },
|
|
50
59
|
pairingText: { width: '100%', minHeight: 86, boxSizing: 'border-box', resize: 'vertical', border: `1px solid ${colors.border}`, borderRadius: 8, padding: '9px 10px', margin: '10px 0', background: 'transparent', color: 'inherit', outline: 'none', fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace', fontSize: 11, lineHeight: 1.45, overflowWrap: 'anywhere' },
|
|
51
60
|
button: { cursor: 'pointer', border: `1px solid ${colors.border}`, background: 'transparent', borderRadius: 8, padding: '7px 11px', color: 'inherit', font: 'inherit' },
|
|
52
|
-
headerButton: { width: 34, height: 34, boxSizing: 'border-box', padding: 0, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 },
|
|
53
|
-
primary: { background: colors.accent, color:
|
|
54
|
-
muted: { color: colors.muted, fontSize: 12, lineHeight: 1.
|
|
61
|
+
headerButton: { width: 34, height: 34, boxSizing: 'border-box', border: 0, padding: 0, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 },
|
|
62
|
+
primary: { background: colors.accent, color: colors.accentText, borderColor: colors.accent, width: '100%' },
|
|
63
|
+
muted: { color: colors.muted, fontSize: 12, lineHeight: 1.45 },
|
|
55
64
|
row: { display: 'grid', gridTemplateColumns: 'minmax(0, 1fr) auto', columnGap: 12, alignItems: 'center', padding: '12px 0', borderBottom: `1px solid ${colors.border}` },
|
|
56
65
|
badge: { display: 'inline-flex', alignItems: 'center', gap: 5, fontSize: 11, marginLeft: 8, color: colors.muted },
|
|
57
66
|
dot: { width: 7, height: 7, borderRadius: 99, display: 'inline-block' },
|
|
58
|
-
error: { color:
|
|
67
|
+
error: { color: colors.danger, background: 'rgba(220,76,100,.12)', borderRadius: 8, padding: 10, marginBottom: 12 },
|
|
59
68
|
qr: { display: 'block', width: 220, height: 220, margin: '12px auto', background: '#fff', borderRadius: 10, padding: 8 },
|
|
60
|
-
helpButton: { width: 20, height: 20, boxSizing: 'border-box', padding: 0, borderRadius: '50%', border:
|
|
61
|
-
helpBubble: { position: 'absolute', top: '100%', left: 0, zIndex: 20, width: 'min(300px, calc(100vw - 88px))', maxHeight: 'min(440px, calc(100vh - 160px))', overflowY: 'auto', boxSizing: 'border-box', padding: 12, borderRadius: 10, border:
|
|
62
|
-
toolsMenu: { position: 'absolute', top: 'calc(100% + 6px)', right: 0, zIndex: 30, width: 250, maxWidth: 'calc(100vw - 48px)', border: `1px solid ${colors.border}`, borderRadius: 10, padding: 5, background:
|
|
69
|
+
helpButton: { width: 20, height: 20, boxSizing: 'border-box', padding: 0, borderRadius: '50%', border: 0, background: 'var(--dsw-alias-interactive-bg-hover, rgba(127,127,127,.1))', color: 'var(--dsw-alias-label-tertiary, #888)', cursor: 'help', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0, fontSize: 12, fontWeight: 600, lineHeight: 1 },
|
|
70
|
+
helpBubble: { position: 'absolute', top: '100%', left: 0, zIndex: 20, width: 'min(300px, calc(100vw - 88px))', maxHeight: 'min(440px, calc(100vh - 160px))', overflowY: 'auto', boxSizing: 'border-box', padding: 12, borderRadius: 10, border: `1px solid ${colors.border}`, background: colors.panel, color: colors.text, boxShadow: '0 12px 28px rgba(0,0,0,.35)', fontSize: 12, lineHeight: 1.5 },
|
|
71
|
+
toolsMenu: { position: 'absolute', top: 'calc(100% + 6px)', right: 0, zIndex: 30, width: 250, maxWidth: 'calc(100vw - 48px)', border: `1px solid ${colors.border}`, borderRadius: 10, padding: 5, background: colors.panel, boxShadow: '0 12px 28px rgba(0,0,0,.35)' },
|
|
63
72
|
toolsItem: { display: 'block', width: '100%', padding: '9px 10px', textAlign: 'left', border: 0, borderRadius: 7, background: 'transparent', color: colors.text, font: 'inherit', cursor: 'pointer' },
|
|
64
73
|
switch: { position: 'relative', display: 'inline-flex', width: 42, height: 24, flexShrink: 0 },
|
|
65
74
|
switchInput: { position: 'absolute', opacity: 0, width: 1, height: 1 },
|
|
@@ -68,6 +77,154 @@ window.__ModuleLoader__.load({
|
|
|
68
77
|
version: { flexShrink: 0, marginTop: 'auto', padding: '18px 2px 0', color: colors.muted, fontSize: 11, lineHeight: 1.4, textAlign: 'right', letterSpacing: '.02em' },
|
|
69
78
|
}
|
|
70
79
|
|
|
80
|
+
const panelCss = `
|
|
81
|
+
@keyframes mgw-pairing-enter {
|
|
82
|
+
from { opacity: 0; transform: scale(.82); }
|
|
83
|
+
to { opacity: 1; transform: scale(1); }
|
|
84
|
+
}
|
|
85
|
+
.mgw-pairing-dialog {
|
|
86
|
+
transform-origin: center;
|
|
87
|
+
animation: mgw-pairing-enter 240ms cubic-bezier(.2, .8, .2, 1);
|
|
88
|
+
}
|
|
89
|
+
@keyframes mgw-pairing-exit {
|
|
90
|
+
from { opacity: 1; transform: scale(1); }
|
|
91
|
+
to { opacity: 0; transform: scale(.82); }
|
|
92
|
+
}
|
|
93
|
+
.mgw-pairing-dialog[data-closing="true"] {
|
|
94
|
+
animation: mgw-pairing-exit 180ms ease-in forwards;
|
|
95
|
+
pointer-events: none;
|
|
96
|
+
}
|
|
97
|
+
@media (prefers-reduced-motion: reduce) {
|
|
98
|
+
.mgw-pairing-dialog { animation: none; }
|
|
99
|
+
}
|
|
100
|
+
.mobile-gateway-panel, .mobile-gateway-panel *, .mgw-pairing-dialog, .mgw-pairing-dialog *, .mgw-select-menu {
|
|
101
|
+
scrollbar-width: none !important;
|
|
102
|
+
-ms-overflow-style: none;
|
|
103
|
+
}
|
|
104
|
+
.mobile-gateway-panel::-webkit-scrollbar, .mobile-gateway-panel *::-webkit-scrollbar,
|
|
105
|
+
.mgw-pairing-dialog::-webkit-scrollbar, .mgw-pairing-dialog *::-webkit-scrollbar, .mgw-select-menu::-webkit-scrollbar {
|
|
106
|
+
display: none !important;
|
|
107
|
+
width: 0;
|
|
108
|
+
height: 0;
|
|
109
|
+
}
|
|
110
|
+
.mobile-gateway-panel .mgw-body { display: grid; grid-template-columns: 180px minmax(0, 1fr); gap: 28px; flex: 1; min-height: 0; }
|
|
111
|
+
.mobile-gateway-panel .mgw-nav { display: flex; flex-direction: column; gap: 4px; }
|
|
112
|
+
.mobile-gateway-panel .mgw-nav button { text-align: left; border: 0; padding: 12px 14px; border-radius: 10px; background: transparent; color: inherit; font: inherit; cursor: pointer; }
|
|
113
|
+
.mobile-gateway-panel .mgw-nav button[aria-current="page"] { background: var(--dsw-alias-bg-overlay, rgba(127,127,127,.12)); font-weight: 500; }
|
|
114
|
+
.mobile-gateway-panel .mgw-nav button:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(127,127,127,.08)); }
|
|
115
|
+
.mobile-gateway-panel .mgw-content { min-width: 0; overflow-y: auto; scrollbar-width: none; padding-right: 4px; }
|
|
116
|
+
.mobile-gateway-panel .mgw-content::-webkit-scrollbar { display: none; }
|
|
117
|
+
.mobile-gateway-panel .mgw-content p { margin: 6px 0 10px; }
|
|
118
|
+
.mobile-gateway-panel strong { font-weight: 550; }
|
|
119
|
+
.mobile-gateway-panel .mgw-section-title { margin: 0 0 10px; font-size: 17px; line-height: 1.4; font-weight: 650; color: inherit; }
|
|
120
|
+
.mobile-gateway-panel .mgw-setting-row { display: flex; flex-direction: column; align-items: flex-start; gap: 8px; margin-bottom: 12px; }
|
|
121
|
+
.mobile-gateway-panel .mgw-setting-row select { width: auto !important; max-width: 100%; min-width: 150px; margin-bottom: 0 !important; flex-shrink: 1; }
|
|
122
|
+
.mobile-gateway-panel .mgw-cloudflare-block input, .mobile-gateway-panel .mgw-cloudflare-block .mgw-select-trigger { background: var(--dsw-alias-bg-layer-2, Canvas) !important; }
|
|
123
|
+
.mobile-gateway-panel .mgw-setting-label { font-size: 14px; font-weight: 550; color: inherit; }
|
|
124
|
+
|
|
125
|
+
.mobile-gateway-panel input:focus-visible, .mobile-gateway-panel select:focus-visible, .mobile-gateway-panel button:focus-visible { outline: 2px solid var(--dsw-alias-label-primary, currentColor); outline-offset: 2px; }
|
|
126
|
+
@media (max-width: 640px) {
|
|
127
|
+
.mobile-gateway-panel .mgw-body { grid-template-columns: minmax(0, 1fr); gap: 8px; }
|
|
128
|
+
.mobile-gateway-panel .mgw-nav { flex-direction: row; }
|
|
129
|
+
.mobile-gateway-panel .mgw-nav button { flex: 1; padding: 10px 6px; text-align: center; }
|
|
130
|
+
}
|
|
131
|
+
`
|
|
132
|
+
|
|
133
|
+
function GatewaySelect({ style, children, value, onChange, disabled, ...props }) {
|
|
134
|
+
const options = React.Children.toArray(children).filter((child) => React.isValidElement(child))
|
|
135
|
+
const [menu, setMenu] = React.useState(null)
|
|
136
|
+
const [active, setActive] = React.useState(0)
|
|
137
|
+
const buttonRef = React.useRef(null)
|
|
138
|
+
const menuRef = React.useRef(null)
|
|
139
|
+
const menuId = React.useId()
|
|
140
|
+
const selected = options.find((option) => option.props.value === value)
|
|
141
|
+
const enabled = options.map((option, index) => option.props.disabled ? -1 : index).filter((index) => index >= 0)
|
|
142
|
+
const close = (restoreFocus = false) => {
|
|
143
|
+
setMenu(null)
|
|
144
|
+
if (restoreFocus) buttonRef.current?.focus()
|
|
145
|
+
}
|
|
146
|
+
const openMenu = () => {
|
|
147
|
+
if (disabled) return
|
|
148
|
+
const rect = buttonRef.current.getBoundingClientRect()
|
|
149
|
+
const computed = window.getComputedStyle(buttonRef.current)
|
|
150
|
+
const fontSize = parseFloat(computed.fontSize) || 14
|
|
151
|
+
const lineHeight = parseFloat(computed.lineHeight) || fontSize * 1.5
|
|
152
|
+
const measure = document.createElement('canvas').getContext('2d')
|
|
153
|
+
if (measure) measure.font = computed.font || `${fontSize}px sans-serif`
|
|
154
|
+
const longest = Math.max(...options.map((option) => measure
|
|
155
|
+
? measure.measureText(String(option.props.children)).width : String(option.props.children).length * fontSize))
|
|
156
|
+
const height = Math.min(options.length * Math.max(40, lineHeight + 16) + 14, window.innerHeight - 24)
|
|
157
|
+
const width = Math.min(Math.max(rect.width, longest + 72, 200), window.innerWidth - 24)
|
|
158
|
+
setActive(Math.max(0, options.findIndex((option) => option.props.value === value && !option.props.disabled)))
|
|
159
|
+
setMenu({ left: Math.max(12, Math.min(rect.left, window.innerWidth - width - 12)),
|
|
160
|
+
top: Math.max(12, rect.bottom + height + 6 <= window.innerHeight - 12 ? rect.bottom + 6 : rect.top - height - 6), width, maxHeight: window.innerHeight - 24 })
|
|
161
|
+
}
|
|
162
|
+
const choose = (index) => {
|
|
163
|
+
const option = options[index]
|
|
164
|
+
if (!option || option.props.disabled) return
|
|
165
|
+
onChange?.({ target: { value: option.props.value } })
|
|
166
|
+
close(true)
|
|
167
|
+
}
|
|
168
|
+
React.useEffect(() => {
|
|
169
|
+
if (!menu) return
|
|
170
|
+
menuRef.current?.focus()
|
|
171
|
+
const outside = (event) => {
|
|
172
|
+
if (!buttonRef.current?.contains(event.target) && !menuRef.current?.contains(event.target)) close()
|
|
173
|
+
}
|
|
174
|
+
const scrolled = (event) => { if (!menuRef.current?.contains(event.target)) close() }
|
|
175
|
+
const resized = () => close()
|
|
176
|
+
document.addEventListener('pointerdown', outside)
|
|
177
|
+
document.addEventListener('scroll', scrolled, true)
|
|
178
|
+
window.addEventListener('resize', resized)
|
|
179
|
+
return () => {
|
|
180
|
+
document.removeEventListener('pointerdown', outside)
|
|
181
|
+
document.removeEventListener('scroll', scrolled, true)
|
|
182
|
+
window.removeEventListener('resize', resized)
|
|
183
|
+
}
|
|
184
|
+
}, [menu])
|
|
185
|
+
React.useEffect(() => {
|
|
186
|
+
if (menu) document.getElementById(`${menuId}-${active}`)?.scrollIntoView({ block: 'nearest' })
|
|
187
|
+
}, [active, menu])
|
|
188
|
+
return React.createElement('span', { style: { display: 'inline-flex', maxWidth: '100%', minWidth: 0 } },
|
|
189
|
+
React.createElement('button', { ...props, className: 'mgw-select-trigger', ref: buttonRef, type: 'button', disabled,
|
|
190
|
+
'aria-haspopup': 'listbox', 'aria-expanded': !!menu, 'aria-controls': menu ? menuId : undefined,
|
|
191
|
+
onClick: () => menu ? close() : openMenu(),
|
|
192
|
+
onKeyDown: (event) => {
|
|
193
|
+
if (['ArrowDown', 'ArrowUp'].includes(event.key)) { event.preventDefault(); openMenu() }
|
|
194
|
+
},
|
|
195
|
+
style: { ...style, width: 'auto', maxWidth: '100%', marginBottom: 0, border: 0, borderRadius: 12,
|
|
196
|
+
padding: '10px 16px', background: colors.card, color: colors.text, font: 'inherit', cursor: disabled ? 'default' : 'pointer',
|
|
197
|
+
display: 'inline-flex', alignItems: 'center', gap: 18, opacity: disabled ? .5 : 1 } },
|
|
198
|
+
React.createElement('span', { style: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, selected?.props.children || value),
|
|
199
|
+
React.createElement('svg', { width: 14, height: 14, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor',
|
|
200
|
+
strokeWidth: 1.8, strokeLinecap: 'round', strokeLinejoin: 'round', 'aria-hidden': true, style: { flexShrink: 0 } },
|
|
201
|
+
React.createElement('path', { d: 'M6 9l6 6 6-6' }))),
|
|
202
|
+
menu ? React.createElement('div', { className: 'mgw-select-menu', ref: menuRef, id: menuId, role: 'listbox', tabIndex: -1,
|
|
203
|
+
'aria-label': props['aria-label'], 'aria-activedescendant': `${menuId}-${active}`,
|
|
204
|
+
style: { ...menu, position: 'fixed', zIndex: 1250, boxSizing: 'border-box', overflowY: 'auto', padding: 6,
|
|
205
|
+
background: colors.panel, color: colors.text, border: `1px solid ${colors.border}`, borderRadius: 14,
|
|
206
|
+
boxShadow: 'var(--dsw-elevation-prominent, 0 8px 28px rgba(0,0,0,.12))', outline: 'none' },
|
|
207
|
+
onKeyDown: (event) => {
|
|
208
|
+
if (event.key === 'Escape') { event.preventDefault(); event.stopPropagation(); close(true) }
|
|
209
|
+
else if (event.key === 'Tab') close()
|
|
210
|
+
else if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); choose(active) }
|
|
211
|
+
else if (['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) {
|
|
212
|
+
event.preventDefault()
|
|
213
|
+
const index = enabled.indexOf(active)
|
|
214
|
+
setActive(event.key === 'Home' ? enabled[0] : event.key === 'End' ? enabled[enabled.length - 1]
|
|
215
|
+
: enabled[(index + (event.key === 'ArrowDown' ? 1 : -1) + enabled.length) % enabled.length])
|
|
216
|
+
}
|
|
217
|
+
} }, options.map((option, index) => React.createElement('div', { key: option.props.value,
|
|
218
|
+
id: `${menuId}-${index}`, role: 'option', 'aria-selected': option.props.value === value, 'aria-disabled': !!option.props.disabled,
|
|
219
|
+
onPointerMove: () => { if (!option.props.disabled) setActive(index) }, onClick: () => choose(index),
|
|
220
|
+
style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 16, minHeight: 40, whiteSpace: 'nowrap',
|
|
221
|
+
boxSizing: 'border-box', padding: '8px 12px', borderRadius: 8, cursor: option.props.disabled ? 'default' : 'pointer',
|
|
222
|
+
opacity: option.props.disabled ? .4 : 1, background: active === index ? 'var(--dsw-alias-interactive-bg-hover, rgba(127,127,127,.08))' : 'transparent' } },
|
|
223
|
+
React.createElement('span', { style: { minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis' }, title: String(option.props.children) }, option.props.children), option.props.value === value ? React.createElement('svg', { width: 16, height: 16, viewBox: '0 0 24 24',
|
|
224
|
+
fill: 'none', stroke: 'currentColor', strokeWidth: 1.8, strokeLinecap: 'round', strokeLinejoin: 'round', 'aria-hidden': true, style: { flexShrink: 0 } },
|
|
225
|
+
React.createElement('path', { d: 'M5 12l4 4L19 6' })) : null))) : null)
|
|
226
|
+
}
|
|
227
|
+
|
|
71
228
|
async function request(path, options) {
|
|
72
229
|
const response = await fetch(path, { credentials: 'same-origin', ...options })
|
|
73
230
|
const body = await response.json().catch(() => ({}))
|
|
@@ -99,10 +256,10 @@ window.__ModuleLoader__.load({
|
|
|
99
256
|
const system = status.platform === 'darwin' ? 'macOS 电脑' : status.platform === 'win32' ? 'Windows 电脑' : status.platform === 'linux' ? 'Linux 服务器' : '当前设备'
|
|
100
257
|
const lanReady = !!lanPairingUrl(status)
|
|
101
258
|
const tunnel = status.cloudflare
|
|
102
|
-
const tunnelReady = !!(tunnel?.enabled && tunnel.
|
|
259
|
+
const tunnelReady = !!(tunnel?.enabled && tunnel.publicReady === true && tunnel.publicUrl)
|
|
103
260
|
const serverReady = status.platform === 'linux' && !!(
|
|
104
261
|
(publicSetup?.configured && publicSetup.publicUrl) || /^wss:\/\//.test(status.publicUrl || ''))
|
|
105
|
-
const detected = `当前检测:${system};局域网入口${lanReady ? '已就绪' : '未就绪'};${serverReady ? '公网服务器入口已配置' : tunnelReady ? `${tunnel.mode === 'quick' ? 'Quick' : '命名'} Tunnel 已连接` : tunnel?.enabled ? 'Cloudflare Tunnel 连接中' : '外网入口未连接'}。`
|
|
262
|
+
const detected = `当前检测:${system};局域网入口${lanReady ? '已就绪' : '未就绪'};${serverReady ? '公网服务器入口已配置' : tunnelReady ? `${tunnel.mode === 'quick' ? 'Quick' : '命名'} Tunnel 已连接` : tunnel?.enabled ? (tunnel.state === 'online' ? '隧道已连接,公网入口未验证或检测失败' : 'Cloudflare Tunnel 连接中') : '外网入口未连接'}。`
|
|
106
263
|
const local = lanReady
|
|
107
264
|
? '手机与 DSH 处于可互访的同一局域网时,建议选“局域网直连”。'
|
|
108
265
|
: '如果手机和 DSH 在同一局域网,先确认局域网入口已开启并监听成功。'
|
|
@@ -125,10 +282,10 @@ window.__ModuleLoader__.load({
|
|
|
125
282
|
return React.createElement('div', { style: styles.row },
|
|
126
283
|
React.createElement('div', { style: { minWidth: 0, overflow: 'hidden' } },
|
|
127
284
|
React.createElement('div', { style: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } },
|
|
128
|
-
device.name,
|
|
285
|
+
device.displayName || device.name,
|
|
129
286
|
React.createElement('span', { style: styles.badge },
|
|
130
287
|
React.createElement('span', { style: { ...styles.dot, background: device.online ? colors.success : colors.muted } }),
|
|
131
|
-
device.online ?
|
|
288
|
+
device.online ? '在线' : '离线')),
|
|
132
289
|
React.createElement('div', { style: { ...styles.muted, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', marginTop: 3 } },
|
|
133
290
|
device.lastSeenAt ? `最近连接:${new Date(device.lastSeenAt).toLocaleString()}` : '尚未连接')),
|
|
134
291
|
React.createElement('button', {
|
|
@@ -143,7 +300,7 @@ window.__ModuleLoader__.load({
|
|
|
143
300
|
minWidth: 54,
|
|
144
301
|
opacity: revoking ? .6 : 1,
|
|
145
302
|
},
|
|
146
|
-
title: `吊销 ${device.name}`,
|
|
303
|
+
title: `吊销 ${device.displayName || device.name}`,
|
|
147
304
|
// Pointer-up is more reliable than click inside the shell overlay on
|
|
148
305
|
// touch devices. The click handler remains for keyboard activation.
|
|
149
306
|
onPointerUp: triggerRevoke,
|
|
@@ -154,8 +311,11 @@ window.__ModuleLoader__.load({
|
|
|
154
311
|
}
|
|
155
312
|
|
|
156
313
|
function DevicePanel() {
|
|
314
|
+
const [activeSection, setActiveSection] = React.useState('general')
|
|
157
315
|
const [devices, setDevices] = React.useState(null)
|
|
316
|
+
const [showAllDevices, setShowAllDevices] = React.useState(false)
|
|
158
317
|
const [status, setStatus] = React.useState(null)
|
|
318
|
+
const [dismissedNotice, setDismissedNotice] = React.useState(() => dismissedRestartInstance)
|
|
159
319
|
const [publicUrl, setPublicUrl] = React.useState('')
|
|
160
320
|
const [pairingRoute, setPairingRoute] = React.useState('auto')
|
|
161
321
|
const [connectionHelpOpen, setConnectionHelpOpen] = React.useState(false)
|
|
@@ -170,8 +330,32 @@ window.__ModuleLoader__.load({
|
|
|
170
330
|
const [cloudflareToken, setCloudflareToken] = React.useState('')
|
|
171
331
|
const [cloudflareMode, setCloudflareMode] = React.useState('quick')
|
|
172
332
|
const [cloudflareBusy, setCloudflareBusy] = React.useState(false)
|
|
173
|
-
const [deviceName, setDeviceName] = React.useState('iPhone')
|
|
174
333
|
const [qr, setQr] = React.useState(null)
|
|
334
|
+
const pairingDialogRef = React.useRef(null)
|
|
335
|
+
const [pairingClosing, setPairingClosing] = React.useState(false)
|
|
336
|
+
const pairingCloseTimer = React.useRef(null)
|
|
337
|
+
const closePairingDialog = () => {
|
|
338
|
+
if (pairingCloseTimer.current !== null) return
|
|
339
|
+
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
|
340
|
+
setQr(null)
|
|
341
|
+
return
|
|
342
|
+
}
|
|
343
|
+
setPairingClosing(true)
|
|
344
|
+
pairingCloseTimer.current = window.setTimeout(() => {
|
|
345
|
+
pairingCloseTimer.current = null
|
|
346
|
+
setQr(null)
|
|
347
|
+
setPairingClosing(false)
|
|
348
|
+
}, 180)
|
|
349
|
+
}
|
|
350
|
+
React.useEffect(() => {
|
|
351
|
+
setPairingClosing(false)
|
|
352
|
+
return () => {
|
|
353
|
+
if (pairingCloseTimer.current !== null) {
|
|
354
|
+
window.clearTimeout(pairingCloseTimer.current)
|
|
355
|
+
pairingCloseTimer.current = null
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}, [qr])
|
|
175
359
|
const [error, setError] = React.useState(null)
|
|
176
360
|
const [busy, setBusy] = React.useState(false)
|
|
177
361
|
const [refreshing, setRefreshing] = React.useState(false)
|
|
@@ -288,6 +472,13 @@ window.__ModuleLoader__.load({
|
|
|
288
472
|
return () => { active = false; window.clearInterval(timer) }
|
|
289
473
|
}, [refresh])
|
|
290
474
|
|
|
475
|
+
React.useEffect(() => {
|
|
476
|
+
if (!qr) return
|
|
477
|
+
const previousFocus = document.activeElement
|
|
478
|
+
pairingDialogRef.current?.querySelector('button')?.focus()
|
|
479
|
+
return () => previousFocus?.focus()
|
|
480
|
+
}, [qr])
|
|
481
|
+
|
|
291
482
|
const choosePairingRoute = (route) => {
|
|
292
483
|
pairingRouteRef.current = route
|
|
293
484
|
setPairingRoute(route)
|
|
@@ -325,7 +516,7 @@ window.__ModuleLoader__.load({
|
|
|
325
516
|
}
|
|
326
517
|
}
|
|
327
518
|
|
|
328
|
-
const configureCloudflare = async (enabled) => {
|
|
519
|
+
const configureCloudflare = async (enabled, mode = cloudflareMode) => {
|
|
329
520
|
if (cloudflareBusy) return
|
|
330
521
|
setCloudflareBusy(true)
|
|
331
522
|
setError(null)
|
|
@@ -334,8 +525,8 @@ window.__ModuleLoader__.load({
|
|
|
334
525
|
method: 'POST',
|
|
335
526
|
headers: { 'Content-Type': 'application/json' },
|
|
336
527
|
body: JSON.stringify({ enabled, ...(enabled ? {
|
|
337
|
-
mode
|
|
338
|
-
...(
|
|
528
|
+
mode,
|
|
529
|
+
...(mode === 'named' ? { hostname: cloudflareHost, token: cloudflareToken } : {}),
|
|
339
530
|
} : {}) }),
|
|
340
531
|
})
|
|
341
532
|
setCloudflareToken('')
|
|
@@ -352,10 +543,37 @@ window.__ModuleLoader__.load({
|
|
|
352
543
|
}
|
|
353
544
|
}
|
|
354
545
|
|
|
546
|
+
const checkCloudflare = async () => {
|
|
547
|
+
if (cloudflareBusy) return
|
|
548
|
+
setCloudflareBusy(true)
|
|
549
|
+
setError(null)
|
|
550
|
+
try {
|
|
551
|
+
const data = await request('/mgw/cloudflare/check', { method: 'POST' })
|
|
552
|
+
setStatus((current) => ({ ...(current || {}), cloudflare: data }))
|
|
553
|
+
} catch (cause) { setError(cause.message) }
|
|
554
|
+
finally { setCloudflareBusy(false) }
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const restartCloudflare = async () => {
|
|
558
|
+
if (cloudflareBusy) return
|
|
559
|
+
setCloudflareBusy(true)
|
|
560
|
+
setError(null)
|
|
561
|
+
try {
|
|
562
|
+
const data = await request('/mgw/cloudflare/restart', { method: 'POST' })
|
|
563
|
+
if (data.mode === 'quick') setQr(null)
|
|
564
|
+
setStatus((current) => ({ ...(current || {}), cloudflare: data }))
|
|
565
|
+
await refresh()
|
|
566
|
+
} catch (cause) {
|
|
567
|
+
setError(cause.message)
|
|
568
|
+
} finally {
|
|
569
|
+
setCloudflareBusy(false)
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
355
573
|
const createPairing = async (endpoint) => request('/mgw/pair', {
|
|
356
574
|
method: 'POST',
|
|
357
575
|
headers: { 'Content-Type': 'application/json' },
|
|
358
|
-
body: JSON.stringify({
|
|
576
|
+
body: JSON.stringify({ publicUrl: endpoint }),
|
|
359
577
|
})
|
|
360
578
|
|
|
361
579
|
const pair = async () => {
|
|
@@ -435,7 +653,7 @@ window.__ModuleLoader__.load({
|
|
|
435
653
|
try {
|
|
436
654
|
await request(`/mgw/devices/${encodeURIComponent(device.id)}/revoke`, { method: 'POST' })
|
|
437
655
|
setDevices((current) => Array.isArray(current) ? current.filter((item) => item.id !== device.id) : current)
|
|
438
|
-
setDeviceNotice(`已吊销 ${device.name}`)
|
|
656
|
+
setDeviceNotice(`已吊销 ${device.displayName || device.name}`)
|
|
439
657
|
window.setTimeout(() => setDeviceNotice(null), 2200)
|
|
440
658
|
} catch (cause) {
|
|
441
659
|
setDeviceNoticeError(true)
|
|
@@ -452,23 +670,59 @@ window.__ModuleLoader__.load({
|
|
|
452
670
|
}
|
|
453
671
|
}
|
|
454
672
|
|
|
673
|
+
const restartNoticeInstance = status?.gatewayInstanceId || (status?.webPid ? String(status.webPid) : null)
|
|
674
|
+
const dismissRestartNotice = () => {
|
|
675
|
+
dismissedRestartInstance = restartNoticeInstance
|
|
676
|
+
setDismissedNotice(restartNoticeInstance)
|
|
677
|
+
try { window.localStorage.setItem(restartNoticeStorageKey, restartNoticeInstance) } catch { /* keep in-memory dismissal */ }
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
const onlineDevices = (devices || []).filter((device) => device.online)
|
|
681
|
+
const offlineDevices = (devices || []).filter((device) => !device.online)
|
|
682
|
+
const visibleDevices = showAllDevices ? [...onlineDevices, ...offlineDevices] : onlineDevices
|
|
683
|
+
|
|
455
684
|
const advice = connectionAdvice(status, publicSetup)
|
|
456
685
|
const helpLine = (name, description) => React.createElement('div', { style: { marginTop: 7 } },
|
|
457
686
|
React.createElement('strong', null, name), ':', description)
|
|
458
687
|
|
|
459
688
|
return React.createElement('div', { style: styles.backdrop, onMouseDown: () => setOpen(false) },
|
|
460
|
-
React.createElement('
|
|
689
|
+
React.createElement('style', null, panelCss),
|
|
690
|
+
qr ? React.createElement('div', { style: { ...styles.backdrop, zIndex: 1201 },
|
|
691
|
+
onMouseDown: (event) => { event.stopPropagation(); closePairingDialog() } },
|
|
692
|
+
React.createElement('div', { className: 'mgw-pairing-dialog', 'data-closing': pairingClosing, ref: pairingDialogRef, role: 'dialog', 'aria-modal': true, 'aria-label': '设备配对二维码', tabIndex: -1,
|
|
693
|
+
style: { ...styles.panel, width: 'min(520px, 100%)', height: 'auto', overflowY: 'auto', scrollbarWidth: 'none' },
|
|
694
|
+
onMouseDown: (event) => event.stopPropagation(),
|
|
695
|
+
onKeyDown: (event) => {
|
|
696
|
+
if (event.key === 'Escape') { event.stopPropagation(); closePairingDialog() }
|
|
697
|
+
if (event.key === 'Tab') {
|
|
698
|
+
const items = [...event.currentTarget.querySelectorAll('button, textarea')]
|
|
699
|
+
const first = items[0], last = items[items.length - 1]
|
|
700
|
+
if (event.shiftKey && (document.activeElement === first || document.activeElement === event.currentTarget)) { event.preventDefault(); last?.focus() }
|
|
701
|
+
else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first?.focus() }
|
|
702
|
+
}
|
|
703
|
+
} },
|
|
704
|
+
React.createElement('div', { style: { display: 'flex', justifyContent: 'flex-end' } },
|
|
705
|
+
React.createElement('button', { type: 'button', style: { ...styles.button, ...styles.headerButton }, 'aria-label': '关闭配对窗口', onClick: closePairingDialog }, '✕')),
|
|
706
|
+
React.createElement('section', { style: { textAlign: 'center' } },
|
|
707
|
+
React.createElement('strong', null, '使用移动客户端扫码'),
|
|
708
|
+
React.createElement('img', { style: styles.qr, src: `data:image/svg+xml;charset=utf-8,${encodeURIComponent(qr.svg)}`, alt: '一次性设备配对二维码' }),
|
|
709
|
+
React.createElement('label', { style: { ...styles.label, textAlign: 'left' } }, 'Base64URL 配对字符串'),
|
|
710
|
+
React.createElement('textarea', { style: styles.pairingText, value: qr.qrPayload, readOnly: true, spellCheck: false, onFocus: (event) => event.target.select() }),
|
|
711
|
+
React.createElement('button', { style: { ...styles.button, width: '100%', marginBottom: 8, borderColor: pairingTokenCopied ? colors.success : colors.border, color: pairingTokenCopied ? colors.success : colors.text }, onClick: copyPairingText }, pairingTokenCopied ? '✓ 已复制' : '复制配对 Token'),
|
|
712
|
+
pairingTokenCopied ? React.createElement('div', { style: { ...styles.muted, color: colors.success, marginBottom: 8 } }, '已复制到剪贴板') : null,
|
|
713
|
+
React.createElement('div', { style: styles.muted }, `有效期至 ${new Date(qr.pairing.expiresAt).toLocaleTimeString()};扫码成功后此二维码立即失效。`)))) : null,
|
|
714
|
+
React.createElement('aside', { className: 'mobile-gateway-panel', inert: qr ? true : undefined, 'aria-hidden': qr ? true : undefined, style: styles.panel, onMouseDown: (event) => { event.stopPropagation(); setToolsOpen(false) }, role: 'dialog', 'aria-modal': true, 'aria-label': '移动设备管理' },
|
|
461
715
|
React.createElement('div', { style: styles.header },
|
|
462
716
|
React.createElement('div', null,
|
|
463
717
|
React.createElement('h2', { style: styles.title }, '移动设备'),
|
|
464
|
-
React.createElement('div', { style: styles.muted }, status
|
|
718
|
+
React.createElement('div', { style: styles.muted }, !status ? '正在加载网关状态…' : status.gatewayEnabled ? `移动网关已开启 · ${status.requireAuth ? '设备鉴权已开启' : '本机设备鉴权已关闭'}` : '移动网关已关闭 · 普通 WebUI 模式')),
|
|
465
719
|
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 6 } },
|
|
466
720
|
React.createElement('div', { style: { position: 'relative' }, onMouseDown: (event) => event.stopPropagation() },
|
|
467
721
|
React.createElement('button', {
|
|
468
722
|
type: 'button', style: { ...styles.button, ...styles.headerButton, borderRadius: '50%' }, title: '工具', 'aria-label': '工具',
|
|
469
723
|
'aria-haspopup': 'menu', 'aria-expanded': toolsOpen,
|
|
470
724
|
onClick: () => setToolsOpen((value) => !value),
|
|
471
|
-
}, React.createElement('svg', { width:
|
|
725
|
+
}, React.createElement('svg', { width: 20, height: 20, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 1.8, strokeLinecap: 'round', strokeLinejoin: 'round', 'aria-hidden': true },
|
|
472
726
|
React.createElement('path', { d: 'M10.3 2.8h3.4l.5 2.2a7.6 7.6 0 0 1 1.7.7l1.9-1.2 2.4 2.4L19 8.8c.3.5.5 1.1.7 1.7l2.2.5v3.4l-2.2.5a7.6 7.6 0 0 1-.7 1.7l1.2 1.9-2.4 2.4-1.9-1.2a7.6 7.6 0 0 1-1.7.7l-.5 2.2h-3.4l-.5-2.2a7.6 7.6 0 0 1-1.7-.7l-1.9 1.2-2.4-2.4L5 16.6a7.6 7.6 0 0 1-.7-1.7l-2.2-.5V11l2.2-.5A7.6 7.6 0 0 1 5 8.8L3.8 6.9l2.4-2.4 1.9 1.2a7.6 7.6 0 0 1 1.7-.7l.5-2.2Z' }),
|
|
473
727
|
React.createElement('circle', { cx: 12, cy: 12.7, r: 3.1 }))),
|
|
474
728
|
toolsOpen ? React.createElement('div', { style: styles.toolsMenu, role: 'menu', 'aria-label': '网关工具' },
|
|
@@ -485,22 +739,40 @@ window.__ModuleLoader__.load({
|
|
|
485
739
|
}, '停止 DSH Web',
|
|
486
740
|
React.createElement('span', { style: { ...styles.muted, display: 'block', marginTop: 2 } }, '结束后台进程,所有连接将断开')))
|
|
487
741
|
: null),
|
|
488
|
-
React.createElement('button', { style: { ...styles.button, ...styles.headerButton }, onClick: () => setOpen(false), 'aria-label': '关闭' },
|
|
742
|
+
React.createElement('button', { style: { ...styles.button, ...styles.headerButton }, onClick: () => setOpen(false), 'aria-label': '关闭' },
|
|
743
|
+
React.createElement('svg', { width: 20, height: 20, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2.2, strokeLinecap: 'round', 'aria-hidden': true },
|
|
744
|
+
React.createElement('path', { d: 'M6 6l12 12M18 6L6 18' }))))),
|
|
745
|
+
React.createElement('div', { className: 'mgw-body' },
|
|
746
|
+
React.createElement('nav', { className: 'mgw-nav', 'aria-label': '移动网关设置分组' },
|
|
747
|
+
[['general', '通用设置'], ['pairing', '连接与配对'], ['devices', '可信设备']].map(([id, label]) =>
|
|
748
|
+
React.createElement('button', { key: id, type: 'button', 'aria-current': activeSection === id ? 'page' : undefined,
|
|
749
|
+
onClick: () => { setActiveSection(id); setConnectionHelpOpen(false); setToolsOpen(false) } }, label))),
|
|
750
|
+
React.createElement('div', { className: 'mgw-content', key: activeSection },
|
|
751
|
+
restartNoticeInstance && dismissedNotice !== restartNoticeInstance ? React.createElement('section', { style: { ...styles.card, position: 'relative', padding: '10px 40px 10px 12px', border: '1px solid', borderRadius: 10, marginBottom: 8, background: 'var(--dsw-alias-state-warn-tertiary, #fff7df)', borderColor: 'color-mix(in srgb, #d99a16 35%, transparent)', fontSize: 12, lineHeight: 1.6 }, role: 'note' },
|
|
752
|
+
React.createElement('button', { type: 'button', 'aria-label': '关闭重启提醒', title: '关闭提醒',
|
|
753
|
+
style: { ...styles.button, position: 'absolute', right: 8, top: 8, border: 0, width: 24, height: 24, padding: 0, color: colors.muted },
|
|
754
|
+
onClick: dismissRestartNotice }, '✕'),
|
|
755
|
+
React.createElement('strong', null, '若安装或更新后连接异常,请重启应用'),
|
|
756
|
+
React.createElement('div', { style: { color: colors.muted, marginTop: 4 } },
|
|
757
|
+
'完全退出并重启 APP/Desktop;Web UI 请重启 DSH Web 服务。'),
|
|
758
|
+
React.createElement('div', { style: { color: colors.muted, marginTop: 4 } },
|
|
759
|
+
'端口仍被占用时,请关闭其他 Harness 实例。')) : null,
|
|
489
760
|
toolNotice ? React.createElement('div', { style: { ...styles.muted, marginBottom: 10 } }, toolNotice) : null,
|
|
490
761
|
error ? React.createElement('div', { style: styles.error }, error) : null,
|
|
762
|
+
React.createElement('div', { hidden: activeSection !== 'general' },
|
|
491
763
|
React.createElement('section', { style: styles.card },
|
|
492
764
|
React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', gap: 14, alignItems: 'center' } },
|
|
493
765
|
React.createElement('div', null,
|
|
494
766
|
React.createElement('strong', null, '网关运行模式'),
|
|
495
767
|
React.createElement('div', { style: { ...styles.muted, marginTop: 4 } }, status && status.gatewayEnabled
|
|
496
768
|
? status.gatewayMode === 'persistent'
|
|
497
|
-
? '
|
|
769
|
+
? '重启后保持开启'
|
|
498
770
|
: status.waitExpiresAt
|
|
499
771
|
? `等待可信设备连接,约 ${Math.max(1, Math.ceil((status.waitExpiresAt - Date.now()) / 60000))} 分钟后自动关闭`
|
|
500
772
|
: '设备已连接过,本次运行保持开启;重启后重新计时'
|
|
501
|
-
: '
|
|
773
|
+
: '不接受移动设备连接')),
|
|
502
774
|
React.createElement('span', { style: { position: 'relative', display: 'inline-flex', flexShrink: 0 } },
|
|
503
|
-
React.createElement(
|
|
775
|
+
React.createElement(GatewaySelect, {
|
|
504
776
|
style: { ...styles.input, width: 'auto', marginBottom: 0, padding: '8px 40px 8px 12px', appearance: 'none', WebkitAppearance: 'none' },
|
|
505
777
|
'aria-label': '网关运行模式',
|
|
506
778
|
value: status ? status.gatewayMode || (status.gatewayEnabled ? 'temporary' : 'disabled') : 'disabled',
|
|
@@ -510,16 +782,9 @@ window.__ModuleLoader__.load({
|
|
|
510
782
|
React.createElement('option', { value: 'disabled' }, '关闭'),
|
|
511
783
|
React.createElement('option', { value: 'temporary' }, '临时开启'),
|
|
512
784
|
React.createElement('option', { value: 'persistent' }, '常驻开启')),
|
|
513
|
-
|
|
514
|
-
'aria-hidden': true,
|
|
515
|
-
style: {
|
|
516
|
-
position: 'absolute', right: 15, top: '50%', width: 7, height: 7,
|
|
517
|
-
borderRight: `1.5px solid ${colors.text}`, borderBottom: `1.5px solid ${colors.text}`,
|
|
518
|
-
transform: 'translateY(-65%) rotate(45deg)', pointerEvents: 'none',
|
|
519
|
-
},
|
|
520
|
-
}))),
|
|
785
|
+
)),
|
|
521
786
|
status && status.gatewayId ? React.createElement('div', { style: { ...styles.muted, marginTop: 12, overflowWrap: 'anywhere' } },
|
|
522
|
-
|
|
787
|
+
status.gatewayName) : null),
|
|
523
788
|
React.createElement('section', { style: styles.card },
|
|
524
789
|
React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', gap: 14, alignItems: 'center' } },
|
|
525
790
|
React.createElement('div', null,
|
|
@@ -541,68 +806,12 @@ window.__ModuleLoader__.load({
|
|
|
541
806
|
status && !status.requireAuth
|
|
542
807
|
? React.createElement('p', { style: { ...styles.error, margin: '12px 0 0' } }, 'Debug 模式:本机 DSH 入口将跳过设备凭证校验;独立局域网入口仍强制鉴权。')
|
|
543
808
|
: null),
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
React.createElement('strong', null, 'Cloudflare Tunnel · PC 外网接入'),
|
|
547
|
-
React.createElement('p', { style: styles.muted }, '首次开启时自动下载并校验 cloudflared;公网入口只接受已鉴权的移动 WebSocket。'),
|
|
548
|
-
React.createElement('label', { style: styles.label }, '接入方式'),
|
|
549
|
-
React.createElement('select', { style: styles.input, value: cloudflareMode, onChange: (event) => setCloudflareMode(event.target.value), disabled: cloudflareBusy },
|
|
550
|
-
React.createElement('option', { value: 'quick' }, 'Quick Tunnel · 无需域名'),
|
|
551
|
-
React.createElement('option', { value: 'named' }, '命名 Tunnel · 固定域名')),
|
|
552
|
-
cloudflareMode === 'quick'
|
|
553
|
-
? React.createElement('p', { style: styles.muted }, 'Cloudflare 会分配随机地址;DSH 重启或隧道重连后地址可能改变,手机需重新扫码确认新地址。适合临时使用。')
|
|
554
|
-
: React.createElement(React.Fragment, null,
|
|
555
|
-
React.createElement('p', { style: styles.muted }, '先在 Cloudflare 创建命名 Tunnel,把公开域名的 Service URL 设为 ', React.createElement('code', null, `http://127.0.0.1:${status.cloudflare.port}`), '。'),
|
|
556
|
-
React.createElement('label', { style: styles.label }, '公开域名'),
|
|
557
|
-
React.createElement('input', { style: styles.input, value: cloudflareHost, onChange: (event) => setCloudflareHost(event.target.value.trim()), placeholder: 'gateway.example.com', spellCheck: false }),
|
|
558
|
-
React.createElement('label', { style: styles.label }, 'Tunnel Token'),
|
|
559
|
-
React.createElement('input', { style: styles.input, type: 'password', value: cloudflareToken, onChange: (event) => setCloudflareToken(event.target.value.trim()), placeholder: status.cloudflare.configured ? '已保存;留空则保持原 Token' : '粘贴 eyJ… Token', autoComplete: 'off', spellCheck: false })),
|
|
560
|
-
React.createElement('button', {
|
|
561
|
-
style: { ...styles.button, ...styles.primary, opacity: cloudflareBusy || (cloudflareMode === 'named' && (!cloudflareHost || (!cloudflareToken && !status.cloudflare.configured))) ? .65 : 1 },
|
|
562
|
-
disabled: cloudflareBusy || (cloudflareMode === 'named' && (!cloudflareHost || (!cloudflareToken && !status.cloudflare.configured))),
|
|
563
|
-
onClick: () => configureCloudflare(true),
|
|
564
|
-
}, cloudflareBusy ? '正在处理…' : status.cloudflare.enabled && cloudflareMode === status.cloudflare.mode ? '更新 Tunnel 配置' : cloudflareMode === 'quick' ? '一键开启 Quick Tunnel' : '开启命名 Tunnel'),
|
|
565
|
-
status.cloudflare.enabled
|
|
566
|
-
? React.createElement('button', { style: { ...styles.button, width: '100%', marginTop: 8 }, disabled: cloudflareBusy, onClick: () => configureCloudflare(false) }, '关闭 Cloudflare Tunnel')
|
|
567
|
-
: null,
|
|
568
|
-
React.createElement('div', { style: { ...styles.muted, marginTop: 9, overflowWrap: 'anywhere', color: status.cloudflare.error ? colors.danger : colors.muted } },
|
|
569
|
-
status.cloudflare.error || ({ disabled: '未开启', starting: '正在启动本机入口…', preparing: '正在准备 cloudflared(首次需下载)…', connecting: '正在连接 Cloudflare…', online: '已连接 Cloudflare', 'waiting-for-gateway': '等待移动网关开启' }[status.cloudflare.state] || status.cloudflare.state),
|
|
570
|
-
status.cloudflare.publicUrl ? ` · ${status.cloudflare.publicUrl}` : ''))
|
|
571
|
-
: null,
|
|
572
|
-
status && status.platform === 'linux' ? React.createElement('section', { style: styles.card },
|
|
573
|
-
React.createElement('strong', null, '公网接入'),
|
|
574
|
-
publicSetup === null
|
|
575
|
-
? React.createElement('p', { style: styles.muted }, '正在检查系统组件…')
|
|
576
|
-
: publicSetup.installed
|
|
577
|
-
? React.createElement(React.Fragment, null,
|
|
578
|
-
React.createElement('div', { style: { ...styles.muted, margin: '5px 0 12px' } }, publicSetup.configured
|
|
579
|
-
? publicSetup.backendPort && status && status.webPort && publicSetup.backendPort !== status.webPort
|
|
580
|
-
? `端口需要更新:Nginx 当前为 ${publicSetup.backendPort},DSH 当前为 ${status.webPort}`
|
|
581
|
-
: `已配置${publicSetup.backendPort ? `,Nginx 转发至 DSH 端口 ${publicSetup.backendPort}` : ''}`
|
|
582
|
-
: `Helper 已就绪,将自动使用当前 DSH 端口${status && status.webPort ? ` ${status.webPort}` : ''}`),
|
|
583
|
-
React.createElement('label', { style: styles.label }, '服务器公网 IPv4'),
|
|
584
|
-
React.createElement('input', {
|
|
585
|
-
style: styles.input,
|
|
586
|
-
value: publicIp,
|
|
587
|
-
onChange: (event) => setPublicIp(event.target.value.trim()),
|
|
588
|
-
placeholder: '203.0.113.10',
|
|
589
|
-
inputMode: 'decimal',
|
|
590
|
-
spellCheck: false,
|
|
591
|
-
}),
|
|
592
|
-
React.createElement('button', {
|
|
593
|
-
style: { ...styles.button, ...styles.primary, opacity: publicSetupBusy || !publicIp ? .65 : 1 },
|
|
594
|
-
disabled: publicSetupBusy || !publicIp,
|
|
595
|
-
onClick: configurePublicAccess,
|
|
596
|
-
}, publicSetupBusy ? '正在配置 Nginx 与证书…' : publicSetup.configured ? '更新公网配置' : '配置公网接入'),
|
|
597
|
-
publicSetup.publicUrl
|
|
598
|
-
? React.createElement('div', { style: { ...styles.muted, marginTop: 9, overflowWrap: 'anywhere' } }, publicSetup.publicUrl)
|
|
599
|
-
: null)
|
|
600
|
-
: React.createElement(React.Fragment, null,
|
|
601
|
-
React.createElement('p', { style: { ...styles.muted, marginBottom: 8 } }, '尚未安装系统 Helper。请在服务器执行一次初始化:'),
|
|
602
|
-
React.createElement('code', { style: { display: 'block', fontSize: 10, lineHeight: 1.5, overflowWrap: 'anywhere', userSelect: 'all' } }, 'npx --yes dsh-plugin-mobile-gateway@latest init'))) : null,
|
|
809
|
+
),
|
|
810
|
+
React.createElement('div', { hidden: activeSection !== 'pairing' },
|
|
603
811
|
React.createElement('section', { style: styles.card },
|
|
812
|
+
React.createElement('div', { className: 'mgw-setting-row' },
|
|
604
813
|
React.createElement('div', { style: { position: 'relative', display: 'flex', alignItems: 'center', gap: 6, marginBottom: 6 }, onMouseLeave: () => setConnectionHelpOpen(false) },
|
|
605
|
-
React.createElement('
|
|
814
|
+
React.createElement('h3', { className: 'mgw-section-title', style: { marginBottom: 0 } }, '配对连接方式'),
|
|
606
815
|
React.createElement('button', {
|
|
607
816
|
type: 'button',
|
|
608
817
|
style: styles.helpButton,
|
|
@@ -621,51 +830,126 @@ window.__ModuleLoader__.load({
|
|
|
621
830
|
helpLine('Cloudflare Channel(命名 Tunnel)', 'PC 主动连接 Cloudflare,外网用固定域名的 wss://;需要 Cloudflare 账户和域名。'),
|
|
622
831
|
helpLine('Quick Channel(Quick Tunnel)', '无需账户或域名,Cloudflare 分配临时 wss:// 地址;隧道重建后地址可能变化。'),
|
|
623
832
|
helpLine('公网服务器连接', 'DSH 运行在有固定公网 IPv4 的 Linux 服务器上,配置公网入口后用 wss:// 公网 IP 连接。'),
|
|
624
|
-
React.createElement('div', { style: { borderTop:
|
|
833
|
+
React.createElement('div', { style: { borderTop: `1px solid ${colors.border}`, marginTop: 10, paddingTop: 8 } },
|
|
625
834
|
React.createElement('strong', null, '结合当前环境'),
|
|
626
|
-
React.createElement('div', { style: { color:
|
|
835
|
+
React.createElement('div', { style: { color: colors.muted, marginTop: 4 } }, advice.detected),
|
|
627
836
|
React.createElement('div', { style: { marginTop: 6 } }, advice.local),
|
|
628
837
|
React.createElement('div', { style: { marginTop: 6 } }, advice.remote),
|
|
629
|
-
React.createElement('div', { style: { color:
|
|
838
|
+
React.createElement('div', { style: { color: colors.muted, marginTop: 6 } }, '面板无法判断手机当前网络;请按手机所在位置选择,提示不会替你修改选择。')))
|
|
630
839
|
: null),
|
|
631
|
-
React.createElement('
|
|
840
|
+
React.createElement('div', { style: { display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: '8px 14px', maxWidth: '100%' } },
|
|
841
|
+
React.createElement(GatewaySelect, { style: styles.input, value: pairingRoute, onChange: (event) => choosePairingRoute(event.target.value), disabled: !status, 'aria-label': '配对连接方式' },
|
|
632
842
|
React.createElement('option', { value: 'auto' }, '自动选择 · 优先外网'),
|
|
633
843
|
lanPairingUrl(status) || pairingRoute === 'lan'
|
|
634
844
|
? React.createElement('option', { value: 'lan', disabled: !lanPairingUrl(status) }, lanPairingUrl(status)
|
|
635
|
-
? '局域网直连
|
|
845
|
+
? '局域网直连'
|
|
636
846
|
: '局域网直连 · 当前不可用')
|
|
637
847
|
: null,
|
|
638
848
|
status?.cloudflare?.enabled ? React.createElement('option', { value: 'cloudflare' }, 'Cloudflare 外网') : null,
|
|
639
849
|
React.createElement('option', { value: 'custom' }, '手动输入地址')),
|
|
850
|
+
(pairingRoute === 'auto' || pairingRoute === 'cloudflare') && status?.cloudflare?.supported
|
|
851
|
+
? React.createElement('span', { style: { fontSize: 12, lineHeight: 1.5, fontWeight: 500,
|
|
852
|
+
color: 'var(--dsw-alias-link, #4176e6)' } }, '外网使用 Cloudflare Tunnel 进行连接,请在下方进行设置。') : null)),
|
|
640
853
|
React.createElement('label', { style: styles.label }, 'WebSocket 地址'),
|
|
641
854
|
React.createElement('input', { style: styles.input, value: publicUrl, onChange: (event) => { choosePairingRoute('custom'); setPublicUrl(event.target.value) }, placeholder: 'ws://192.168.1.10:3081/ws/mobile', spellCheck: false }),
|
|
642
855
|
status && status.lan && status.lan.enabled
|
|
643
|
-
? React.createElement('div', { style: { ...styles.muted, margin: '-7px 0 10px', color: status.lan.error ? colors.danger : colors.
|
|
856
|
+
? React.createElement('div', { style: { ...styles.muted, margin: '-7px 0 10px', fontWeight: 500, color: status.lan.error ? colors.danger : status.lan.listening ? colors.success : 'var(--dsw-alias-link, #4176e6)' } },
|
|
644
857
|
status.lan.error
|
|
645
858
|
? `局域网监听失败:${status.lan.error}`
|
|
646
859
|
: status.lan.listening
|
|
647
860
|
? `局域网入口已开启${status.lan.urls && status.lan.urls.length ? `:${status.lan.urls.join('、')}` : `,端口 ${status.lan.port}`}`
|
|
648
861
|
: '局域网入口启动中…')
|
|
649
862
|
: null,
|
|
650
|
-
React.createElement('label', { style: styles.label }, '设备名称'),
|
|
651
|
-
React.createElement('input', { style: styles.input, value: deviceName, onChange: (event) => setDeviceName(event.target.value), maxLength: 80, placeholder: 'iPhone' }),
|
|
652
863
|
React.createElement('button', {
|
|
653
864
|
style: { ...styles.button, ...styles.primary, opacity: busy || !(status && status.gatewayEnabled) || !publicUrl.trim() ? .65 : 1 },
|
|
654
865
|
disabled: busy || !(status && status.gatewayEnabled) || !publicUrl.trim(),
|
|
655
866
|
onClick: pair,
|
|
656
867
|
}, busy ? '正在生成…' : !(status && status.gatewayEnabled) ? '请先开启移动网关' : !publicUrl.trim() ? '等待连接地址' : '生成配对二维码'),
|
|
657
|
-
React.createElement('p', { style: { ...styles.muted, margin: '10px 0 0' } }, '
|
|
658
|
-
|
|
659
|
-
React.createElement('
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
868
|
+
React.createElement('p', { style: { ...styles.muted, margin: '10px 0 0' } }, '配对码仅可使用一次,5 分钟内有效。')),
|
|
869
|
+
(pairingRoute === 'auto' || pairingRoute === 'cloudflare') && status?.cloudflare?.supported
|
|
870
|
+
? React.createElement('section', { className: 'mgw-cloudflare-block', style: { ...styles.card, background: colors.card, border: 0, borderRadius: 16, padding: 18, marginTop: 16 } },
|
|
871
|
+
React.createElement('h3', { className: 'mgw-section-title' }, 'Cloudflare Tunnel · 外网接入'),
|
|
872
|
+
React.createElement('p', { style: styles.muted }, '首次开启自动准备 Tunnel 组件。'),
|
|
873
|
+
React.createElement('div', { className: 'mgw-setting-row' },
|
|
874
|
+
React.createElement('span', { className: 'mgw-setting-label' }, '接入方式'),
|
|
875
|
+
React.createElement(GatewaySelect, { style: styles.input, value: cloudflareMode, 'aria-label': 'Cloudflare 接入方式', onChange: (event) => {
|
|
876
|
+
const mode = event.target.value
|
|
877
|
+
setCloudflareMode(mode)
|
|
878
|
+
if (status.cloudflare.enabled && mode !== status.cloudflare.mode &&
|
|
879
|
+
(mode === 'quick' || (status.cloudflare.configured && cloudflareHost === status.cloudflare.hostname && !cloudflareToken))) {
|
|
880
|
+
void configureCloudflare(true, mode)
|
|
881
|
+
}
|
|
882
|
+
}, disabled: cloudflareBusy },
|
|
883
|
+
React.createElement('option', { value: 'quick' }, 'Quick Tunnel · 无需域名'),
|
|
884
|
+
React.createElement('option', { value: 'named' }, '命名 Tunnel · 固定域名'))),
|
|
885
|
+
cloudflareMode === 'quick'
|
|
886
|
+
? React.createElement('p', { style: styles.muted }, '临时地址可能变化,变化后需重新扫码。')
|
|
887
|
+
: React.createElement(React.Fragment, null,
|
|
888
|
+
React.createElement('p', { style: styles.muted }, '先在 Cloudflare 创建命名 Tunnel,把公开域名的 Service URL 设为 ', React.createElement('code', null, `http://127.0.0.1:${status.cloudflare.port}`), '。Tunnel Token 仅授权连接,不能自动创建公开主机名或 DNS;还需确认域名存在指向 Tunnel UUID.cfargotunnel.com 的已代理 CNAME 记录。'),
|
|
889
|
+
React.createElement('label', { style: styles.label }, '公开域名'),
|
|
890
|
+
React.createElement('input', { style: styles.input, value: cloudflareHost, onChange: (event) => setCloudflareHost(event.target.value.trim()), placeholder: 'gateway.example.com', spellCheck: false }),
|
|
891
|
+
React.createElement('label', { style: styles.label }, 'Tunnel Token'),
|
|
892
|
+
React.createElement('input', { style: styles.input, type: 'password', value: cloudflareToken, onChange: (event) => setCloudflareToken(event.target.value.trim()), placeholder: status.cloudflare.configured ? '已保存;留空则保持原 Token' : '粘贴 eyJ… Token', autoComplete: 'off', spellCheck: false })),
|
|
893
|
+
(!status.cloudflare.enabled || (cloudflareMode === 'named' &&
|
|
894
|
+
(status.cloudflare.mode !== 'named' || cloudflareHost !== status.cloudflare.hostname || !!cloudflareToken)))
|
|
895
|
+
? React.createElement('button', {
|
|
896
|
+
style: { ...styles.button, ...styles.primary, opacity: cloudflareBusy || (cloudflareMode === 'named' && (!cloudflareHost || (!cloudflareToken && !status.cloudflare.configured))) ? .65 : 1 },
|
|
897
|
+
disabled: cloudflareBusy || (cloudflareMode === 'named' && (!cloudflareHost || (!cloudflareToken && !status.cloudflare.configured))),
|
|
898
|
+
onClick: () => configureCloudflare(true),
|
|
899
|
+
}, cloudflareBusy ? '正在处理…' : status.cloudflare.enabled ? '保存并连接' : cloudflareMode === 'quick' ? '一键开启 Quick Tunnel' : '开启命名 Tunnel') : null,
|
|
900
|
+
status.cloudflare.enabled
|
|
901
|
+
? React.createElement(React.Fragment, null,
|
|
902
|
+
React.createElement('button', { style: { ...styles.button, width: '100%', marginTop: 8 }, disabled: cloudflareBusy || status.cloudflare.state !== 'online', onClick: checkCloudflare }, '检测公网 DNS / TLS / WebSocket'),
|
|
903
|
+
React.createElement('button', { style: { ...styles.button, width: '100%', marginTop: 8 }, disabled: cloudflareBusy || !status.gatewayEnabled, onClick: restartCloudflare }, '重启 Tunnel'),
|
|
904
|
+
React.createElement('button', { style: { ...styles.button, width: '100%', marginTop: 8 }, disabled: cloudflareBusy, onClick: () => configureCloudflare(false) }, '关闭 Cloudflare Tunnel'),
|
|
905
|
+
React.createElement('p', { style: styles.muted }, '重启会短暂断开连接。'))
|
|
906
|
+
: null,
|
|
907
|
+
React.createElement('div', { style: { ...styles.muted, marginTop: 9, overflowWrap: 'anywhere', fontWeight: 500, color: status.cloudflare.error ? colors.danger : status.cloudflare.state === 'online' ? colors.success : ['starting', 'preparing', 'connecting'].includes(status.cloudflare.state) ? 'var(--dsw-alias-link, #4176e6)' : colors.muted } },
|
|
908
|
+
status.cloudflare.error || ({ disabled: '未开启', starting: '正在启动本机入口…', preparing: '正在准备 cloudflared(首次需下载)…', connecting: '正在连接 Cloudflare…', online: '隧道传输已连接(不等于公网入口可达)', 'waiting-for-gateway': '等待移动网关开启' }[status.cloudflare.state] || status.cloudflare.state),
|
|
909
|
+
status.cloudflare.publicUrl ? ` · ${status.cloudflare.publicUrl}` : ''),
|
|
910
|
+
status.cloudflare.enabled && status.cloudflare.endpointHealth
|
|
911
|
+
? React.createElement('p', { style: { ...styles.muted, overflowWrap: 'anywhere', fontWeight: 500, color: status.cloudflare.endpointHealth.state === 'reachable' ? colors.success : status.cloudflare.endpointHealth.state === 'checking' ? 'var(--dsw-alias-link, #4176e6)' : status.cloudflare.endpointHealth.state === 'unchecked' ? colors.muted : colors.danger } },
|
|
912
|
+
status.cloudflare.endpointHealth.message,
|
|
913
|
+
status.cloudflare.endpointHealth.checkedAt ? `(检测时间:${status.cloudflare.endpointHealth.checkedAt})` : '')
|
|
914
|
+
: null)
|
|
915
|
+
: null,
|
|
916
|
+
status && status.platform === 'linux' ? React.createElement('section', { style: styles.card },
|
|
917
|
+
React.createElement('h3', { className: 'mgw-section-title' }, '公网接入'),
|
|
918
|
+
publicSetup === null
|
|
919
|
+
? React.createElement('p', { style: styles.muted }, '正在检查系统组件…')
|
|
920
|
+
: publicSetup.installed
|
|
921
|
+
? React.createElement(React.Fragment, null,
|
|
922
|
+
React.createElement('div', { style: { ...styles.muted, margin: '5px 0 12px' } }, publicSetup.configured
|
|
923
|
+
? publicSetup.backendPort && status && status.webPort && publicSetup.backendPort !== status.webPort
|
|
924
|
+
? `端口需要更新:Nginx 当前为 ${publicSetup.backendPort},DSH 当前为 ${status.webPort}`
|
|
925
|
+
: `已配置${publicSetup.backendPort ? `,Nginx 转发至 DSH 端口 ${publicSetup.backendPort}` : ''}`
|
|
926
|
+
: `Helper 已就绪,将自动使用当前 DSH 端口${status && status.webPort ? ` ${status.webPort}` : ''}`),
|
|
927
|
+
React.createElement('label', { style: styles.label }, '服务器公网 IPv4'),
|
|
928
|
+
React.createElement('input', {
|
|
929
|
+
style: styles.input,
|
|
930
|
+
value: publicIp,
|
|
931
|
+
onChange: (event) => setPublicIp(event.target.value.trim()),
|
|
932
|
+
placeholder: '203.0.113.10',
|
|
933
|
+
inputMode: 'decimal',
|
|
934
|
+
spellCheck: false,
|
|
935
|
+
}),
|
|
936
|
+
React.createElement('button', {
|
|
937
|
+
style: { ...styles.button, ...styles.primary, opacity: publicSetupBusy || !publicIp ? .65 : 1 },
|
|
938
|
+
disabled: publicSetupBusy || !publicIp,
|
|
939
|
+
onClick: configurePublicAccess,
|
|
940
|
+
}, publicSetupBusy ? '正在配置 Nginx 与证书…' : publicSetup.configured ? '更新公网配置' : '配置公网接入'),
|
|
941
|
+
publicSetup.publicUrl
|
|
942
|
+
? React.createElement('div', { style: { ...styles.muted, marginTop: 9, overflowWrap: 'anywhere' } }, publicSetup.publicUrl)
|
|
943
|
+
: null)
|
|
944
|
+
: React.createElement(React.Fragment, null,
|
|
945
|
+
React.createElement('p', { style: { ...styles.muted, marginBottom: 8 } }, '尚未安装系统 Helper。请在服务器执行一次初始化:'),
|
|
946
|
+
React.createElement('code', { style: { display: 'block', fontSize: 10, lineHeight: 1.5, overflowWrap: 'anywhere', userSelect: 'all' } }, 'npx --yes dsh-plugin-mobile-gateway@latest init'))) : null,
|
|
947
|
+
|
|
948
|
+
),
|
|
949
|
+
React.createElement('div', { hidden: activeSection !== 'devices' },
|
|
666
950
|
React.createElement('section', { style: styles.card },
|
|
667
951
|
React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
|
|
668
|
-
React.createElement('
|
|
952
|
+
React.createElement('h3', { className: 'mgw-section-title', style: { marginBottom: 0 } }, '可信设备'),
|
|
669
953
|
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 10 } },
|
|
670
954
|
deviceNotice || refreshNotice ? React.createElement('span', {
|
|
671
955
|
style: { ...styles.muted, color: deviceNoticeError ? colors.danger : colors.success },
|
|
@@ -679,12 +963,22 @@ window.__ModuleLoader__.load({
|
|
|
679
963
|
? React.createElement('p', { style: styles.muted }, '加载中…')
|
|
680
964
|
: devices.length === 0
|
|
681
965
|
? React.createElement('p', { style: styles.muted }, '暂无已配对设备。')
|
|
682
|
-
:
|
|
966
|
+
: React.createElement(React.Fragment, null,
|
|
967
|
+
visibleDevices.length === 0 ? React.createElement('p', { style: styles.muted }, '当前没有在线设备。') : null,
|
|
968
|
+
visibleDevices.map((device) => React.createElement(DeviceRow, {
|
|
683
969
|
key: device.id,
|
|
684
970
|
device,
|
|
685
971
|
onRevoke: revoke,
|
|
686
972
|
revoking: revokingIds.has(device.id),
|
|
687
|
-
}))
|
|
973
|
+
})),
|
|
974
|
+
offlineDevices.length > 0 ? React.createElement('button', {
|
|
975
|
+
type: 'button',
|
|
976
|
+
style: { ...styles.button, marginTop: 10, color: colors.muted },
|
|
977
|
+
'aria-expanded': showAllDevices,
|
|
978
|
+
onClick: () => setShowAllDevices((value) => !value),
|
|
979
|
+
}, showAllDevices ? '收起离线设备' : `展开其他已配对设备(${offlineDevices.length})`) : null)),
|
|
980
|
+
)),
|
|
981
|
+
),
|
|
688
982
|
status && status.version
|
|
689
983
|
? React.createElement('div', { style: styles.version, 'aria-label': `插件版本 ${status.version}` }, `v${status.version}`)
|
|
690
984
|
: null))
|