Skip to content

Commit 7c42aed

Browse files
committed
feat: 添加全局 toast 通知系统替换 alert 提示
实现全局 toast 通知组件和 store,替换多处 alert 提示 添加成功、错误、警告和信息四种类型的 toast 支持自动消失和手动关闭功能
1 parent 8277627 commit 7c42aed

File tree

7 files changed

+131
-17
lines changed

7 files changed

+131
-17
lines changed

frontend/src/App.svelte

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import CustomDeployment from './components/CustomDeployment/CustomDeployment.svelte';
2222
import SSHManager from './components/SSH/SSHManager.svelte';
2323
import WelcomeDialog from './components/Welcome/WelcomeDialog.svelte';
24+
import Toast from './components/Toast/Toast.svelte';
2425
2526
let cases = $state([]);
2627
let templates = $state([]);
@@ -446,6 +447,9 @@
446447

447448
<!-- Welcome Dialog -->
448449
<WelcomeDialog {t} show={welcomeDialogReady} onClose={() => welcomeDialogReady = false} />
450+
451+
<!-- Global Toast -->
452+
<Toast />
449453
</div>
450454
</div>
451455

frontend/src/components/Cases/Cases.svelte

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { onMount, onDestroy } from 'svelte';
44
import { ListCases, ListTemplates, StartCase, StopCase, RemoveCase, CreateCase, CreateAndRunCase, GetCaseOutputs, GetTemplateVariables, GetCostEstimate, AnalyzeCaseError, GetActiveProfile, GetCasePlanPreview, SetCaseTags, GetAllTagNames, CloneCase } from '../../../wailsjs/go/main/App.js';
55
import { EventsOn } from '../../../wailsjs/runtime/runtime.js';
6+
import { toast } from '../../lib/toast.js';
67
import SSHModal from './SSHModal.svelte';
78
import ScheduleDialog from './ScheduleDialog.svelte';
89
import ScheduledTasksManager from './ScheduledTasksManager.svelte';
@@ -382,7 +383,7 @@ let { t, onTabChange = () => {} } = $props();
382383
async function handleAIAnalysis() {
383384
const errorMessage = getPersistentError()?.detail || createStatusDetail;
384385
if (!errorMessage) {
385-
alert(t.noErrorToAnalyze || '没有错误信息可以分析');
386+
toast.warning(t.noErrorToAnalyze || '没有错误信息可以分析');
386387
return;
387388
}
388389
@@ -400,11 +401,11 @@ let { t, onTabChange = () => {} } = $props();
400401
try {
401402
const profile = await GetActiveProfile();
402403
if (!profile || !profile.aiConfig || !profile.aiConfig.apiKey) {
403-
alert(t.configureAIServiceFirst || '请先在设置中配置 AI 服务');
404+
toast.warning(t.configureAIServiceFirst || '请先在设置中配置 AI 服务');
404405
return;
405406
}
406407
} catch (err) {
407-
alert(`检查 AI 配置失败: ${err.message || err}`);
408+
toast.error(`检查 AI 配置失败: ${err.message || err}`);
408409
return;
409410
}
410411
@@ -423,7 +424,7 @@ let { t, onTabChange = () => {} } = $props();
423424
try {
424425
await AnalyzeCaseError(caseId, errorMessage, getProviderFromTemplate(templateName), templateName);
425426
} catch (err) {
426-
alert(`AI 分析失败: ${err.message || err}`);
427+
toast.error(`AI 分析失败: ${err.message || err}`);
427428
aiAnalyzing[caseId] = false;
428429
aiAnalyzing = { ...aiAnalyzing };
429430
}

frontend/src/components/CustomDeployment/CustomDeployment.svelte

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<script lang="ts">
22
import { onMount } from 'svelte';
33
import { GetBaseTemplates, GetTemplateMetadata, EstimateDeploymentCost, CreateCustomDeployment } from '../../../wailsjs/go/main/App.js';
4+
import { toast } from '../../lib/toast.js';
45
import TemplateSelector from './TemplateSelector.svelte';
56
import ConfigEditor from './ConfigEditor.svelte';
67
import DeploymentPreview from './DeploymentPreview.svelte';
@@ -200,7 +201,7 @@
200201
deploymentResult = result;
201202
202203
// Show success message
203-
alert(`${t.deploymentCreated || '部署创建成功'}!\n\n${t.deploymentId || '部署 ID'}: ${result.id}\n\n${t.checkDeploymentManagement || '您可以在"部署管理"页面查看部署详情和日志。'}`);
204+
toast.success(`${t.deploymentCreated || '部署创建成功'} — ID: ${result.id}`);
204205
205206
// Optionally navigate to deployment management page
206207
// TODO: Add navigation when deployment management page is ready
@@ -210,7 +211,7 @@
210211
deploymentError = err.message || String(err);
211212
212213
// Show error message
213-
alert(`部署失败${deploymentError}\n\n请检查配置并重试。`);
214+
toast.error(`部署失败: ${deploymentError}`);
214215
} finally {
215216
isDeploying = false;
216217
}

