feat(ui): 添加全局Snackbar组件并替换局部实现
将原本分散在各组件的Snackbar功能统一为全局组件,通过Pinia store管理状态 移除CreateTemplateScreen中的局部Snackbar实现,改用全局组件 添加Snackbar store提供多种消息类型方法(success/error/warning/info)
This commit is contained in:
1 parent
55a85e7111
commit
ae20a30616
5 files changed
+191
-35
No files matched your search
@@ -0,0 +1,94 @@
|
|||||||
|
# IFLOW 上下文文档
|
||||||
|
|
||||||
|
## 项目概述
|
||||||
|
|
||||||
|
这是一个基于 **Vite + Vue 3 + Electron** 的桌面应用程序开发模板,名为 **Vutron**。它提供了一个快速搭建跨平台桌面应用的环境,内置了热重载、TypeScript 支持、多语言支持、主题切换(暗黑/明亮)、全局状态管理(Pinia)等功能。
|
||||||
|
|
||||||
|
项目使用 `pnpm` 作为包管理器,主要技术栈包括:
|
||||||
|
|
||||||
|
- 前端框架:Vue 3 (Composition API)
|
||||||
|
- 构建工具:Vite
|
||||||
|
- 桌面应用框架:Electron
|
||||||
|
- UI 库:Vuetify
|
||||||
|
- 状态管理:Pinia
|
||||||
|
- 国际化:vue-i18n
|
||||||
|
- 自动更新:electron-updater
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── main/ # Electron 主进程代码
|
||||||
|
│ ├── AutoUpdater.ts # 自动更新功能
|
||||||
|
│ ├── IPCs.ts # IPC 通信处理
|
||||||
|
│ ├── MainRunner.ts # 主窗口和错误窗口创建
|
||||||
|
│ ├── index.dev.ts # 开发环境入口
|
||||||
|
│ ├── index.ts # 生产环境入口
|
||||||
|
│ ├── tray.ts # 系统托盘功能
|
||||||
|
│ └── utils/
|
||||||
|
│ └── Constants.ts # 常量定义
|
||||||
|
├── preload/ # Electron 预加载脚本
|
||||||
|
├── public/ # 静态资源
|
||||||
|
└── renderer/ # 渲染进程(Vue 应用)
|
||||||
|
├── App.vue # 根组件
|
||||||
|
├── main.ts # Vue 应用入口
|
||||||
|
├── components/ # 公共组件
|
||||||
|
├── screens/ # 页面组件
|
||||||
|
├── plugins/ # 插件配置(vuetify, i18n, pinia)
|
||||||
|
├── router/ # 路由配置
|
||||||
|
├── store/ # Pinia 状态管理
|
||||||
|
├── locales/ # 语言包
|
||||||
|
└── utils/ # 工具函数
|
||||||
|
|
||||||
|
buildAssets/ # 构建相关资源
|
||||||
|
docs/ # 项目文档
|
||||||
|
tests/ # 测试文件
|
||||||
|
```
|
||||||
|
|
||||||
|
## 构建和运行
|
||||||
|
|
||||||
|
### 开发环境
|
||||||
|
|
||||||
|
1. 安装依赖:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 启动开发服务器:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
这将启动 Vite 开发服务器,并在 Electron 中打开应用。
|
||||||
|
|
||||||
|
3. 构建项目:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm build
|
||||||
|
```
|
||||||
|
|
||||||
|
构建应用到 `dist/` 目录,并使用 `electron-builder` 打包成可执行文件。
|
||||||
|
|
||||||
|
4. 运行测试:
|
||||||
|
```bash
|
||||||
|
pnpm test
|
||||||
|
```
|
||||||
|
运行 Playwright 自动化测试。
|
||||||
|
|
||||||
|
### 生产环境
|
||||||
|
|
||||||
|
构建后的应用位于 `release/` 目录下,根据不同平台生成对应的安装包。
|
||||||
|
|
||||||
|
## 开发约定
|
||||||
|
|
||||||
|
- 使用 TypeScript 进行开发,类型检查通过 `vue-tsc` 完成。
|
||||||
|
- 代码风格遵循 ESLint 和 Prettier 规范。
|
||||||
|
- 使用 Pinia 进行全局状态管理。
|
||||||
|
- 国际化支持通过 vue-i18n 实现。
|
||||||
|
- 主进程与渲染进程通过 IPC 进行通信。
|
||||||
|
- 日志记录使用 `electron-log` 模块。
|
||||||
|
- 自动更新功能通过 `electron-updater` 实现。
|
||||||
|
|
||||||
|
如果有修改代码,任务结束前需要先运行 `pnpm typecheck` 检查类型错误。不要直接运行 `pnpm dev` 或 `pnpm build`。
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
import { DefaultLayout } from '@/renderer/components/layout'
|
import { DefaultLayout } from '@/renderer/components/layout'
|
||||||
import GlobalDialog from '@/renderer/components/GlobalDialog.vue'
|
import GlobalDialog from '@/renderer/components/GlobalDialog.vue'
|
||||||
import UpdateDialog from '@/renderer/components/UpdateDialog.vue'
|
import UpdateDialog from '@/renderer/components/UpdateDialog.vue'
|
||||||
|
import GlobalSnackbar from '@/renderer/components/GlobalSnackbar.vue'
|
||||||
import { onMounted } from 'vue'
|
import { onMounted } from 'vue'
|
||||||
import { useAppStore } from '@/renderer/store/app'
|
import { useAppStore } from '@/renderer/store/app'
|
||||||
|
|
||||||
@@ -75,6 +76,7 @@ const checkForUpdates = async () => {
|
|||||||
</DefaultLayout>
|
</DefaultLayout>
|
||||||
<GlobalDialog />
|
<GlobalDialog />
|
||||||
<UpdateDialog />
|
<UpdateDialog />
|
||||||
|
<GlobalSnackbar />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<template>
|
||||||
|
<v-snackbar
|
||||||
|
v-model="snackbar.isVisible"
|
||||||
|
:color="snackbar.color"
|
||||||
|
:timeout="snackbar.timeout"
|
||||||
|
location="top"
|
||||||
|
>
|
||||||
|
{{ snackbar.message }}
|
||||||
|
<template #actions>
|
||||||
|
<v-btn
|
||||||
|
v-for="(action, index) in snackbar.actions"
|
||||||
|
:key="index"
|
||||||
|
:color="action.color || 'white'"
|
||||||
|
variant="text"
|
||||||
|
@click="action.handler && action.handler()"
|
||||||
|
>
|
||||||
|
{{ action.text }}
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
variant="text"
|
||||||
|
@click="snackbar.hide"
|
||||||
|
>
|
||||||
|
<v-icon>mdi-close</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
</v-snackbar>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useSnackbarStore } from '@/renderer/store/snackbar'
|
||||||
|
|
||||||
|
const snackbar = useSnackbarStore()
|
||||||
|
</script>
|
||||||
@@ -255,23 +255,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Snackbar for error messages -->
|
|
||||||
<v-snackbar
|
|
||||||
v-model="snackbarVisible"
|
|
||||||
:color="snackbarColor"
|
|
||||||
:timeout="2000"
|
|
||||||
location="top"
|
|
||||||
>
|
|
||||||
{{ snackbarMessage }}
|
|
||||||
<template #actions>
|
|
||||||
<v-btn
|
|
||||||
icon
|
|
||||||
@click="snackbarVisible = false"
|
|
||||||
>
|
|
||||||
<v-icon>mdi-close</v-icon>
|
|
||||||
</v-btn>
|
|
||||||
</template>
|
|
||||||
</v-snackbar>
|
|
||||||
</v-container>
|
</v-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -279,12 +263,14 @@
|
|||||||
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { useTemplateStore, TreeNode } from '@/renderer/store/template'
|
import { useTemplateStore, TreeNode } from '@/renderer/store/template'
|
||||||
|
import { useSnackbarStore } from '@/renderer/store/snackbar'
|
||||||
import TemplateTree from '@/renderer/components/TemplateTree.vue'
|
import TemplateTree from '@/renderer/components/TemplateTree.vue'
|
||||||
import { ConfirmDialogService } from '@/renderer/utils/confirm-dialog-service'
|
import { ConfirmDialogService } from '@/renderer/utils/confirm-dialog-service'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const templateStore = useTemplateStore()
|
const templateStore = useTemplateStore()
|
||||||
|
const snackbar = useSnackbarStore()
|
||||||
|
|
||||||
// Extract methods from store
|
// Extract methods from store
|
||||||
const { getTemplateById } = templateStore
|
const { getTemplateById } = templateStore
|
||||||
@@ -305,10 +291,7 @@ const fileFormValid = ref(false)
|
|||||||
const directoryInputRef = ref<HTMLInputElement | null>(null)
|
const directoryInputRef = ref<HTMLInputElement | null>(null)
|
||||||
const fileInputRef = ref<HTMLInputElement | null>(null)
|
const fileInputRef = ref<HTMLInputElement | null>(null)
|
||||||
|
|
||||||
// Snackbar variables
|
|
||||||
const snackbarVisible = ref(false)
|
|
||||||
const snackbarMessage = ref('')
|
|
||||||
const snackbarColor = ref('error') // Can be 'error', 'success', 'warning', etc.
|
|
||||||
|
|
||||||
// New item state
|
// New item state
|
||||||
const newDirectoryPath = ref('')
|
const newDirectoryPath = ref('')
|
||||||
@@ -460,7 +443,7 @@ const confirmAddDirectory = () => {
|
|||||||
(child) => child.name === newDirectoryPath.value
|
(child) => child.name === newDirectoryPath.value
|
||||||
)
|
)
|
||||||
if (duplicateExists) {
|
if (duplicateExists) {
|
||||||
showSnackbar('当前目录下已存在同名节点!', 'error')
|
snackbar.error('当前目录下已存在同名节点!')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -470,7 +453,7 @@ const confirmAddDirectory = () => {
|
|||||||
(node) => node.name === newDirectoryPath.value
|
(node) => node.name === newDirectoryPath.value
|
||||||
)
|
)
|
||||||
if (duplicateExists) {
|
if (duplicateExists) {
|
||||||
showSnackbar('根目录下已存在同名节点!', 'error')
|
snackbar.error('根目录下已存在同名节点!')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -636,7 +619,7 @@ const selectAndAddFile = async () => {
|
|||||||
(child) => child.name === newFilePath.value
|
(child) => child.name === newFilePath.value
|
||||||
)
|
)
|
||||||
if (duplicateExists) {
|
if (duplicateExists) {
|
||||||
showSnackbar('当前目录下已存在同名节点!', 'error')
|
snackbar.error('当前目录下已存在同名节点!')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -646,7 +629,7 @@ const selectAndAddFile = async () => {
|
|||||||
(node) => node.name === newFilePath.value
|
(node) => node.name === newFilePath.value
|
||||||
)
|
)
|
||||||
if (duplicateExists) {
|
if (duplicateExists) {
|
||||||
showSnackbar('根目录下已存在同名节点!', 'error')
|
snackbar.error('根目录下已存在同名节点!')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -654,7 +637,7 @@ const selectAndAddFile = async () => {
|
|||||||
// Also check for duplicates using the full path (for backward compatibility)
|
// Also check for duplicates using the full path (for backward compatibility)
|
||||||
const hasDuplicate = checkForDuplicateInTree(treeStructure.value, fullPath)
|
const hasDuplicate = checkForDuplicateInTree(treeStructure.value, fullPath)
|
||||||
if (hasDuplicate) {
|
if (hasDuplicate) {
|
||||||
showSnackbar('该文件路径已存在!')
|
snackbar.error('该文件路径已存在!')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -681,7 +664,7 @@ const selectAndAddFile = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (duplicateNameExists) {
|
if (duplicateNameExists) {
|
||||||
showSnackbar('当前目录下已存在同名节点!', 'error')
|
snackbar.error('当前目录下已存在同名节点!')
|
||||||
} else {
|
} else {
|
||||||
const newFileNode: TreeNode = {
|
const newFileNode: TreeNode = {
|
||||||
id: newId,
|
id: newId,
|
||||||
@@ -722,7 +705,7 @@ const selectAndAddFile = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, show file selection dialog
|
// Otherwise, show file selection dialog
|
||||||
showSnackbar('请先选择一个文件或文件夹', 'warning')
|
snackbar.warning('请先选择一个文件或文件夹')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -818,7 +801,7 @@ const editItem = (item: TreeItem) => {
|
|||||||
showEditFileDialog(nodeData)
|
showEditFileDialog(nodeData)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
showSnackbar('无法找到对应的节点', 'error')
|
snackbar.error('无法找到对应的节点')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -916,12 +899,7 @@ const removeItemFromTree = (nodes: TreeNode[], itemId: string): TreeNode[] => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show snackbar message
|
|
||||||
const showSnackbar = (message: string, color: string = 'error') => {
|
|
||||||
snackbarMessage.value = message
|
|
||||||
snackbarColor.value = color
|
|
||||||
snackbarVisible.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Methods for file/folder selection
|
// Methods for file/folder selection
|
||||||
const selectSourceFileOrFolder = async () => {
|
const selectSourceFileOrFolder = async () => {
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
export interface SnackbarAction {
|
||||||
|
text: string
|
||||||
|
color?: string
|
||||||
|
handler?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useSnackbarStore = defineStore('snackbar', () => {
|
||||||
|
const isVisible = ref(false)
|
||||||
|
const message = ref('')
|
||||||
|
const color = ref('info')
|
||||||
|
const timeout = ref(3000)
|
||||||
|
const actions = ref<SnackbarAction[]>([])
|
||||||
|
|
||||||
|
const show = (msg: string, clr = 'info', tout = 3000, actionList: SnackbarAction[] = []) => {
|
||||||
|
message.value = msg
|
||||||
|
color.value = clr
|
||||||
|
timeout.value = tout
|
||||||
|
actions.value = actionList
|
||||||
|
isVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const success = (msg: string, tout = 3000) => show(msg, 'success', tout)
|
||||||
|
const error = (msg: string, tout = 3000) => show(msg, 'error', tout)
|
||||||
|
const warning = (msg: string, tout = 3000) => show(msg, 'warning', tout)
|
||||||
|
const info = (msg: string, tout = 3000) => show(msg, 'info', tout)
|
||||||
|
|
||||||
|
const hide = () => {
|
||||||
|
isVisible.value = false
|
||||||
|
actions.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isVisible,
|
||||||
|
message,
|
||||||
|
color,
|
||||||
|
timeout,
|
||||||
|
actions,
|
||||||
|
show,
|
||||||
|
success,
|
||||||
|
error,
|
||||||
|
warning,
|
||||||
|
info,
|
||||||
|
hide
|
||||||
|
}
|
||||||
|
})
|
||||||
Reference in new issue
Block a user