dsh-config-manager 0.1.12 → 0.1.13
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 +30 -0
- package/README.zh-CN.md +30 -0
- package/lib/adapters/test-helpers.d.ts +4 -2
- package/lib/adapters/test-helpers.js +11 -3
- package/lib/adapters/test-helpers.js.map +1 -1
- package/lib/cli/index.d.ts +44 -0
- package/lib/cli/index.js +230 -0
- package/lib/cli/index.js.map +1 -0
- package/lib/client.d.ts +260 -5
- package/lib/client.js +2519 -820
- package/lib/client.js.map +1 -1
- package/lib/core/analyzer.d.ts +18 -1
- package/lib/core/analyzer.js +29 -0
- package/lib/core/analyzer.js.map +1 -1
- package/lib/core/backup.d.ts +3 -1
- package/lib/core/backup.js +70 -0
- package/lib/core/backup.js.map +1 -1
- package/lib/core/exporter.d.ts +11 -0
- package/lib/core/exporter.js +11 -0
- package/lib/core/exporter.js.map +1 -1
- package/lib/core/importer.d.ts +3 -0
- package/lib/core/importer.js +1 -0
- package/lib/core/importer.js.map +1 -1
- package/lib/core/index.d.ts +1 -0
- package/lib/core/index.js +1 -0
- package/lib/core/index.js.map +1 -1
- package/lib/core/restore.d.ts +92 -0
- package/lib/core/restore.js +505 -0
- package/lib/core/restore.js.map +1 -0
- package/lib/core/run-registry.d.ts +65 -0
- package/lib/core/run-registry.js +120 -0
- package/lib/core/run-registry.js.map +1 -0
- package/lib/core/types.d.ts +20 -0
- package/lib/core/types.js.map +1 -1
- package/lib/index.d.ts +45 -0
- package/lib/index.js +442 -8
- package/lib/index.js.map +1 -1
- package/lib/sync/fs.d.ts +18 -0
- package/lib/sync/fs.js +47 -0
- package/lib/sync/fs.js.map +1 -0
- package/lib/sync/git/git-transport.d.ts +87 -0
- package/lib/sync/git/git-transport.js +333 -0
- package/lib/sync/git/git-transport.js.map +1 -0
- package/lib/sync/layout.d.ts +29 -0
- package/lib/sync/layout.js +166 -0
- package/lib/sync/layout.js.map +1 -0
- package/lib/sync/sync-config.d.ts +16 -0
- package/lib/sync/sync-config.js +67 -0
- package/lib/sync/sync-config.js.map +1 -0
- package/lib/sync/sync-engine.d.ts +96 -0
- package/lib/sync/sync-engine.js +236 -0
- package/lib/sync/sync-engine.js.map +1 -0
- package/lib/sync/sync-state.d.ts +31 -0
- package/lib/sync/sync-state.js +103 -0
- package/lib/sync/sync-state.js.map +1 -0
- package/lib/sync/transport.d.ts +50 -0
- package/lib/sync/transport.js +22 -0
- package/lib/sync/transport.js.map +1 -0
- package/package.json +4 -1
- package/src/adapters/test-helpers.ts +10 -3
- package/src/cli/index.ts +278 -0
- package/src/client/ConfigManagerSection.tsx +49 -14
- package/src/client/api.ts +53 -5
- package/src/client/client-types.ts +0 -6
- package/src/client/common/ProgressBar.tsx +31 -19
- package/src/client/common/progress-view-edge.test.ts +60 -0
- package/src/client/common/progress-view.test.ts +79 -0
- package/src/client/common/progress-view.ts +94 -0
- package/src/client/config-manager.module.css +99 -20
- package/src/client/export/ExportView.tsx +63 -49
- package/src/client/import/ImportWizardView.tsx +107 -66
- package/src/client/index.ts +22 -0
- package/src/client/locales.ts +53 -0
- package/src/client/run-store.test.ts +500 -0
- package/src/client/run-store.ts +772 -0
- package/src/client/snapshots/SnapshotsPanel.tsx +240 -0
- package/src/client/sync/SyncSettingsView.tsx +261 -0
- package/src/client/sync/sync-api.test.ts +117 -0
- package/src/client/sync/sync-api.ts +132 -0
- package/src/client/sync/sync-locales.ts +77 -0
- package/src/client/sync/sync-view.test.ts +211 -0
- package/src/client/sync/sync-view.ts +202 -0
- package/src/core/analyzer.ts +50 -1
- package/src/core/backup.ts +78 -2
- package/src/core/exporter.ts +21 -0
- package/src/core/importer.ts +4 -0
- package/src/core/index.ts +5 -0
- package/src/core/restore.ts +587 -0
- package/src/core/run-progress.test.ts +237 -0
- package/src/core/run-registry.test.ts +139 -0
- package/src/core/run-registry.ts +166 -0
- package/src/core/smoke.test.ts +5 -0
- package/src/core/types.ts +22 -0
- package/src/index.ts +476 -8
- package/src/security/security.test.ts +5 -0
- package/src/sync/fs.ts +46 -0
- package/src/sync/git/git-transport.test.ts +456 -0
- package/src/sync/git/git-transport.ts +390 -0
- package/src/sync/layout.test.ts +226 -0
- package/src/sync/layout.ts +183 -0
- package/src/sync/sync-config.ts +72 -0
- package/src/sync/sync-engine.test.ts +299 -0
- package/src/sync/sync-engine.ts +318 -0
- package/src/sync/sync-state.test.ts +118 -0
- package/src/sync/sync-state.ts +123 -0
- package/src/sync/transport.test.ts +110 -0
- package/src/sync/transport.ts +73 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* m3 progress-view 单测:computeProgressView 渲染模型(纯函数,node 可测)。
|
|
3
|
+
*
|
|
4
|
+
* 覆盖(验收 m3-render):
|
|
5
|
+
* - 兼容路径:普通 ProgressEvent(无 section/item)→ 阶段文案 + step/total 百分比;
|
|
6
|
+
* - 分区徽章(导出:settings · 3/12,current=item 序号、total=sectionTotal);
|
|
7
|
+
* - 内部计数徽章(导入:plugins · 6/18);
|
|
8
|
+
* - 导出冗余:itemTotal === sectionTotal 时内部计数徽章去重;
|
|
9
|
+
* - detail 与分区 id 相同时去冗余(当前项名);
|
|
10
|
+
* - null / 不定态(无 step/total → percent null)。
|
|
11
|
+
*/
|
|
12
|
+
import test from 'node:test'
|
|
13
|
+
import assert from 'node:assert/strict'
|
|
14
|
+
|
|
15
|
+
import { computeProgressView } from './progress-view.ts'
|
|
16
|
+
import type { RunProgress } from './progress-view.ts'
|
|
17
|
+
|
|
18
|
+
test('m3-render: null 事件 → 空渲染模型', () => {
|
|
19
|
+
const view = computeProgressView(null)
|
|
20
|
+
assert.equal(view.label, '')
|
|
21
|
+
assert.equal(view.percent, null)
|
|
22
|
+
assert.equal(view.sectionBadge, null)
|
|
23
|
+
assert.equal(view.countBadge, null)
|
|
24
|
+
assert.equal(view.detail, null)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test('m3-render: 兼容路径 —— 普通 ProgressEvent 走原逻辑(阶段 + 百分比)', () => {
|
|
28
|
+
const event: RunProgress = { stage: 'validating', step: 2, total: 4 }
|
|
29
|
+
const view = computeProgressView(event)
|
|
30
|
+
assert.equal(view.label, 'Validating backup...')
|
|
31
|
+
assert.equal(view.percent, 50)
|
|
32
|
+
assert.equal(view.sectionBadge, null)
|
|
33
|
+
assert.equal(view.countBadge, null)
|
|
34
|
+
assert.equal(view.detail, null)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('m3-render: 无 step/total → 不定态(percent null)', () => {
|
|
38
|
+
const view = computeProgressView({ stage: 'executing', detail: 'running' })
|
|
39
|
+
assert.equal(view.percent, null)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('m3-render: 导出进度 → 分区徽章 settings · 3/12(内部计数去冗余)', () => {
|
|
43
|
+
// m1 导出埋点:section=分区 id、sectionTotal=分区总数、item=1-based 分区序号、itemTotal=总数
|
|
44
|
+
const event: RunProgress = {
|
|
45
|
+
stage: 'exporting',
|
|
46
|
+
section: 'settings',
|
|
47
|
+
sectionTotal: 12,
|
|
48
|
+
item: 3,
|
|
49
|
+
itemTotal: 12,
|
|
50
|
+
detail: 'settings',
|
|
51
|
+
}
|
|
52
|
+
const view = computeProgressView(event)
|
|
53
|
+
assert.equal(view.percent, 25, '百分比 = item/itemTotal = 3/12')
|
|
54
|
+
assert.deepEqual(view.sectionBadge, { label: 'settings', current: 3, total: 12 })
|
|
55
|
+
assert.equal(view.countBadge, null, '导出内部计数与分区计数同源 → 去冗余')
|
|
56
|
+
assert.equal(view.detail, null, 'detail 与分区 id 相同 → 去冗余')
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('m3-render: 导入进度 → 内部计数徽章 plugins · 6/18 + 当前项名', () => {
|
|
60
|
+
// m1 导入埋点:section=当前 adapter、item/itemTotal=计划项进度、detail=计划项 id
|
|
61
|
+
const event: RunProgress = {
|
|
62
|
+
stage: 'executing',
|
|
63
|
+
section: 'plugins',
|
|
64
|
+
sectionTotal: null,
|
|
65
|
+
item: 6,
|
|
66
|
+
itemTotal: 18,
|
|
67
|
+
detail: 'plugin:pkg-a',
|
|
68
|
+
}
|
|
69
|
+
const view = computeProgressView(event)
|
|
70
|
+
assert.equal(view.percent, 33)
|
|
71
|
+
assert.equal(view.sectionBadge, null, '导入无分区总数语义')
|
|
72
|
+
assert.deepEqual(view.countBadge, { label: 'plugins', current: 6, total: 18 })
|
|
73
|
+
assert.equal(view.detail, 'plugin:pkg-a', '当前项名保留')
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('m3-render: 未知阶段回退显示 stage id', () => {
|
|
77
|
+
const view = computeProgressView({ stage: 'weird-stage' })
|
|
78
|
+
assert.equal(view.label, 'weird-stage')
|
|
79
|
+
})
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* m3: 进度展示的纯逻辑层(框架无关,node 可测)。
|
|
3
|
+
*
|
|
4
|
+
* 背景:m1 的 RunState 携带真实进度(section/sectionTotal = 当前分区 3/12、
|
|
5
|
+
* item/itemTotal = 内部计数如插件 6/18、detail = 当前项名);m2 的 store 把
|
|
6
|
+
* RunState 映射为 ProgressEvent 并存入 progress 字段。本模块定义客户端的
|
|
7
|
+
* RunProgress(ProgressEvent 超集,额外携带分区/内部计数,JSON 可序列化),
|
|
8
|
+
* 并提供 computeProgressView() 把进度事件转成渲染模型 —— ProgressBar 只做
|
|
9
|
+
* 纯渲染,所有判定逻辑集中在此、可单测。
|
|
10
|
+
*
|
|
11
|
+
* 兼容路径:无 section/item 的普通 ProgressEvent(控制器 onProgress 发出)
|
|
12
|
+
* 走原逻辑 —— 阶段文案 + step/total 百分比,无徽章。
|
|
13
|
+
*/
|
|
14
|
+
import { stageText } from '../../ui/progress.ts'
|
|
15
|
+
import type { ProgressEvent } from '../../ui/types.ts'
|
|
16
|
+
|
|
17
|
+
/** m3: 客户端进度事件 = ProgressEvent 超集(RunState 轮询映射产物;可 JSON 序列化)。 */
|
|
18
|
+
export interface RunProgress extends ProgressEvent {
|
|
19
|
+
/** 当前分区(adapter id / 阶段名)与分区总数(如 settings · 3/12);导入无此语义为 null */
|
|
20
|
+
section?: string | null
|
|
21
|
+
sectionTotal?: number | null
|
|
22
|
+
/** 内部计数(如插件 6/18;导出 = 分区序号,导入 = 已处理计划项数) */
|
|
23
|
+
item?: number | null
|
|
24
|
+
itemTotal?: number | null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** 徽章渲染数据(label · current/total)。 */
|
|
28
|
+
export interface ProgressBadge {
|
|
29
|
+
label: string
|
|
30
|
+
current: number
|
|
31
|
+
total: number
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** ProgressBar 的渲染模型(纯数据,测试断言用)。 */
|
|
35
|
+
export interface ProgressView {
|
|
36
|
+
/** 阶段文案(stageText) */
|
|
37
|
+
label: string
|
|
38
|
+
/** 百分比(step/total;无则 null = 不定态动画) */
|
|
39
|
+
percent: number | null
|
|
40
|
+
/** 当前分区徽章(settings · 3/12) */
|
|
41
|
+
sectionBadge: ProgressBadge | null
|
|
42
|
+
/** 内部计数徽章(plugins · 6/18;导出时与分区计数同源则去冗余) */
|
|
43
|
+
countBadge: ProgressBadge | null
|
|
44
|
+
/** 当前项名(detail;与分区名相同时去冗余) */
|
|
45
|
+
detail: string | null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 把进度事件换算成渲染模型。
|
|
50
|
+
* - percent:优先 ProgressEvent.step/total(控制器阶段进度 / 轮询的内部计数);
|
|
51
|
+
* - sectionBadge(当前分区,如 settings · 3/12):label=分区 id、current=item
|
|
52
|
+
* (1-based 分区序号,见 core exporter onSection)、total=sectionTotal;
|
|
53
|
+
* - countBadge(内部计数,如 plugins · 6/18):label=分区 id、current=item、
|
|
54
|
+
* total=itemTotal;导出时 itemTotal 与 sectionTotal 同值(分区序号即内部计数)
|
|
55
|
+
* → 置 null,避免「3/12 3/12」重复;
|
|
56
|
+
* - detail(当前项名):非空且不等于分区 id(导出时 detail 恒为分区 id,去冗余)。
|
|
57
|
+
*/
|
|
58
|
+
export function computeProgressView(event: RunProgress | null): ProgressView {
|
|
59
|
+
if (event === null) {
|
|
60
|
+
return { label: '', percent: null, sectionBadge: null, countBadge: null, detail: null }
|
|
61
|
+
}
|
|
62
|
+
const section = event.section ?? null
|
|
63
|
+
const sectionTotal = event.sectionTotal ?? null
|
|
64
|
+
const item = event.item ?? null
|
|
65
|
+
const itemTotal = event.itemTotal ?? null
|
|
66
|
+
|
|
67
|
+
const percent =
|
|
68
|
+
event.step !== undefined && event.total !== undefined && event.total > 0
|
|
69
|
+
? Math.round((event.step / event.total) * 100)
|
|
70
|
+
: item !== null && itemTotal !== null && itemTotal > 0
|
|
71
|
+
? Math.round((item / itemTotal) * 100)
|
|
72
|
+
: null
|
|
73
|
+
|
|
74
|
+
const sectionBadge: ProgressBadge | null =
|
|
75
|
+
section !== null && item !== null && sectionTotal !== null && sectionTotal > 0
|
|
76
|
+
? { label: section, current: item, total: sectionTotal }
|
|
77
|
+
: null
|
|
78
|
+
|
|
79
|
+
let countBadge: ProgressBadge | null =
|
|
80
|
+
section !== null && item !== null && itemTotal !== null && itemTotal > 0
|
|
81
|
+
? { label: section, current: item, total: itemTotal }
|
|
82
|
+
: null
|
|
83
|
+
if (countBadge !== null && sectionBadge !== null && itemTotal === sectionTotal) {
|
|
84
|
+
// 导出:item/itemTotal 与 section/sectionTotal 同源(当前分区序号),去冗余
|
|
85
|
+
countBadge = null
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const detail =
|
|
89
|
+
typeof event.detail === 'string' && event.detail !== '' && event.detail !== section
|
|
90
|
+
? event.detail
|
|
91
|
+
: null
|
|
92
|
+
|
|
93
|
+
return { label: stageText(event.stage), percent, sectionBadge, countBadge, detail }
|
|
94
|
+
}
|
|
@@ -69,26 +69,6 @@
|
|
|
69
69
|
padding: 4px 2px 16px;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
.iconButton {
|
|
73
|
-
display: inline-flex;
|
|
74
|
-
align-items: center;
|
|
75
|
-
justify-content: center;
|
|
76
|
-
width: 26px;
|
|
77
|
-
height: 26px;
|
|
78
|
-
padding: 0;
|
|
79
|
-
background: transparent;
|
|
80
|
-
border: none;
|
|
81
|
-
border-radius: 6px;
|
|
82
|
-
color: var(--dsw-alias-label-secondary);
|
|
83
|
-
cursor: pointer;
|
|
84
|
-
font-size: 15px;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
.iconButton:hover {
|
|
88
|
-
background: var(--dsw-alias-interactive-bg-hover);
|
|
89
|
-
color: var(--dsw-alias-label-primary);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
72
|
/* ---------------- 区块标题 ---------------- */
|
|
93
73
|
|
|
94
74
|
.sectionTitleBlock {
|
|
@@ -464,6 +444,33 @@
|
|
|
464
444
|
|
|
465
445
|
.progressDetail {
|
|
466
446
|
color: var(--dsw-alias-label-tertiary);
|
|
447
|
+
overflow: hidden;
|
|
448
|
+
text-overflow: ellipsis;
|
|
449
|
+
white-space: nowrap;
|
|
450
|
+
max-width: 320px;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/* m3:分区徽章 / 内部计数徽章 */
|
|
454
|
+
.progressBadge {
|
|
455
|
+
display: inline-flex;
|
|
456
|
+
align-items: center;
|
|
457
|
+
gap: 2px;
|
|
458
|
+
padding: 1px 8px;
|
|
459
|
+
border-radius: 999px;
|
|
460
|
+
font-size: 11px;
|
|
461
|
+
font-variant-numeric: tabular-nums;
|
|
462
|
+
line-height: 18px;
|
|
463
|
+
white-space: nowrap;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
.progressBadgeSection {
|
|
467
|
+
background: color-mix(in srgb, var(--dsw-alias-state-business-primary) 14%, transparent);
|
|
468
|
+
color: var(--dsw-alias-state-business-primary);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
.progressBadgeCount {
|
|
472
|
+
background: color-mix(in srgb, var(--dsw-alias-state-info-primary) 14%, transparent);
|
|
473
|
+
color: var(--dsw-alias-state-info-primary);
|
|
467
474
|
}
|
|
468
475
|
|
|
469
476
|
.progressPercent {
|
|
@@ -757,3 +764,75 @@
|
|
|
757
764
|
@keyframes dshCmSpin {
|
|
758
765
|
to { transform: rotate(360deg); }
|
|
759
766
|
}
|
|
767
|
+
|
|
768
|
+
/* ---------------- 快照恢复面板(M4) ---------------- */
|
|
769
|
+
|
|
770
|
+
.snapshotList {
|
|
771
|
+
display: flex;
|
|
772
|
+
flex-direction: column;
|
|
773
|
+
gap: 2px;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
.snapshotRowHeader,
|
|
777
|
+
.snapshotRow {
|
|
778
|
+
display: grid;
|
|
779
|
+
grid-template-columns: minmax(120px, 1.2fr) minmax(140px, 1.6fr) 90px 56px 56px;
|
|
780
|
+
gap: 8px;
|
|
781
|
+
align-items: center;
|
|
782
|
+
padding: 6px 10px;
|
|
783
|
+
font-size: 12px;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
.snapshotRowHeader {
|
|
787
|
+
color: var(--dsw-alias-label-tertiary);
|
|
788
|
+
font-size: 11px;
|
|
789
|
+
border-bottom: 1px solid var(--dsw-alias-border-l1);
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
.snapshotRow {
|
|
793
|
+
width: 100%;
|
|
794
|
+
text-align: left;
|
|
795
|
+
color: var(--dsw-alias-label-primary);
|
|
796
|
+
background: transparent;
|
|
797
|
+
border: 1px solid transparent;
|
|
798
|
+
border-radius: 6px;
|
|
799
|
+
cursor: pointer;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
.snapshotRow:hover {
|
|
803
|
+
background: var(--dsw-alias-interactive-bg-hover);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
.snapshotRow[data-active] {
|
|
807
|
+
border-color: var(--dsw-alias-state-business-primary);
|
|
808
|
+
background: color-mix(in srgb, var(--dsw-alias-state-business-primary) 10%, transparent);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
.warnText {
|
|
812
|
+
color: var(--dsw-alias-state-warn-primary);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
.reportList {
|
|
816
|
+
margin: 8px 0 4px;
|
|
817
|
+
padding-left: 18px;
|
|
818
|
+
display: flex;
|
|
819
|
+
flex-direction: column;
|
|
820
|
+
gap: 3px;
|
|
821
|
+
font-size: 12px;
|
|
822
|
+
color: var(--dsw-alias-label-primary);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
.kindTag {
|
|
826
|
+
display: inline-block;
|
|
827
|
+
padding: 1px 6px;
|
|
828
|
+
border-radius: 4px;
|
|
829
|
+
font-size: 10.5px;
|
|
830
|
+
background: var(--dsw-alias-interactive-bg-hover);
|
|
831
|
+
color: var(--dsw-alias-label-secondary);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
.rowActions {
|
|
835
|
+
display: flex;
|
|
836
|
+
gap: 8px;
|
|
837
|
+
margin-top: 10px;
|
|
838
|
+
}
|
|
@@ -5,19 +5,21 @@
|
|
|
5
5
|
* - Custom:按 §1 分组目录(EXPORT_GROUPS × DEFAULT_CATEGORIES)逐项勾选,
|
|
6
6
|
* ExportFlow.validateSelection() 给出设备相关分区警告;
|
|
7
7
|
* - Include secrets:可选加密备份(需设置加密密码,密码仅本次内存使用,
|
|
8
|
-
* 经 api.exportPassword 随请求体传给 Host 半,绝不落盘/入 manifest
|
|
8
|
+
* 经 api.exportPassword 随请求体传给 Host 半,绝不落盘/入 manifest,
|
|
9
|
+
* **也绝不进入 sessionStorage** —— m2 白名单剔除,刷新后要求重输);
|
|
9
10
|
* - 进度:ExportFlow.run 发出 ProgressEvent → ProgressBar;
|
|
10
11
|
* - 结果:ReportView(export) + 下载按钮(File System Access API 流式落盘)。
|
|
12
|
+
*
|
|
13
|
+
* m2:全部 UI 状态由模块级 runStore 持有(切 tab/关面板不重建、刷新恢复),
|
|
14
|
+
* 控制器实例(ExportFlow)由 store 缓存复用。
|
|
11
15
|
*/
|
|
12
|
-
import { useCallback,
|
|
16
|
+
import { useCallback, useSyncExternalStore } from 'react'
|
|
13
17
|
import type { ChangeEvent } from 'react'
|
|
14
|
-
import { ExportFlow, DEFAULT_CATEGORIES } from '../../ui/export-flow.ts'
|
|
15
18
|
import { EXPORT_GROUPS } from '../../ui/types.ts'
|
|
16
|
-
import type { ExportRunResult } from '../../ui/export-flow.ts'
|
|
17
|
-
import type { ProgressEvent } from '../../ui/types.ts'
|
|
18
19
|
import type { SectionId } from '../../schema/types.ts'
|
|
19
20
|
import type { TranslateNS } from '../client-types.ts'
|
|
20
21
|
import type { ConfigManagerApi } from '../api.ts'
|
|
22
|
+
import { runStore, type ExportMode } from '../run-store.ts'
|
|
21
23
|
import { Badge, Banner, Button, Card, Checkbox, SectionTitle, Spinner } from '../common/ui.tsx'
|
|
22
24
|
import { ErrorBanner } from '../common/ErrorBanner.tsx'
|
|
23
25
|
import { ProgressBar } from '../common/ProgressBar.tsx'
|
|
@@ -29,42 +31,46 @@ export interface ExportViewProps {
|
|
|
29
31
|
t: TranslateNS<'config-manager'>
|
|
30
32
|
}
|
|
31
33
|
|
|
32
|
-
type ExportMode = 'quick' | 'custom'
|
|
33
|
-
|
|
34
|
-
/** Custom 模式的初始勾选 = 推荐分区(可再调整) */
|
|
35
|
-
function defaultCustomSelection(): SectionId[] {
|
|
36
|
-
return DEFAULT_CATEGORIES.filter((c) => c.defaultIncluded).map((c) => c.id)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
34
|
/**
|
|
40
35
|
* 导出主视图:Quick/Custom 切换 → 勾选/密码 → 执行 → 进度 → 报告 → 下载。
|
|
41
36
|
*/
|
|
42
37
|
export function ExportView({ api, t }: ExportViewProps) {
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
const
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
38
|
+
// m2:状态统一来自模块级 store(sessionStorage 持久化;切 tab 不重建)
|
|
39
|
+
const state = useSyncExternalStore(runStore.subscribe, runStore.getSnapshot)
|
|
40
|
+
const exp = state.export
|
|
41
|
+
// 控制器实例由 store 缓存复用(切 tab / 关面板不重建)
|
|
42
|
+
const flow = runStore.exportFlow(api)
|
|
43
|
+
|
|
44
|
+
const mode = exp.mode
|
|
45
|
+
const selection = exp.selection
|
|
46
|
+
const includeSecrets = exp.includeSecrets
|
|
47
|
+
// 密码字段仅内存(store 的敏感字段,绝不序列化进 sessionStorage)
|
|
48
|
+
const password = exp.password
|
|
49
|
+
const passwordConfirm = exp.passwordConfirm
|
|
50
|
+
const running = exp.running
|
|
51
|
+
const progress = exp.progress
|
|
52
|
+
const result = exp.result
|
|
53
|
+
const error = exp.error
|
|
54
|
+
const downloaded = exp.downloaded
|
|
55
|
+
|
|
56
|
+
const setMode = (next: ExportMode): void => {
|
|
57
|
+
runStore.patch({ export: { mode: next } })
|
|
58
|
+
}
|
|
59
|
+
const setIncludeSecrets = (next: boolean): void => {
|
|
60
|
+
runStore.patch({ export: { includeSecrets: next } })
|
|
61
|
+
}
|
|
62
|
+
const setPassword = (value: string): void => {
|
|
63
|
+
runStore.patch({ export: { password: value } })
|
|
64
|
+
}
|
|
65
|
+
const setPasswordConfirm = (value: string): void => {
|
|
66
|
+
runStore.patch({ export: { passwordConfirm: value } })
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const toggleSection = useCallback((id: SectionId, checked: boolean): void => {
|
|
70
|
+
const has = selection.includes(id)
|
|
71
|
+
if (checked && !has) runStore.patch({ export: { selection: [...selection, id] } })
|
|
72
|
+
if (!checked && has) runStore.patch({ export: { selection: selection.filter((s) => s !== id) } })
|
|
73
|
+
}, [selection])
|
|
68
74
|
|
|
69
75
|
const passwordInvalid =
|
|
70
76
|
includeSecrets && (password === '' || password !== passwordConfirm)
|
|
@@ -72,21 +78,29 @@ export function ExportView({ api, t }: ExportViewProps) {
|
|
|
72
78
|
/** 执行导出(Quick 或 Custom) */
|
|
73
79
|
const runExport = async (): Promise<void> => {
|
|
74
80
|
if (passwordInvalid) return
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
81
|
+
runStore.patch({
|
|
82
|
+
export: { running: true, error: null, result: null, downloaded: false, progress: null, runId: null },
|
|
83
|
+
})
|
|
84
|
+
// m3:请求进行期间经 /runs 发现 runId 并轮询 /progress(500ms)显示真实进度
|
|
85
|
+
runStore.watchRunning('export', 500)
|
|
80
86
|
try {
|
|
81
87
|
// 加密密码随本次导出请求体传给 Host 半(仅内存)
|
|
82
88
|
api.exportPassword = includeSecrets ? password : null
|
|
83
89
|
const run = await flow.run(mode, selection, { includeSecrets })
|
|
84
|
-
|
|
90
|
+
// ExportResponse 携带 runId(/progress 查询与刷新恢复用);控制器类型不含,运行时对象有
|
|
91
|
+
const runId = (run as { runId?: unknown }).runId
|
|
92
|
+
runStore.patch({
|
|
93
|
+
export: {
|
|
94
|
+
result: run,
|
|
95
|
+
runId: typeof runId === 'string' ? runId : null,
|
|
96
|
+
progress: { stage: 'done', step: 1, total: 1 },
|
|
97
|
+
},
|
|
98
|
+
})
|
|
85
99
|
} catch (err) {
|
|
86
|
-
|
|
100
|
+
runStore.patch({ export: { error: err instanceof Error ? err.message : String(err) } })
|
|
87
101
|
} finally {
|
|
88
|
-
|
|
89
|
-
|
|
102
|
+
runStore.stopRunWatch('export')
|
|
103
|
+
runStore.patch({ export: { running: false } })
|
|
90
104
|
}
|
|
91
105
|
}
|
|
92
106
|
|
|
@@ -94,12 +108,12 @@ export function ExportView({ api, t }: ExportViewProps) {
|
|
|
94
108
|
const download = async (): Promise<void> => {
|
|
95
109
|
if (result === null) return
|
|
96
110
|
try {
|
|
97
|
-
|
|
111
|
+
runStore.patch({ export: { error: null } })
|
|
98
112
|
const outcome = await api.download(result.zipPath)
|
|
99
|
-
|
|
113
|
+
runStore.patch({ export: { downloaded: true } })
|
|
100
114
|
void outcome // blob/streamed 由 api 处理;此处仅确认成功
|
|
101
115
|
} catch (err) {
|
|
102
|
-
|
|
116
|
+
runStore.patch({ export: { error: err instanceof Error ? err.message : String(err) } })
|
|
103
117
|
}
|
|
104
118
|
}
|
|
105
119
|
|