diff --git a/src/App.vue b/src/App.vue index 289daf13..4e69a3cb 100644 --- a/src/App.vue +++ b/src/App.vue @@ -39,21 +39,14 @@ - - - - + + + @@ -65,9 +58,10 @@ import AppHeader from './components/AppHeader.vue' import CodeEditor from './components/CodeEditor.vue' import OutputPanel from './components/OutputPanel.vue' import StatusBar from './components/StatusBar.vue' -import Toast from './components/Toast.vue' import About from './components/About.vue' import Settings from './components/Settings.vue' +import Toast from './components/Toast.vue' +import { useToast } from './plugins/toast' interface ExecutionResult { @@ -155,6 +149,7 @@ print(f"Original: {numbers}") print(f"Squared: {squared}")` } +const toast = useToast() const code = ref('') const currentLanguage = ref('python2') const output = ref('') @@ -183,16 +178,6 @@ const envInfo = ref({ language: 'python' }) -const toast = ref({ - show: false, - message: '', - type: 'success' as 'success' | 'error' | 'info' -}) - -const showToast = (message: string, type: 'success' | 'error' | 'info' = 'success') => { - toast.value = { show: true, message, type } -} - const getLanguageDisplayName = (languageValue: string) => { const language = supportedLanguages.value.find(lang => lang.value === languageValue) return language ? language.name : languageValue @@ -256,12 +241,12 @@ print("Hello from ${ getLanguageDisplayName(newLanguage) }!")` // 刷新环境信息 await refreshEnvInfo() - showToast(`已切换到 ${ getLanguageDisplayName(newLanguage) }`, 'info') + toast.info(`已切换到 ${ getLanguageDisplayName(newLanguage) }`) } const runCode = async () => { if (!envInfo.value.installed) { - showToast(`${ envInfo.value.language } 环境未安装`, 'error') + toast.error(`${ envInfo.value.language } 环境未安装`) return } @@ -284,16 +269,16 @@ const runCode = async () => { if (result.stderr) { output.value += '\n' + result.stderr } - showToast(`代码执行成功,用时 ${ result.execution_time } 毫秒`) + toast.success(`代码执行成功,用时 ${ result.execution_time } 毫秒`) } else { output.value = result.stderr || '代码执行失败 (无输出)' - showToast('代码执行失败,查看输出的错误信息', 'error') + toast.error('代码执行失败,查看输出的错误信息') } } catch (error) { output.value = `代码执行失败: ${ error }` - showToast('代码执行失败,请检查日志', 'error') + toast.error('代码执行失败,请检查日志') } finally { isRunning.value = false @@ -302,7 +287,7 @@ const runCode = async () => { const clearOutput = () => { output.value = '' - showToast('输出已清空', 'info') + toast.info('输出已清空') } window.addEventListener('contextmenu', (e) => e.preventDefault(), false) @@ -340,4 +325,4 @@ onUnmounted(() => { unlistenSettingsFn() } }) - \ No newline at end of file + diff --git a/src/components/Settings.vue b/src/components/Settings.vue index 6775a044..ff9f47ef 100644 --- a/src/components/Settings.vue +++ b/src/components/Settings.vue @@ -138,7 +138,9 @@ import { open as openDialog } from '@tauri-apps/plugin-dialog' import { openPath } from '@tauri-apps/plugin-opener' import { FileText, Folder, Settings2, X } from 'lucide-vue-next' import Select from '../ui/Select.vue' +import { useToast } from '../plugins/toast' +const toast = useToast() const isVisible = ref(false) const currentLogDir = ref('') const newLogDir = ref('') @@ -205,12 +207,11 @@ const applyLogDirChange = async () => { await invoke('set_log_directory', { path: newLogDir.value }) currentLogDir.value = newLogDir.value await loadLogFiles() - // 这里可以显示成功提示 - console.log('日志目录已更新') + toast.success('日志目录已更新') } catch (error) { console.error('Failed to set log directory:', error) - // 这里可以显示错误提示 + toast.error('日志目录更新失败, 错误信息: ' + error) } } @@ -228,10 +229,11 @@ const resetLogDirectory = async () => { await invoke('reset_log_directory') await loadLogDirectory() await loadLogFiles() - console.log('日志目录已重置为默认') + toast.success('日志目录已重置为默认') } catch (error) { console.error('Failed to reset log directory:', error) + toast.error('日志目录重置失败, 错误信息: ' + error) } } @@ -249,10 +251,11 @@ const clearLogs = async () => { try { await invoke('clear_logs', { keepDays: parseInt(keepDays.value.toString()) }) await loadLogFiles() - console.log(`已清理 ${ keepDays.value } 天前的日志`) + toast.success(`已清理 ${ keepDays.value } 天前的日志`) } catch (error) { console.error('Failed to clear old logs:', error) + toast.error('清理日志失败, 错误信息: ' + error) } } diff --git a/src/components/Toast.vue b/src/components/Toast.vue index 5b331d00..02d5099d 100644 --- a/src/components/Toast.vue +++ b/src/components/Toast.vue @@ -1,97 +1,70 @@ diff --git a/src/main.ts b/src/main.ts index fe5bae39..72293083 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,5 +1,8 @@ import { createApp } from 'vue' import App from './App.vue' import './style.css' +import ToastPlugin from './plugins/toast' -createApp(App).mount('#app') +createApp(App) + .use(ToastPlugin) + .mount('#app') diff --git a/src/plugins/toast.ts b/src/plugins/toast.ts new file mode 100644 index 00000000..0fff4f63 --- /dev/null +++ b/src/plugins/toast.ts @@ -0,0 +1,125 @@ +import { App, reactive } from 'vue' + +export interface ToastOptions +{ + message: string + type?: 'success' | 'error' | 'warning' | 'info' + duration?: number + showProgress?: boolean +} + +interface ToastItem + extends ToastOptions +{ + id: number + show: boolean +} + +export class ToastManager +{ + private toasts = reactive([]) + private toastId = 0 + + show(options: ToastOptions | string) + { + const config: ToastOptions = typeof options === 'string' + ? { message: options } + : options + + // 清除现有的 toast + if (this.toasts.length > 0) { + this.toasts.splice(0, this.toasts.length) + } + + const toast: ToastItem = { + id: ++this.toastId, + show: true, + type: 'success', + duration: 3000, + showProgress: true, + ...config + } + + this.toasts.push(toast) + + // 自动移除 + if (toast.duration as number > 0) { + setTimeout(() => { + this.remove(toast.id) + }, toast.duration) + } + + return toast.id + } + + success(message: string, options?: Partial) + { + return this.show({ message, type: 'success', ...options }) + } + + error(message: string, options?: Partial) + { + return this.show({ message, type: 'error', ...options }) + } + + warning(message: string, options?: Partial) + { + return this.show({ message, type: 'warning', ...options }) + } + + info(message: string, options?: Partial) + { + return this.show({ message, type: 'info', ...options }) + } + + remove(id: number) + { + const index = this.toasts.findIndex(toast => toast.id === id) + if (index > -1) { + this.toasts[index].show = false + // 等待动画完成后移除 + setTimeout(() => { + const currentIndex = this.toasts.findIndex(toast => toast.id === id) + if (currentIndex > -1) { + this.toasts.splice(currentIndex, 1) + } + }, 300) + } + } + + clear() + { + this.toasts.forEach(toast => { + toast.show = false + }) + setTimeout(() => { + this.toasts.length = 0 + }, 300) + } + + getToasts() + { + return this.toasts + } +} + +const toastManager = new ToastManager() + +export const ToastPlugin = { + install(app: App) + { + // 全局属性 + app.config.globalProperties.$toast = toastManager + app.config.globalProperties.$Toast = toastManager + + // 提供/注入 + app.provide('toast', toastManager) + app.provide('$toast', toastManager) + } +} + +export const useToast = () => toastManager +export const $toast = toastManager + +// 默认导出 +export default ToastPlugin diff --git a/src/types/global.d.ts b/src/types/global.d.ts new file mode 100644 index 00000000..9cb1be2c --- /dev/null +++ b/src/types/global.d.ts @@ -0,0 +1,10 @@ +import { ToastManager } from '../plugins/toast.ts' + +declare module '@vue/runtime-core' +{ + interface ComponentCustomProperties + { + $toast: ToastManager + $Toast: ToastManager + } +} diff --git a/src/ui/Select.vue b/src/ui/Select.vue index 3762827f..1674235f 100644 --- a/src/ui/Select.vue +++ b/src/ui/Select.vue @@ -9,22 +9,27 @@ @keydown.arrow-down.prevent="openDropdown" @keydown.arrow-up.prevent="openDropdown" :class="[ - 'relative w-full cursor-pointer rounded-lg border bg-white py-1 pl-3 pr-10 text-left shadow-sm transition-all duration-200', + 'relative w-full cursor-pointer rounded-lg border bg-white py-1 pl-3 pr-10 text-left transition-all duration-200 ring-1 ring-blue-200', disabled ? 'cursor-not-allowed bg-gray-50 text-gray-400' : 'hover:border-gray-400', ...buttonClasses ]" :disabled="disabled" :aria-expanded="isOpen" :aria-haspopup="true" + :style="{ + border: 'none !important', + outline: 'none !important', + boxShadow: '0 0 0 1px rgb(147 197 253)' + }" role="combobox"> {{ selectedLabel || placeholder }} - + @@ -40,7 +45,7 @@ @before-leave="$emit('before-close')" @after-leave="$emit('after-close')">
@@ -94,7 +99,7 @@