Skip to content

Commit 1e3b9ec

Browse files
committed
add toast
1 parent fca3822 commit 1e3b9ec

File tree

5 files changed

+294
-2
lines changed

5 files changed

+294
-2
lines changed

src/components/DataManagement.vue

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ import { useI18n } from "vue-i18n";
125125
import { invoke } from "@tauri-apps/api/core";
126126
import { save } from "@tauri-apps/plugin-dialog";
127127
import { snippetApi, initDatabase } from "../services/tauri";
128+
import { toast } from "../composables/useToast";
128129
129130
const { t } = useI18n();
130131
@@ -151,7 +152,7 @@ const exportSnippets = async () => {
151152
152153
if (snippets.length === 0) {
153154
console.log("没有代码片段可导出");
154-
alert("没有代码片段可导出");
155+
toast.warning("没有代码片段可导出");
155156
return;
156157
}
157158
@@ -182,8 +183,10 @@ const exportSnippets = async () => {
182183
contents: jsonData,
183184
});
184185
console.log("文件已保存到:", filePath);
186+
toast.success("代码片段导出成功");
185187
} else {
186188
console.log("用户取消了文件保存");
189+
toast.info("导出已取消");
187190
}
188191
} catch (error) {
189192
console.error("Tauri 文件保存失败,尝试浏览器下载:", error);
@@ -202,12 +205,15 @@ const exportSnippets = async () => {
202205
document.body.removeChild(a);
203206
URL.revokeObjectURL(url);
204207
console.log("使用浏览器下载完成");
208+
toast.success("代码片段导出成功");
205209
}
206210
207211
console.log("导出完成");
208212
} catch (error) {
209213
console.error("Failed to export snippets:", error);
210-
alert("导出失败: " + error);
214+
toast.error(
215+
"导出失败: " + (error instanceof Error ? error.message : String(error))
216+
);
211217
} finally {
212218
isExporting.value = false;
213219
}
@@ -256,10 +262,16 @@ const handleFileSelect = async (event: Event) => {
256262
// 重新加载数据统计
257263
await loadDataStatistics();
258264
265+
// 显示成功通知
266+
toast.success(`成功导入 ${snippets.length} 个代码片段`);
267+
259268
// 清空文件输入
260269
target.value = "";
261270
} catch (error) {
262271
console.error("Failed to import data:", error);
272+
toast.error(
273+
"导入失败: " + (error instanceof Error ? error.message : String(error))
274+
);
263275
} finally {
264276
isImporting.value = false;
265277
}
@@ -281,6 +293,7 @@ const loadDataStatistics = async () => {
281293
console.log("代码片段数量:", snippetsCount.value);
282294
} catch (error) {
283295
console.error("Failed to load data statistics:", error);
296+
toast.error("加载数据统计失败");
284297
}
285298
};
286299

src/components/Toast.vue

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
<template>
2+
<Teleport to="body">
3+
<div v-if="visible" class="fixed top-4 right-4 z-50 max-w-sm">
4+
<Transition
5+
enter-active-class="transition-all duration-300 ease-out transform"
6+
enter-from-class="translate-x-full opacity-0"
7+
enter-to-class="translate-x-0 opacity-100"
8+
leave-active-class="transition-all duration-200 ease-in transform"
9+
leave-from-class="translate-x-0 opacity-100"
10+
leave-to-class="translate-x-full opacity-0"
11+
>
12+
<div
13+
v-if="visible"
14+
:class="[
15+
'px-4 py-3 rounded-lg shadow-lg backdrop-blur-sm border',
16+
typeClasses[type],
17+
]"
18+
>
19+
<div class="flex items-center space-x-2">
20+
<div class="flex-shrink-0">
21+
<component :is="iconComponent" class="w-5 h-5" />
22+
</div>
23+
<div class="flex-1">
24+
<p class="text-sm font-medium">{{ message }}</p>
25+
</div>
26+
<button
27+
v-if="closable"
28+
@click="close"
29+
class="flex-shrink-0 ml-2 text-current opacity-60 hover:opacity-100 transition-opacity"
30+
>
31+
<svg
32+
class="w-4 h-4"
33+
fill="none"
34+
stroke="currentColor"
35+
viewBox="0 0 24 24"
36+
>
37+
<path
38+
stroke-linecap="round"
39+
stroke-linejoin="round"
40+
stroke-width="2"
41+
d="M6 18L18 6M6 6l12 12"
42+
></path>
43+
</svg>
44+
</button>
45+
</div>
46+
</div>
47+
</Transition>
48+
</div>
49+
</Teleport>
50+
</template>
51+
52+
<script setup lang="ts">
53+
import { ref, computed, onMounted } from "vue";
54+
55+
export interface ToastProps {
56+
message: string;
57+
type?: "success" | "error" | "warning" | "info";
58+
duration?: number;
59+
closable?: boolean;
60+
}
61+
62+
const props = withDefaults(defineProps<ToastProps>(), {
63+
type: "success",
64+
duration: 3000,
65+
closable: true,
66+
});
67+
68+
const emit = defineEmits<{
69+
close: [];
70+
}>();
71+
72+
const visible = ref(false);
73+
let timer: NodeJS.Timeout | null = null;
74+
75+
const typeClasses = {
76+
success:
77+
"bg-green-50/95 dark:bg-green-900/95 text-green-800 dark:text-green-200 border-green-200 dark:border-green-700",
78+
error:
79+
"bg-red-50/95 dark:bg-red-900/95 text-red-800 dark:text-red-200 border-red-200 dark:border-red-700",
80+
warning:
81+
"bg-yellow-50/95 dark:bg-yellow-900/95 text-yellow-800 dark:text-yellow-200 border-yellow-200 dark:border-yellow-700",
82+
info: "bg-blue-50/95 dark:bg-blue-900/95 text-blue-800 dark:text-blue-200 border-blue-200 dark:border-blue-700",
83+
};
84+
85+
const iconComponent = computed(() => {
86+
switch (props.type) {
87+
case "success":
88+
return {
89+
template: `<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
90+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
91+
</svg>`,
92+
};
93+
case "error":
94+
return {
95+
template: `<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
96+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
97+
</svg>`,
98+
};
99+
case "warning":
100+
return {
101+
template: `<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
102+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.732-.833-2.5 0L4.268 18.5c-.77.833.192 2.5 1.732 2.5z"></path>
103+
</svg>`,
104+
};
105+
case "info":
106+
return {
107+
template: `<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
108+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
109+
</svg>`,
110+
};
111+
default:
112+
return null;
113+
}
114+
});
115+
116+
const show = () => {
117+
visible.value = true;
118+
119+
if (props.duration > 0) {
120+
timer = setTimeout(() => {
121+
close();
122+
}, props.duration);
123+
}
124+
};
125+
126+
const close = () => {
127+
visible.value = false;
128+
129+
if (timer) {
130+
clearTimeout(timer);
131+
timer = null;
132+
}
133+
134+
setTimeout(() => {
135+
emit("close");
136+
}, 200);
137+
};
138+
139+
onMounted(() => {
140+
show();
141+
});
142+
143+
defineExpose({
144+
show,
145+
close,
146+
});
147+
</script>

