dsh-my-computer 0.0.0-stage → 0.2.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/lib/client.js ADDED
@@ -0,0 +1,1566 @@
1
+ // dsh-my-computer —— 浏览器半侧(DSH client bundle)
2
+ //
3
+ // 格式照 DSH 的 __ModuleLoader__ 约定:整文件是一个 load({ id, factory }),
4
+ // factory 里拿到宿主提供的 require,能取到 react;返回值就是这个插件模块的
5
+ // exports({ apply, inject }),apply(ctx) 里往 slots 注册两个东西:
6
+ // · sidebar.panellist → 右栏图标入口「我的电脑」
7
+ // · main → 点开后渲染的面板本体(key 必须与图标 id 一致)
8
+ //
9
+ // 注意:全部用 React.createElement 手写,不用 JSX、不用构建步骤。
10
+ // 所有请求走同源相对路径 /api/my-computer/*(DSH 的鉴权就在 /api 通道上)。
11
+ try {
12
+ window.__ModuleLoader__.load({
13
+ id: 'dsh-my-computer',
14
+ factory: function (require) {
15
+ var module = { exports: {} }
16
+ var exports = module.exports
17
+
18
+ var React = null
19
+ try {
20
+ React = require('react')
21
+ } catch (error) {
22
+ React = null
23
+ }
24
+ if (!React) React = globalThis.React
25
+ if (!React) throw new Error('dsh-my-computer: 拿不到 React 运行时')
26
+
27
+ var h = React.createElement
28
+ var useState = React.useState
29
+ var useEffect = React.useEffect
30
+ var useRef = React.useRef
31
+ var useCallback = React.useCallback
32
+ var useMemo = React.useMemo
33
+
34
+ // 面板 id 必须与侧栏图标 id 一致,点图标才会切到这个面板
35
+ var PANEL_ID = 'my-computer'
36
+ // DSH 的精确 fetch 路由挂在 /api 共享通道下(鉴权在那一层做),所以真实前缀是 /api/my-computer
37
+ var BASE = '/api/my-computer'
38
+ // 与 package.json 同步;宿主 /health 会报它自己的版本,两边不一致说明
39
+ // 「客户端已经更新、宿主还没重启」,这时不能放用户去点新路由(会 404)
40
+ var CLIENT_VERSION = '0.2.0'
41
+ var LS_KEY = 'dsh-my-computer:last-path'
42
+
43
+ /* ------------------------------------------------------------ *
44
+ * 小工具
45
+ * ------------------------------------------------------------ */
46
+
47
+ function enc(value) {
48
+ return encodeURIComponent(String(value == null ? '' : value))
49
+ }
50
+
51
+ function fmtSize(bytes) {
52
+ if (bytes == null || !isFinite(bytes)) return ''
53
+ if (bytes < 1024) return bytes + ' B'
54
+ var units = ['KB', 'MB', 'GB', 'TB', 'PB']
55
+ var value = bytes
56
+ var index = -1
57
+ while (value >= 1024 && index < units.length - 1) {
58
+ value = value / 1024
59
+ index += 1
60
+ }
61
+ return (value < 10 ? value.toFixed(1) : String(Math.round(value))) + ' ' + units[index]
62
+ }
63
+
64
+ function pad2(n) {
65
+ return n < 10 ? '0' + n : String(n)
66
+ }
67
+
68
+ function fmtTime(ms) {
69
+ if (!ms) return ''
70
+ var d = new Date(ms)
71
+ var now = new Date()
72
+ var sameYear = d.getFullYear() === now.getFullYear()
73
+ var head = sameYear ? '' : d.getFullYear() + '-'
74
+ return head + pad2(d.getMonth() + 1) + '-' + pad2(d.getDate()) + ' ' + pad2(d.getHours()) + ':' + pad2(d.getMinutes())
75
+ }
76
+
77
+ function isRoot(p) {
78
+ return /^[A-Za-z]:\\$/.test(String(p || ''))
79
+ }
80
+
81
+ function parentOf(p) {
82
+ var s = String(p || '').replace(/\\+$/, '')
83
+ if (!s || /^[A-Za-z]:$/.test(s)) return null
84
+ var cut = s.lastIndexOf('\\')
85
+ if (cut < 0) return null
86
+ if (cut === 2) return s.slice(0, 3) + '\\'
87
+ return s.slice(0, cut)
88
+ }
89
+
90
+ function join(dir, name) {
91
+ var d = String(dir || '')
92
+ return d.endsWith('\\') ? d + name : d + '\\' + name
93
+ }
94
+
95
+ /** D:\a\b → [{label:'D:',path:'D:\'},{label:'a',path:'D:\a'},{label:'b',path:'D:\a\b'}] */
96
+ function crumbsOf(p) {
97
+ var s = String(p || '')
98
+ var out = []
99
+ if (!/^[A-Za-z]:[\\/]/.test(s)) return out
100
+ var root = s.slice(0, 2).toUpperCase() + '\\'
101
+ out.push({ label: s.slice(0, 2).toUpperCase(), path: root })
102
+ var rest = s.slice(3).split('\\').filter(function (x) { return x.length > 0 })
103
+ var acc = root
104
+ for (var i = 0; i < rest.length; i += 1) {
105
+ acc = join(acc, rest[i])
106
+ out.push({ label: rest[i], path: acc })
107
+ }
108
+ return out
109
+ }
110
+
111
+ var IMAGE_EXT = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.ico', '.avif', '.svg']
112
+ var TEXT_EXT = [
113
+ '.txt', '.md', '.markdown', '.log', '.json', '.jsonl', '.yml', '.yaml', '.toml', '.ini',
114
+ '.cfg', '.conf', '.csv', '.tsv', '.html', '.htm', '.css', '.js', '.mjs', '.cjs', '.ts',
115
+ '.tsx', '.jsx', '.py', '.sh', '.bat', '.cmd', '.ps1', '.xml', '.env', '.gitignore', '.vue',
116
+ '.java', '.c', '.h', '.cpp', '.hpp', '.rs', '.go', '.rb', '.php', '.sql', '.lua', '.kt',
117
+ ]
118
+
119
+ function extOf(name) {
120
+ var lower = String(name || '').toLowerCase()
121
+ var dot = lower.lastIndexOf('.')
122
+ return dot <= 0 ? '' : lower.slice(dot)
123
+ }
124
+
125
+ function kindOfExt(name) {
126
+ var ext = extOf(name)
127
+ if (IMAGE_EXT.indexOf(ext) >= 0) return 'image'
128
+ if (TEXT_EXT.indexOf(ext) >= 0) return 'text'
129
+ return 'other'
130
+ }
131
+
132
+ function iconFor(entry) {
133
+ var effective = entry.kind === 'link' ? entry.targetKind : entry.kind
134
+ if (effective === 'dir') return '📁'
135
+ if (entry.kind === 'link') return '🔗'
136
+ var group = kindOfExt(entry.name)
137
+ if (group === 'image') return '🖼️'
138
+ if (group === 'text') return '📄'
139
+ return '📦'
140
+ }
141
+
142
+ /* ------------------------------------------------------------ *
143
+ * HTTP
144
+ * ------------------------------------------------------------ */
145
+
146
+ function parseResponse(res) {
147
+ return res.text().then(function (raw) {
148
+ var data = null
149
+ try {
150
+ data = raw ? JSON.parse(raw) : null
151
+ } catch (error) {
152
+ throw new Error('服务端返回的不是 JSON(HTTP ' + res.status + '):' + raw.slice(0, 160))
153
+ }
154
+ if (data && Array.isArray(data.failed) && data.failed.length > 0) {
155
+ var detail = data.failed
156
+ .map(function (item) {
157
+ return item.path + '(' + (item.message || item.code) + ')'
158
+ })
159
+ .join(';')
160
+ throw new Error('部分操作失败:' + detail)
161
+ }
162
+ if (!res.ok || (data && data.ok === false)) {
163
+ var problem = data && data.error
164
+ var code = problem && problem.code ? problem.code + ': ' : ''
165
+ throw new Error(code + ((problem && problem.message) || 'HTTP ' + res.status))
166
+ }
167
+ return data
168
+ })
169
+ }
170
+
171
+ function apiGet(pathWithQuery) {
172
+ return fetch(BASE + pathWithQuery, { credentials: 'same-origin' }).then(parseResponse)
173
+ }
174
+
175
+ function apiPost(pathName, body) {
176
+ return fetch(BASE + pathName, {
177
+ method: 'POST',
178
+ credentials: 'same-origin',
179
+ headers: { 'content-type': 'application/json' },
180
+ body: JSON.stringify(body || {}),
181
+ }).then(parseResponse)
182
+ }
183
+
184
+ function apiUpload(dir, file, overwrite) {
185
+ var query =
186
+ '?path=' + enc(dir) + '&name=' + enc(file.name) + (overwrite ? '&overwrite=1' : '')
187
+ return fetch(BASE + '/upload' + query, {
188
+ method: 'PUT',
189
+ credentials: 'same-origin',
190
+ headers: { 'content-type': 'application/octet-stream' },
191
+ body: file,
192
+ }).then(parseResponse)
193
+ }
194
+
195
+ function downloadUrl(p) {
196
+ return BASE + '/download?path=' + enc(p)
197
+ }
198
+
199
+ function downloadDirUrl(p) {
200
+ return BASE + '/download-dir?path=' + enc(p)
201
+ }
202
+
203
+ /* 用隐藏 <a> 触发浏览器原生下载:走 cookie 鉴权、直接落盘,
204
+ 不用 fetch+blob(手机上几百 MB 的包塞进内存会直接崩标签页)。 */
205
+ function triggerAnchor(url) {
206
+ var anchor = document.createElement('a')
207
+ anchor.href = url
208
+ anchor.setAttribute('download', '')
209
+ anchor.rel = 'noopener'
210
+ document.body.appendChild(anchor)
211
+ anchor.click()
212
+ setTimeout(function () {
213
+ anchor.remove()
214
+ }, 0)
215
+ }
216
+
217
+ function triggerDownload(p) {
218
+ triggerAnchor(downloadUrl(p))
219
+ }
220
+
221
+ function triggerDirDownload(p) {
222
+ triggerAnchor(downloadDirUrl(p))
223
+ }
224
+
225
+ /* ------------------------------------------------------------ *
226
+ * 样式:跟随宿主主题变量(--dsw-alias-*),拿不到时退到明/暗两套兜底
227
+ * ------------------------------------------------------------ */
228
+
229
+ var CSS = [
230
+ '.mc-root{display:flex;flex-direction:column;width:100%;box-sizing:border-box;height:100%;',
231
+ ' min-height:calc(100dvh - 96px);color:var(--mc-fg);background:var(--mc-bg);font-size:13px;',
232
+ ' --mc-bg:var(--dsw-alias-bg-layer-1,#fff);--mc-bg2:var(--dsw-alias-bg-layer-2,#f6f7f9);',
233
+ ' --mc-fg:var(--dsw-alias-label-primary,#1a1a1a);--mc-fg2:var(--dsw-alias-label-secondary,#6b6b6b);',
234
+ ' --mc-line:var(--dsw-alias-border-l1,rgba(0,0,0,.12));--mc-hover:var(--dsw-alias-bg-hover,rgba(0,0,0,.05));',
235
+ ' --mc-accent:var(--dsw-alias-brand-primary,#2f6fed);--mc-danger:#e5484d;}',
236
+ '@media (prefers-color-scheme:dark){.mc-root{',
237
+ ' --mc-bg:var(--dsw-alias-bg-layer-1,#161616);--mc-bg2:var(--dsw-alias-bg-layer-2,#202020);',
238
+ ' --mc-fg:var(--dsw-alias-label-primary,#e9e9e9);--mc-fg2:var(--dsw-alias-label-secondary,#9b9b9b);',
239
+ ' --mc-line:var(--dsw-alias-border-l1,rgba(255,255,255,.14));--mc-hover:var(--dsw-alias-bg-hover,rgba(255,255,255,.08));',
240
+ ' --mc-accent:var(--dsw-alias-brand-primary,#5b8cff);}}',
241
+ '.mc-head{flex:none;display:flex;flex-direction:column;gap:6px;padding:8px 10px 6px;}',
242
+ '.mc-line{display:flex;align-items:center;gap:6px;min-width:0;}',
243
+ '.mc-drives{display:flex;gap:6px;overflow-x:auto;flex:1 1 auto;padding-bottom:2px;',
244
+ ' scrollbar-width:none;-ms-overflow-style:none;}',
245
+ '.mc-drives::-webkit-scrollbar{display:none;}',
246
+ '.mc-chip{flex:none;border:1px solid var(--mc-line);background:transparent;color:var(--mc-fg);',
247
+ ' border-radius:999px;padding:3px 11px;font-size:12px;line-height:1.6;cursor:pointer;white-space:nowrap;}',
248
+ '.mc-chip:hover{background:var(--mc-hover);}',
249
+ '.mc-chip.on{background:var(--mc-accent);border-color:transparent;color:#fff;}',
250
+ '.mc-chip small{opacity:.7;font-size:10.5px;margin-left:4px;}',
251
+ '.mc-crumbs{display:flex;align-items:center;gap:2px;overflow-x:auto;flex:1 1 auto;min-width:0;',
252
+ ' white-space:nowrap;scrollbar-width:none;font-size:12.5px;}',
253
+ '.mc-crumbs::-webkit-scrollbar{display:none;}',
254
+ '.mc-crumb{appearance:none;border:0;background:transparent;color:var(--mc-fg2);cursor:pointer;',
255
+ ' padding:2px 4px;border-radius:5px;font-size:12.5px;max-width:42vw;overflow:hidden;',
256
+ ' text-overflow:ellipsis;white-space:nowrap;}',
257
+ '.mc-crumb:hover{background:var(--mc-hover);color:var(--mc-fg);}',
258
+ '.mc-crumb.last{color:var(--mc-fg);font-weight:600;}',
259
+ '.mc-sep{color:var(--mc-fg2);opacity:.55;font-size:11px;}',
260
+ '.mc-input{width:100%;box-sizing:border-box;background:var(--mc-bg2);color:var(--mc-fg);',
261
+ ' border:1px solid var(--mc-line);border-radius:8px;padding:7px 9px;font-size:12.5px;font-family:inherit;}',
262
+ '.mc-input:focus{outline:none;border-color:var(--mc-accent);}',
263
+ '.mc-tools{flex:none;display:flex;align-items:center;gap:6px;padding:0 10px 8px;flex-wrap:wrap;}',
264
+ '.mc-btn{appearance:none;border:1px solid var(--mc-line);background:transparent;color:var(--mc-fg);',
265
+ ' border-radius:8px;padding:4px 10px;font-size:12px;line-height:1.6;cursor:pointer;white-space:nowrap;}',
266
+ '.mc-btn:hover{background:var(--mc-hover);}',
267
+ '.mc-btn:disabled{opacity:.45;cursor:default;}',
268
+ '.mc-btn.primary{background:var(--mc-accent);border-color:transparent;color:#fff;}',
269
+ '.mc-btn.danger{color:var(--mc-danger);border-color:rgba(229,72,77,.45);}',
270
+ '.mc-btn.danger.solid{background:var(--mc-danger);color:#fff;border-color:transparent;}',
271
+ '.mc-flex{flex:1 1 auto;}',
272
+ '.mc-search{flex:1 1 130px;min-width:110px;}',
273
+ '.mc-list{flex:1 1 auto;min-height:240px;overflow:auto;border-top:1px solid var(--mc-line);',
274
+ ' -webkit-overflow-scrolling:touch;}',
275
+ '.mc-row{display:flex;align-items:center;gap:8px;padding:7px 10px;cursor:pointer;',
276
+ ' border-bottom:1px solid var(--mc-line);}',
277
+ '.mc-row:hover{background:var(--mc-hover);}',
278
+ '.mc-row.on{background:rgba(47,111,237,.13);}',
279
+ '.mc-check{flex:none;width:16px;height:16px;margin:0;accent-color:var(--mc-accent);}',
280
+ '.mc-ico{flex:none;width:20px;text-align:center;font-size:15px;line-height:1;}',
281
+ '.mc-main{flex:1 1 auto;min-width:0;display:flex;flex-direction:column;gap:1px;}',
282
+ '.mc-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}',
283
+ '.mc-sub{font-size:11px;color:var(--mc-fg2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}',
284
+ '.mc-acts{flex:none;display:flex;gap:1px;}',
285
+ '.mc-ibtn{appearance:none;border:0;background:transparent;color:var(--mc-fg2);cursor:pointer;',
286
+ ' font-size:14px;line-height:1;padding:5px;border-radius:6px;}',
287
+ '.mc-ibtn:hover{background:var(--mc-hover);color:var(--mc-fg);}',
288
+ '.mc-ibtn.danger:hover{color:var(--mc-danger);}',
289
+ '.mc-foot{flex:none;display:flex;align-items:center;gap:10px;padding:6px 10px;font-size:11.5px;',
290
+ ' color:var(--mc-fg2);border-top:1px solid var(--mc-line);}',
291
+ '.mc-err{flex:none;margin:0 10px 6px;padding:7px 9px;border-radius:8px;font-size:12px;',
292
+ ' color:#c9343b;background:rgba(229,72,77,.12);border:1px solid rgba(229,72,77,.35);',
293
+ ' overflow-wrap:anywhere;}',
294
+ '.mc-ok{flex:none;margin:0 10px 6px;padding:7px 9px;border-radius:8px;font-size:12px;',
295
+ ' color:#1a7f4b;background:rgba(26,127,75,.12);border:1px solid rgba(26,127,75,.3);',
296
+ ' overflow-wrap:anywhere;}',
297
+ '@media (prefers-color-scheme:dark){.mc-err{color:#ff8f94;}.mc-ok{color:#6ddba0;}}',
298
+ '.mc-empty{padding:26px 12px;text-align:center;color:var(--mc-fg2);font-size:12.5px;}',
299
+ '.mc-modal-bg{position:fixed;inset:0;z-index:9000;background:rgba(0,0,0,.45);display:flex;',
300
+ ' align-items:center;justify-content:center;padding:14px;}',
301
+ '.mc-modal{width:min(520px,100%);max-height:86dvh;overflow:auto;box-sizing:border-box;',
302
+ ' background:var(--mc-bg);color:var(--mc-fg);border:1px solid var(--mc-line);border-radius:12px;',
303
+ ' padding:14px;display:flex;flex-direction:column;gap:10px;box-shadow:0 14px 44px rgba(0,0,0,.4);}',
304
+ '.mc-modal.wide{width:min(780px,100%);}',
305
+ '.mc-modal h3{margin:0;font-size:14px;font-weight:600;}',
306
+ '.mc-modal p{margin:0;font-size:12.5px;color:var(--mc-fg2);overflow-wrap:anywhere;}',
307
+ '.mc-modal-acts{display:flex;justify-content:flex-end;gap:8px;flex-wrap:wrap;}',
308
+ '.mc-editor{width:100%;box-sizing:border-box;min-height:260px;max-height:62dvh;resize:vertical;',
309
+ ' background:var(--mc-bg2);color:var(--mc-fg);border:1px solid var(--mc-line);border-radius:8px;',
310
+ ' padding:9px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;',
311
+ ' line-height:1.55;white-space:pre;overflow:auto;tab-size:2;}',
312
+ '.mc-img{max-width:100%;max-height:62dvh;object-fit:contain;border-radius:8px;',
313
+ ' background:var(--mc-bg2);display:block;margin:0 auto;}',
314
+ '.mc-hint{font-size:11.5px;color:var(--mc-fg2);}',
315
+ '@media (max-width:420px){',
316
+ ' .mc-root{min-height:calc(100dvh - 72px);font-size:12.5px;}',
317
+ ' .mc-head{padding:6px 8px 4px;}',
318
+ ' .mc-tools{padding:0 8px 6px;}',
319
+ ' .mc-btn{padding:4px 8px;font-size:11.5px;}',
320
+ ' .mc-row{padding:8px;}',
321
+ ' .mc-foot{padding:5px 8px;}}',
322
+ ].join('\n')
323
+
324
+ function ensureStyle() {
325
+ if (typeof document === 'undefined') return function () {}
326
+ var existing = document.querySelector('style[data-dsh-my-computer]')
327
+ if (existing) return function () {}
328
+ var styleEl = document.createElement('style')
329
+ styleEl.setAttribute('data-dsh-my-computer', '')
330
+ styleEl.textContent = CSS
331
+ document.head.appendChild(styleEl)
332
+ return function () {
333
+ styleEl.remove()
334
+ }
335
+ }
336
+
337
+ /* ------------------------------------------------------------ *
338
+ * 面板
339
+ * ------------------------------------------------------------ */
340
+
341
+ function MyComputerPanel() {
342
+ var _cwd = useState('')
343
+ var cwd = _cwd[0]
344
+ var setCwd = _cwd[1]
345
+
346
+ var _entries = useState([])
347
+ var entries = _entries[0]
348
+ var setEntries = _entries[1]
349
+
350
+ var _drives = useState([])
351
+ var drives = _drives[0]
352
+ var setDrives = _drives[1]
353
+
354
+ var _cap = useState({ total: null, free: null })
355
+ var capacity = _cap[0]
356
+ var setCapacity = _cap[1]
357
+
358
+ var _loading = useState(false)
359
+ var loading = _loading[0]
360
+ var setLoading = _loading[1]
361
+
362
+ var _error = useState('')
363
+ var errorText = _error[0]
364
+ var setErrorText = _error[1]
365
+
366
+ var _notice = useState('')
367
+ var notice = _notice[0]
368
+ var setNotice = _notice[1]
369
+
370
+ var _sel = useState([])
371
+ var selected = _sel[0]
372
+ var setSelected = _sel[1]
373
+
374
+ var _query = useState('')
375
+ var query = _query[0]
376
+ var setQuery = _query[1]
377
+
378
+ var _pathInput = useState('')
379
+ var pathInput = _pathInput[0]
380
+ var setPathInput = _pathInput[1]
381
+
382
+ var _dialog = useState(null)
383
+ var dialog = _dialog[0]
384
+ var setDialog = _dialog[1]
385
+
386
+ var _busy = useState('')
387
+ var busy = _busy[0]
388
+ var setBusy = _busy[1]
389
+
390
+ // 剪切板:{ paths: [...], label: '文件名' },只活在内存里(刷新就没,避免误粘)
391
+ var _clip = useState(null)
392
+ var clip = _clip[0]
393
+ var setClip = _clip[1]
394
+
395
+ var _hostVer = useState('')
396
+ var hostVersion = _hostVer[0]
397
+ var setHostVersion = _hostVer[1]
398
+
399
+ var fileInputRef = useRef(null)
400
+ var mountedRef = useRef(true)
401
+
402
+ useEffect(function () {
403
+ mountedRef.current = true
404
+ return function () {
405
+ mountedRef.current = false
406
+ }
407
+ }, [])
408
+
409
+ function reportError(error) {
410
+ setErrorText(String((error && error.message) || error || '未知错误'))
411
+ }
412
+
413
+ function clearMessages() {
414
+ setErrorText('')
415
+ setNotice('')
416
+ }
417
+
418
+ /* ---- 加载 ---- */
419
+
420
+ var navigate = useCallback(function (target) {
421
+ if (!target) return Promise.resolve()
422
+ clearMessages()
423
+ setLoading(true)
424
+ return apiGet('/list?path=' + enc(target))
425
+ .then(function (data) {
426
+ if (!mountedRef.current) return
427
+ setCwd(data.path)
428
+ setEntries(data.entries || [])
429
+ setCapacity({ total: data.total, free: data.free })
430
+ setSelected([])
431
+ setQuery('')
432
+ setPathInput(data.path)
433
+ try {
434
+ window.localStorage.setItem(LS_KEY, data.path)
435
+ } catch (error) {
436
+ /* 隐私模式下 localStorage 可能不可用 */
437
+ }
438
+ })
439
+ .catch(function (error) {
440
+ if (mountedRef.current) reportError(error)
441
+ })
442
+ .then(function () {
443
+ if (mountedRef.current) setLoading(false)
444
+ })
445
+ }, [])
446
+
447
+ function pasteClip() {
448
+ if (!clip || !clip.paths || clip.paths.length === 0) return
449
+ var target = cwd
450
+ if (!target) return
451
+ clearMessages()
452
+ setBusy('正在移动 ' + clip.paths.length + ' 项…')
453
+ apiPost('/move', { sources: clip.paths, targetDir: target })
454
+ .then(function (data) {
455
+ if (!mountedRef.current) return
456
+ var count = (data.moved || []).length
457
+ var skipped = (data.failed || []).length
458
+ setClip(null)
459
+ return navigate(target).then(function () {
460
+ if (!mountedRef.current) return
461
+ setNotice('已移动 ' + count + ' 项到 ' + target + (skipped ? '(' + skipped + ' 项失败)' : ''))
462
+ })
463
+ })
464
+ .catch(function (error) {
465
+ if (mountedRef.current) reportError(error)
466
+ })
467
+ .then(function () {
468
+ if (mountedRef.current) setBusy('')
469
+ })
470
+ }
471
+
472
+ function setClipFrom(paths, label) {
473
+ if (!paths || paths.length === 0) return
474
+ setClip({ paths: paths.slice(), label: label })
475
+ setSelected([])
476
+ clearMessages()
477
+ setNotice('已剪切:' + label + '。到目标目录后点「粘贴到此处」')
478
+ }
479
+
480
+ useEffect(function () {
481
+ apiGet('/health')
482
+ .then(function (data) {
483
+ if (mountedRef.current && data && data.version) setHostVersion(String(data.version))
484
+ })
485
+ .catch(function () {
486
+ /* 探不到就当未知,不做任何限制 */
487
+ })
488
+ }, [])
489
+
490
+ var loadDrives = useCallback(function () {
491
+ return apiGet('/drives')
492
+ .then(function (data) {
493
+ if (!mountedRef.current) return []
494
+ var list = data.drives || []
495
+ setDrives(list)
496
+ return list
497
+ })
498
+ .catch(function (error) {
499
+ if (mountedRef.current) reportError(error)
500
+ return []
501
+ })
502
+ }, [])
503
+
504
+ // 首屏:先探测盘符,再跳到最后一次待过的目录(没有就 D:\,再没有就用第一个盘)
505
+ useEffect(function () {
506
+ var startPath = ''
507
+ try {
508
+ startPath = window.localStorage.getItem(LS_KEY) || ''
509
+ } catch (error) {
510
+ startPath = ''
511
+ }
512
+ loadDrives().then(function (list) {
513
+ if (!mountedRef.current) return
514
+ var preferred = ['D:', 'C:']
515
+ var chosen = ''
516
+ if (startPath) chosen = startPath
517
+ if (!chosen) {
518
+ for (var i = 0; i < preferred.length; i += 1) {
519
+ for (var j = 0; j < list.length; j += 1) {
520
+ if (list[j].letter.toUpperCase() === preferred[i]) chosen = list[j].letter + '\\'
521
+ }
522
+ if (chosen) break
523
+ }
524
+ }
525
+ if (!chosen && list.length > 0) chosen = list[0].letter + '\\'
526
+ if (!chosen) {
527
+ setErrorText('没有探测到任何可用的磁盘')
528
+ return
529
+ }
530
+ navigate(chosen)
531
+ })
532
+ }, [loadDrives, navigate])
533
+
534
+ /* ---- 导航动作 ---- */
535
+
536
+ function goUp() {
537
+ var up = parentOf(cwd)
538
+ if (up) navigate(up)
539
+ }
540
+
541
+ function switchDrive(letter) {
542
+ navigate(letter + '\\')
543
+ }
544
+
545
+ function submitPath(event) {
546
+ event.preventDefault()
547
+ var target = String(pathInput || '').trim()
548
+ if (!target) return
549
+ navigate(target)
550
+ }
551
+
552
+ function refresh() {
553
+ if (cwd) navigate(cwd)
554
+ loadDrives()
555
+ }
556
+
557
+ /* ---- 选择 ---- */
558
+
559
+ function isSelected(p) {
560
+ return selected.indexOf(p) >= 0
561
+ }
562
+
563
+ function toggleSelect(p) {
564
+ setSelected(function (prev) {
565
+ var at = prev.indexOf(p)
566
+ if (at >= 0) {
567
+ var next = prev.slice()
568
+ next.splice(at, 1)
569
+ return next
570
+ }
571
+ return prev.concat([p])
572
+ })
573
+ }
574
+
575
+ function toggleSelectAll() {
576
+ var visible = visibleEntries.map(function (entry) {
577
+ return entry.path
578
+ })
579
+ var allSelected =
580
+ visible.length > 0 &&
581
+ visible.every(function (p) {
582
+ return isSelected(p)
583
+ })
584
+ setSelected(allSelected ? [] : visible)
585
+ }
586
+
587
+ /* ---- 行内动作 ---- */
588
+
589
+ function openEntry(entry) {
590
+ var effective = entry.kind === 'link' ? entry.targetKind : entry.kind
591
+ if (effective === 'dir') {
592
+ navigate(entry.path)
593
+ return
594
+ }
595
+ if (entry.kind === 'link' && entry.targetKind === 'unknown') {
596
+ setErrorText('这是一个断链(目标不存在):' + entry.path)
597
+ return
598
+ }
599
+ setDialog({
600
+ kind: 'preview',
601
+ path: entry.path,
602
+ name: entry.name,
603
+ size: entry.size,
604
+ mtime: entry.mtime,
605
+ group: kindOfExt(entry.name),
606
+ text: null,
607
+ original: '',
608
+ truncated: false,
609
+ encoding: '',
610
+ loaded: false,
611
+ loadError: '',
612
+ })
613
+ }
614
+
615
+ function removePaths(paths) {
616
+ if (!paths || paths.length === 0) return
617
+ setDialog({ kind: 'delete', paths: paths.slice() })
618
+ }
619
+
620
+ /* ---- 上传 ---- */
621
+
622
+ function pickFiles() {
623
+ if (fileInputRef.current) fileInputRef.current.click()
624
+ }
625
+
626
+ function uploadFiles(files, overwrite) {
627
+ if (!files || files.length === 0) return Promise.resolve()
628
+ setBusy('上传中 0/' + files.length)
629
+ clearMessages()
630
+ var conflicts = []
631
+ var done = 0
632
+ var failed = []
633
+ var chain = Promise.resolve()
634
+ files.forEach(function (file) {
635
+ chain = chain.then(function () {
636
+ return apiUpload(cwd, file, Boolean(overwrite))
637
+ .then(function () {
638
+ done += 1
639
+ if (mountedRef.current) setBusy('上传中 ' + done + '/' + files.length)
640
+ })
641
+ .catch(function (error) {
642
+ var message = String((error && error.message) || error)
643
+ if (!overwrite && message.indexOf('ALREADY_EXISTS') === 0) conflicts.push(file)
644
+ else failed.push(file.name + ':' + message)
645
+ })
646
+ })
647
+ })
648
+ return chain.then(function () {
649
+ if (mountedRef.current) setBusy('')
650
+ if (failed.length > 0) {
651
+ setErrorText('上传失败 ' + failed.length + ' 个:' + failed.join(';'))
652
+ } else if (done > 0) {
653
+ setNotice('已上传 ' + done + ' 个文件')
654
+ }
655
+ if (conflicts.length > 0) {
656
+ setDialog({ kind: 'overwrite', files: conflicts })
657
+ }
658
+ return navigate(cwd)
659
+ })
660
+ }
661
+
662
+ function onFilesPicked(event) {
663
+ var files = event.target.files ? Array.prototype.slice.call(event.target.files) : []
664
+ event.target.value = ''
665
+ uploadFiles(files, false)
666
+ }
667
+
668
+ /* ---- 计算可见列表 ---- */
669
+
670
+ var visibleEntries = useMemo(function () {
671
+ var keyword = query.trim().toLowerCase()
672
+ if (!keyword) return entries
673
+ return entries.filter(function (entry) {
674
+ return String(entry.name || '').toLowerCase().indexOf(keyword) >= 0
675
+ })
676
+ }, [entries, query])
677
+
678
+ var crumbList = useMemo(function () {
679
+ return crumbsOf(cwd)
680
+ }, [cwd])
681
+
682
+ var currentDrive = cwd ? cwd.slice(0, 2).toUpperCase() : ''
683
+ var allVisibleSelected =
684
+ visibleEntries.length > 0 &&
685
+ visibleEntries.every(function (entry) {
686
+ return isSelected(entry.path)
687
+ })
688
+
689
+ /* ---- 渲染 ---- */
690
+
691
+ var header = h(
692
+ 'div',
693
+ { className: 'mc-head' },
694
+ h(
695
+ 'div',
696
+ { className: 'mc-line' },
697
+ h(
698
+ 'div',
699
+ { className: 'mc-drives' },
700
+ drives.length === 0
701
+ ? h('span', { className: 'mc-hint' }, '正在探测磁盘…')
702
+ : drives.map(function (drive) {
703
+ var active = drive.letter.toUpperCase() === currentDrive
704
+ return h(
705
+ 'button',
706
+ {
707
+ key: drive.letter,
708
+ type: 'button',
709
+ className: 'mc-chip' + (active ? ' on' : ''),
710
+ onClick: function () {
711
+ switchDrive(drive.letter)
712
+ },
713
+ title: (drive.label || '') + (drive.total ? ' · 可用 ' + fmtSize(drive.free) : ''),
714
+ },
715
+ drive.letter,
716
+ drive.total
717
+ ? h('small', null, fmtSize(drive.free) + ' 可用')
718
+ : null,
719
+ )
720
+ }),
721
+ ),
722
+ h(
723
+ 'button',
724
+ {
725
+ type: 'button',
726
+ className: 'mc-btn',
727
+ onClick: refresh,
728
+ disabled: loading,
729
+ title: '刷新当前目录',
730
+ },
731
+ loading ? '…' : '刷新',
732
+ ),
733
+ ),
734
+ h(
735
+ 'div',
736
+ { className: 'mc-line' },
737
+ h(
738
+ 'button',
739
+ {
740
+ type: 'button',
741
+ className: 'mc-btn',
742
+ onClick: goUp,
743
+ disabled: !parentOf(cwd),
744
+ title: '回到上级目录',
745
+ },
746
+ '↑ 上级',
747
+ ),
748
+ h(
749
+ 'div',
750
+ { className: 'mc-crumbs' },
751
+ crumbList.length === 0
752
+ ? h('span', { className: 'mc-hint' }, cwd || '未选择位置')
753
+ : crumbList.map(function (crumb, index) {
754
+ var nodes = []
755
+ if (index > 0) nodes.push(h('span', { key: 'sep' + index, className: 'mc-sep' }, '›'))
756
+ nodes.push(
757
+ h(
758
+ 'button',
759
+ {
760
+ key: 'crumb' + index,
761
+ type: 'button',
762
+ className: 'mc-crumb' + (index === crumbList.length - 1 ? ' last' : ''),
763
+ onClick: function () {
764
+ navigate(crumb.path)
765
+ },
766
+ title: crumb.path,
767
+ },
768
+ crumb.label,
769
+ ),
770
+ )
771
+ return nodes
772
+ }),
773
+ ),
774
+ ),
775
+ h(
776
+ 'form',
777
+ { className: 'mc-line', onSubmit: submitPath },
778
+ h('input', {
779
+ className: 'mc-input',
780
+ value: pathInput,
781
+ placeholder: '粘贴绝对路径后回车,例如 D:\\dsh-workspace\\vault',
782
+ spellCheck: false,
783
+ autoCapitalize: 'off',
784
+ autoCorrect: 'off',
785
+ onChange: function (event) {
786
+ setPathInput(event.target.value)
787
+ },
788
+ }),
789
+ ),
790
+ )
791
+
792
+ var tools = h(
793
+ 'div',
794
+ { className: 'mc-tools' },
795
+ h(
796
+ 'button',
797
+ { type: 'button', className: 'mc-btn', onClick: function () { setDialog({ kind: 'mkdir', value: '' }) } },
798
+ '+ 新建文件夹',
799
+ ),
800
+ h('button', { type: 'button', className: 'mc-btn', onClick: pickFiles }, '⬆ 上传'),
801
+ selected.length > 0
802
+ ? h(
803
+ 'button',
804
+ {
805
+ type: 'button',
806
+ className: 'mc-btn',
807
+ title: '剪切选中项,去别的目录粘贴',
808
+ onClick: function () {
809
+ setClipFrom(selected, selected.length + ' 项')
810
+ },
811
+ },
812
+ '✂ 剪切 ' + selected.length + ' 项',
813
+ )
814
+ : null,
815
+ clip
816
+ ? h(
817
+ 'button',
818
+ {
819
+ type: 'button',
820
+ className: 'mc-btn solid',
821
+ title: '把剪切板里的内容移动到当前目录:' + cwd,
822
+ onClick: pasteClip,
823
+ },
824
+ '📋 粘贴「' + clip.label + '」到此处',
825
+ )
826
+ : null,
827
+ clip
828
+ ? h(
829
+ 'button',
830
+ {
831
+ type: 'button',
832
+ className: 'mc-btn',
833
+ title: '清空剪切板',
834
+ onClick: function () {
835
+ setClip(null)
836
+ clearMessages()
837
+ },
838
+ },
839
+ '取消剪切',
840
+ )
841
+ : null,
842
+ h(
843
+ 'button',
844
+ { type: 'button', className: 'mc-btn', onClick: toggleSelectAll, disabled: visibleEntries.length === 0 },
845
+ allVisibleSelected ? '取消全选' : '全选',
846
+ ),
847
+ selected.length > 0
848
+ ? h(
849
+ 'button',
850
+ {
851
+ type: 'button',
852
+ className: 'mc-btn danger solid',
853
+ onClick: function () {
854
+ removePaths(selected)
855
+ },
856
+ },
857
+ '删除 ' + selected.length + ' 项',
858
+ )
859
+ : null,
860
+ h('input', {
861
+ className: 'mc-input mc-search',
862
+ value: query,
863
+ placeholder: '筛选当前目录',
864
+ spellCheck: false,
865
+ onChange: function (event) {
866
+ setQuery(event.target.value)
867
+ },
868
+ }),
869
+ h('input', {
870
+ type: 'file',
871
+ multiple: true,
872
+ ref: fileInputRef,
873
+ style: { display: 'none' },
874
+ onChange: onFilesPicked,
875
+ }),
876
+ )
877
+
878
+ var rows = visibleEntries.map(function (entry) {
879
+ var effective = entry.kind === 'link' ? entry.targetKind : entry.kind
880
+ var metaParts = []
881
+ if (effective === 'dir') metaParts.push('文件夹')
882
+ else if (entry.size != null) metaParts.push(fmtSize(entry.size))
883
+ else metaParts.push('—')
884
+ if (entry.kind === 'link') metaParts.push('快捷方式')
885
+ if (entry.statError) metaParts.push('无法读取:' + entry.statError)
886
+ else if (entry.mtime) metaParts.push(fmtTime(entry.mtime))
887
+
888
+ return h(
889
+ 'div',
890
+ {
891
+ key: entry.path,
892
+ className: 'mc-row' + (isSelected(entry.path) ? ' on' : ''),
893
+ onClick: function () {
894
+ openEntry(entry)
895
+ },
896
+ title: entry.path,
897
+ },
898
+ h('input', {
899
+ type: 'checkbox',
900
+ className: 'mc-check',
901
+ checked: isSelected(entry.path),
902
+ onClick: function (event) {
903
+ event.stopPropagation()
904
+ },
905
+ onChange: function () {
906
+ toggleSelect(entry.path)
907
+ },
908
+ }),
909
+ h('span', { className: 'mc-ico' }, iconFor(entry)),
910
+ h(
911
+ 'div',
912
+ { className: 'mc-main' },
913
+ h('div', { className: 'mc-name' }, entry.name),
914
+ h('div', { className: 'mc-sub' }, metaParts.join(' · ')),
915
+ ),
916
+ h(
917
+ 'div',
918
+ {
919
+ className: 'mc-acts',
920
+ onClick: function (event) {
921
+ event.stopPropagation()
922
+ },
923
+ },
924
+ effective === 'file'
925
+ ? h(
926
+ 'button',
927
+ {
928
+ type: 'button',
929
+ className: 'mc-ibtn',
930
+ title: '下载',
931
+ onClick: function () {
932
+ triggerDownload(entry.path)
933
+ },
934
+ },
935
+ '⤓',
936
+ )
937
+ : null,
938
+ effective === 'dir' && !entry.statError
939
+ ? h(
940
+ 'button',
941
+ {
942
+ type: 'button',
943
+ className: 'mc-ibtn',
944
+ title: '打包下载这个目录(zip)',
945
+ onClick: function () {
946
+ if (hostVersion && hostVersion !== CLIENT_VERSION) {
947
+ clearMessages()
948
+ setNotice(
949
+ '宿主侧还是 v' + hostVersion + ',「打包下载」要重启 dsh web 后才生效(本面板 v' + CLIENT_VERSION + ')',
950
+ )
951
+ return
952
+ }
953
+ triggerDirDownload(entry.path)
954
+ },
955
+ },
956
+ '🗜',
957
+ )
958
+ : null,
959
+ h(
960
+ 'button',
961
+ {
962
+ type: 'button',
963
+ className: 'mc-ibtn',
964
+ title: '剪切(到别的目录粘贴)',
965
+ onClick: function () {
966
+ setClipFrom([entry.path], entry.name)
967
+ },
968
+ },
969
+ '✂',
970
+ ),
971
+ h(
972
+ 'button',
973
+ {
974
+ type: 'button',
975
+ className: 'mc-ibtn',
976
+ title: '重命名',
977
+ onClick: function () {
978
+ setDialog({ kind: 'rename', path: entry.path, value: entry.name })
979
+ },
980
+ },
981
+ '✎',
982
+ ),
983
+ h(
984
+ 'button',
985
+ {
986
+ type: 'button',
987
+ className: 'mc-ibtn danger',
988
+ title: '删除',
989
+ onClick: function () {
990
+ removePaths([entry.path])
991
+ },
992
+ },
993
+ '🗑',
994
+ ),
995
+ ),
996
+ )
997
+ })
998
+
999
+ var listBody =
1000
+ loading && entries.length === 0
1001
+ ? h('div', { className: 'mc-empty' }, '加载中…')
1002
+ : rows.length === 0
1003
+ ? h(
1004
+ 'div',
1005
+ { className: 'mc-empty' },
1006
+ query.trim() ? '没有匹配「' + query.trim() + '」的条目' : '这个目录是空的',
1007
+ )
1008
+ : rows
1009
+
1010
+ var footer = h(
1011
+ 'div',
1012
+ { className: 'mc-foot' },
1013
+ h('span', null, '共 ' + visibleEntries.length + ' 项' + (query.trim() ? '(已筛选)' : '')),
1014
+ capacity.total != null
1015
+ ? h(
1016
+ 'span',
1017
+ null,
1018
+ '可用 ' + fmtSize(capacity.free) + ' / ' + fmtSize(capacity.total),
1019
+ )
1020
+ : null,
1021
+ selected.length > 0 ? h('span', null, '已选 ' + selected.length) : null,
1022
+ busy ? h('span', { className: 'mc-hint' }, busy) : null,
1023
+ )
1024
+
1025
+ return h(
1026
+ 'div',
1027
+ { className: 'mc-root' },
1028
+ header,
1029
+ tools,
1030
+ errorText ? h('div', { className: 'mc-err' }, errorText) : null,
1031
+ notice ? h('div', { className: 'mc-ok' }, notice) : null,
1032
+ h('div', { className: 'mc-list' }, listBody),
1033
+ footer,
1034
+ dialog ? renderDialog(dialog, setDialog, navigate, cwd, uploadFiles, reportError, setNotice, setBusy) : null,
1035
+ )
1036
+ }
1037
+
1038
+ /* ------------------------------------------------------------ *
1039
+ * 弹窗(新建 / 改名 / 删除确认 / 覆盖确认 / 预览)
1040
+ * ------------------------------------------------------------ */
1041
+
1042
+ function ModalShell(props) {
1043
+ return h(
1044
+ 'div',
1045
+ {
1046
+ className: 'mc-modal-bg',
1047
+ onClick: function (event) {
1048
+ if (event.target === event.currentTarget && props.onCancel) props.onCancel()
1049
+ },
1050
+ },
1051
+ h(
1052
+ 'div',
1053
+ { className: 'mc-modal' + (props.wide ? ' wide' : '') },
1054
+ h('h3', null, props.title),
1055
+ props.children,
1056
+ ),
1057
+ )
1058
+ }
1059
+
1060
+ function NameDialog(props) {
1061
+ var _value = useState(props.initial || '')
1062
+ var value = _value[0]
1063
+ var setValue = _value[1]
1064
+ var _busy = useState(false)
1065
+ var busy = _busy[0]
1066
+ var setBusy = _busy[1]
1067
+ var inputRef = useRef(null)
1068
+
1069
+ useEffect(function () {
1070
+ if (inputRef.current) inputRef.current.focus()
1071
+ }, [])
1072
+
1073
+ function submit(event) {
1074
+ if (event) event.preventDefault()
1075
+ var name = String(value || '').trim()
1076
+ if (!name || busy) return
1077
+ setBusy(true)
1078
+ props
1079
+ .onSubmit(name)
1080
+ .catch(function (error) {
1081
+ props.onError(error)
1082
+ setBusy(false)
1083
+ })
1084
+ }
1085
+
1086
+ return h(
1087
+ ModalShell,
1088
+ { title: props.title, onCancel: busy ? null : props.onCancel },
1089
+ h('p', null, props.hint || ''),
1090
+ h('form', { onSubmit: submit },
1091
+ h('input', {
1092
+ className: 'mc-input',
1093
+ ref: inputRef,
1094
+ value: value,
1095
+ placeholder: '名称',
1096
+ spellCheck: false,
1097
+ onChange: function (event) {
1098
+ setValue(event.target.value)
1099
+ },
1100
+ }),
1101
+ ),
1102
+ h(
1103
+ 'div',
1104
+ { className: 'mc-modal-acts' },
1105
+ h('button', { type: 'button', className: 'mc-btn', onClick: props.onCancel, disabled: busy }, '取消'),
1106
+ h('button', { type: 'button', className: 'mc-btn primary', onClick: submit, disabled: busy }, busy ? '处理中…' : '确定'),
1107
+ ),
1108
+ )
1109
+ }
1110
+
1111
+ function PreviewDialog(props) {
1112
+ var _state = useState({
1113
+ text: props.text || '',
1114
+ original: props.text || '',
1115
+ loaded: props.loaded,
1116
+ loadError: props.loadError || '',
1117
+ truncated: props.truncated,
1118
+ encoding: props.encoding,
1119
+ saving: false,
1120
+ })
1121
+ var state = _state[0]
1122
+ var setState = _state[1]
1123
+
1124
+ useEffect(function () {
1125
+ if (state.loaded || state.loadError) return
1126
+ apiGet('/read?path=' + enc(props.path) + '&maxBytes=' + String(512 * 1024))
1127
+ .then(function (data) {
1128
+ setState(function (prev) {
1129
+ return Object.assign({}, prev, {
1130
+ text: data.text,
1131
+ original: data.text,
1132
+ loaded: true,
1133
+ truncated: data.truncated,
1134
+ encoding: data.encoding,
1135
+ })
1136
+ })
1137
+ })
1138
+ .catch(function (error) {
1139
+ setState(function (prev) {
1140
+ return Object.assign({}, prev, {
1141
+ loaded: true,
1142
+ loadError: String((error && error.message) || error),
1143
+ })
1144
+ })
1145
+ })
1146
+ }, [])
1147
+
1148
+ var dirty = state.loaded && state.text !== state.original
1149
+
1150
+ function save() {
1151
+ setState(function (prev) {
1152
+ return Object.assign({}, prev, { saving: true })
1153
+ })
1154
+ apiPost('/write', { path: props.path, text: state.text })
1155
+ .then(function () {
1156
+ setState(function (prev) {
1157
+ return Object.assign({}, prev, { saving: false, original: prev.text })
1158
+ })
1159
+ props.onSaved('已保存 ' + props.name)
1160
+ })
1161
+ .catch(function (error) {
1162
+ setState(function (prev) {
1163
+ return Object.assign({}, prev, { saving: false })
1164
+ })
1165
+ props.onError(error)
1166
+ })
1167
+ }
1168
+
1169
+ var body
1170
+ if (props.group === 'image') {
1171
+ body = h('img', { className: 'mc-img', src: downloadUrl(props.path), alt: props.name })
1172
+ } else if (props.group === 'text') {
1173
+ if (state.loadError) {
1174
+ body = h(
1175
+ 'div',
1176
+ null,
1177
+ h('p', null, state.loadError),
1178
+ h(
1179
+ 'button',
1180
+ {
1181
+ type: 'button',
1182
+ className: 'mc-btn primary',
1183
+ onClick: function () {
1184
+ triggerDownload(props.path)
1185
+ },
1186
+ },
1187
+ '下载文件',
1188
+ ),
1189
+ )
1190
+ } else if (!state.loaded) {
1191
+ body = h('p', null, '读取中…')
1192
+ } else {
1193
+ body = h(
1194
+ 'div',
1195
+ { style: { display: 'flex', flexDirection: 'column', gap: 8 } },
1196
+ h('textarea', {
1197
+ className: 'mc-editor',
1198
+ value: state.text,
1199
+ spellCheck: false,
1200
+ onChange: function (event) {
1201
+ var next = event.target.value
1202
+ setState(function (prev) {
1203
+ return Object.assign({}, prev, { text: next })
1204
+ })
1205
+ },
1206
+ }),
1207
+ h(
1208
+ 'div',
1209
+ { className: 'mc-modal-acts' },
1210
+ h(
1211
+ 'span',
1212
+ { className: 'mc-hint', style: { marginRight: 'auto' } },
1213
+ (state.encoding || 'utf-8') +
1214
+ (state.truncated ? ' · 已截断(大于 512KB,只能看前缀,保存会覆盖全文件)' : '') +
1215
+ (dirty ? ' · 已修改' : ''),
1216
+ ),
1217
+ h(
1218
+ 'button',
1219
+ {
1220
+ type: 'button',
1221
+ className: 'mc-btn',
1222
+ onClick: function () {
1223
+ triggerDownload(props.path)
1224
+ },
1225
+ },
1226
+ '下载',
1227
+ ),
1228
+ h(
1229
+ 'button',
1230
+ {
1231
+ type: 'button',
1232
+ className: 'mc-btn primary',
1233
+ disabled: !dirty || state.saving,
1234
+ onClick: save,
1235
+ },
1236
+ state.saving ? '保存中…' : '保存',
1237
+ ),
1238
+ ),
1239
+ )
1240
+ }
1241
+ } else {
1242
+ body = h(
1243
+ 'div',
1244
+ { style: { display: 'flex', flexDirection: 'column', gap: 10 } },
1245
+ h('p', null, '这种文件不支持在线预览,可以直接下载到手机/电脑。'),
1246
+ h(
1247
+ 'button',
1248
+ {
1249
+ type: 'button',
1250
+ className: 'mc-btn primary',
1251
+ onClick: function () {
1252
+ triggerDownload(props.path)
1253
+ },
1254
+ },
1255
+ '下载 ' + props.name,
1256
+ ),
1257
+ )
1258
+ }
1259
+
1260
+ return h(
1261
+ ModalShell,
1262
+ { title: props.name, wide: true, onCancel: props.onCancel },
1263
+ h(
1264
+ 'p',
1265
+ { className: 'mc-hint' },
1266
+ props.path + (props.size != null ? ' · ' + fmtSize(props.size) : ''),
1267
+ ),
1268
+ body,
1269
+ props.group === 'image' || props.group === 'other'
1270
+ ? h(
1271
+ 'div',
1272
+ { className: 'mc-modal-acts' },
1273
+ h('button', { type: 'button', className: 'mc-btn', onClick: props.onCancel }, '关闭'),
1274
+ )
1275
+ : h(
1276
+ 'div',
1277
+ { className: 'mc-modal-acts' },
1278
+ h('button', { type: 'button', className: 'mc-btn', onClick: props.onCancel }, '关闭'),
1279
+ ),
1280
+ )
1281
+ }
1282
+
1283
+ function renderDialog(dialog, setDialog, navigate, cwd, uploadFiles, reportError, setNotice, setBusy) {
1284
+ function close() {
1285
+ setDialog(null)
1286
+ }
1287
+
1288
+ if (dialog.kind === 'mkdir') {
1289
+ return h(NameDialog, {
1290
+ title: '新建文件夹',
1291
+ hint: '将在 ' + cwd + ' 下创建',
1292
+ initial: '',
1293
+ onCancel: close,
1294
+ onError: reportError,
1295
+ onSubmit: function (name) {
1296
+ return apiPost('/mkdir', { path: cwd, name: name }).then(function () {
1297
+ setDialog(null)
1298
+ setNotice('已创建文件夹 ' + name)
1299
+ return navigate(cwd)
1300
+ })
1301
+ },
1302
+ })
1303
+ }
1304
+
1305
+ if (dialog.kind === 'rename') {
1306
+ return h(NameDialog, {
1307
+ title: '重命名',
1308
+ hint: dialog.path,
1309
+ initial: dialog.value,
1310
+ onCancel: close,
1311
+ onError: reportError,
1312
+ onSubmit: function (name) {
1313
+ return apiPost('/rename', { path: dialog.path, name: name }).then(function () {
1314
+ setDialog(null)
1315
+ setNotice('已重命名为 ' + name)
1316
+ return navigate(cwd)
1317
+ })
1318
+ },
1319
+ })
1320
+ }
1321
+
1322
+ if (dialog.kind === 'delete') {
1323
+ var preview = dialog.paths
1324
+ .slice(0, 4)
1325
+ .map(function (p) {
1326
+ return p.split('\\').pop()
1327
+ })
1328
+ .join('、')
1329
+ var more = dialog.paths.length > 4 ? ' 等 ' + dialog.paths.length + ' 项' : ''
1330
+ return h(
1331
+ ModalShell,
1332
+ { title: '确认删除', onCancel: close },
1333
+ h('p', null, '将永久删除:' + preview + more + '。'),
1334
+ h('p', null, '删除是递归且不可恢复的(不进回收站),请确认路径没有写错。'),
1335
+ h(
1336
+ 'div',
1337
+ { className: 'mc-modal-acts' },
1338
+ h('button', { type: 'button', className: 'mc-btn', onClick: close }, '取消'),
1339
+ h(
1340
+ 'button',
1341
+ {
1342
+ type: 'button',
1343
+ className: 'mc-btn danger solid',
1344
+ onClick: function () {
1345
+ setBusy('正在删除…')
1346
+ apiPost('/delete', { paths: dialog.paths, confirm: true })
1347
+ .then(function () {
1348
+ setBusy('')
1349
+ setDialog(null)
1350
+ setNotice('已删除 ' + dialog.paths.length + ' 项')
1351
+ return navigate(cwd)
1352
+ })
1353
+ .catch(function (error) {
1354
+ setBusy('')
1355
+ setDialog(null)
1356
+ reportError(error)
1357
+ return navigate(cwd)
1358
+ })
1359
+ },
1360
+ },
1361
+ '永久删除',
1362
+ ),
1363
+ ),
1364
+ )
1365
+ }
1366
+
1367
+ if (dialog.kind === 'overwrite') {
1368
+ return h(
1369
+ ModalShell,
1370
+ { title: '同名文件已存在', onCancel: close },
1371
+ h(
1372
+ 'p',
1373
+ null,
1374
+ dialog.files.length + ' 个文件在目标目录里已有同名文件:' +
1375
+ dialog.files
1376
+ .slice(0, 4)
1377
+ .map(function (file) {
1378
+ return file.name
1379
+ })
1380
+ .join('、'),
1381
+ ),
1382
+ h('p', null, '覆盖会直接替换原文件,不可恢复。'),
1383
+ h(
1384
+ 'div',
1385
+ { className: 'mc-modal-acts' },
1386
+ h('button', { type: 'button', className: 'mc-btn', onClick: close }, '跳过'),
1387
+ h(
1388
+ 'button',
1389
+ {
1390
+ type: 'button',
1391
+ className: 'mc-btn danger solid',
1392
+ onClick: function () {
1393
+ var files = dialog.files
1394
+ setDialog(null)
1395
+ uploadFiles(files, true)
1396
+ },
1397
+ },
1398
+ '覆盖上传',
1399
+ ),
1400
+ ),
1401
+ )
1402
+ }
1403
+
1404
+ if (dialog.kind === 'preview') {
1405
+ return h(PreviewDialog, {
1406
+ path: dialog.path,
1407
+ name: dialog.name,
1408
+ size: dialog.size,
1409
+ group: dialog.group,
1410
+ text: dialog.text,
1411
+ loaded: dialog.loaded,
1412
+ loadError: dialog.loadError,
1413
+ truncated: dialog.truncated,
1414
+ encoding: dialog.encoding,
1415
+ onCancel: close,
1416
+ onError: reportError,
1417
+ onSaved: function (message) {
1418
+ setNotice(message)
1419
+ close()
1420
+ navigate(cwd)
1421
+ },
1422
+ })
1423
+ }
1424
+
1425
+ return null
1426
+ }
1427
+
1428
+ /* ------------------------------------------------------------ *
1429
+ * 侧栏图标
1430
+ * ------------------------------------------------------------ */
1431
+
1432
+ function MyComputerIcon() {
1433
+ return h(
1434
+ 'svg',
1435
+ {
1436
+ xmlns: 'http://www.w3.org/2000/svg',
1437
+ width: 20,
1438
+ height: 20,
1439
+ viewBox: '0 0 24 24',
1440
+ fill: 'none',
1441
+ stroke: 'currentColor',
1442
+ strokeWidth: 1.7,
1443
+ strokeLinecap: 'round',
1444
+ strokeLinejoin: 'round',
1445
+ 'aria-hidden': 'true',
1446
+ },
1447
+ h('rect', { key: 'screen', x: 2.5, y: 4, width: 19, height: 12.5, rx: 2 }),
1448
+ h('path', { key: 'stand', d: 'M9 20.5h6' }),
1449
+ h('path', { key: 'neck', d: 'M12 16.5v4' }),
1450
+ h('path', { key: 'drive', d: 'M5.5 8h4' }),
1451
+ )
1452
+ }
1453
+
1454
+ /* ------------------------------------------------------------ *
1455
+ * 插件入口
1456
+ * ------------------------------------------------------------ */
1457
+
1458
+ function PanelTitle() {
1459
+ return h(
1460
+ 'span',
1461
+ { style: { display: 'inline-flex', alignItems: 'center', gap: '6px' } },
1462
+ h(MyComputerIcon, { size: 14 }),
1463
+ h('span', null, '我的电脑'),
1464
+ )
1465
+ }
1466
+
1467
+ function apply(ctx) {
1468
+ var disposeStyle = ensureStyle()
1469
+ ctx.effect(function () {
1470
+ return disposeStyle
1471
+ }, 'dsh-my-computer: styles')
1472
+
1473
+ /* ---- 1) 右侧栏 tab 类型(这才是用户看到的「+」菜单里的那一项) ----
1474
+ * 契约见 @deepseek-ai/dsh-client-ui-sidebar-right:
1475
+ * ctx.sidebarRightTabs.register({ id, kind, title, guide })
1476
+ * 正文 ctx.slots.register({ name:'sidebar.right.pane.tab', key:id })
1477
+ * chip 标题 ctx.slots.register({ name:'sidebar.right.pane.tab.title', key:id })
1478
+ * guide 数组是「添加 tab」菜单里的入口框,不给就不出现在菜单里。
1479
+ */
1480
+ ctx.inject(['sidebarRightTabs'], function (c) {
1481
+ var disposers = []
1482
+ function keep(d) {
1483
+ if (typeof d === 'function') disposers.push(d)
1484
+ }
1485
+ function release() {
1486
+ while (disposers.length) {
1487
+ try {
1488
+ disposers.pop()()
1489
+ } catch (e) {}
1490
+ }
1491
+ }
1492
+ try {
1493
+ if (!c || !c.sidebarRightTabs || typeof c.sidebarRightTabs.register !== 'function') {
1494
+ return release
1495
+ }
1496
+ keep(
1497
+ c.sidebarRightTabs.register({
1498
+ id: PANEL_ID,
1499
+ kind: PANEL_ID,
1500
+ title: function () {
1501
+ return '我的电脑'
1502
+ },
1503
+ guide: [
1504
+ {
1505
+ id: PANEL_ID,
1506
+ order: 5,
1507
+ title: function () {
1508
+ return '我的电脑'
1509
+ },
1510
+ description: function () {
1511
+ return '浏览本机全部磁盘(C: / D: …),可上传、下载、改名、删除、编辑'
1512
+ },
1513
+ icon: MyComputerIcon,
1514
+ },
1515
+ ],
1516
+ }),
1517
+ )
1518
+ keep(
1519
+ c.slots.inject('sidebar.right.pane.tab', function () {
1520
+ return c.slots.register({ name: 'sidebar.right.pane.tab', key: PANEL_ID }, MyComputerPanel)
1521
+ }),
1522
+ )
1523
+ keep(
1524
+ c.slots.inject('sidebar.right.pane.tab.title', function () {
1525
+ return c.slots.register({ name: 'sidebar.right.pane.tab.title', key: PANEL_ID }, PanelTitle)
1526
+ }),
1527
+ )
1528
+ } catch (error) {
1529
+ try {
1530
+ window.__MYCOMP_ERR = String((error && error.message) || error)
1531
+ } catch (e) {}
1532
+ release()
1533
+ return
1534
+ }
1535
+ return release
1536
+ })
1537
+
1538
+ /* ---- 2) 左侧栏面板入口(手机上是整屏打开,桌面在主栏) ---- */
1539
+ ctx.slots.inject('sidebar.panellist', function () {
1540
+ return ctx.slots.register(
1541
+ {
1542
+ name: 'sidebar.panellist',
1543
+ id: PANEL_ID,
1544
+ order: -5,
1545
+ label: function () {
1546
+ return '我的电脑'
1547
+ },
1548
+ },
1549
+ MyComputerIcon,
1550
+ )
1551
+ })
1552
+ ctx.slots.inject('main', function () {
1553
+ return ctx.slots.register({ name: 'main', key: PANEL_ID }, MyComputerPanel)
1554
+ })
1555
+ }
1556
+
1557
+ exports.apply = apply
1558
+ exports.inject = ['slots']
1559
+ exports.PANEL_ID = PANEL_ID
1560
+ return module.exports
1561
+ },
1562
+ })
1563
+ } catch (error) {
1564
+ var text = String((error && error.message) || error)
1565
+ if (!/duplicate factory registration/.test(text)) throw error
1566
+ }