feat: 添加 @vueuse/core 依赖并重构模板树组件,优化创建模板界面

This commit is contained in:
zhanggy committed 2025-10-27 23:28:22 +08:00
1 parent 2f85832b89
commit 87ce5cb24f
5 files changed
+306 -156

No files matched your search

+1
View File
@@ -43,6 +43,7 @@
}, },
"dependencies": { "dependencies": {
"@mdi/font": "^7.4.47", "@mdi/font": "^7.4.47",
"@vueuse/core": "^14.0.0",
"electron-log": "^5.4.3", "electron-log": "^5.4.3",
"fs-extra": "^11.3.2", "fs-extra": "^11.3.2",
"pinia": "^3.0.3", "pinia": "^3.0.3",
+38 -4
View File
@@ -11,6 +11,9 @@ importers:
'@mdi/font': '@mdi/font':
specifier: ^7.4.47 specifier: ^7.4.47
version: 7.4.47 version: 7.4.47
'@vueuse/core':
specifier: ^14.0.0
version: 14.0.0(vue@3.5.22(typescript@5.9.3))
electron-log: electron-log:
specifier: ^5.4.3 specifier: ^5.4.3
version: 5.4.3 version: 5.4.3
@@ -286,8 +289,8 @@ packages:
resolution: {integrity: sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==} resolution: {integrity: sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==}
engines: {node: '>=12'} engines: {node: '>=12'}
'@electron/node-gyp@git+https://git@github.com:electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2': '@electron/node-gyp@https://codeload.github.com/electron/node-gyp/tar.gz/06b29aafb7708acef8b3669835c8a7857ebc92d2':
resolution: {commit: 06b29aafb7708acef8b3669835c8a7857ebc92d2, repo: git@github.com:electron/node-gyp.git, type: git} resolution: {tarball: https://codeload.github.com/electron/node-gyp/tar.gz/06b29aafb7708acef8b3669835c8a7857ebc92d2}
version: 10.2.0-electron.1 version: 10.2.0-electron.1
engines: {node: '>=12.13.0'} engines: {node: '>=12.13.0'}
hasBin: true hasBin: true
@@ -802,6 +805,9 @@ packages:
'@types/verror@1.10.11': '@types/verror@1.10.11':
resolution: {integrity: sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==} resolution: {integrity: sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==}
'@types/web-bluetooth@0.0.21':
resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==}
'@types/yauzl@2.10.3': '@types/yauzl@2.10.3':
resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==}
@@ -976,6 +982,19 @@ packages:
vue: ^3.0.0 vue: ^3.0.0
vuetify: ^3.0.0 vuetify: ^3.0.0
'@vueuse/core@14.0.0':
resolution: {integrity: sha512-d6tKRWkZE8IQElX2aHBxXOMD478fHIYV+Dzm2y9Ag122ICBpNKtGICiXKOhWU3L1kKdttDD9dCMS4bGP3jhCTQ==}
peerDependencies:
vue: ^3.5.0
'@vueuse/metadata@14.0.0':
resolution: {integrity: sha512-6yoGqbJcMldVCevkFiHDBTB1V5Hq+G/haPlGIuaFZHpXC0HADB0EN1ryQAAceiW+ryS3niUwvdFbGiqHqBrfVA==}
'@vueuse/shared@14.0.0':
resolution: {integrity: sha512-mTCA0uczBgurRlwVaQHfG0Ja7UdGe4g9mwffiJmvLiTtp1G4AQyIjej6si/k8c8pUwTfVpNufck+23gXptPAkw==}
peerDependencies:
vue: ^3.5.0
'@xmldom/xmldom@0.8.10': '@xmldom/xmldom@0.8.10':
resolution: {integrity: sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==} resolution: {integrity: sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==}
engines: {node: '>=10.0.0'} engines: {node: '>=10.0.0'}
@@ -3403,7 +3422,7 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@electron/node-gyp@git+https://git@github.com:electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2': '@electron/node-gyp@https://codeload.github.com/electron/node-gyp/tar.gz/06b29aafb7708acef8b3669835c8a7857ebc92d2':
dependencies: dependencies:
env-paths: 2.2.1 env-paths: 2.2.1
exponential-backoff: 3.1.2 exponential-backoff: 3.1.2
@@ -3440,7 +3459,7 @@ snapshots:
'@electron/rebuild@3.7.2': '@electron/rebuild@3.7.2':
dependencies: dependencies:
'@electron/node-gyp': git+https://git@github.com:electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2 '@electron/node-gyp': https://codeload.github.com/electron/node-gyp/tar.gz/06b29aafb7708acef8b3669835c8a7857ebc92d2
'@malept/cross-spawn-promise': 2.0.0 '@malept/cross-spawn-promise': 2.0.0
chalk: 4.1.2 chalk: 4.1.2
debug: 4.4.0 debug: 4.4.0
@@ -3853,6 +3872,8 @@ snapshots:
'@types/verror@1.10.11': '@types/verror@1.10.11':
optional: true optional: true
'@types/web-bluetooth@0.0.21': {}
'@types/yauzl@2.10.3': '@types/yauzl@2.10.3':
dependencies: dependencies:
'@types/node': 22.15.15 '@types/node': 22.15.15
@@ -4136,6 +4157,19 @@ snapshots:
vue: 3.5.22(typescript@5.9.3) vue: 3.5.22(typescript@5.9.3)
vuetify: 3.10.5(typescript@5.9.3)(vite-plugin-vuetify@2.1.2)(vue@3.5.22(typescript@5.9.3)) vuetify: 3.10.5(typescript@5.9.3)(vite-plugin-vuetify@2.1.2)(vue@3.5.22(typescript@5.9.3))
'@vueuse/core@14.0.0(vue@3.5.22(typescript@5.9.3))':
dependencies:
'@types/web-bluetooth': 0.0.21
'@vueuse/metadata': 14.0.0
'@vueuse/shared': 14.0.0(vue@3.5.22(typescript@5.9.3))
vue: 3.5.22(typescript@5.9.3)
'@vueuse/metadata@14.0.0': {}
'@vueuse/shared@14.0.0(vue@3.5.22(typescript@5.9.3))':
dependencies:
vue: 3.5.22(typescript@5.9.3)
'@xmldom/xmldom@0.8.10': {} '@xmldom/xmldom@0.8.10': {}
abbrev@1.1.1: {} abbrev@1.1.1: {}
+224
View File
@@ -0,0 +1,224 @@
<template>
<v-card
variant="outlined"
class="pa-2"
:height="height"
@contextmenu.prevent="emit('showContextMenu', $event, null)"
>
<v-treeview
v-model:expanded="localExpanded"
:items="items"
item-title="name"
item-key="id"
item-children="children"
activatable
open-on-click
:return-object="false"
>
<template #prepend="{ item }">
<v-icon
:color="item.type === 'directory' ? 'warning' : 'info'"
size="large"
>
{{
item.type === 'directory'
? 'mdi-folder-outline'
: 'mdi-file-document-outline'
}}
</v-icon>
</template>
<template #title="{ item }">
<span
@contextmenu.prevent="emit('showContextMenu', $event, item)"
class="cursor-pointer d-flex align-center"
>
{{ item.name }}
<v-btn
v-if="!readOnly && item.type === 'directory'"
icon
variant="text"
size="small"
class="ml-2"
@click.stop="emit('addNewItemToCurrentDirectory', item)"
>
<v-icon size="small">mdi-plus</v-icon>
<v-tooltip
v-if="!readOnly"
activator="parent"
location="top"
>
添加到目录
</v-tooltip>
</v-btn>
</span>
</template>
<template #append="{ item }">
<div
v-if="!readOnly"
class="d-flex align-center ga-1"
>
<v-btn
v-if="item.type === 'file'"
icon
variant="text"
size="small"
@click.stop="emit('editFile', item)"
>
<v-icon size="small"> mdi-pencil </v-icon>
<v-tooltip
activator="parent"
location="top"
>
编辑内容
</v-tooltip>
</v-btn>
<v-btn
icon
variant="text"
size="small"
@click.stop="emit('deleteItem', item)"
>
<v-icon
v-if="!readOnly"
size="small"
color="error"
>
mdi-delete
</v-icon>
<v-tooltip
activator="parent"
location="top"
>
删除
</v-tooltip>
</v-btn>
</div>
</template>
</v-treeview>
<div
v-if="items.length === 0"
class="text-center pa-4"
>
<v-icon
size="48"
color="grey-lighten-1"
>
mdi-folder-open-outline
</v-icon>
<p class="text-body-2 mt-2">
{{ emptyText }}
</p>
<p
v-if="!readOnly"
class="text-caption"
>
右键点击空白区域添加根目录或文件
</p>
</div>
</v-card>
<!-- Context Menu -->
<v-menu
v-if="!readOnly"
v-model="localShowContextMenuVar"
:position-x="contextMenuX"
:position-y="contextMenuY"
absolute
>
<v-list density="compact">
<v-list-item @click="emit('addDirectory', currentContextItem)">
<template #prepend>
<v-icon>mdi-folder-plus</v-icon>
</template>
<v-list-item-title>
{{ currentContextItem ? '添加子目录' : '添加根目录' }}
</v-list-item-title>
</v-list-item>
<v-list-item @click="emit('addFile', currentContextItem)">
<template #prepend>
<v-icon>mdi-file-plus</v-icon>
</template>
<v-list-item-title>
{{ currentContextItem ? '添加子文件' : '添加根文件' }}
</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</template>
<script setup lang="ts">
import { useVModel } from '@vueuse/core'
interface TreeItem {
id: string
name: string
type: 'directory' | 'file'
children?: TreeItem[]
}
interface Props {
items: TreeItem[]
height?: number
readOnly?: boolean
emptyText?: string
expanded?: string[]
showContextMenuVar?: boolean
contextMenuX?: number
contextMenuY?: number
currentContextItem?: TreeItem | null
}
const props = withDefaults(defineProps<Props>(), {
height: 600,
readOnly: false,
emptyText: '暂无文件或目录',
expanded: () => [],
showContextMenuVar: false,
contextMenuX: 0,
contextMenuY: 0,
currentContextItem: null
})
const emit = defineEmits([
'showContextMenu',
'addNewItemToCurrentDirectory',
'editFile',
'deleteItem',
'addDirectory',
'addFile',
'update:showContextMenuVar',
'update:expanded'
])
// Use useVModel to create two-way binding for props
const localShowContextMenuVar = useVModel(props, 'showContextMenuVar', emit)
const localExpanded = useVModel(props, 'expanded', emit)
</script>
<style scoped>
.cursor-pointer {
cursor: pointer;
}
:deep(.v-treeview-node__root),
:deep(.v-treeview-node__children .v-treeview-node__root) {
padding: 4px;
margin: 2px 0;
border-radius: 4px;
transition: background-color 0.2s;
}
:deep(.v-treeview-node__root:hover),
:deep(.v-treeview-node__children .v-treeview-node__root:hover) {
background-color: rgba(0, 0, 0, 0.04);
}
:deep(.v-treeview-node__level) {
margin-right: 4px;
}
</style>
+28 -138
View File
@@ -138,143 +138,25 @@
</v-btn> </v-btn>
</div> </div>
<v-card <TemplateTree
variant="outlined" :items="treeItems"
class="pa-2"
:height="treeHeight" :height="treeHeight"
@contextmenu.prevent="showContextMenu($event, null)" :expanded="expandedNodes"
> :show-context-menu-var="showContextMenuVar"
<v-treeview :context-menu-x="contextMenuX"
v-model:expanded="expandedNodes" :context-menu-y="contextMenuY"
:items="treeItems" :current-context-item="currentContextItem"
item-title="name" :empty-text="'暂无文件或目录'"
item-key="id" :read-only="false"
item-children="children" @showContextMenu="showContextMenu"
activatable @add-new-item-to-current-directory="addNewItemToCurrentDirectory"
open-on-click @edit-file="editFile"
:return-object="false" @delete-item="deleteItem"
> @add-directory="addDirectory"
<template #prepend="{ item }"> @add-file="addFile"
<v-icon @update:showContextMenuVar="handleContextMenuUpdate"
:color="item.type === 'directory' ? 'warning' : 'info'" @update:expanded="expandedNodes = $event"
size="large" />
>
{{
item.type === 'directory'
? 'mdi-folder-outline'
: 'mdi-file-document-outline'
}}
</v-icon>
</template>
<template #title="{ item }">
<span
@contextmenu.prevent="showContextMenu($event, item)"
class="cursor-pointer d-flex align-center"
>
{{ item.name }}
<v-btn
v-if="item.type === 'directory'"
icon
variant="text"
size="small"
class="ml-2"
@click.stop="addNewItemToCurrentDirectory(item)"
>
<v-icon size="small">mdi-plus</v-icon>
<v-tooltip
activator="parent"
location="top"
>
添加到目录
</v-tooltip>
</v-btn>
</span>
</template>
<template #append="{ item }">
<div class="d-flex align-center ga-1">
<v-btn
v-if="item.type === 'file'"
icon
variant="text"
size="small"
@click.stop="editFile(item)"
>
<v-icon size="small"> mdi-pencil </v-icon>
<v-tooltip
activator="parent"
location="top"
>
编辑内容
</v-tooltip>
</v-btn>
<v-btn
icon
variant="text"
size="small"
@click.stop="deleteItem(item)"
>
<v-icon
size="small"
color="error"
>
mdi-delete
</v-icon>
<v-tooltip
activator="parent"
location="top"
>
删除
</v-tooltip>
</v-btn>
</div>
</template>
</v-treeview>
<div
v-if="treeItems.length === 0"
class="text-center pa-4"
>
<v-icon
size="48"
color="grey-lighten-1"
>
mdi-folder-open-outline
</v-icon>
<p class="text-body-2 mt-2"> 暂无文件或目录 </p>
<p class="text-caption"> 右键点击空白区域添加根目录或文件 </p>
</div>
</v-card>
<!-- Context Menu -->
<v-menu
v-model="showContextMenuVar"
:position-x="contextMenuX"
:position-y="contextMenuY"
absolute
>
<v-list density="compact">
<v-list-item @click="addDirectory(currentContextItem)">
<template #prepend>
<v-icon>mdi-folder-plus</v-icon>
</template>
<v-list-item-title>
{{ currentContextItem ? '添加子目录' : '添加根目录' }}
</v-list-item-title>
</v-list-item>
<v-list-item @click="addFile(currentContextItem)">
<template #prepend>
<v-icon>mdi-file-plus</v-icon>
</template>
<v-list-item-title>
{{ currentContextItem ? '添加子文件' : '添加根文件' }}
</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</v-col> </v-col>
</v-row> </v-row>
@@ -295,6 +177,7 @@
required required
density="comfortable" density="comfortable"
ref="directoryInputRef" ref="directoryInputRef"
@keydown.enter="confirmAddDirectory"
/> />
</v-form> </v-form>
</v-card-text> </v-card-text>
@@ -329,6 +212,7 @@
required required
density="comfortable" density="comfortable"
ref="fileInputRef" ref="fileInputRef"
@keydown.enter="confirmAddFile"
/> />
</v-form> </v-form>
</v-card-text> </v-card-text>
@@ -394,6 +278,7 @@ import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { useTemplateStore } from '@/renderer/store/template' import { useTemplateStore } from '@/renderer/store/template'
import { Template } from '@/renderer/store/template' import { Template } from '@/renderer/store/template'
import TemplateTree from '@/renderer/components/TemplateTree.vue'
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
@@ -425,6 +310,11 @@ const contextMenuX = ref(0)
const contextMenuY = ref(0) const contextMenuY = ref(0)
const currentContextItem = ref<TreeItem | null>(null) const currentContextItem = ref<TreeItem | null>(null)
// Handler for context menu visibility updates from TemplateTree component
const handleContextMenuUpdate = (value: boolean) => {
showContextMenuVar.value = value
}
// New item state // New item state
const newDirectoryPath = ref('') const newDirectoryPath = ref('')
const newFilePath = ref('') const newFilePath = ref('')
@@ -677,14 +567,14 @@ const deleteItem = (item: TreeItem) => {
const autoFocusDirectoryInput = async () => { const autoFocusDirectoryInput = async () => {
await nextTick() await nextTick()
if (directoryInputRef.value) { if (directoryInputRef.value) {
(directoryInputRef.value as HTMLInputElement).focus() ;(directoryInputRef.value as HTMLInputElement).focus()
} }
} }
const autoFocusFileInput = async () => { const autoFocusFileInput = async () => {
await nextTick() await nextTick()
if (fileInputRef.value) { if (fileInputRef.value) {
(fileInputRef.value as HTMLInputElement).focus() ;(fileInputRef.value as HTMLInputElement).focus()
} }
} }
+15 -14
View File
@@ -98,18 +98,14 @@
<v-spacer /> <v-spacer />
</v-card-title> </v-card-title>
<v-card-text> <v-card-text>
<v-treeview :items="templateTree" item-title="name" item-key="id" item-children="children" activatable <TemplateTree
open-all> :items="templateTree"
<template v-slot:prepend="{ item }"> :height="400"
<v-icon> :read-only="true"
{{ :empty-text="'模板结构为空'"
item.type === 'directory' :expanded="expandedTreeNodes"
? 'mdi-folder' @update:expanded="expandedTreeNodes = $event"
: 'mdi-file' />
}}
</v-icon>
</template>
</v-treeview>
</v-card-text> </v-card-text>
</v-card> </v-card>
</v-dialog> </v-dialog>
@@ -144,6 +140,7 @@ import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useTemplateStore } from '@/renderer/store/template' import { useTemplateStore } from '@/renderer/store/template'
import { Template } from '@/renderer/store/template' import { Template } from '@/renderer/store/template'
import TemplateTree from '@/renderer/components/TemplateTree.vue'
const router = useRouter() const router = useRouter()
const templateStore = useTemplateStore() const templateStore = useTemplateStore()
@@ -160,6 +157,7 @@ const templates = computed(() => templateStore.templates)
// Template detail view // Template detail view
const showTemplateDetail = ref(false) const showTemplateDetail = ref(false)
const selectedTemplate = ref<Template | null>(null) const selectedTemplate = ref<Template | null>(null)
const expandedTreeNodes = ref<string[]>([])
// Execute template // Execute template
const showExecuteDialog = ref(false) const showExecuteDialog = ref(false)
@@ -184,7 +182,7 @@ const templateTree = computed<TreeNode[]>(() => {
selectedTemplate.value.directories.forEach((dir) => { selectedTemplate.value.directories.forEach((dir) => {
items.push({ items.push({
id: `dir-${dir.path}`, id: `dir-${dir.path}`,
name: dir.path, name: dir.path.split('/').pop() || dir.path, // Show just the directory name, not full path
type: 'directory', type: 'directory',
children: [] children: []
}) })
@@ -194,11 +192,14 @@ const templateTree = computed<TreeNode[]>(() => {
selectedTemplate.value.files.forEach((file) => { selectedTemplate.value.files.forEach((file) => {
items.push({ items.push({
id: `file-${file.path}`, id: `file-${file.path}`,
name: file.path, name: file.path.split('/').pop() || file.path, // Show just the file name, not full path
type: 'file' type: 'file'
}) })
}) })
// Auto-expand all nodes when showing template details
expandedTreeNodes.value = items.map(item => item.id)
return items return items
}) })