@lokvis/ui-react 0.2.1 → 0.2.2
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/dist/components/AssetPanel.d.ts.map +1 -1
- package/dist/components/AssetPanel.js +14 -14
- package/dist/components/AssetPanel.js.map +1 -1
- package/dist/components/Canvas.d.ts +4 -4
- package/dist/components/Canvas.d.ts.map +1 -1
- package/dist/components/Canvas.js +8 -8
- package/dist/components/Canvas.js.map +1 -1
- package/dist/components/CommandPalette.d.ts +7 -7
- package/dist/components/CommandPalette.d.ts.map +1 -1
- package/dist/components/CommandPalette.js +10 -10
- package/dist/components/CommandPalette.js.map +1 -1
- package/dist/components/CompareSlider.d.ts +6 -6
- package/dist/components/CompareSlider.d.ts.map +1 -1
- package/dist/components/CompareSlider.js +7 -7
- package/dist/components/CompareSlider.js.map +1 -1
- package/dist/components/DownloadPanel.d.ts +3 -3
- package/dist/components/DownloadPanel.d.ts.map +1 -1
- package/dist/components/DownloadPanel.js +9 -9
- package/dist/components/DownloadPanel.js.map +1 -1
- package/dist/components/ErrorBanner.d.ts +2 -2
- package/dist/components/ErrorBanner.d.ts.map +1 -1
- package/dist/components/ErrorBanner.js +3 -3
- package/dist/components/ErrorBanner.js.map +1 -1
- package/dist/components/ExifPanel.d.ts +1 -1
- package/dist/components/ExifPanel.d.ts.map +1 -1
- package/dist/components/ExifPanel.js +2 -2
- package/dist/components/ExifPanel.js.map +1 -1
- package/dist/components/GlobalDropzone.d.ts +5 -5
- package/dist/components/GlobalDropzone.d.ts.map +1 -1
- package/dist/components/GlobalDropzone.js +6 -6
- package/dist/components/GlobalDropzone.js.map +1 -1
- package/dist/components/HistoryPanel.d.ts +2 -2
- package/dist/components/HistoryPanel.d.ts.map +1 -1
- package/dist/components/HistoryPanel.js +23 -23
- package/dist/components/HistoryPanel.js.map +1 -1
- package/dist/components/Inspector.d.ts.map +1 -1
- package/dist/components/Inspector.js +6 -6
- package/dist/components/Inspector.js.map +1 -1
- package/dist/components/ParamForm.d.ts.map +1 -1
- package/dist/components/ParamForm.js +7 -7
- package/dist/components/ParamForm.js.map +1 -1
- package/dist/components/PipelineBar.d.ts.map +1 -1
- package/dist/components/PipelineBar.js +13 -13
- package/dist/components/PipelineBar.js.map +1 -1
- package/dist/components/ProgressBar.d.ts +4 -4
- package/dist/components/ProgressBar.d.ts.map +1 -1
- package/dist/components/ProgressBar.js +8 -8
- package/dist/components/ProgressBar.js.map +1 -1
- package/dist/components/StatusBar.d.ts +3 -3
- package/dist/components/StatusBar.d.ts.map +1 -1
- package/dist/components/StatusBar.js +11 -11
- package/dist/components/StatusBar.js.map +1 -1
- package/dist/components/ThemeToggle.d.ts +4 -4
- package/dist/components/ThemeToggle.d.ts.map +1 -1
- package/dist/components/ThemeToggle.js +7 -7
- package/dist/components/ThemeToggle.js.map +1 -1
- package/dist/components/Toolbar.d.ts.map +1 -1
- package/dist/components/Toolbar.js +2 -2
- package/dist/components/Toolbar.js.map +1 -1
- package/dist/components/WorkflowEditor.d.ts +6 -6
- package/dist/components/WorkflowEditor.d.ts.map +1 -1
- package/dist/components/WorkflowEditor.js +25 -25
- package/dist/components/WorkflowEditor.js.map +1 -1
- package/dist/components/WorkflowTemplates.d.ts +2 -2
- package/dist/components/WorkflowTemplates.d.ts.map +1 -1
- package/dist/components/WorkflowTemplates.js +3 -3
- package/dist/components/WorkflowTemplates.js.map +1 -1
- package/dist/components/Workspace.d.ts +22 -22
- package/dist/components/Workspace.d.ts.map +1 -1
- package/dist/components/Workspace.js +30 -30
- package/dist/components/Workspace.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
- package/src/components/AssetPanel.tsx +263 -263
- package/src/components/Canvas.tsx +174 -174
- package/src/components/CommandPalette.tsx +242 -242
- package/src/components/CompareSlider.tsx +163 -163
- package/src/components/DownloadPanel.tsx +203 -203
- package/src/components/ErrorBanner.tsx +64 -64
- package/src/components/ExifPanel.tsx +154 -154
- package/src/components/GlobalDropzone.tsx +170 -170
- package/src/components/HistoryPanel.tsx +219 -219
- package/src/components/Inspector.tsx +136 -136
- package/src/components/ParamForm.tsx +136 -136
- package/src/components/PipelineBar.tsx +98 -98
- package/src/components/ProgressBar.tsx +78 -78
- package/src/components/StatusBar.tsx +148 -148
- package/src/components/ThemeToggle.tsx +98 -98
- package/src/components/Toolbar.tsx +65 -65
- package/src/components/WorkflowEditor.tsx +321 -321
- package/src/components/WorkflowTemplates.tsx +77 -77
- package/src/components/Workspace.tsx +223 -223
- package/src/css-modules.d.ts +3 -0
- package/src/index.ts +4 -0
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* DownloadPanel - 工作流输出下载面板(W9.5)
|
|
3
3
|
*
|
|
4
4
|
* 展示上次工作流执行的输出资产,提供:
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* - 单项下载(从 runtime.exportAsset 取 blob,触发浏览器下载)
|
|
6
|
+
* - 全部下载(逐项触发,BATCH_DOWNLOAD_DELAY_MS 间隔,避免浏览器拦截;长期方案见该常量注释)
|
|
7
|
+
* - 清空输出列表
|
|
8
8
|
*
|
|
9
9
|
* 与 apps/playground 的 DownloadManager(独立工具页)不同:本组件是 Workspace
|
|
10
10
|
* 内嵌的轻量面板,数据来自 store.lastOutputIds(由 workflow-slice.run 写入)。
|
|
@@ -18,14 +18,14 @@ import { Icon } from '@lokvis/ui-core';
|
|
|
18
18
|
import { useWorkspaceStore } from '../store/index.js';
|
|
19
19
|
|
|
20
20
|
export interface DownloadPanelProps {
|
|
21
|
-
|
|
21
|
+
className?: string;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
/** 格式化字节数 */
|
|
25
25
|
function formatBytes(bytes: number): string {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
27
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
28
|
+
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
/**
|
|
@@ -46,207 +46,207 @@ const sleep = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms
|
|
|
46
46
|
|
|
47
47
|
/** 从扩展名或 mime 推断下载扩展名 */
|
|
48
48
|
function extFromMime(mime: string): string {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
const sub = mime.split('/')[1] ?? 'bin';
|
|
50
|
+
// image/svg+xml → svg+xml → svg(取 + 前部分)
|
|
51
|
+
return sub.split('+')[0] ?? sub;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
/** 触发浏览器下载 */
|
|
55
55
|
function downloadBlob(blob: Blob, filename: string): void {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
56
|
+
const url = URL.createObjectURL(blob);
|
|
57
|
+
const a = document.createElement('a');
|
|
58
|
+
a.href = url;
|
|
59
|
+
a.download = filename;
|
|
60
|
+
document.body.appendChild(a);
|
|
61
|
+
a.click();
|
|
62
|
+
document.body.removeChild(a);
|
|
63
|
+
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
export function DownloadPanel({ className = '' }: DownloadPanelProps) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
67
|
+
const assets = useWorkspaceStore((s) => s.assets);
|
|
68
|
+
const lastOutputIds = useWorkspaceStore((s) => s.lastOutputIds);
|
|
69
|
+
const runtime = useWorkspaceStore((s) => s.runtime);
|
|
70
|
+
const clearOutputs = useWorkspaceStore((s) => s.clearOutputs);
|
|
71
|
+
const setStatus = useWorkspaceStore((s) => s.setStatus);
|
|
72
|
+
|
|
73
|
+
const [downloaded, setDownloaded] = React.useState<Set<string>>(new Set());
|
|
74
|
+
const [batchDownloading, setBatchDownloading] = React.useState(false);
|
|
75
|
+
const [error, setError] = React.useState<string | null>(null);
|
|
76
|
+
|
|
77
|
+
// 卸载标志:批量下载是异步循环,卸载后不应再 setState
|
|
78
|
+
const mountedRef = React.useRef(true);
|
|
79
|
+
React.useEffect(() => {
|
|
80
|
+
mountedRef.current = true;
|
|
81
|
+
return () => {
|
|
82
|
+
mountedRef.current = false;
|
|
83
|
+
};
|
|
84
|
+
}, []);
|
|
85
|
+
|
|
86
|
+
// 输出资产列表(从 store.assets 中按 lastOutputIds 顺序取)
|
|
87
|
+
const outputAssets = React.useMemo(() => {
|
|
88
|
+
return lastOutputIds
|
|
89
|
+
.map((id) => assets.find((a) => a.id === id))
|
|
90
|
+
.filter((a): a is NonNullable<typeof a> => a !== undefined);
|
|
91
|
+
}, [lastOutputIds, assets]);
|
|
92
|
+
|
|
93
|
+
// outputs 变化时清空 downloaded 状态
|
|
94
|
+
React.useEffect(() => {
|
|
95
|
+
setDownloaded(new Set());
|
|
96
|
+
setError(null);
|
|
97
|
+
}, [lastOutputIds]);
|
|
98
|
+
|
|
99
|
+
const totalSize = outputAssets.reduce((sum, a) => sum + a.metadata.size, 0);
|
|
100
|
+
const downloadedCount = outputAssets.filter((a) => downloaded.has(a.id)).length;
|
|
101
|
+
const allDownloaded = outputAssets.length > 0 && downloadedCount === outputAssets.length;
|
|
102
|
+
|
|
103
|
+
async function handleDownloadOne(id: string) {
|
|
104
|
+
if (!runtime) return;
|
|
105
|
+
const asset = outputAssets.find((a) => a.id === id);
|
|
106
|
+
if (!asset) return;
|
|
107
|
+
try {
|
|
108
|
+
const blob = await runtime.exportAsset(id);
|
|
109
|
+
const ext = extFromMime(asset.metadata.mimeType);
|
|
110
|
+
const filename = `lokvis-output-${asset.id.slice(0, 8)}.${ext}`;
|
|
111
|
+
downloadBlob(blob, filename);
|
|
112
|
+
if (mountedRef.current) {
|
|
113
|
+
setDownloaded((prev) => new Set(prev).add(id));
|
|
114
|
+
}
|
|
115
|
+
} catch (err) {
|
|
116
|
+
if (mountedRef.current) {
|
|
117
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function handleDownloadAll() {
|
|
123
|
+
if (!runtime || batchDownloading) return;
|
|
124
|
+
setBatchDownloading(true);
|
|
125
|
+
setError(null);
|
|
126
|
+
setStatus('Downloading outputs...');
|
|
127
|
+
// 用本地 Set 跟踪本次循环已下载 id,避免 downloaded 闭包快照与 state 不同步
|
|
128
|
+
// (review 反馈:循环中 setDownloaded 更新 state 但闭包 downloaded 不变,
|
|
129
|
+
// 虽有 batchDownloading guard 阻止并发,逻辑上仍应读取最新已下载集合)
|
|
130
|
+
const localDownloaded = new Set(downloaded);
|
|
131
|
+
try {
|
|
132
|
+
for (const asset of outputAssets) {
|
|
133
|
+
// 卸载后立即停止循环(避免对已卸载组件 setState)
|
|
134
|
+
if (!mountedRef.current) return;
|
|
135
|
+
if (localDownloaded.has(asset.id)) continue;
|
|
136
|
+
try {
|
|
137
|
+
const blob = await runtime.exportAsset(asset.id);
|
|
138
|
+
const ext = extFromMime(asset.metadata.mimeType);
|
|
139
|
+
const filename = `lokvis-output-${asset.id.slice(0, 8)}.${ext}`;
|
|
140
|
+
downloadBlob(blob, filename);
|
|
141
|
+
localDownloaded.add(asset.id);
|
|
142
|
+
if (mountedRef.current) {
|
|
143
|
+
setDownloaded((prev) => new Set(prev).add(asset.id));
|
|
144
|
+
}
|
|
145
|
+
await sleep(BATCH_DOWNLOAD_DELAY_MS);
|
|
146
|
+
} catch (err) {
|
|
147
|
+
if (mountedRef.current) {
|
|
148
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
149
|
+
}
|
|
150
|
+
// 单项失败不阻断后续
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (mountedRef.current) {
|
|
154
|
+
setStatus('Downloads complete');
|
|
155
|
+
}
|
|
156
|
+
} finally {
|
|
157
|
+
if (mountedRef.current) {
|
|
158
|
+
setBatchDownloading(false);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function handleClear() {
|
|
164
|
+
clearOutputs();
|
|
165
|
+
setDownloaded(new Set());
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (outputAssets.length === 0) return null;
|
|
169
|
+
|
|
170
|
+
return (
|
|
171
|
+
<div
|
|
172
|
+
className={`flex flex-col border-t border-[var(--lokvis-border)] bg-[var(--lokvis-surface)] ${className}`}
|
|
173
|
+
aria-label="Outputs panel"
|
|
174
|
+
>
|
|
175
|
+
<div className="flex items-center justify-between px-3 h-9 shrink-0 border-b border-[var(--lokvis-border)]">
|
|
176
|
+
<span className="text-[11px] font-semibold uppercase tracking-wider text-[var(--lokvis-fg-subtle)]">
|
|
177
|
+
Outputs
|
|
178
|
+
</span>
|
|
179
|
+
<span className="text-[11px] tabular-nums text-[var(--lokvis-fg-subtle)]">
|
|
180
|
+
{downloadedCount}/{outputAssets.length} · {formatBytes(totalSize)}
|
|
181
|
+
</span>
|
|
182
|
+
</div>
|
|
183
|
+
|
|
184
|
+
<div className="flex items-center gap-2 px-3 py-2 overflow-x-auto">
|
|
185
|
+
{outputAssets.map((asset) => {
|
|
186
|
+
const isDownloaded = downloaded.has(asset.id);
|
|
187
|
+
const dims = asset.metadata.dimensions;
|
|
188
|
+
return (
|
|
189
|
+
<div
|
|
190
|
+
key={asset.id}
|
|
191
|
+
className="flex shrink-0 flex-col gap-1 rounded-md border border-[var(--lokvis-border)] p-2"
|
|
192
|
+
>
|
|
193
|
+
<div className="flex items-center gap-2">
|
|
194
|
+
<span className="rounded bg-[var(--lokvis-surface-muted)] px-1 py-0.5 text-[9px] font-medium text-[var(--lokvis-fg-muted)]">
|
|
195
|
+
{asset.metadata.format.toUpperCase()}
|
|
196
|
+
</span>
|
|
197
|
+
{dims && (
|
|
198
|
+
<span className="text-[10px] text-[var(--lokvis-fg-muted)] tabular-nums">
|
|
199
|
+
{dims.width}×{dims.height}
|
|
200
|
+
</span>
|
|
201
|
+
)}
|
|
202
|
+
<span className="text-[10px] text-[var(--lokvis-fg-subtle)] tabular-nums">
|
|
203
|
+
{formatBytes(asset.metadata.size)}
|
|
204
|
+
</span>
|
|
205
|
+
</div>
|
|
206
|
+
<div className="flex items-center gap-1">
|
|
207
|
+
<button
|
|
208
|
+
type="button"
|
|
209
|
+
onClick={() => void handleDownloadOne(asset.id)}
|
|
210
|
+
className={`flex items-center gap-1 rounded px-2 py-0.5 text-[10px] font-medium transition-colors ${
|
|
211
|
+
isDownloaded
|
|
212
|
+
? 'bg-[var(--lokvis-success)]/15 text-[var(--lokvis-success)]'
|
|
213
|
+
: 'bg-[var(--lokvis-primary)]/15 text-[var(--lokvis-primary)] hover:bg-[var(--lokvis-primary)]/25'
|
|
214
|
+
}`}
|
|
215
|
+
>
|
|
216
|
+
<Icon size={10} strokeWidth={2}>
|
|
217
|
+
<path d="M12 3v12m0 0l-4-4m4 4l4-4M3 17v2a2 2 0 002 2h14a2 2 0 002-2v-2" />
|
|
218
|
+
</Icon>
|
|
219
|
+
{isDownloaded ? 'Downloaded' : 'Download'}
|
|
220
|
+
</button>
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
);
|
|
224
|
+
})}
|
|
225
|
+
|
|
226
|
+
<div className="flex shrink-0 items-center gap-1">
|
|
227
|
+
<button
|
|
228
|
+
type="button"
|
|
229
|
+
onClick={() => void handleDownloadAll()}
|
|
230
|
+
disabled={batchDownloading || allDownloaded}
|
|
231
|
+
className="rounded-md bg-[var(--lokvis-primary-hover)] px-2.5 py-1 text-[10px] font-medium text-[var(--lokvis-primary-fg)] hover:bg-[var(--lokvis-primary)] disabled:cursor-not-allowed disabled:opacity-50"
|
|
232
|
+
>
|
|
233
|
+
{batchDownloading ? 'Downloading...' : 'Download All'}
|
|
234
|
+
</button>
|
|
235
|
+
<button
|
|
236
|
+
type="button"
|
|
237
|
+
onClick={handleClear}
|
|
238
|
+
className="rounded-md px-2 py-1 text-[10px] text-[var(--lokvis-fg-muted)] hover:bg-[var(--lokvis-surface-muted)] hover:text-[var(--lokvis-fg-muted)]"
|
|
239
|
+
>
|
|
240
|
+
Clear
|
|
241
|
+
</button>
|
|
242
|
+
</div>
|
|
243
|
+
</div>
|
|
244
|
+
|
|
245
|
+
{error && (
|
|
246
|
+
<p className="px-3 pb-2 text-[10px] text-[var(--lokvis-danger)]" role="alert">
|
|
247
|
+
{error}
|
|
248
|
+
</p>
|
|
249
|
+
)}
|
|
250
|
+
</div>
|
|
251
|
+
);
|
|
252
252
|
}
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* ErrorBanner - 错误信息横幅(W11.3)
|
|
3
3
|
*
|
|
4
4
|
* 当 store.error 非空时显示,展示错误信息并提供:
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* - 关闭按钮(清除 error)
|
|
6
|
+
* - 重试按钮(重新执行工作流)
|
|
7
7
|
*
|
|
8
8
|
* 节点失败高亮由 WorkflowEditor 的节点状态颜色处理(已实现);
|
|
9
9
|
* 本组件负责全局错误信息的可见性。
|
|
@@ -20,76 +20,76 @@ import { Icon } from '@lokvis/ui-core';
|
|
|
20
20
|
import { useWorkspaceStore } from '../store/index.js';
|
|
21
21
|
|
|
22
22
|
export interface ErrorBannerProps {
|
|
23
|
-
|
|
23
|
+
className?: string;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
export function ErrorBanner({ className = '' }: ErrorBannerProps) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
27
|
+
const error = useWorkspaceStore((s) => s.error);
|
|
28
|
+
const errorSeq = useWorkspaceStore((s) => s.errorSeq);
|
|
29
|
+
const setError = useWorkspaceStore((s) => s.setError);
|
|
30
|
+
const run = useWorkspaceStore((s) => s.run);
|
|
31
|
+
const nodes = useWorkspaceStore((s) => s.nodes);
|
|
32
|
+
const running = useWorkspaceStore((s) => s.running);
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
// 用户关闭后隐藏;新错误(errorSeq 变化)时重新显示。
|
|
35
|
+
// 用 errorSeq 而非 error 字符串本身:store.run() 在 setError(null) 之后才设新 error,
|
|
36
|
+
// errorSeq 在 setError(非 null) 时递增,即使错误消息相同也能感知到"新的错误事件"。
|
|
37
|
+
const [dismissedSeq, setDismissedSeq] = React.useState<number | null>(null);
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
const visible = error !== null && dismissedSeq !== errorSeq;
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
const handleDismiss = React.useCallback(() => {
|
|
42
|
+
setDismissedSeq(errorSeq);
|
|
43
|
+
setError(null);
|
|
44
|
+
}, [errorSeq, setError]);
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
46
|
+
const handleRetry = React.useCallback(() => {
|
|
47
|
+
// 先清 error 再 run,避免 run 同步抛错时 banner 已隐藏且 errorSeq 未变化
|
|
48
|
+
setError(null);
|
|
49
|
+
void run();
|
|
50
|
+
}, [setError, run]);
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
if (!visible || !error) return null;
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
const canRetry = nodes.length > 0 && !running;
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
56
|
+
return (
|
|
57
|
+
<div
|
|
58
|
+
className={`flex items-start gap-2 border-b border-[var(--lokvis-danger)]/30 bg-[var(--lokvis-danger)]/10 px-3 py-2 ${className}`}
|
|
59
|
+
role="alert"
|
|
60
|
+
>
|
|
61
|
+
<Icon size={14} className="mt-0.5 shrink-0 text-[var(--lokvis-danger)]">
|
|
62
|
+
<circle cx="12" cy="12" r="10" />
|
|
63
|
+
<line x1="12" y1="8" x2="12" y2="12" />
|
|
64
|
+
<line x1="12" y1="16" x2="12.01" y2="16" />
|
|
65
|
+
</Icon>
|
|
66
|
+
<div className="flex-1 min-w-0">
|
|
67
|
+
<p className="text-[11px] font-medium text-[var(--lokvis-danger)]">
|
|
68
|
+
执行出错
|
|
69
|
+
</p>
|
|
70
|
+
<p className="mt-0.5 break-words text-[10px] text-[var(--lokvis-danger)]/80">
|
|
71
|
+
{error}
|
|
72
|
+
</p>
|
|
73
|
+
</div>
|
|
74
|
+
<div className="flex shrink-0 items-center gap-1">
|
|
75
|
+
{canRetry && (
|
|
76
|
+
<button
|
|
77
|
+
type="button"
|
|
78
|
+
onClick={handleRetry}
|
|
79
|
+
className="rounded bg-[var(--lokvis-danger)]/15 px-2 py-0.5 text-[10px] font-medium text-[var(--lokvis-danger)] transition-colors hover:bg-[var(--lokvis-danger)]/30"
|
|
80
|
+
>
|
|
81
|
+
重试
|
|
82
|
+
</button>
|
|
83
|
+
)}
|
|
84
|
+
<button
|
|
85
|
+
type="button"
|
|
86
|
+
onClick={handleDismiss}
|
|
87
|
+
aria-label="关闭错误信息"
|
|
88
|
+
className="rounded p-0.5 text-[var(--lokvis-danger)] transition-colors hover:bg-[var(--lokvis-danger)]/15 hover:text-[var(--lokvis-danger)]"
|
|
89
|
+
>
|
|
90
|
+
<Icon size={12} strokeWidth={3}><path d="M6 18L18 6M6 6l12 12" /></Icon>
|
|
91
|
+
</button>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
95
|
}
|