diff --git a/frontend/components.d.ts b/frontend/components.d.ts
index 6b74695..09cefb3 100644
--- a/frontend/components.d.ts
+++ b/frontend/components.d.ts
@@ -22,6 +22,8 @@ declare module 'vue' {
TabContainer: typeof import('./src/components/tabs/TabContainer.vue')['default']
TabContextMenu: typeof import('./src/components/tabs/TabContextMenu.vue')['default']
TabItem: typeof import('./src/components/tabs/TabItem.vue')['default']
+ Toast: typeof import('./src/components/toast/Toast.vue')['default']
+ ToastContainer: typeof import('./src/components/toast/ToastContainer.vue')['default']
Toolbar: typeof import('./src/components/toolbar/Toolbar.vue')['default']
WindowsTitleBar: typeof import('./src/components/titlebar/WindowsTitleBar.vue')['default']
WindowTitleBar: typeof import('./src/components/titlebar/WindowTitleBar.vue')['default']
diff --git a/frontend/src/App.vue b/frontend/src/App.vue
index 2725e15..4551002 100644
--- a/frontend/src/App.vue
+++ b/frontend/src/App.vue
@@ -6,6 +6,7 @@ import {useKeybindingStore} from '@/stores/keybindingStore';
import {useThemeStore} from '@/stores/themeStore';
import {useUpdateStore} from '@/stores/updateStore';
import WindowTitleBar from '@/components/titlebar/WindowTitleBar.vue';
+import ToastContainer from '@/components/toast/ToastContainer.vue';
import {useTranslationStore} from "@/stores/translationStore";
import {useI18n} from "vue-i18n";
import {LanguageType} from "../bindings/voidraft/internal/models";
@@ -41,6 +42,7 @@ onBeforeMount(async () => {
+
diff --git a/frontend/src/components/toast/Toast.vue b/frontend/src/components/toast/Toast.vue
new file mode 100644
index 0000000..9a6d64a
--- /dev/null
+++ b/frontend/src/components/toast/Toast.vue
@@ -0,0 +1,292 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ title }}
+
{{ message }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/toast/ToastContainer.vue b/frontend/src/components/toast/ToastContainer.vue
new file mode 100644
index 0000000..405c47d
--- /dev/null
+++ b/frontend/src/components/toast/ToastContainer.vue
@@ -0,0 +1,168 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/toast/index.ts b/frontend/src/components/toast/index.ts
new file mode 100644
index 0000000..9a9b94c
--- /dev/null
+++ b/frontend/src/components/toast/index.ts
@@ -0,0 +1,80 @@
+import { useToastStore } from './toastStore';
+import type { ToastOptions } from './types';
+
+class ToastService {
+ private getStore() {
+ return useToastStore();
+ }
+
+ /**
+ * 显示一个通知
+ */
+ show(options: ToastOptions): string {
+ return this.getStore().add(options);
+ }
+
+ /**
+ * 显示成功通知
+ */
+ success(message: string, title?: string, options?: Partial): string {
+ return this.show({
+ message,
+ title,
+ type: 'success',
+ ...options,
+ });
+ }
+
+ /**
+ * 显示错误通知
+ */
+ error(message: string, title?: string, options?: Partial): string {
+ return this.show({
+ message,
+ title,
+ type: 'error',
+ ...options,
+ });
+ }
+
+ /**
+ * 显示警告通知
+ */
+ warning(message: string, title?: string, options?: Partial): string {
+ return this.show({
+ message,
+ title,
+ type: 'warning',
+ ...options,
+ });
+ }
+
+ /**
+ * 显示信息通知
+ */
+ info(message: string, title?: string, options?: Partial): string {
+ return this.show({
+ message,
+ title,
+ type: 'info',
+ ...options,
+ });
+ }
+
+ /**
+ * 关闭指定的通知
+ */
+ close(id: string): void {
+ this.getStore().remove(id);
+ }
+
+ /**
+ * 清空所有通知
+ */
+ clear(): void {
+ this.getStore().clear();
+ }
+}
+export const toast = new ToastService();
+export default toast;
+
diff --git a/frontend/src/components/toast/toastStore.ts b/frontend/src/components/toast/toastStore.ts
new file mode 100644
index 0000000..33f9af8
--- /dev/null
+++ b/frontend/src/components/toast/toastStore.ts
@@ -0,0 +1,55 @@
+import { defineStore } from 'pinia';
+import { ref } from 'vue';
+import type { Toast, ToastOptions } from './types';
+
+export const useToastStore = defineStore('toast', () => {
+ const toasts = ref([]);
+ let idCounter = 0;
+
+ /**
+ * 添加一个 Toast
+ */
+ const add = (options: ToastOptions): string => {
+ const id = `toast-${Date.now()}-${idCounter++}`;
+
+ const toast: Toast = {
+ id,
+ message: options.message,
+ type: options.type || 'info',
+ title: options.title,
+ duration: options.duration ?? 4000,
+ position: options.position || 'top-right',
+ closable: options.closable ?? true,
+ createdAt: Date.now(),
+ };
+
+ toasts.value.push(toast);
+
+ return id;
+ };
+
+ /**
+ * 移除指定 Toast
+ */
+ const remove = (id: string) => {
+ const index = toasts.value.findIndex(t => t.id === id);
+ if (index > -1) {
+ toasts.value.splice(index, 1);
+ }
+ };
+
+ /**
+ * 清空所有 Toast
+ */
+ const clear = () => {
+ toasts.value = [];
+ };
+
+ return {
+ toasts,
+ add,
+ remove,
+ clear,
+ };
+});
+
diff --git a/frontend/src/components/toast/types.ts b/frontend/src/components/toast/types.ts
new file mode 100644
index 0000000..550fcbb
--- /dev/null
+++ b/frontend/src/components/toast/types.ts
@@ -0,0 +1,52 @@
+/**
+ * Toast 通知类型定义
+ */
+
+export type ToastType = 'success' | 'error' | 'warning' | 'info';
+
+export type ToastPosition =
+ | 'top-right'
+ | 'top-left'
+ | 'bottom-right'
+ | 'bottom-left'
+ | 'top-center'
+ | 'bottom-center';
+
+export interface ToastOptions {
+ /**
+ * Toast 消息内容
+ */
+ message: string;
+
+ /**
+ * Toast 类型
+ */
+ type?: ToastType;
+
+ /**
+ * 标题(可选)
+ */
+ title?: string;
+
+ /**
+ * 持续时间(毫秒),0 表示不自动关闭
+ */
+ duration?: number;
+
+ /**
+ * 显示位置
+ */
+ position?: ToastPosition;
+
+ /**
+ * 是否可关闭
+ */
+ closable?: boolean;
+}
+
+export interface Toast extends Required> {
+ id: string;
+ title?: string;
+ createdAt: number;
+}
+
diff --git a/frontend/src/components/toolbar/BlockLanguageSelector.vue b/frontend/src/components/toolbar/BlockLanguageSelector.vue
index 2ba0558..071fdc3 100644
--- a/frontend/src/components/toolbar/BlockLanguageSelector.vue
+++ b/frontend/src/components/toolbar/BlockLanguageSelector.vue
@@ -294,9 +294,11 @@ const scrollToCurrentLanguage = () => {
▲
-
+
diff --git a/frontend/src/stores/backupStore.ts b/frontend/src/stores/backupStore.ts
index 8d13e36..7ab02b6 100644
--- a/frontend/src/stores/backupStore.ts
+++ b/frontend/src/stores/backupStore.ts
@@ -4,7 +4,6 @@ import { BackupService } from '@/../bindings/voidraft/internal/services';
export const useBackupStore = defineStore('backup', () => {
const isSyncing = ref(false);
- const error = ref(null);
const sync = async (): Promise => {
if (isSyncing.value) {
@@ -12,12 +11,11 @@ export const useBackupStore = defineStore('backup', () => {
}
isSyncing.value = true;
- error.value = null;
try {
await BackupService.Sync();
} catch (e) {
- error.value = e instanceof Error ? e.message : String(e);
+ throw e;
} finally {
isSyncing.value = false;
}
@@ -25,7 +23,6 @@ export const useBackupStore = defineStore('backup', () => {
return {
isSyncing,
- error,
sync
};
});
\ No newline at end of file
diff --git a/frontend/src/stores/documentStore.ts b/frontend/src/stores/documentStore.ts
index d715321..e1cf3f5 100644
--- a/frontend/src/stores/documentStore.ts
+++ b/frontend/src/stores/documentStore.ts
@@ -16,33 +16,15 @@ export const useDocumentStore = defineStore('document', () => {
// === UI状态 ===
const showDocumentSelector = ref(false);
- const selectorError = ref<{ docId: number; message: string } | null>(null);
const isLoading = ref(false);
- // === 错误处理 ===
- const setError = (docId: number, message: string) => {
- selectorError.value = {docId, message};
- // 3秒后自动清除错误状态
- setTimeout(() => {
- if (selectorError.value?.docId === docId) {
- selectorError.value = null;
- }
- }, 3000);
- };
-
- const clearError = () => {
- selectorError.value = null;
- };
-
// === UI控制方法 ===
const openDocumentSelector = () => {
showDocumentSelector.value = true;
- clearError();
};
const closeDocumentSelector = () => {
showDocumentSelector.value = false;
- clearError();
};
@@ -217,7 +199,6 @@ export const useDocumentStore = defineStore('document', () => {
currentDocumentId,
currentDocument,
showDocumentSelector,
- selectorError,
isLoading,
getDocumentList,
@@ -236,8 +217,6 @@ export const useDocumentStore = defineStore('document', () => {
// UI 控制
openDocumentSelector,
closeDocumentSelector,
- setError,
- clearError,
// 初始化
initDocument,
diff --git a/frontend/src/views/settings/pages/BackupPage.vue b/frontend/src/views/settings/pages/BackupPage.vue
index b7f4a17..36c37f1 100644
--- a/frontend/src/views/settings/pages/BackupPage.vue
+++ b/frontend/src/views/settings/pages/BackupPage.vue
@@ -2,48 +2,18 @@
import {useConfigStore} from '@/stores/configStore';
import {useBackupStore} from '@/stores/backupStore';
import {useI18n} from 'vue-i18n';
-import {computed, ref, watch, onUnmounted} from 'vue';
+import {computed} from 'vue';
import SettingSection from '../components/SettingSection.vue';
import SettingItem from '../components/SettingItem.vue';
import ToggleSwitch from '../components/ToggleSwitch.vue';
import {AuthMethod} from '@/../bindings/voidraft/internal/models/models';
import {DialogService} from '@/../bindings/voidraft/internal/services';
+import toast from '@/components/toast';
const {t} = useI18n();
const configStore = useConfigStore();
const backupStore = useBackupStore();
-// 消息显示状态
-const message = ref(null);
-const isError = ref(false);
-let messageTimer: ReturnType | null = null;
-
-const clearMessage = () => {
- if (messageTimer) {
- clearTimeout(messageTimer);
- messageTimer = null;
- }
- message.value = null;
-};
-
-// 监听同步完成,显示消息并自动消失
-watch(() => backupStore.isSyncing, (syncing, wasSyncing) => {
- if (wasSyncing && !syncing) {
- clearMessage();
- if (backupStore.error) {
- message.value = backupStore.error;
- isError.value = true;
- messageTimer = setTimeout(clearMessage, 5000);
- } else {
- message.value = 'Sync successful';
- isError.value = false;
- messageTimer = setTimeout(clearMessage, 3000);
- }
- }
-});
-
-onUnmounted(clearMessage);
-
const authMethodOptions = computed(() => [
{value: AuthMethod.Token, label: t('settings.backup.authMethods.token')},
{value: AuthMethod.SSHKey, label: t('settings.backup.authMethods.sshKey')},
@@ -64,6 +34,15 @@ const selectSshKeyFile = async () => {
configStore.setSshKeyPath(selectedPath.trim());
}
};
+
+const handleSync = async () => {
+ try {
+ await backupStore.sync();
+ toast.success('Sync successful');
+ } catch (e) {
+ toast.error(e instanceof Error ? e.message : String(e));
+ }
+};
@@ -202,14 +181,10 @@ const selectSshKeyFile = async () => {
-
+
\ No newline at end of file
diff --git a/frontend/src/views/settings/pages/TestPage.vue b/frontend/src/views/settings/pages/TestPage.vue
index ae993bb..8e146a1 100644
--- a/frontend/src/views/settings/pages/TestPage.vue
+++ b/frontend/src/views/settings/pages/TestPage.vue
@@ -72,6 +72,72 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -91,6 +157,8 @@ import { ref } from 'vue';
import * as TestService from '@/../bindings/voidraft/internal/services/testservice';
import SettingSection from '../components/SettingSection.vue';
import SettingItem from '../components/SettingItem.vue';
+import toast from '@/components/toast';
+import type { ToastPosition, ToastType } from '@/components/toast/types';
// Badge测试状态
const badgeText = ref('');
@@ -102,6 +170,12 @@ const notificationSubtitle = ref('');
const notificationBody = ref('');
const notificationStatus = ref<{ type: string; message: string } | null>(null);
+// Toast 测试状态
+const toastMessage = ref('This is a test toast notification!');
+const toastTitle = ref('');
+const toastPosition = ref('top-right');
+const toastDuration = ref(4000);
+
// 清除状态
const clearStatus = ref<{ type: string; message: string } | null>(null);
@@ -172,13 +246,57 @@ const clearAll = async () => {
showStatus(clearStatus, 'error', `Failed to clear test states: ${error.message || error}`);
}
};
+
+// Toast 相关函数
+const showToast = (type: ToastType) => {
+ const message = toastMessage.value || `This is a ${type} toast notification!`;
+ const title = toastTitle.value || undefined;
+
+ const options = {
+ position: toastPosition.value,
+ duration: toastDuration.value,
+ };
+
+ switch (type) {
+ case 'success':
+ toast.success(message, title, options);
+ break;
+ case 'error':
+ toast.error(message, title, options);
+ break;
+ case 'warning':
+ toast.warning(message, title, options);
+ break;
+ case 'info':
+ toast.info(message, title, options);
+ break;
+ }
+};
+
+const showMultipleToasts = () => {
+ const positions: ToastPosition[] = ['top-right', 'top-left', 'bottom-right', 'bottom-left'];
+ const types: ToastType[] = ['success', 'error', 'warning', 'info'];
+
+ positions.forEach((position, index) => {
+ setTimeout(() => {
+ const type = types[index % types.length];
+ toast.show({
+ type,
+ message: `Toast from ${position}`,
+ title: `${type.charAt(0).toUpperCase() + type.slice(1)} Toast`,
+ position,
+ duration: 5000,
+ });
+ }, index * 200);
+ });
+};
+
+const clearAllToasts = () => {
+ toast.clear();
+};