@civilization/dsh-workspace-files 0.2.0 → 0.3.1
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/README.md +22 -1
- package/lib/client.js +655 -60
- package/lib/client.js.map +1 -1
- package/lib/index.js +198 -36
- package/package.json +8 -2
- package/src/client/icons.tsx +27 -0
- package/src/client/index.tsx +587 -8
- package/src/files.ts +75 -34
- package/src/index.ts +132 -4
package/src/client/index.tsx
CHANGED
|
@@ -8,10 +8,13 @@ import {
|
|
|
8
8
|
FileGlyphIcon,
|
|
9
9
|
IconAt,
|
|
10
10
|
IconCheck,
|
|
11
|
+
IconChevronDown,
|
|
11
12
|
IconClose,
|
|
12
13
|
IconCollapseAll,
|
|
13
14
|
IconCopy,
|
|
14
15
|
IconExpandAll,
|
|
16
|
+
IconExplorer,
|
|
17
|
+
IconExternalApp,
|
|
15
18
|
IconFolder,
|
|
16
19
|
IconList,
|
|
17
20
|
IconRefresh,
|
|
@@ -111,6 +114,436 @@ export function apply(ctx: Context): void {
|
|
|
111
114
|
key: OFFICIAL_FILES_TAB_ID,
|
|
112
115
|
}, WorkspaceFilesTabTitle)), 'dsh-workspace-files: official tab title')
|
|
113
116
|
}
|
|
117
|
+
|
|
118
|
+
// 注入设置中心“插件配置”卡片(命名空间必须为 workspace-files,与 Host settings 服务注册一致)
|
|
119
|
+
const registerSettingsCard = (targetCtx: any) => {
|
|
120
|
+
if (!targetCtx?.slots) return
|
|
121
|
+
targetCtx.effect?.(() => targetCtx.slots.inject('settings.plugin.item', function* () {
|
|
122
|
+
yield targetCtx.slots.register({
|
|
123
|
+
name: 'settings.plugin.item',
|
|
124
|
+
key: 'workspace-files',
|
|
125
|
+
}, WorkspaceFilesSettingsCard)
|
|
126
|
+
yield targetCtx.slots.register({
|
|
127
|
+
name: 'settings.plugin.item',
|
|
128
|
+
key: '@civilization/dsh-workspace-files',
|
|
129
|
+
}, WorkspaceFilesSettingsCard)
|
|
130
|
+
}), 'dsh-workspace-files: settings card')
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (slots) {
|
|
134
|
+
registerSettingsCard(ctx)
|
|
135
|
+
}
|
|
136
|
+
if ((ctx as any).inject) {
|
|
137
|
+
(ctx as any).inject(['settingsScope'], (scoped: any) => {
|
|
138
|
+
registerSettingsCard(scoped)
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/* ── Workspace Files Configuration Store ── */
|
|
144
|
+
export interface WorkspaceFilesConfig {
|
|
145
|
+
maxDepth: number
|
|
146
|
+
showHidden: boolean
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const CONFIG_STORAGE_KEY = 'dsh_workspace_files_config'
|
|
150
|
+
|
|
151
|
+
export function loadWorkspaceFilesConfig(): WorkspaceFilesConfig {
|
|
152
|
+
try {
|
|
153
|
+
const raw = typeof localStorage !== 'undefined' ? localStorage.getItem(CONFIG_STORAGE_KEY) : null
|
|
154
|
+
if (raw) {
|
|
155
|
+
const parsed = JSON.parse(raw)
|
|
156
|
+
return {
|
|
157
|
+
maxDepth: typeof parsed.maxDepth === 'number' && parsed.maxDepth > 0 ? parsed.maxDepth : 16,
|
|
158
|
+
showHidden: Boolean(parsed.showHidden),
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
} catch {}
|
|
162
|
+
return { maxDepth: 16, showHidden: false }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function saveWorkspaceFilesConfig(cfg: WorkspaceFilesConfig): void {
|
|
166
|
+
try {
|
|
167
|
+
if (typeof localStorage !== 'undefined') {
|
|
168
|
+
localStorage.setItem(CONFIG_STORAGE_KEY, JSON.stringify(cfg))
|
|
169
|
+
}
|
|
170
|
+
if (typeof window !== 'undefined') {
|
|
171
|
+
window.dispatchEvent(new CustomEvent('dsh-workspace-files:config-change', { detail: cfg }))
|
|
172
|
+
}
|
|
173
|
+
fetch('/dsh-workspace-files/api/config.set', {
|
|
174
|
+
method: 'POST',
|
|
175
|
+
headers: { 'content-type': 'application/json' },
|
|
176
|
+
body: JSON.stringify(cfg),
|
|
177
|
+
}).catch(() => {})
|
|
178
|
+
} catch {}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function ensureSettingsStyle() {
|
|
182
|
+
if (typeof document === 'undefined') return
|
|
183
|
+
const id = 'dsh-workspace-files-settings-style'
|
|
184
|
+
if (document.getElementById(id)) return
|
|
185
|
+
const style = document.createElement('style')
|
|
186
|
+
style.id = id
|
|
187
|
+
style.textContent = `
|
|
188
|
+
.dsh-wf-card {
|
|
189
|
+
border: 1px solid var(--dsw-alias-border-l2, #e5e7eb);
|
|
190
|
+
background: var(--dsw-alias-bg-layer-3, #fff);
|
|
191
|
+
border-radius: 12px;
|
|
192
|
+
list-style: none;
|
|
193
|
+
transition: border-color .16s, background .16s;
|
|
194
|
+
box-sizing: border-box;
|
|
195
|
+
}
|
|
196
|
+
.dsh-wf-card-open {
|
|
197
|
+
background: var(--dsw-alias-bg-layer-2, #f7f8fa);
|
|
198
|
+
border-color: var(--dsw-alias-label-dimmed, #c8ccd4);
|
|
199
|
+
}
|
|
200
|
+
.dsh-wf-header {
|
|
201
|
+
-webkit-appearance: none;
|
|
202
|
+
appearance: none;
|
|
203
|
+
width: 100%;
|
|
204
|
+
font: inherit;
|
|
205
|
+
color: inherit;
|
|
206
|
+
text-align: left;
|
|
207
|
+
cursor: pointer;
|
|
208
|
+
background: 0 0;
|
|
209
|
+
border: 0;
|
|
210
|
+
border-radius: 12px;
|
|
211
|
+
align-items: center;
|
|
212
|
+
gap: 12px;
|
|
213
|
+
padding: 14px 16px;
|
|
214
|
+
display: flex;
|
|
215
|
+
}
|
|
216
|
+
.dsh-wf-head-text {
|
|
217
|
+
flex-direction: column;
|
|
218
|
+
flex: 1;
|
|
219
|
+
gap: 4px;
|
|
220
|
+
min-width: 0;
|
|
221
|
+
display: flex;
|
|
222
|
+
}
|
|
223
|
+
.dsh-wf-name {
|
|
224
|
+
color: var(--dsw-alias-label-primary, #1f2328);
|
|
225
|
+
font-size: 15px;
|
|
226
|
+
font-weight: 600;
|
|
227
|
+
line-height: 1.4;
|
|
228
|
+
display: flex;
|
|
229
|
+
align-items: center;
|
|
230
|
+
gap: 8px;
|
|
231
|
+
}
|
|
232
|
+
.dsh-wf-version {
|
|
233
|
+
font-size: 12px;
|
|
234
|
+
font-weight: 400;
|
|
235
|
+
color: var(--dsw-alias-label-tertiary, #8b93a1);
|
|
236
|
+
}
|
|
237
|
+
.dsh-wf-desc {
|
|
238
|
+
color: var(--dsw-alias-label-tertiary, #8b93a1);
|
|
239
|
+
font-size: 13px;
|
|
240
|
+
line-height: 1.5;
|
|
241
|
+
}
|
|
242
|
+
.dsh-wf-chevron {
|
|
243
|
+
color: var(--dsw-alias-label-tertiary, #8b93a1);
|
|
244
|
+
flex: none;
|
|
245
|
+
transition: transform .16s ease-in-out;
|
|
246
|
+
display: inline-flex;
|
|
247
|
+
}
|
|
248
|
+
.dsh-wf-chevron-open {
|
|
249
|
+
transform: rotate(180deg);
|
|
250
|
+
}
|
|
251
|
+
.dsh-wf-body {
|
|
252
|
+
border-top: 1px solid var(--dsw-alias-border-l2, #e5e7eb);
|
|
253
|
+
margin: 0 16px;
|
|
254
|
+
padding: 8px 0 12px;
|
|
255
|
+
}
|
|
256
|
+
.dsh-wf-row {
|
|
257
|
+
align-items: center;
|
|
258
|
+
gap: 16px;
|
|
259
|
+
padding: 12px 0;
|
|
260
|
+
display: flex;
|
|
261
|
+
justify-content: space-between;
|
|
262
|
+
}
|
|
263
|
+
.dsh-wf-row + .dsh-wf-row {
|
|
264
|
+
border-top: 1px solid var(--dsw-alias-border-l2, rgba(118, 137, 166, 0.12));
|
|
265
|
+
}
|
|
266
|
+
.dsh-wf-label-box {
|
|
267
|
+
flex-direction: column;
|
|
268
|
+
flex: 1;
|
|
269
|
+
gap: 3px;
|
|
270
|
+
min-width: 0;
|
|
271
|
+
display: flex;
|
|
272
|
+
}
|
|
273
|
+
.dsh-wf-label {
|
|
274
|
+
font-size: 13.5px;
|
|
275
|
+
font-weight: 550;
|
|
276
|
+
color: var(--dsw-alias-label-primary, #1f2328);
|
|
277
|
+
}
|
|
278
|
+
.dsh-wf-hint {
|
|
279
|
+
color: var(--dsw-alias-label-tertiary, #8b93a1);
|
|
280
|
+
font-size: 12px;
|
|
281
|
+
line-height: 18px;
|
|
282
|
+
}
|
|
283
|
+
.dsh-wf-ctrl {
|
|
284
|
+
flex-shrink: 0;
|
|
285
|
+
display: flex;
|
|
286
|
+
align-items: center;
|
|
287
|
+
gap: 8px;
|
|
288
|
+
}
|
|
289
|
+
.dsh-wf-seg {
|
|
290
|
+
border: 1px solid var(--dsw-alias-border-l2, #e5e7eb);
|
|
291
|
+
border-radius: 8px;
|
|
292
|
+
gap: 2px;
|
|
293
|
+
padding: 2px;
|
|
294
|
+
display: inline-flex;
|
|
295
|
+
background: var(--dsw-alias-bg-layer-1, rgba(255, 255, 255, 0.05));
|
|
296
|
+
}
|
|
297
|
+
.dsh-wf-seg-btn {
|
|
298
|
+
font: inherit;
|
|
299
|
+
color: var(--dsw-alias-label-secondary, #6b7280);
|
|
300
|
+
cursor: pointer;
|
|
301
|
+
background: 0 0;
|
|
302
|
+
border: none;
|
|
303
|
+
border-radius: 6px;
|
|
304
|
+
padding: 4px 10px;
|
|
305
|
+
font-size: 12px;
|
|
306
|
+
line-height: 18px;
|
|
307
|
+
transition: all 0.15s;
|
|
308
|
+
}
|
|
309
|
+
.dsh-wf-seg-btn:hover {
|
|
310
|
+
color: var(--dsw-alias-label-primary, #1f2328);
|
|
311
|
+
}
|
|
312
|
+
.dsh-wf-seg-active {
|
|
313
|
+
background: var(--dsw-alias-bg-layer-3, #eef0f4);
|
|
314
|
+
color: var(--dsw-alias-brand-primary, #4f6ef7);
|
|
315
|
+
font-weight: 600;
|
|
316
|
+
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
|
|
317
|
+
}
|
|
318
|
+
.dsh-wf-input {
|
|
319
|
+
box-sizing: border-box;
|
|
320
|
+
border: 1px solid var(--dsw-alias-border-l2, #d1d5db);
|
|
321
|
+
background: var(--dsw-alias-bg-layer-3, #fff);
|
|
322
|
+
width: 64px;
|
|
323
|
+
text-align: center;
|
|
324
|
+
color: var(--dsw-alias-label-primary, #1f2328);
|
|
325
|
+
font: inherit;
|
|
326
|
+
border-radius: 7px;
|
|
327
|
+
padding: 4px 6px;
|
|
328
|
+
font-size: 12.5px;
|
|
329
|
+
line-height: 18px;
|
|
330
|
+
}
|
|
331
|
+
.dsh-wf-input:focus {
|
|
332
|
+
border-color: var(--dsw-alias-brand-primary, #4f6ef7);
|
|
333
|
+
outline: 2px solid color-mix(in srgb, var(--dsw-alias-brand-primary, #4f6ef7) 18%, transparent);
|
|
334
|
+
}
|
|
335
|
+
.dsh-wf-toggle {
|
|
336
|
+
position: relative;
|
|
337
|
+
display: inline-block;
|
|
338
|
+
width: 40px;
|
|
339
|
+
height: 22px;
|
|
340
|
+
cursor: pointer;
|
|
341
|
+
}
|
|
342
|
+
.dsh-wf-toggle input {
|
|
343
|
+
opacity: 0;
|
|
344
|
+
width: 0;
|
|
345
|
+
height: 0;
|
|
346
|
+
position: absolute;
|
|
347
|
+
}
|
|
348
|
+
.dsh-wf-toggle-track {
|
|
349
|
+
position: absolute;
|
|
350
|
+
top: 0; left: 0; right: 0; bottom: 0;
|
|
351
|
+
background-color: var(--dsw-alias-border-l2, #ccc);
|
|
352
|
+
transition: .2s cubic-bezier(.4, 0, .2, 1);
|
|
353
|
+
border-radius: 22px;
|
|
354
|
+
}
|
|
355
|
+
.dsh-wf-toggle input:checked + .dsh-wf-toggle-track {
|
|
356
|
+
background-color: var(--dsw-alias-brand-primary, #4f6ef7);
|
|
357
|
+
}
|
|
358
|
+
.dsh-wf-toggle-thumb {
|
|
359
|
+
position: absolute;
|
|
360
|
+
height: 16px;
|
|
361
|
+
width: 16px;
|
|
362
|
+
left: 3px;
|
|
363
|
+
bottom: 3px;
|
|
364
|
+
background-color: white;
|
|
365
|
+
transition: .2s cubic-bezier(.4, 0, .2, 1);
|
|
366
|
+
border-radius: 50%;
|
|
367
|
+
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
|
|
368
|
+
}
|
|
369
|
+
.dsh-wf-toggle input:checked + .dsh-wf-toggle-track .dsh-wf-toggle-thumb {
|
|
370
|
+
transform: translateX(18px);
|
|
371
|
+
}
|
|
372
|
+
.dsh-wf-footer {
|
|
373
|
+
margin-top: 8px;
|
|
374
|
+
padding-top: 10px;
|
|
375
|
+
border-top: 1px solid var(--dsw-alias-border-l2, rgba(118, 137, 166, 0.12));
|
|
376
|
+
display: flex;
|
|
377
|
+
align-items: center;
|
|
378
|
+
justify-content: space-between;
|
|
379
|
+
}
|
|
380
|
+
.dsh-wf-saved-hint {
|
|
381
|
+
font-size: 11.5px;
|
|
382
|
+
color: var(--dsw-alias-state-success-primary, #10b981);
|
|
383
|
+
display: flex;
|
|
384
|
+
align-items: center;
|
|
385
|
+
gap: 4px;
|
|
386
|
+
}
|
|
387
|
+
.dsh-wf-btn-reset {
|
|
388
|
+
background: transparent;
|
|
389
|
+
border: 1px solid var(--dsw-alias-border-l2, #e5e7eb);
|
|
390
|
+
border-radius: 6px;
|
|
391
|
+
padding: 3px 10px;
|
|
392
|
+
font-size: 12px;
|
|
393
|
+
color: var(--dsw-alias-label-secondary, #6b7280);
|
|
394
|
+
cursor: pointer;
|
|
395
|
+
transition: all 0.15s;
|
|
396
|
+
}
|
|
397
|
+
.dsh-wf-btn-reset:hover {
|
|
398
|
+
background: var(--dsw-alias-bg-layer-2, rgba(255,255,255,0.08));
|
|
399
|
+
color: var(--dsw-alias-label-primary, #1f2328);
|
|
400
|
+
}
|
|
401
|
+
`
|
|
402
|
+
document.head.appendChild(style)
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
export function WorkspaceFilesSettingsCard(): JSX.Element {
|
|
406
|
+
const [open, setOpen] = useState(false)
|
|
407
|
+
const [config, setConfig] = useState<WorkspaceFilesConfig>(() => loadWorkspaceFilesConfig())
|
|
408
|
+
const [savedTick, setSavedTick] = useState(false)
|
|
409
|
+
|
|
410
|
+
useEffect(() => {
|
|
411
|
+
ensureSettingsStyle()
|
|
412
|
+
}, [])
|
|
413
|
+
|
|
414
|
+
const updateConfig = (patch: Partial<WorkspaceFilesConfig>) => {
|
|
415
|
+
const next = { ...config, ...patch }
|
|
416
|
+
setConfig(next)
|
|
417
|
+
saveWorkspaceFilesConfig(next)
|
|
418
|
+
setSavedTick(true)
|
|
419
|
+
setTimeout(() => setSavedTick(false), 2000)
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const handleDepthSelect = (depth: number) => {
|
|
423
|
+
updateConfig({ maxDepth: depth })
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const handleDepthInput = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
427
|
+
const val = parseInt(e.target.value, 10)
|
|
428
|
+
if (!isNaN(val) && val >= 1 && val <= 100) {
|
|
429
|
+
updateConfig({ maxDepth: val })
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const handleToggleHidden = () => {
|
|
434
|
+
updateConfig({ showHidden: !config.showHidden })
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const handleReset = () => {
|
|
438
|
+
const def = { maxDepth: 16, showHidden: false }
|
|
439
|
+
setConfig(def)
|
|
440
|
+
saveWorkspaceFilesConfig(def)
|
|
441
|
+
setSavedTick(true)
|
|
442
|
+
setTimeout(() => setSavedTick(false), 2000)
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return (
|
|
446
|
+
<div className={open ? 'dsh-wf-card dsh-wf-card-open' : 'dsh-wf-card'}>
|
|
447
|
+
<button
|
|
448
|
+
type="button"
|
|
449
|
+
className="dsh-wf-header"
|
|
450
|
+
aria-expanded={open}
|
|
451
|
+
onClick={() => setOpen(!open)}
|
|
452
|
+
>
|
|
453
|
+
<div className="dsh-wf-head-text">
|
|
454
|
+
<div className="dsh-wf-name">
|
|
455
|
+
<span>工作区文件</span>
|
|
456
|
+
<span className="dsh-wf-version">v0.3.0</span>
|
|
457
|
+
</div>
|
|
458
|
+
<div className="dsh-wf-desc">
|
|
459
|
+
浏览与检索工作区文件树,支持递归目录扫描与一键引用。
|
|
460
|
+
</div>
|
|
461
|
+
</div>
|
|
462
|
+
<span className={open ? 'dsh-wf-chevron dsh-wf-chevron-open' : 'dsh-wf-chevron'}>
|
|
463
|
+
<IconChevronDown size={14} />
|
|
464
|
+
</span>
|
|
465
|
+
</button>
|
|
466
|
+
|
|
467
|
+
{open && (
|
|
468
|
+
<div className="dsh-wf-body">
|
|
469
|
+
<div className="dsh-wf-row">
|
|
470
|
+
<div className="dsh-wf-label-box">
|
|
471
|
+
<div className="dsh-wf-label">目录扫描最大深度</div>
|
|
472
|
+
<div className="dsh-wf-hint">
|
|
473
|
+
工作区递归扫描的最大目录层级(默认 16,范围 1~100)。若工程目录较深可适当调大。
|
|
474
|
+
</div>
|
|
475
|
+
</div>
|
|
476
|
+
<div className="dsh-wf-ctrl">
|
|
477
|
+
<div className="dsh-wf-seg">
|
|
478
|
+
{[8, 16, 32, 64].map((d) => (
|
|
479
|
+
<button
|
|
480
|
+
key={d}
|
|
481
|
+
type="button"
|
|
482
|
+
className={config.maxDepth === d ? 'dsh-wf-seg-btn dsh-wf-seg-active' : 'dsh-wf-seg-btn'}
|
|
483
|
+
onClick={() => handleDepthSelect(d)}
|
|
484
|
+
>
|
|
485
|
+
{d}
|
|
486
|
+
</button>
|
|
487
|
+
))}
|
|
488
|
+
</div>
|
|
489
|
+
<input
|
|
490
|
+
type="number"
|
|
491
|
+
min="1"
|
|
492
|
+
max="100"
|
|
493
|
+
className="dsh-wf-input"
|
|
494
|
+
value={config.maxDepth}
|
|
495
|
+
onChange={handleDepthInput}
|
|
496
|
+
title="自定义深度 (1-100)"
|
|
497
|
+
/>
|
|
498
|
+
</div>
|
|
499
|
+
</div>
|
|
500
|
+
|
|
501
|
+
<div className="dsh-wf-row">
|
|
502
|
+
<div className="dsh-wf-label-box">
|
|
503
|
+
<div className="dsh-wf-label">默认显示隐藏文件</div>
|
|
504
|
+
<div className="dsh-wf-hint">
|
|
505
|
+
打开文件树时是否默认展示以点开头的隐藏文件或文件夹(如 .gitignore, .env 等)。
|
|
506
|
+
</div>
|
|
507
|
+
</div>
|
|
508
|
+
<div className="dsh-wf-ctrl">
|
|
509
|
+
<label className="dsh-wf-toggle">
|
|
510
|
+
<input
|
|
511
|
+
type="checkbox"
|
|
512
|
+
checked={config.showHidden}
|
|
513
|
+
onChange={handleToggleHidden}
|
|
514
|
+
/>
|
|
515
|
+
<span className="dsh-wf-toggle-track">
|
|
516
|
+
<span className="dsh-wf-toggle-thumb" />
|
|
517
|
+
</span>
|
|
518
|
+
</label>
|
|
519
|
+
</div>
|
|
520
|
+
</div>
|
|
521
|
+
|
|
522
|
+
<div className="dsh-wf-footer">
|
|
523
|
+
<div>
|
|
524
|
+
{savedTick ? (
|
|
525
|
+
<span className="dsh-wf-saved-hint">
|
|
526
|
+
<IconCheck size={12} />
|
|
527
|
+
<span>已保存设置</span>
|
|
528
|
+
</span>
|
|
529
|
+
) : (
|
|
530
|
+
<span style={{ fontSize: 11, color: 'var(--dsw-alias-label-tertiary, #8b93a1)' }}>
|
|
531
|
+
设置改动即时生效并同步至工作区文件树
|
|
532
|
+
</span>
|
|
533
|
+
)}
|
|
534
|
+
</div>
|
|
535
|
+
<button
|
|
536
|
+
type="button"
|
|
537
|
+
className="dsh-wf-btn-reset"
|
|
538
|
+
onClick={handleReset}
|
|
539
|
+
>
|
|
540
|
+
恢复默认
|
|
541
|
+
</button>
|
|
542
|
+
</div>
|
|
543
|
+
</div>
|
|
544
|
+
)}
|
|
545
|
+
</div>
|
|
546
|
+
)
|
|
114
547
|
}
|
|
115
548
|
|
|
116
549
|
/* ── Main Workspace Files View ── */
|
|
@@ -129,7 +562,8 @@ function WorkspaceFilesView({
|
|
|
129
562
|
const [query, setQuery] = useState('')
|
|
130
563
|
const [toast, setToast] = useState<string | null>(null)
|
|
131
564
|
const [expandedPaths, setExpandedPaths] = useState<Set<string>>(() => new Set())
|
|
132
|
-
const [
|
|
565
|
+
const [activeConfig, setActiveConfig] = useState<WorkspaceFilesConfig>(() => loadWorkspaceFilesConfig())
|
|
566
|
+
const [showHidden, setShowHidden] = useState(() => loadWorkspaceFilesConfig().showHidden)
|
|
133
567
|
|
|
134
568
|
const toastTimer = useRef<number | null>(null)
|
|
135
569
|
|
|
@@ -139,11 +573,28 @@ function WorkspaceFilesView({
|
|
|
139
573
|
toastTimer.current = window.setTimeout(() => setToast(null), 2000)
|
|
140
574
|
}, [])
|
|
141
575
|
|
|
142
|
-
|
|
576
|
+
useEffect(() => {
|
|
577
|
+
const handleCfgChange = (e: any) => {
|
|
578
|
+
if (e.detail) {
|
|
579
|
+
setActiveConfig(e.detail)
|
|
580
|
+
setShowHidden(Boolean(e.detail.showHidden))
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
window.addEventListener('dsh-workspace-files:config-change', handleCfgChange)
|
|
584
|
+
return () => window.removeEventListener('dsh-workspace-files:config-change', handleCfgChange)
|
|
585
|
+
}, [])
|
|
586
|
+
|
|
587
|
+
const loadTree = useCallback(async (quiet = false, forceRefresh = false) => {
|
|
143
588
|
if (!quiet) setLoading(true)
|
|
144
589
|
setError(null)
|
|
145
590
|
try {
|
|
146
|
-
const res = await call<WorkspaceFilesResult>('files.tree', {
|
|
591
|
+
const res = await call<WorkspaceFilesResult>('files.tree', {
|
|
592
|
+
sessionId,
|
|
593
|
+
cwd,
|
|
594
|
+
showHidden,
|
|
595
|
+
maxDepth: activeConfig.maxDepth,
|
|
596
|
+
forceRefresh,
|
|
597
|
+
})
|
|
147
598
|
setData(res)
|
|
148
599
|
|
|
149
600
|
// 默认折叠,并从 localStorage 恢复已记录的展开状态
|
|
@@ -159,7 +610,7 @@ function WorkspaceFilesView({
|
|
|
159
610
|
} finally {
|
|
160
611
|
if (!quiet) setLoading(false)
|
|
161
612
|
}
|
|
162
|
-
}, [sessionId, cwd, showHidden])
|
|
613
|
+
}, [sessionId, cwd, showHidden, activeConfig.maxDepth])
|
|
163
614
|
|
|
164
615
|
useEffect(() => {
|
|
165
616
|
void loadTree()
|
|
@@ -277,6 +728,60 @@ function WorkspaceFilesView({
|
|
|
277
728
|
})
|
|
278
729
|
}
|
|
279
730
|
|
|
731
|
+
// 在文件资源管理器中定位/打开
|
|
732
|
+
const handleRevealInExplorer = (e: React.MouseEvent, item: FileItem) => {
|
|
733
|
+
e.stopPropagation()
|
|
734
|
+
fetch('/dsh-workspace-files/api/files.open', {
|
|
735
|
+
method: 'POST',
|
|
736
|
+
headers: { 'content-type': 'application/json' },
|
|
737
|
+
body: JSON.stringify({
|
|
738
|
+
action: 'reveal',
|
|
739
|
+
path: item.path,
|
|
740
|
+
isDirectory: Boolean(item.isDirectory),
|
|
741
|
+
sessionId,
|
|
742
|
+
cwd,
|
|
743
|
+
}),
|
|
744
|
+
})
|
|
745
|
+
.then(res => res.json())
|
|
746
|
+
.then(data => {
|
|
747
|
+
if (data.ok) {
|
|
748
|
+
showToastMsg(`已在资源管理器中显示: ${item.name}`)
|
|
749
|
+
} else {
|
|
750
|
+
showToastMsg(`打开失败: ${data.error?.message || '未知错误'}`)
|
|
751
|
+
}
|
|
752
|
+
})
|
|
753
|
+
.catch(err => {
|
|
754
|
+
showToastMsg(`请求失败: ${err?.message || String(err)}`)
|
|
755
|
+
})
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// 用系统默认应用打开
|
|
759
|
+
const handleOpenDefaultApp = (e: React.MouseEvent, item: FileItem) => {
|
|
760
|
+
e.stopPropagation()
|
|
761
|
+
fetch('/dsh-workspace-files/api/files.open', {
|
|
762
|
+
method: 'POST',
|
|
763
|
+
headers: { 'content-type': 'application/json' },
|
|
764
|
+
body: JSON.stringify({
|
|
765
|
+
action: 'openDefault',
|
|
766
|
+
path: item.path,
|
|
767
|
+
isDirectory: Boolean(item.isDirectory),
|
|
768
|
+
sessionId,
|
|
769
|
+
cwd,
|
|
770
|
+
}),
|
|
771
|
+
})
|
|
772
|
+
.then(res => res.json())
|
|
773
|
+
.then(data => {
|
|
774
|
+
if (data.ok) {
|
|
775
|
+
showToastMsg(`已启动系统应用打开: ${item.name}`)
|
|
776
|
+
} else {
|
|
777
|
+
showToastMsg(`打开失败: ${data.error?.message || '未知错误'}`)
|
|
778
|
+
}
|
|
779
|
+
})
|
|
780
|
+
.catch(err => {
|
|
781
|
+
showToastMsg(`请求失败: ${err?.message || String(err)}`)
|
|
782
|
+
})
|
|
783
|
+
}
|
|
784
|
+
|
|
280
785
|
const [searchViewMode, setSearchViewMode] = useState<'flat' | 'tree'>('flat')
|
|
281
786
|
const [searchExpandedPaths, setSearchExpandedPaths] = useState<Set<string>>(() => new Set())
|
|
282
787
|
|
|
@@ -437,7 +942,7 @@ function WorkspaceFilesView({
|
|
|
437
942
|
<button
|
|
438
943
|
type="button"
|
|
439
944
|
className="dsh-files-tool-btn"
|
|
440
|
-
onClick={() => void loadTree(true)}
|
|
945
|
+
onClick={() => void loadTree(false, true)}
|
|
441
946
|
disabled={loading}
|
|
442
947
|
title="刷新文件树"
|
|
443
948
|
>
|
|
@@ -479,7 +984,10 @@ function WorkspaceFilesView({
|
|
|
479
984
|
/* ── 扁平搜索结果列表 ── */
|
|
480
985
|
<div className="dsh-files-search-container">
|
|
481
986
|
<div className="dsh-files-search-header">
|
|
482
|
-
<span className="dsh-files-search-count"
|
|
987
|
+
<span className="dsh-files-search-count">
|
|
988
|
+
找到 {flatSearchResults.length} 个匹配项
|
|
989
|
+
{flatSearchResults.length > 150 ? '(展示前 150 项)' : ''}
|
|
990
|
+
</span>
|
|
483
991
|
<button
|
|
484
992
|
type="button"
|
|
485
993
|
className="dsh-files-view-toggle-btn"
|
|
@@ -491,7 +999,7 @@ function WorkspaceFilesView({
|
|
|
491
999
|
</button>
|
|
492
1000
|
</div>
|
|
493
1001
|
<div className="dsh-files-flat-list">
|
|
494
|
-
{flatSearchResults.map(({ item, parentDir }) => (
|
|
1002
|
+
{flatSearchResults.slice(0, 150).map(({ item, parentDir }) => (
|
|
495
1003
|
<div
|
|
496
1004
|
key={item.path}
|
|
497
1005
|
className={`dsh-files-flat-row ${item.isDirectory ? 'is-directory' : ''}`}
|
|
@@ -536,6 +1044,22 @@ function WorkspaceFilesView({
|
|
|
536
1044
|
<IconAt size={13} />
|
|
537
1045
|
<span className="dsh-files-action-at-text">@引用</span>
|
|
538
1046
|
</button>
|
|
1047
|
+
<button
|
|
1048
|
+
type="button"
|
|
1049
|
+
className="dsh-files-action-btn"
|
|
1050
|
+
onClick={e => handleRevealInExplorer(e, item)}
|
|
1051
|
+
title={`在文件资源管理器中显示此${item.isDirectory ? '目录' : '文件'}`}
|
|
1052
|
+
>
|
|
1053
|
+
<IconExplorer size={12} />
|
|
1054
|
+
</button>
|
|
1055
|
+
<button
|
|
1056
|
+
type="button"
|
|
1057
|
+
className="dsh-files-action-btn"
|
|
1058
|
+
onClick={e => handleOpenDefaultApp(e, item)}
|
|
1059
|
+
title={`用系统默认应用打开此${item.isDirectory ? '目录' : '文件'}`}
|
|
1060
|
+
>
|
|
1061
|
+
<IconExternalApp size={12} />
|
|
1062
|
+
</button>
|
|
539
1063
|
<button
|
|
540
1064
|
type="button"
|
|
541
1065
|
className="dsh-files-action-btn"
|
|
@@ -547,6 +1071,11 @@ function WorkspaceFilesView({
|
|
|
547
1071
|
</div>
|
|
548
1072
|
</div>
|
|
549
1073
|
))}
|
|
1074
|
+
{flatSearchResults.length > 150 && (
|
|
1075
|
+
<div style={{ padding: '10px 14px', fontSize: 11, textAlign: 'center', color: 'var(--dsw-alias-label-tertiary, #8b93a1)', borderTop: '1px dashed var(--dsw-alias-border-l2, #e5e7eb)' }}>
|
|
1076
|
+
匹配项较多,已限制展示前 150 项以保证流畅度,请输入更详细关键字
|
|
1077
|
+
</div>
|
|
1078
|
+
)}
|
|
550
1079
|
</div>
|
|
551
1080
|
</div>
|
|
552
1081
|
) : (
|
|
@@ -589,6 +1118,8 @@ function WorkspaceFilesView({
|
|
|
589
1118
|
onOpenFile={onOpenFile}
|
|
590
1119
|
onMention={handleMention}
|
|
591
1120
|
onCopyPath={handleCopyPath}
|
|
1121
|
+
onRevealInExplorer={handleRevealInExplorer}
|
|
1122
|
+
onOpenDefaultApp={handleOpenDefaultApp}
|
|
592
1123
|
/>
|
|
593
1124
|
))}
|
|
594
1125
|
</div>
|
|
@@ -617,8 +1148,22 @@ function FileTreeNode(props: {
|
|
|
617
1148
|
onOpenFile: (path: string) => void
|
|
618
1149
|
onMention: (e: React.MouseEvent, item: FileItem) => void
|
|
619
1150
|
onCopyPath: (e: React.MouseEvent, path: string) => void
|
|
1151
|
+
onRevealInExplorer: (e: React.MouseEvent, item: FileItem) => void
|
|
1152
|
+
onOpenDefaultApp: (e: React.MouseEvent, item: FileItem) => void
|
|
620
1153
|
}): JSX.Element {
|
|
621
|
-
const {
|
|
1154
|
+
const {
|
|
1155
|
+
item,
|
|
1156
|
+
depth,
|
|
1157
|
+
query,
|
|
1158
|
+
isSearchMode,
|
|
1159
|
+
expandedPaths,
|
|
1160
|
+
onToggleExpand,
|
|
1161
|
+
onOpenFile,
|
|
1162
|
+
onMention,
|
|
1163
|
+
onCopyPath,
|
|
1164
|
+
onRevealInExplorer,
|
|
1165
|
+
onOpenDefaultApp,
|
|
1166
|
+
} = props
|
|
622
1167
|
const isExpanded = expandedPaths.has(item.path)
|
|
623
1168
|
|
|
624
1169
|
if (item.isDirectory) {
|
|
@@ -650,6 +1195,22 @@ function FileTreeNode(props: {
|
|
|
650
1195
|
<IconAt size={13} />
|
|
651
1196
|
<span className="dsh-files-action-at-text">@引用</span>
|
|
652
1197
|
</button>
|
|
1198
|
+
<button
|
|
1199
|
+
type="button"
|
|
1200
|
+
className="dsh-files-action-btn"
|
|
1201
|
+
onClick={e => onRevealInExplorer(e, item)}
|
|
1202
|
+
title="在文件资源管理器中打开此目录"
|
|
1203
|
+
>
|
|
1204
|
+
<IconExplorer size={12} />
|
|
1205
|
+
</button>
|
|
1206
|
+
<button
|
|
1207
|
+
type="button"
|
|
1208
|
+
className="dsh-files-action-btn"
|
|
1209
|
+
onClick={e => onOpenDefaultApp(e, item)}
|
|
1210
|
+
title="用系统默认应用打开此目录"
|
|
1211
|
+
>
|
|
1212
|
+
<IconExternalApp size={12} />
|
|
1213
|
+
</button>
|
|
653
1214
|
<button
|
|
654
1215
|
type="button"
|
|
655
1216
|
className="dsh-files-action-btn"
|
|
@@ -674,6 +1235,8 @@ function FileTreeNode(props: {
|
|
|
674
1235
|
onOpenFile={onOpenFile}
|
|
675
1236
|
onMention={onMention}
|
|
676
1237
|
onCopyPath={onCopyPath}
|
|
1238
|
+
onRevealInExplorer={onRevealInExplorer}
|
|
1239
|
+
onOpenDefaultApp={onOpenDefaultApp}
|
|
677
1240
|
/>
|
|
678
1241
|
))}
|
|
679
1242
|
</div>
|
|
@@ -713,6 +1276,22 @@ function FileTreeNode(props: {
|
|
|
713
1276
|
<IconAt size={13} />
|
|
714
1277
|
<span className="dsh-files-action-at-text">@引用</span>
|
|
715
1278
|
</button>
|
|
1279
|
+
<button
|
|
1280
|
+
type="button"
|
|
1281
|
+
className="dsh-files-action-btn"
|
|
1282
|
+
onClick={e => onRevealInExplorer(e, item)}
|
|
1283
|
+
title="在文件资源管理器中显示此文件"
|
|
1284
|
+
>
|
|
1285
|
+
<IconExplorer size={12} />
|
|
1286
|
+
</button>
|
|
1287
|
+
<button
|
|
1288
|
+
type="button"
|
|
1289
|
+
className="dsh-files-action-btn"
|
|
1290
|
+
onClick={e => onOpenDefaultApp(e, item)}
|
|
1291
|
+
title="用系统默认应用打开此文件"
|
|
1292
|
+
>
|
|
1293
|
+
<IconExternalApp size={12} />
|
|
1294
|
+
</button>
|
|
716
1295
|
<button
|
|
717
1296
|
type="button"
|
|
718
1297
|
className="dsh-files-action-btn"
|