frontend/src/components/CustomDeployment/CustomDeploymentList.svelte

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import { onMount } from 'svelte';
33
import { ListCustomDeployments, StartCustomDeployment, StopCustomDeployment, DeleteCustomDeployment, BatchStartCustomDeployments, BatchStopCustomDeployments, BatchDeleteCustomDeployments, AnalyzeDeploymentError, GetActiveProfile, GetDeploymentPlanPreview, SetCaseTags, GetAllCaseTags, GetAllTagNames, CloneCustomDeployment } from '../../../wailsjs/go/main/App';
44
import { EventsOn } from '../../../wailsjs/runtime/runtime.js';
5+
import { toast } from '../../lib/toast.js';
56
import SSHModal from '../Cases/SSHModal.svelte';
67
import ScheduleDialog from '../Cases/ScheduleDialog.svelte';
78
import ELK from 'elkjs/lib/elk.bundled.js';
@@ -275,7 +276,7 @@
275276
await loadDeployments();
276277
onRefresh();
277278
} catch (err: any) {
278-
alert(`启动失败: ${err.message || err}`);
279+
toast.error(`启动失败: ${err.message || err}`);
279280
// 失败后重新加载以恢复正确状态
280281
await loadDeployments();
281282
}
@@ -297,7 +298,7 @@
297298
await CloneCustomDeployment(deploymentId, cloneName);
298299
await loadDeployments();
299300
} catch (err: any) {
300-
alert(`${t.cloneFailed || '克隆失败'}: ${err.message || err}`);
301+
toast.error(`${t.cloneFailed || '克隆失败'}: ${err.message || err}`);
301302
} finally {
302303
cloneLoading = false;
303304
}
@@ -324,7 +325,7 @@
324325
await loadDeployments();
325326
onRefresh();
326327
} catch (err: any) {
327-
alert(`停止失败: ${err.message || err}`);
328+
toast.error(`停止失败: ${err.message || err}`);
328329
await loadDeployments();
329330
}
330331
}
@@ -354,27 +355,27 @@
354355
await loadDeployments();
355356
onRefresh();
356357
} catch (err: any) {
357-
alert(`删除失败: ${err.message || err}`);
358+
toast.error(`删除失败: ${err.message || err}`);
358359
// 失败后重新加载以恢复正确状态
359360
await loadDeployments();
360361
}
361362
}
362363
363364
async function handleAIAnalysis(deploymentId: string, errorMessage: string, provider: string, templateName: string) {
364365
if (!errorMessage) {
365-
alert(t.noErrorToAnalyze || '没有错误信息可以分析');
366+
toast.warning(t.noErrorToAnalyze || '没有错误信息可以分析');
366367
return;
367368
}
368369
369370
// 先检查 AI 配置
370371
try {
371372
const profile = await GetActiveProfile();
372373
if (!profile || !profile.aiConfig || !profile.aiConfig.apiKey) {
373-
alert(t.configureAIServiceFirst || '请先在设置中配置 AI 服务');
374+
toast.warning(t.configureAIServiceFirst || '请先在设置中配置 AI 服务');
374375
return;
375376
}
376377
} catch (err: any) {
377-
alert(`检查 AI 配置失败: ${err.message || err}`);
378+
toast.error(`检查 AI 配置失败: ${err.message || err}`);
378379
return;
379380
}
380381
@@ -387,7 +388,7 @@
387388
try {
388389
await AnalyzeDeploymentError(deploymentId, errorMessage, provider, templateName);
389390
} catch (err: any) {
390-
alert(`AI 分析失败: ${err.message || err}`);
391+
toast.error(`AI 分析失败: ${err.message || err}`);
391392
aiAnalyzing[deploymentId] = false;
392393
aiAnalyzing = { ...aiAnalyzing };
393394
}

frontend/src/components/Sidebar/Sidebar.svelte

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import { BrowserOpenURL } from '../../../wailsjs/runtime/runtime.js';
55
import { Environment } from '../../../wailsjs/runtime/runtime.js';
66
import { ListProjects, GetCurrentProject, SwitchProject, CreateProject } from '../../../wailsjs/go/main/App.js';
7+
import { toast } from '../../lib/toast.js';
78
89
let {
910
t,
@@ -93,14 +94,14 @@ let {
9394
window.location.reload();
9495
} catch (err) {
9596
console.error('Failed to switch project:', err);
96-
alert((t.switchProjectFailed || '切换项目失败') + ': ' + err.message);
97+
toast.error((t.switchProjectFailed || '切换项目失败') + ': ' + err.message);
9798
}
9899
}
99100
100101
// Create a new project
101102
async function handleCreateProject() {
102103
if (!newProjectName.trim()) {
103-
alert(t.pleaseEnterProjectName || '请输入项目名称');
104+
toast.warning(t.pleaseEnterProjectName || '请输入项目名称');
104105
return;
105106
}
106107
try {
@@ -112,7 +113,7 @@ let {
112113
await handleSwitchProject(newProjectName.trim());
113114
} catch (err) {
114115
console.error('Failed to create project:', err);
115-
alert((t.createProjectFailed || '创建项目失败') + ': ' + err.message);
116+
toast.error((t.createProjectFailed || '创建项目失败') + ': ' + err.message);
116117
}
117118
}
118119
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<script>
2+
import { onMount, onDestroy } from 'svelte';
3+
import { onToastChange, getToasts, removeToast } from '../../lib/toast.js';
4+
5+
let toasts = $state(getToasts());
6+
let unsubscribe = null;
7+
8+
onMount(() => {
9+
unsubscribe = onToastChange((list) => { toasts = list; });
10+
});
11+
onDestroy(() => { if (unsubscribe) unsubscribe(); });
12+
13+
const icons = {
14+
success: 'M4.5 12.75l6 6 9-13.5',
15+
error: 'M6 18L18 6M6 6l12 12',
16+
warning: 'M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z',
17+
info: 'M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z',
18+
};
19+
20+
const styles = {
21+
success: { bg: 'bg-emerald-50', border: 'border-emerald-200', icon: 'text-emerald-500', text: 'text-emerald-800' },
22+
error: { bg: 'bg-red-50', border: 'border-red-200', icon: 'text-red-500', text: 'text-red-800' },
23+
warning: { bg: 'bg-amber-50', border: 'border-amber-200', icon: 'text-amber-500', text: 'text-amber-800' },
24+
info: { bg: 'bg-blue-50', border: 'border-blue-200', icon: 'text-blue-500', text: 'text-blue-800' },
25+
};
26+
</script>
27+
28+
{#if toasts.length > 0}
29+
<div class="fixed top-4 right-4 z-[9999] flex flex-col gap-2 pointer-events-none" style="max-width: 380px;">
30+
{#each toasts as t (t.id)}
31+
{@const s = styles[t.type] || styles.info}
32+
<div class="pointer-events-auto flex items-start gap-2.5 px-4 py-3 rounded-xl border shadow-lg backdrop-blur-sm animate-toast-in {s.bg} {s.border}">
33+
<svg class="w-4 h-4 flex-shrink-0 mt-0.5 {s.icon}" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5">
34+
<path stroke-linecap="round" stroke-linejoin="round" d={icons[t.type] || icons.info} />
35+
</svg>
36+
<span class="flex-1 text-[12px] leading-relaxed {s.text}">{t.message}</span>
37+
<button
38+
class="flex-shrink-0 p-0.5 rounded hover:bg-black/5 transition-colors cursor-pointer {s.text} opacity-50 hover:opacity-100"
39+
onclick={() => removeToast(t.id)}
40+
aria-label="Close"
41+
>
42+
<svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
43+
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
44+
</svg>
45+
</button>
46+
</div>
47+
{/each}
48+
</div>
49+
{/if}
50+
51+
<style>
52+
@keyframes toastIn {
53+
from { opacity: 0; transform: translateX(20px); }
54+
to { opacity: 1; transform: translateX(0); }
55+
}
56+
:global(.animate-toast-in) {
57+
animation: toastIn 0.25s ease-out;
58+
}
59+
</style>

frontend/src/lib/toast.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Global toast notification store
2+
// Usage: import { toast } from '../lib/toast.js';
3+
// toast.success('保存成功');
4+
// toast.error('操作失败: ...');
5+
// toast.warning('请注意...');
6+
// toast.info('提示信息');
7+
8+
let _toasts = [];
9+
let _id = 0;
10+
let _listeners = [];
11+
12+
function _notify() {
13+
const snapshot = [..._toasts];
14+
_listeners.forEach(fn => fn(snapshot));
15+
}
16+
17+
function addToast(type, message, duration = 3500) {
18+
const id = ++_id;
19+
_toasts = [..._toasts, { id, type, message }];
20+
_notify();
21+
if (duration > 0) {
22+
setTimeout(() => removeToast(id), duration);
23+
}
24+
}
25+
26+
function removeToast(id) {
27+
_toasts = _toasts.filter(t => t.id !== id);
28+
_notify();
29+
}
30+
31+
export const toast = {
32+
success: (msg, duration) => addToast('success', msg, duration),
33+
error: (msg, duration) => addToast('error', msg, duration ?? 5000),
34+
warning: (msg, duration) => addToast('warning', msg, duration),
35+
info: (msg, duration) => addToast('info', msg, duration),
36+
};
37+
38+
export function onToastChange(fn) {
39+
_listeners.push(fn);
40+
return () => { _listeners = _listeners.filter(l => l !== fn); };
41+
}
42+
43+
export function getToasts() {
44+
return [..._toasts];
45+
}
46+
47+
export { removeToast };

0 commit comments

Comments
 (0)