feat: toast样式优化

This commit is contained in:
digua
2026-04-06 21:48:12 +08:00
committed by digua
parent e74af1a8b6
commit fbd3155991
15 changed files with 127 additions and 188 deletions
+7 -1
View File
@@ -26,6 +26,12 @@ const tooltip = {
delayDuration: 100,
}
const toaster = {
position: 'top-center' as const,
progress: false,
duration: 2000,
}
// 应用启动时初始化
onMounted(async () => {
// 平台检测 - 设置 CSS 类名以驱动平台差异化样式(如标题栏安全区域高度)
@@ -46,7 +52,7 @@ onMounted(async () => {
</script>
<template>
<UApp :tooltip="tooltip">
<UApp :tooltip="tooltip" :toaster="toaster">
<!-- 自定义标题栏 - 拖拽区域 + 窗口控制按钮 -->
<TitleBar />
<div class="relative flex h-screen w-full overflow-hidden bg-gray-50 dark:bg-gray-900">
+3 -10
View File
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useToast } from '@nuxt/ui/runtime/composables/useToast.js'
import { useToast } from '@/composables/useToast'
import ConversationList from './chat/ConversationList.vue'
import DataSourcePanel from './chat/DataSourcePanel.vue'
import ChatMessage from './chat/ChatMessage.vue'
@@ -162,20 +162,13 @@ const showWelcomeCard = computed(() => {
})
function showRunningTaskToast() {
toast.add({
title: t('ai.chat.backgroundTask.runningTitle'),
toast.warn(t('ai.chat.backgroundTask.runningTitle'), {
description: t('ai.chat.backgroundTask.runningDescription'),
color: 'warning',
icon: 'i-heroicons-sparkles',
})
}
function showLockedActionToast() {
toast.add({
title: t('ai.chat.backgroundTask.blockedAction'),
color: 'warning',
icon: 'i-heroicons-lock-closed',
})
toast.warn(t('ai.chat.backgroundTask.blockedAction'))
}
// 选择助手
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { ref, watch, computed, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { useToast } from '@nuxt/ui/runtime/composables/useToast.js'
import { useToast } from '@/composables/useToast'
import { useAssistantStore, type AssistantConfigFull } from '@/stores/assistant'
const { t } = useI18n()
@@ -123,7 +123,7 @@ async function loadConfig(id: string) {
}
} catch (error) {
console.error('[AssistantConfigModal] Failed to load config:', error)
toast.add({ title: t('ai.assistant.toast.loadFailed'), description: String(error), color: 'error' })
toast.fail(t('ai.assistant.toast.loadFailed'), { description: String(error) })
} finally {
isLoading.value = false
}
@@ -146,34 +146,30 @@ async function handleSave() {
if (isCreateMode.value) {
const result = await assistantStore.createAssistant(payload)
if (result.success) {
toast.add({ title: t('ai.assistant.toast.createSuccess'), color: 'success' })
toast.success(t('ai.assistant.toast.createSuccess'))
emit('created', result.id!)
closeModal()
} else {
toast.add({
title: t('ai.assistant.toast.createFailed'),
toast.fail(t('ai.assistant.toast.createFailed'), {
description: result.error || t('ai.assistant.toast.unknownError'),
color: 'error',
})
}
} else {
if (!props.assistantId) return
const result = await assistantStore.updateAssistant(props.assistantId, payload)
if (result.success) {
toast.add({ title: t('ai.assistant.toast.saveSuccess'), color: 'success' })
toast.success(t('ai.assistant.toast.saveSuccess'))
emit('saved')
closeModal()
} else {
toast.add({
title: t('ai.assistant.toast.saveFailed'),
toast.fail(t('ai.assistant.toast.saveFailed'), {
description: result.error || t('ai.assistant.toast.unknownError'),
color: 'error',
})
}
}
} catch (error) {
console.error('[AssistantConfigModal] Save failed:', error)
toast.add({ title: t('ai.assistant.toast.saveFailed'), description: String(error), color: 'error' })
toast.fail(t('ai.assistant.toast.saveFailed'), { description: String(error) })
} finally {
isSaving.value = false
}
@@ -186,18 +182,16 @@ async function handleReset() {
try {
const result = await assistantStore.resetAssistant(props.assistantId)
if (result.success) {
toast.add({ title: t('ai.assistant.toast.resetSuccess'), color: 'success' })
toast.success(t('ai.assistant.toast.resetSuccess'))
await loadConfig(props.assistantId)
emit('saved')
} else {
toast.add({
title: t('ai.assistant.toast.resetFailed'),
toast.fail(t('ai.assistant.toast.resetFailed'), {
description: result.error || t('ai.assistant.toast.unknownError'),
color: 'error',
})
}
} catch (error) {
toast.add({ title: t('ai.assistant.toast.resetFailed'), description: String(error), color: 'error' })
toast.fail(t('ai.assistant.toast.resetFailed'), { description: String(error) })
} finally {
isSaving.value = false
}
@@ -3,7 +3,7 @@ import { ref, watch, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { storeToRefs } from 'pinia'
import { useAssistantStore, type CloudAssistantItem } from '@/stores/assistant'
import { useToast } from '@nuxt/ui/runtime/composables/useToast.js'
import { useToast } from '@/composables/useToast'
const { t, locale } = useI18n()
const toast = useToast()
@@ -74,9 +74,9 @@ async function handleImportCloud(item: CloudAssistantItem) {
try {
const result = await assistantStore.importFromCloud(item)
if (result.success) {
toast.add({ title: t('ai.assistant.market.importSuccess'), color: 'success' })
toast.success(t('ai.assistant.market.importSuccess'))
} else {
toast.add({ title: t('ai.assistant.market.importFailed'), description: result.error, color: 'error' })
toast.fail(t('ai.assistant.market.importFailed'), { description: result.error })
}
} finally {
const next = new Set(importingIds.value)
@@ -91,14 +91,14 @@ async function handleReimportCloud(item: CloudAssistantItem) {
try {
const deleteResult = await assistantStore.deleteAssistant(item.id)
if (!deleteResult.success) {
toast.add({ title: t('ai.assistant.market.importFailed'), description: deleteResult.error, color: 'error' })
toast.fail(t('ai.assistant.market.importFailed'), { description: deleteResult.error })
return
}
const importResult = await assistantStore.importFromCloud(item)
if (importResult.success) {
toast.add({ title: t('ai.assistant.market.importSuccess'), color: 'success' })
toast.success(t('ai.assistant.market.importSuccess'))
} else {
toast.add({ title: t('ai.assistant.market.importFailed'), description: importResult.error, color: 'error' })
toast.fail(t('ai.assistant.market.importFailed'), { description: importResult.error })
}
} finally {
const next = new Set(importingIds.value)
+3 -14
View File
@@ -1,11 +1,11 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useToast } from '@nuxt/ui/runtime/composables/useToast.js'
import dayjs from 'dayjs'
import MarkdownIt from 'markdown-it'
import type { ContentBlock, ToolBlockContent } from '@/composables/useAIChat'
import CaptureButton from '@/components/common/CaptureButton.vue'
import { useToast } from '@/composables/useToast'
const { t, te, locale } = useI18n()
const toast = useToast()
@@ -286,20 +286,9 @@ async function handleCopyMarkdown() {
try {
await navigator.clipboard.writeText(copyMarkdownText.value)
toast.add({
title: t('ai.chat.message.copy.success'),
color: 'primary',
icon: 'i-heroicons-clipboard-document-check',
duration: 2000,
})
toast.success(t('ai.chat.message.copy.success'))
} catch (error) {
toast.add({
title: t('ai.chat.message.copy.failed'),
description: String(error),
color: 'error',
icon: 'i-heroicons-x-circle',
duration: 3000,
})
toast.fail(t('ai.chat.message.copy.failed'), { description: String(error) })
}
}
</script>
+7 -41
View File
@@ -2,7 +2,7 @@
import { ref, computed } from 'vue'
import { storeToRefs } from 'pinia'
import { useI18n } from 'vue-i18n'
import { useToast } from '@nuxt/ui/runtime/composables/useToast.js'
import { useToast } from '@/composables/useToast'
import { useRouter } from 'vue-router'
import { usePromptStore } from '@/stores/prompt'
import { useLLMStore } from '@/stores/llm'
@@ -95,12 +95,7 @@ async function switchModelConfig(configId: string) {
if (success) {
isModelPopoverOpen.value = false
} else {
toast.add({
title: t('ai.chat.statusBar.model.switchFailed'),
icon: 'i-heroicons-x-circle',
color: 'error',
duration: 2000,
})
toast.fail(t('ai.chat.statusBar.model.switchFailed'))
}
}
@@ -123,12 +118,7 @@ async function handleExportConversation() {
])
if (!conv || messages.length === 0) {
toast.add({
title: t('ai.chat.conversation.export.noMessages'),
icon: 'i-heroicons-exclamation-triangle',
color: 'warning',
duration: 2000,
})
toast.warn(t('ai.chat.conversation.export.noMessages'))
return
}
@@ -152,9 +142,7 @@ async function handleExportConversation() {
toast.add({
title: t('common.exportSuccess'),
description: filename,
icon: 'i-heroicons-check-circle',
color: 'primary',
duration: 2000,
actions: [
{
label: t('common.openFolder'),
@@ -165,23 +153,11 @@ async function handleExportConversation() {
],
})
} else {
toast.add({
title: t('common.exportFailed'),
description: result.error,
icon: 'i-heroicons-x-circle',
color: 'error',
duration: 2000,
})
toast.fail(t('common.exportFailed'), { description: result.error })
}
} catch (error) {
console.error('导出对话失败:', error)
toast.add({
title: t('common.exportFailed'),
description: String(error),
icon: 'i-heroicons-x-circle',
color: 'error',
duration: 2000,
})
toast.fail(t('common.exportFailed'), { description: String(error) })
} finally {
isExporting.value = false
}
@@ -194,23 +170,13 @@ async function openAiLogFile() {
try {
const result = await window.aiApi.showAiLogFile()
if (!result?.success) {
toast.add({
title: t('ai.chat.statusBar.log.openFailed'),
toast.fail(t('ai.chat.statusBar.log.openFailed'), {
description: result?.error || t('ai.chat.statusBar.log.openFailedDesc'),
icon: 'i-heroicons-x-circle',
color: 'error',
duration: 2000,
})
}
} catch (error) {
console.error('打开 AI 日志失败:', error)
toast.add({
title: t('ai.chat.statusBar.log.openFailed'),
description: String(error),
icon: 'i-heroicons-x-circle',
color: 'error',
duration: 2000,
})
toast.fail(t('ai.chat.statusBar.log.openFailed'), { description: String(error) })
} finally {
isOpeningLog.value = false
}
@@ -3,7 +3,7 @@ import { ref, onMounted, watch, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { storeToRefs } from 'pinia'
import { useSkillStore, type CloudSkillItem } from '@/stores/skill'
import { useToast } from '@nuxt/ui/runtime/composables/useToast.js'
import { useToast } from '@/composables/useToast'
const { t, locale } = useI18n()
const toast = useToast()
@@ -71,9 +71,9 @@ async function handleImportCloud(item: CloudSkillItem) {
try {
const result = await skillStore.importFromCloud(item)
if (result.success) {
toast.add({ title: t('ai.skill.market.importSuccess'), color: 'success' })
toast.success(t('ai.skill.market.importSuccess'))
} else {
toast.add({ title: t('ai.skill.market.importFailed'), description: result.error, color: 'error' })
toast.fail(t('ai.skill.market.importFailed'), { description: result.error })
}
} finally {
const next = new Set(importingIds.value)
@@ -88,14 +88,14 @@ async function handleReimportCloud(item: CloudSkillItem) {
try {
const deleteResult = await skillStore.deleteSkill(item.id)
if (!deleteResult.success) {
toast.add({ title: t('ai.skill.market.importFailed'), description: deleteResult.error, color: 'error' })
toast.fail(t('ai.skill.market.importFailed'), { description: deleteResult.error })
return
}
const importResult = await skillStore.importFromCloud(item)
if (importResult.success) {
toast.add({ title: t('ai.skill.market.importSuccess'), color: 'success' })
toast.success(t('ai.skill.market.importSuccess'))
} else {
toast.add({ title: t('ai.skill.market.importFailed'), description: importResult.error, color: 'error' })
toast.fail(t('ai.skill.market.importFailed'), { description: importResult.error })
}
} finally {
const next = new Set(importingIds.value)
@@ -10,7 +10,7 @@
import { ref, computed, watch, toRaw } from 'vue'
import { useI18n } from 'vue-i18n'
import { useToast } from '@nuxt/ui/runtime/composables/useToast.js'
import { useToast } from '@/composables/useToast'
import { useSessionStore } from '@/stores/session'
import ConditionPanel from './ConditionPanel.vue'
import SessionPanel from './SessionPanel.vue'
@@ -274,28 +274,15 @@ async function exportFeedPack() {
const exportResult = await window.aiApi.exportFilterResultToFile(exportParams)
if (exportResult.success && exportResult.filePath) {
toast.add({
title: t('analysis.filter.exportSuccess'),
description: exportResult.filePath,
color: 'green',
icon: 'i-heroicons-check-circle',
})
toast.success(t('analysis.filter.exportSuccess'), { description: exportResult.filePath })
} else {
toast.add({
title: t('analysis.filter.exportFailed'),
toast.fail(t('analysis.filter.exportFailed'), {
description: exportResult.error || t('common.error.unknown'),
color: 'red',
icon: 'i-heroicons-x-circle',
})
}
} catch (error) {
console.error('导出失败:', error)
toast.add({
title: t('analysis.filter.exportFailed'),
description: String(error),
color: 'red',
icon: 'i-heroicons-x-circle',
})
toast.fail(t('analysis.filter.exportFailed'), { description: String(error) })
} finally {
stopExportProgressListener()
isExporting.value = false
+3 -17
View File
@@ -2,7 +2,7 @@
import { ref, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { storeToRefs } from 'pinia'
import { useToast } from '@nuxt/ui/runtime/composables/useToast.js'
import { useToast } from '@/composables/useToast'
import MarkdownIt from 'markdown-it'
import dayjs from 'dayjs'
import type { SQLResult } from './types'
@@ -240,9 +240,7 @@ async function exportResult() {
toast.add({
title: t('common.exportSuccess'),
description: filename,
icon: 'i-heroicons-check-circle',
color: 'primary',
duration: 3000,
actions: [
{
label: t('common.openFolder'),
@@ -253,23 +251,11 @@ async function exportResult() {
],
})
} else {
toast.add({
title: t('common.exportFailed'),
description: result.error,
icon: 'i-heroicons-x-circle',
color: 'error',
duration: 3000,
})
toast.fail(t('common.exportFailed'), { description: result.error })
}
} catch (err) {
console.error('导出失败:', err)
toast.add({
title: t('common.exportFailed'),
description: String(err),
icon: 'i-heroicons-x-circle',
color: 'error',
duration: 3000,
})
toast.fail(t('common.exportFailed'), { description: String(err) })
} finally {
isExporting.value = false
}
@@ -5,7 +5,7 @@
*/
import { ref, computed, watch, nextTick } from 'vue'
import { useI18n } from 'vue-i18n'
import { useToast } from '@nuxt/ui/runtime/composables/useToast.js'
import { useToast } from '@/composables/useToast'
import { useVirtualizer } from '@tanstack/vue-virtual'
import BatchSummaryModal from './BatchSummaryModal.vue'
@@ -247,18 +247,12 @@ async function generateSummary(session: ChatSessionItem, event: Event) {
allSessions.value[index] = { ...allSessions.value[index], summary: result.summary }
}
} else {
toast.add({
title: t('records.summaryFailed', '摘要生成失败'),
toast.fail(t('records.summaryFailed', '摘要生成失败'), {
description: result.error || t('records.summaryUnknownError', '未知错误'),
color: 'error',
})
}
} catch (error) {
toast.add({
title: t('records.summaryFailed', '摘要生成失败'),
description: String(error),
color: 'error',
})
toast.fail(t('records.summaryFailed', '摘要生成失败'), { description: String(error) })
} finally {
generatingSummaryIds.value.delete(session.id)
console.log('[SessionTimeline] 生成完成')
+1
View File
@@ -4,6 +4,7 @@
export { useAsyncData, useMultipleAsyncData } from './useAsyncData'
export { usePageAnchors, type AnchorItem } from './usePageAnchors'
export { useAIChat, type ChatMessage, type SourceMessage } from './useAIChat'
export { useToast } from './useToast'
export { useScreenCapture, type ScreenCaptureOptions } from './useScreenCapture'
export { useTimeSelect } from './useTimeSelect'
export { useSessionAnalysisPageBase, useSessionHeaderDescription } from './useSessionAnalysisPageBase'
+4 -26
View File
@@ -4,7 +4,7 @@
*/
import { ref } from 'vue'
import { captureAsImageData } from '@/utils/snapCapture'
import { useToast } from '@nuxt/ui/runtime/composables/useToast.js'
import { useToast } from '@/composables/useToast'
import { useLayoutStore } from '@/stores/layout'
/** 默认移动端最大宽度 */
@@ -53,9 +53,7 @@ export function useScreenCapture() {
toast.add({
title: '截图已保存',
description: `已保存到下载目录:${filename}`,
icon: 'i-heroicons-check-circle',
color: 'primary',
duration: 3000,
actions: [
{
label: '打开目录',
@@ -70,13 +68,7 @@ export function useScreenCapture() {
}
} catch (error) {
console.error('保存图片失败:', error)
toast.add({
title: '保存失败',
description: String(error),
icon: 'i-heroicons-x-circle',
color: 'error',
duration: 3000,
})
toast.fail('保存失败', { description: String(error) })
}
}
@@ -88,9 +80,7 @@ export function useScreenCapture() {
toast.add({
title: '截图已复制到剪贴板',
icon: 'i-heroicons-check-circle',
color: 'primary',
duration: 3000,
actions: [
{
label: '预览截图',
@@ -430,13 +420,7 @@ export function useScreenCapture() {
showSuccessToast(imageData)
} else {
// 复制失败时,直接打开预览弹窗
toast.add({
title: '截图完成',
description: '复制到剪贴板失败,请手动保存',
icon: 'i-heroicons-exclamation-triangle',
color: 'warning',
duration: 3000,
})
toast.warn('截图完成', { description: '复制到剪贴板失败,请手动保存' })
layoutStore.openScreenCaptureModal(imageData)
}
@@ -451,13 +435,7 @@ export function useScreenCapture() {
errorMessage = '页面包含无法处理的特殊字符,请尝试截屏其他区域'
}
toast.add({
title: '截屏失败',
description: errorMessage,
icon: 'i-heroicons-x-circle',
color: 'error',
duration: 3000,
})
toast.fail('截屏失败', { description: errorMessage })
return false
} finally {
// 移除水印
+64
View File
@@ -0,0 +1,64 @@
import { useToast as useNuxtToast } from '@nuxt/ui/composables'
interface AppToastOptions {
description?: string
duration?: number
}
interface AppToastPayload extends AppToastOptions {
title: string
color?: 'primary' | 'success' | 'warning' | 'error' | 'neutral'
}
const DEFAULT_TOAST_DURATION = 2000
export function useToast() {
const toast = useNuxtToast()
function add(payload: AppToastPayload) {
toast.add({
...payload,
duration: payload.duration ?? DEFAULT_TOAST_DURATION,
})
}
function success(title: string, options: AppToastOptions = {}) {
add({
title,
color: 'success',
...options,
})
}
function fail(title: string, options: AppToastOptions = {}) {
add({
title,
color: 'error',
...options,
})
}
function info(title: string, options: AppToastOptions = {}) {
add({
title,
color: 'primary',
...options,
})
}
function warn(title: string, options: AppToastOptions = {}) {
add({
title,
color: 'warning',
...options,
})
}
return {
add,
success,
fail,
info,
warn,
}
}
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { useToast } from '@nuxt/ui/runtime/composables/useToast.js'
import { useToast } from '@/composables/useToast'
interface LegacyPromptStoreData {
customPromptPresets?: Array<{
@@ -79,20 +79,9 @@ async function handleCopyJson() {
try {
await navigator.clipboard.writeText(formattedPromptStoreJson.value)
toast.add({
title: t('settings.aiPrompt.legacyPrompt.copySuccess'),
color: 'primary',
icon: 'i-heroicons-clipboard-document-check',
duration: 2000,
})
toast.success(t('settings.aiPrompt.legacyPrompt.copySuccess'))
} catch (error) {
toast.add({
title: t('settings.aiPrompt.legacyPrompt.copyFailed'),
description: String(error),
color: 'error',
icon: 'i-heroicons-x-circle',
duration: 3000,
})
toast.fail(t('settings.aiPrompt.legacyPrompt.copyFailed'), { description: String(error) })
}
}
@@ -2,7 +2,7 @@
import { ref, computed, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { storeToRefs } from 'pinia'
import { useToast } from '@nuxt/ui/runtime/composables/useToast.js'
import { useToast } from '@/composables/useToast'
import { useSessionStore } from '@/stores/session'
import type { AnalysisSession } from '@/types/base'
import dayjs from 'dayjs'
@@ -296,18 +296,10 @@ async function executeMerge() {
showMergeModal.value = false
// 提示成功
toast.add({
title: t('tools.batchManage.mergeSuccess', { count: selectedSessionIds.length }),
icon: 'i-heroicons-check-circle',
color: 'success',
})
toast.success(t('tools.batchManage.mergeSuccess', { count: selectedSessionIds.length }))
} catch (error) {
console.error('[BatchDelete] 合并失败:', error)
toast.add({
title: t('tools.batchManage.mergeError', { error: String(error) }),
icon: 'i-heroicons-exclamation-circle',
color: 'error',
})
toast.fail(t('tools.batchManage.mergeError', { error: String(error) }))
// 清理临时文件
if (tempFiles.length > 0) {