From 533f732c53bdf993846ba9e55edc25a988d37ad0 Mon Sep 17 00:00:00 2001 From: landaiqing Date: Fri, 2 Jan 2026 01:27:51 +0800 Subject: [PATCH] :sparkles: Added toast notification function and optimized related component styles --- frontend/components.d.ts | 2 + frontend/src/App.vue | 2 + frontend/src/components/toast/Toast.vue | 292 ++++++++++++++++++ .../src/components/toast/ToastContainer.vue | 168 ++++++++++ frontend/src/components/toast/index.ts | 80 +++++ frontend/src/components/toast/toastStore.ts | 55 ++++ frontend/src/components/toast/types.ts | 52 ++++ .../toolbar/BlockLanguageSelector.vue | 59 ++-- .../components/toolbar/DocumentSelector.vue | 81 ++--- frontend/src/stores/backupStore.ts | 5 +- frontend/src/stores/documentStore.ts | 21 -- .../src/views/settings/pages/BackupPage.vue | 55 +--- .../src/views/settings/pages/GeneralPage.vue | 66 +--- .../src/views/settings/pages/TestPage.vue | 170 +++++++++- 14 files changed, 909 insertions(+), 199 deletions(-) create mode 100644 frontend/src/components/toast/Toast.vue create mode 100644 frontend/src/components/toast/ToastContainer.vue create mode 100644 frontend/src/components/toast/index.ts create mode 100644 frontend/src/components/toast/toastStore.ts create mode 100644 frontend/src/components/toast/types.ts 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 @@ + + + + + + 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 = () => { -
- -
+ + +
+ +
{ {{ t('toolbar.noLanguageFound') }}
-
+
+ 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)); + } +}; \ 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(); +};