dsh-remote-plugin 0.5.0 → 0.5.3

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/public/admin.js CHANGED
@@ -2,6 +2,9 @@
2
2
  'use strict'
3
3
 
4
4
  const $ = (id) => document.getElementById(id)
5
+ const I18N = window.I18N
6
+ const t = (k, v) => I18N.t(k, v)
7
+ I18N.init(window.ADMIN_STR)
5
8
  // 插件内嵌(/remote/ 或 ?embedded=1)直接进管理面板, 不需要任何令牌门禁;
6
9
  // 独立网关模式(/admin/)仍保留令牌输入。路径判断兼容无尾斜杠 /remote。
7
10
  const pluginMode = location.pathname === '/remote'
@@ -31,10 +34,10 @@ function toast(text, kind = '') {
31
34
  }
32
35
 
33
36
  function fmtUptime(sec) {
34
- if (sec < 60) return sec + ''
35
- if (sec < 3600) return Math.floor(sec / 60) + ' 分钟'
36
- if (sec < 86400) return Math.floor(sec / 3600) + ' 小时 ' + Math.floor(sec % 3600 / 60) + ''
37
- return Math.floor(sec / 86400) + '' + Math.floor(sec % 86400 / 3600) + ' 小时'
37
+ if (sec < 60) return sec + t('unit.sec')
38
+ if (sec < 3600) return Math.floor(sec / 60) + t('unit.min')
39
+ if (sec < 86400) return Math.floor(sec / 3600) + t('unit.hour') + Math.floor(sec % 3600 / 60) + t('unit.minShort')
40
+ return Math.floor(sec / 86400) + t('unit.day') + Math.floor(sec % 86400 / 3600) + t('unit.hour')
38
41
  }
39
42
 