src/composables/useToast.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { ref, createApp } from "vue";
2+
import Toast from "../components/Toast.vue";
3+
4+
// 全局状态
5+
const toasts = ref<Array<{ id: string; app: any }>>([]);
6+
let toastCounter = 0;
7+
8+
export interface UseToastOptions {
9+
message: string;
10+
type?: "success" | "error" | "warning" | "info";
11+
duration?: number;
12+
closable?: boolean;
13+
}
14+
15+
export function useToast() {
16+
const showToast = (options: UseToastOptions) => {
17+
// 生成唯一ID
18+
const id = `toast-${++toastCounter}`;
19+
20+
// 创建容器元素
21+
const container = document.createElement("div");
22+
container.id = id;
23+
document.body.appendChild(container);
24+
25+
// 创建Vue应用实例
26+
const app = createApp(Toast, {
27+
...options,
28+
onClose: () => {
29+
// 销毁应用并移除DOM元素
30+
setTimeout(() => {
31+
app.unmount();
32+
if (container.parentNode) {
33+
container.parentNode.removeChild(container);
34+
}
35+
// 从toasts数组中移除
36+
const index = toasts.value.findIndex((toast) => toast.id === id);
37+
if (index > -1) {
38+
toasts.value.splice(index, 1);
39+
}
40+
}, 300); // 等待动画完成
41+
},
42+
});
43+
44+
// 挂载应用
45+
app.mount(container);
46+
47+
// 记录toast实例
48+
toasts.value.push({ id, app });
49+
50+
return {
51+
id,
52+
close: () => {
53+
const toastInstance = app._instance?.exposed;
54+
if (toastInstance && typeof toastInstance.close === "function") {
55+
toastInstance.close();
56+
}
57+
},
58+
};
59+
};
60+
61+
// 便捷方法
62+
const success = (message: string, options?: Partial<UseToastOptions>) => {
63+
return showToast({ message, type: "success", ...options });
64+
};
65+
66+
const error = (message: string, options?: Partial<UseToastOptions>) => {
67+
return showToast({ message, type: "error", ...options });
68+
};
69+
70+
const warning = (message: string, options?: Partial<UseToastOptions>) => {
71+
return showToast({ message, type: "warning", ...options });
72+
};
73+
74+
const info = (message: string, options?: Partial<UseToastOptions>) => {
75+
return showToast({ message, type: "info", ...options });
76+
};
77+
78+
// 关闭所有toast
79+
const clearAll = () => {
80+
toasts.value.forEach((toast) => {
81+
const toastInstance = toast.app._instance?.exposed;
82+
if (toastInstance && typeof toastInstance.close === "function") {
83+
toastInstance.close();
84+
}
85+
});
86+
toasts.value = [];
87+
};
88+
89+
return {
90+
showToast,
91+
success,
92+
error,
93+
warning,
94+
info,
95+
clearAll,
96+
};
97+
}
98+
99+
// 创建全局实例,可以在任何地方导入使用
100+
export const toast = useToast();

src/i18n/locales/en-US.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,22 @@ export default {
124124
placeholder: "Clipboard content...",
125125
},
126126

127+
// Toast notifications
128+
toast: {
129+
success: "Success",
130+
error: "Error",
131+
warning: "Warning",
132+
info: "Info",
133+
copied: "Copied",
134+
saved: "Saved",
135+
deleted: "Deleted",
136+
updated: "Updated",
137+
created: "Created",
138+
failed: "Failed",
139+
networkError: "Network Error",
140+
unknownError: "Unknown Error",
141+
},
142+
127143
// Settings
128144
settings: {
129145
title: "Settings",

src/i18n/locales/zh-CN.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,22 @@ export default {
122122
placeholder: "剪贴板内容...",
123123
},
124124

125+
// Toast 通知
126+
toast: {
127+
success: "成功",
128+
error: "错误",
129+
warning: "警告",
130+
info: "信息",
131+
copied: "已复制",
132+
saved: "已保存",
133+
deleted: "已删除",
134+
updated: "已更新",
135+
created: "已创建",
136+
failed: "失败",
137+
networkError: "网络错误",
138+
unknownError: "未知错误",
139+
},
140+
125141
// 设置
126142
settings: {
127143
title: "设置",

0 commit comments

Comments
 (0)