40
43
  function fmtTime(ts) {
@@ -54,10 +57,10 @@ async function loadState() {
54
57
  render(st)
55
58
  } catch (e) {
56
59
  if (e.message === 'AUTH') {
57
- toast('令牌无效', 'err')
60
+ toast(t('toast.tokenInvalid'), 'err')
58
61
  logout()
59
62
  } else {
60
- $('conn-badge').textContent = '连接失败'
63
+ $('conn-badge').textContent = t('toast.connFailed')
61
64
  $('conn-badge').className = 'conn-badge off'
62
65
  }
63
66
  }
@@ -68,9 +71,10 @@ function render(st) {
68
71
  const isPlugin = st.mode === 'plugin'
69
72
  const isGateway = st.mode === 'gateway'
70
73
  shownToken = st.token || token
71
- $('conn-badge').textContent = isPlugin ? '内嵌' : isGateway ? '网关' : '已连接'
72
- $('conn-badge').className = 'conn-badge on'
73
- $('token-full').textContent = shownToken || (isPlugin ? '插件模式 · 未接网关, 无需令牌' : '未获取到令牌')
74
+ $('conn-badge').textContent = t(isPlugin ? 'badge.embedded' : isGateway ? 'badge.gateway' : 'badge.connected')
75
+ $('conn-badge').className = 'conn-badge ' + (isPlugin || isGateway ? 'on' : 'off')
76
+ $('conn-badge').title = t(isGateway ? 'badge.gateway.title' : 'badge.gatewayDown')
77
+ $('token-full').textContent = shownToken || t(isPlugin ? 'token.pluginNoGateway' : 'token.unavailable')
74
78
  // 主机端插件模式: 显示真实令牌(复制可用), 只隐藏退出按钮; 令牌门禁本身不存在
75
79
  $('btn-copy').classList.toggle('hidden', !shownToken)
76
80
  $('btn-logout').classList.toggle('hidden', pluginMode)
@@ -82,65 +86,70 @@ function render(st) {
82
86
  gatewayRunning = isGateway
83
87
  $('btn-gateway').classList.toggle('hidden', !pluginMode)
84
88
  $('btn-gateway').textContent = gatewayBusy
85
- ? (gatewayRunning ? '停止中…' : '启动中…')
86
- : (gatewayRunning ? '停止网关' : '启动网关')
89
+ ? t(gatewayRunning ? 'stopping' : 'starting')
90
+ : t(gatewayRunning ? 'stopGateway' : 'startGateway')
87
91
  $('btn-gateway').disabled = gatewayBusy
88
92
  const upOk = st.upstream.reachable
89
- const hostIPs = (st.lanIPs || []).join('') || '127.0.0.1'
93
+ const hostIPs = (st.lanIPs || []).join(t('stat.ipSep')) || '127.0.0.1'
90
94
  const latestHtml = st.latest?.newer
91
- ? `<div class="v">v${st.latest.version} 可用</div><div class="k">当前 v${st.version} · <a href="${st.latest.url || '#'}" target="_blank" rel="noopener" style="color:var(--orange)">去下载</a></div>`
92
- : `<div class="v">v${st.version}</div><div class="k">${isPlugin ? 'DSH 内嵌 · 免网关' : st.latest?.error ? '更新检查: ' + st.latest.error : st.latest?.version ? '已是最新(来源检查)' : '未检查更新'}</div>`
95
+ ? `<div class="v">${t('stat.updateAvailable', { version: st.latest.version })}</div><div class="k">${t('stat.currentV', { version: st.version })} · <a href="${st.latest.url || '#'}" target="_blank" rel="noopener" style="color:var(--orange)">${t('stat.download')}</a></div>`
96
+ : `<div class="v">v${st.version}</div><div class="k">${isPlugin ? t('stat.embedded') : st.latest?.error ? t('stat.updateCheck', { error: st.latest.error }) : st.latest?.version ? t('stat.latest') : t('stat.notChecked')}</div>`
93
97
  $('stats').innerHTML = `
94
- <div class="stat-card"><div class="v">v${st.version}</div><div class="k">${isPlugin ? '插件版本' : '网关版本'}</div></div>
98
+ <div class="stat-card"><div class="v">v${st.version}</div><div class="k">${t(isPlugin ? 'stat.pluginVersion' : 'stat.gatewayVersion')}</div></div>
95
99
  <div class="stat-card ${st.latest?.newer ? 'warn' : 'ok'}">${latestHtml}</div>
96
- <div class="stat-card ok"><div class="v" style="font-size:15px">${hostIPs}</div><div class="k">主机 IP · ${st.hostname}${isPlugin ? ' (手机连 8787 网关)' : ' (手机连这个地址)'}</div></div>
97
- <div class="stat-card ${upOk ? 'ok' : 'warn'}"><div class="v">${upOk ? '可达' : '不可达'}</div><div class="k">DSH 上游 ${st.upstream.url}</div></div>
98
- <div class="stat-card"><div class="v">${st.onlineCount}/${st.deviceCount}</div><div class="k">设备在线 / 累计</div></div>
99
- <div class="stat-card"><div class="v">${st.totalRequests}</div><div class="k">总请求数</div></div>
100
- <div class="stat-card"><div class="v">${st.authFailures}</div><div class="k">认证失败</div></div>
101
- <div class="stat-card"><div class="v">${fmtUptime(st.uptimeSec)}</div><div class="k">运行时长 · ${st.host}:${st.port}</div></div>`
100
+ <div class="stat-card ok"><div class="v" style="font-size:13px">${hostIPs}</div><div class="k">${t('stat.hostIP', { hostname: st.hostname })}${isPlugin ? t('stat.phoneGateway') : t('stat.phoneThis')}</div></div>
101
+ <div class="stat-card ${upOk ? 'ok' : 'warn'}"><div class="v">${t(upOk ? 'stat.reachable' : 'stat.unreachable')}</div><div class="k">${t('stat.dshUpstream', { url: st.upstream.url })}</div></div>
102
+ <div class="stat-card"><div class="v">${st.onlineCount}/${st.deviceCount}</div><div class="k">${t('stat.devicesOnline')}</div></div>
103
+ <div class="stat-card"><div class="v">${st.totalRequests}</div><div class="k">${t('stat.totalRequests')}</div></div>
104
+ <div class="stat-card"><div class="v">${st.authFailures}</div><div class="k">${t('stat.authFailures')}</div></div>
105
+ <div class="stat-card"><div class="v">${fmtUptime(st.uptimeSec)}</div><div class="k">${t('stat.uptime', { host: st.host, port: st.port })}</div></div>`
102
106
 
103
107
  $('device-summary').textContent = isPlugin
104
- ? (st.gatewayInstalled ? '网关已安装 · 当前未运行' : '未检测到网关程序')
105
- : `${st.devices.length} 个 IP · 每 5 秒刷新`
108
+ ? t(st.gatewayInstalled ? 'device.installedNotRunning' : 'device.noGatewayBinary')
109
+ : t('device.ipRefresh', { n: st.devices.length })
106
110
  if (isPlugin && !st.devices.length) {
107
111
  $('device-rows').innerHTML = ''
108
112
  const rel = 'https://github.com/Blank-not-black/dsh-Remote/releases/latest/download/'
109
- const apkBtn = `<a class="mini-btn" href="${rel}dsh-remote.apk" target="_blank" rel="noopener">下载手机 App</a>`
113
+ const apkBtn = `<a class="mini-btn" href="${rel}dsh-remote.apk" target="_blank" rel="noopener">${t('device.downloadApp')}</a>`
110
114
  if (!st.gatewayInstalled) {
111
115
  // 只有插件包真的没有内置网关程序时, 才引导下载网关
112
116
  const isWin = /windows|win32/i.test(navigator.userAgent)
113
117
  const gwAsset = isWin ? 'dsh-remote-win-x64.exe' : 'dsh-remote-linux-x64'
114
118
  $('device-empty').innerHTML = `
115
- <div>本插件包未包含网关程序:下载对应系统的网关并运行</div>
119
+ <div>${t('device.noBinaryGuide')}</div>
116
120
  <div class="empty-actions">
117
- <a class="mini-btn" href="${rel}${gwAsset}" target="_blank" rel="noopener">下载网关 (${isWin ? 'Windows x64' : 'Linux x64'})</a>
121
+ <a class="mini-btn" href="${rel}${gwAsset}" target="_blank" rel="noopener">${t('device.downloadGateway', { os: isWin ? 'Windows x64' : 'Linux x64' })}</a>
118
122
  ${apkBtn}
119
123
  </div>
120
- <div class="muted" style="margin-top:10px">运行网关后回到本页刷新,即可看到设备监控与完整令牌</div>`
124
+ <div class="muted" style="margin-top:10px">${t('device.afterRunGuide')}</div>`
121
125
  } else {
122
126
  $('device-empty').innerHTML = `
123
- <div>网关已随插件安装,当前未运行 — 点击上方「启动网关」开启</div>
127
+ <div>${t('device.installedGuide')}</div>
124
128
  <div class="empty-actions">${apkBtn}</div>
125
- <div class="muted" style="margin-top:10px">启动后本页会自动刷新为网关模式(完整设备监控 + 令牌)</div>`
129
+ <div class="muted" style="margin-top:10px">${t('device.afterStartGuide')}</div>`
126
130
  }
127
131
  $('device-empty').classList.remove('hidden')
128
132
  } else {
129
133
  // 网关模式: 清掉可能残留的引导文案, 设备为空时只显示中性提示
130
- $('device-empty').textContent = '暂无设备记录'
134
+ $('device-empty').textContent = t('noDevices')
131
135
  $('device-empty').classList.toggle('hidden', st.devices.length > 0)
132
- $('device-rows').innerHTML = st.devices.map(d => `
136
+ $('device-rows').innerHTML = st.devices.map(d => {
137
+ const kindText = t(d.kind === 'app' ? 'device.kind.app' : d.kind === 'admin' ? 'device.kind.admin' : d.kind === 'web' ? 'device.kind.web' : 'device.kind.unknown')
138
+ const noteHtml = d.note ? `<b>${d.note.replace(/[<>&"]/g, c => ({'<':'&lt;','>':'&gt;','&':'&amp;','"':'&quot;'}[c]))}</b>` : '<span class="muted">—</span>'
139
+ const ch = `${d.channels.mux ? 'mux' : ''}${d.channels.mux && d.channels.host ? ' · ' : ''}${d.channels.host ? 'host' : ''}${!d.channels.mux && !d.channels.host ? '—' : ''}`
140
+ return `
133
141
  <tr>
134
- <td><span class="dot ${d.online ? 'on' : 'off'}"></span>${d.online ? '在线' : '离线'}</td>
135
- <td>${d.note ? `<b>${d.note.replace(/[<>&"]/g, c => ({'<':'&lt;','>':'&gt;','&':'&amp;','"':'&quot;'}[c]))}</b>` : '<span class="muted">—</span>'}<button class="mini-btn" data-note-ip="${d.ip}" data-note="${d.note.replace(/"/g, '&quot;')}" style="margin-left:6px;padding:1px 7px">备注</button></td>
136
- <td><span class="badge ${d.kind}">${d.kind === 'app' ? '手机App' : d.kind === 'admin' ? '管理页' : d.kind === 'web' ? '浏览器' : '未知'}</span></td>
137
- <td class="mono">${d.ip}</td>
138
- <td class="mono">${d.channels.mux ? 'mux' : ''}${d.channels.mux && d.channels.host ? ' · ' : ''}${d.channels.host ? 'host' : ''}${!d.channels.mux && !d.channels.host ? '—' : ''}</td>
139
- <td>${d.requests}</td>
140
- <td>${fmtTime(d.lastSeen)}</td>
141
- <td class="ua" title="${d.ua.replace(/"/g, '&quot;')}">${d.ua || '—'}</td>
142
- <td>${d.online && d.kind !== 'admin' ? `<button class="mini-btn" data-kick="${d.ip}">断开</button>` : ''}</td>
143
- </tr>`).join('')
142
+ <td data-label="${t('th.status')}"><span class="dot ${d.online ? 'on' : 'off'}"></span>${t(d.online ? 'device.online' : 'device.offline')}</td>
143
+ <td data-label="${t('th.name')}">${noteHtml}<button class="mini-btn" data-note-ip="${d.ip}" data-note="${d.note.replace(/"/g, '&quot;')}" style="margin-left:6px;padding:1px 7px">${t('device.note')}</button></td>
144
+ <td data-label="${t('th.type')}"><span class="badge ${d.kind}">${kindText}</span></td>
145
+ <td data-label="${t('th.ip')}" class="mono nowrap">${d.ip}</td>
146
+ <td data-label="${t('th.channels')}" class="mono nowrap">${ch}</td>
147
+ <td data-label="${t('th.requests')}">${d.requests}</td>
148
+ <td data-label="${t('th.lastSeen')}" class="nowrap">${fmtTime(d.lastSeen)}</td>
149
+ <td data-label="${t('th.ua')}" class="ua" title="${d.ua.replace(/"/g, '&quot;')}">${d.ua || '—'}</td>
150
+ <td class="act">${d.online && d.kind !== 'admin' ? `<button class="mini-btn" data-kick="${d.ip}">${t('device.kick')}</button>` : ''}</td>
151
+ </tr>`
152
+ }).join('')
144
153
  }
145
154
  document.querySelectorAll('[data-kick]').forEach(btn =>
146
155
  btn.addEventListener('click', () => kick(btn.dataset.kick)))
@@ -166,21 +175,21 @@ function renderQr(st) {
166
175
  return
167
176
  }
168
177
  try {
169
- const t = pairTarget(st)
178
+ const pt = pairTarget(st)
170
179
  const qr = window.qrcode(0, 'M')
171
- qr.addData(t.url)
180
+ qr.addData(pt.url)
172
181
  qr.make()
173
182
  $('pair-qr').innerHTML = qr.createSvgTag({ cellSize: 4, margin: 2, scalable: true })
174
- $('pair-hint').textContent = `${t.base} · App「设置 扫码连接」,或系统相机扫一扫自动打开 App`
183
+ $('pair-hint').textContent = t('pair.hint', { base: pt.base })
175
184
  box.classList.remove('hidden')
176
185
  } catch (e) {
177
- $('pair-qr').textContent = '二维码生成失败'
186
+ $('pair-qr').textContent = t('pair.failed')
178
187
  box.classList.remove('hidden')
179
188
  }
180
189
  }
181
190
 
182
191
  async function setNote(ip, current) {
183
- const name = prompt('' + ip + ' 设置备注(留空清除):', current || '')
192
+ const name = prompt(t('prompt.note', { ip }), current || '')
184
193
  if (name === null) return
185
194
  const res = await fetch(`${API}/note`, {
186
195
  method: 'POST',
@@ -188,33 +197,33 @@ async function setNote(ip, current) {
188
197
  body: JSON.stringify({ ip, name })
189
198
  })
190
199
  if (res.ok) {
191
- toast('备注已保存', 'ok')
200
+ toast(t('toast.noteSaved'), 'ok')
192
201
  setTimeout(loadState, 300)
193
202
  } else {
194
- toast('保存失败', 'err')
203
+ toast(t('toast.noteFailed'), 'err')
195
204
  }
196
205
  }
197
206
 
198
207
  async function kick(ip) {
199
- if (!confirm('断开该设备的连接?')) return
208
+ if (!confirm(t('confirm.kick'))) return
200
209
  const res = await fetch(`${API}/kick`, {
201
210
  method: 'POST',
202
211
  headers: { 'content-type': 'application/json', authorization: 'Bearer ' + token, 'x-dsh-remote-client': 'admin' },
203
212
  body: JSON.stringify({ ip })
204
213
  })
205
214
  if (res.ok) {
206
- toast('已断开 ' + ip, 'ok')
215
+ toast(t('toast.kicked', { ip }), 'ok')
207
216
  setTimeout(loadState, 400)
208
217
  } else {
209
- toast('操作失败', 'err')
218
+ toast(t('toast.opFailed'), 'err')
210
219
  }
211
220
  }
212
221
 
213
222
  function enter() {
214
- const t = $('token-input').value.trim()
215
- if (!t) return
216
- token = t
217
- store.set('dshAdminToken', t)
223
+ const val = $('token-input').value.trim()
224
+ if (!val) return
225
+ token = val
226
+ store.set('dshAdminToken', val)
218
227
  history.replaceState(null, '', location.pathname)
219
228
  showMain()
220
229
  loadState()
@@ -232,7 +241,7 @@ function logout() {
232
241
  clearInterval(timer)
233
242
  $('main-view').classList.add('hidden')
234
243
  $('login-view').classList.remove('hidden')
235
- $('conn-badge').textContent = '未认证'
244
+ $('conn-badge').textContent = t('unauth')
236
245
  $('conn-badge').className = 'conn-badge off'
237
246
  $('token-input').value = ''
238
247
  }
@@ -247,9 +256,9 @@ $('btn-close-drawer').addEventListener('click', () => {
247
256
  $('btn-copy').addEventListener('click', async () => {
248
257
  try {
249
258
  await navigator.clipboard.writeText(shownToken || token)
250
- toast('令牌已复制', 'ok')
259
+ toast(t('toast.tokenCopied'), 'ok')
251
260
  } catch {
252
- toast('复制失败,请手动选择', 'err')
261
+ toast(t('toast.copyFailed'), 'err')
253
262
  }
254
263
  })
255
264
 
@@ -258,8 +267,22 @@ $('btn-qr').addEventListener('click', () => {
258
267
  renderQr(lastState || { mode: '', token: shownToken })
259
268
  })
260
269
 
270
+ /* 右上角「网关」徽章: 新标签页打开独立网关管理面板(带 token 免登录) */
271
+ $('conn-badge').addEventListener('click', () => {
272
+ const st = lastState
273
+ if (!st || st.mode !== 'gateway') { toast(t('toast.gatewayDown'), 'err'); return }
274
+ const host = location.hostname || '127.0.0.1'
275
+ const port = st.port || 8787
276
+ const url = `http://${host}:${port}/admin?token=${encodeURIComponent(shownToken || token)}`
277
+ try {
278
+ window.open(url, '_blank', 'noopener')
279
+ } catch {
280
+ toast(t('toast.popupBlocked'), 'err')
281
+ }
282
+ })
283
+
261
284
  $('btn-rotate').addEventListener('click', async () => {
262
- if (!confirm('轮换后旧令牌立即失效,手机与浏览器都需要重新扫码/输入。继续?')) return
285
+ if (!confirm(t('confirm.rotate'))) return
263
286
  try {
264
287
  const res = await fetch(`${API}/token/rotate`, {
265
288
  method: 'POST',
@@ -269,13 +292,13 @@ $('btn-rotate').addEventListener('click', async () => {
269
292
  if (out.ok && out.token) {
270
293
  token = out.token
271
294
  store.set('dshAdminToken', out.token)
272
- toast('令牌已轮换,请重新给设备配对', 'ok')
295
+ toast(t('toast.rotated'), 'ok')
273
296
  setTimeout(loadState, 300)
274
297
  } else {
275
- toast(out.detail || out.error || '轮换失败', 'err')
298
+ toast(out.detail || out.error || t('toast.rotateFailed'), 'err')
276
299
  }
277
300
  } catch (e) {
278
- toast('轮换失败:' + (e.message || e), 'err')
301
+ toast(t('toast.rotateFailedMsg', { msg: e.message || e }), 'err')
279
302
  }
280
303
  })
281
304
 
@@ -284,7 +307,7 @@ $('btn-gateway').addEventListener('click', async () => {
284
307
  gatewayBusy = true
285
308
  const btn = $('btn-gateway')
286
309
  btn.disabled = true
287
- btn.textContent = gatewayRunning ? '停止中…' : '启动中…'
310
+ btn.textContent = t(gatewayRunning ? 'stopping' : 'starting')
288
311
  try {
289
312
  const res = await fetch(`${API}/gateway`, {
290
313
  method: 'POST',
@@ -293,17 +316,29 @@ $('btn-gateway').addEventListener('click', async () => {
293
316
  })
294
317
  const out = await res.json().catch(() => ({}))
295
318
  if (out.ok) {
296
- toast(out.started ? '网关已启动' : out.running ? '网关已在运行' : gatewayRunning ? '网关已停止' : (out.pending ? '网关启动中,稍后刷新' : '已执行'), 'ok')
319
+ toast(out.started ? t('toast.gatewayStarted') : out.running ? t('toast.gatewayAlready') : gatewayRunning ? t('toast.gatewayStopped') : (out.pending ? t('toast.gatewayPending') : t('toast.done')), 'ok')
297
320
  } else {
298
- toast(out.error || '操作失败', 'err')
321
+ toast(out.error || t('toast.opFailed'), 'err')
299
322
  }
300
323
  } catch (e) {
301
- toast('操作失败:' + (e.message || e), 'err')
324
+ toast(t('toast.opFailedMsg', { msg: e.message || e }), 'err')
302
325
  }
303
326
  gatewayBusy = false
304
327
  setTimeout(loadState, 700)
305
328
  })
306
329
 
330
+ function renderLangBtn() {
331
+ const btn = $('btn-lang')
332
+ if (btn) btn.textContent = I18N.lang === 'zh' ? 'EN' : '中文'
333
+ document.title = t('login.title')
334
+ }
335
+ $('btn-lang').addEventListener('click', () => {
336
+ I18N.setLang(I18N.lang === 'zh' ? 'en' : 'zh')
337
+ renderLangBtn()
338
+ if (lastState) render(lastState)
339
+ else if (!token && !pluginMode) $('conn-badge').textContent = t('unauth')
340
+ })
341
+
307
342
  function start(showLogin) {
308
343
  if (!showLogin) {
309
344
  $('login-view').classList.add('hidden')
@@ -327,3 +362,4 @@ if (pluginMode) {
327
362
  $('main-view').classList.add('hidden')
328
363
  $('login-view').classList.remove('hidden')
329
364
  }
365
+ renderLangBtn()