feat: add @mdi/font and electron-devtools-installer dependencies

- Added @mdi/font for Material Design Icons support.
- Included electron-devtools-installer to facilitate Vue DevTools installation in development.

refactor: enhance CreateTemplateScreen layout and functionality

- Improved layout with a responsive design.
- Added functionality for adding directories and files with validation.
- Implemented a file content editor dialog for editing file contents.
- Updated template save logic to handle changes more effectively.

fix: update TemplateListScreen for better UX

- Enhanced template list display with improved card layout.
- Added action buttons for viewing, executing, editing, and deleting templates.
- Improved dialog handling for template details and execution.

chore: update vuetify plugin configuration

- Removed unused icon set configuration.
- Updated locale settings to use simplified Chinese.

style: clean up HeaderLayout and vuetify styles

- Removed commented-out code and unnecessary styles.
- Improved overall styling for better readability and consistency.
This commit is contained in:
zhanggy committed 2025-10-27 18:19:39 +08:00
1 parent 1aecd5f4b6
commit 4f859a4933
8 files changed
+609 -350

No files matched your search

+4 -1
View File
@@ -21,5 +21,8 @@
"*.mjs": "javascript", "*.mjs": "javascript",
"*.cjs": "javascript", "*.cjs": "javascript",
"*.mts": "typescript" "*.mts": "typescript"
} },
"i18n-ally.localesPaths": [
"src/renderer/locales"
]
} }
+2
View File
@@ -42,6 +42,7 @@
"node": ">=20.0.0" "node": ">=20.0.0"
}, },
"dependencies": { "dependencies": {
"@mdi/font": "^7.4.47",
"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",
@@ -61,6 +62,7 @@
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
"electron": "^38.3.0", "electron": "^38.3.0",
"electron-builder": "^26.0.14", "electron-builder": "^26.0.14",
"electron-devtools-installer": "^4.0.0",
"electron-extension-installer": "^2.0.0", "electron-extension-installer": "^2.0.0",
"eslint": "^9.37.0", "eslint": "^9.37.0",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
+36 -4
View File
@@ -8,6 +8,9 @@ importers:
.: .:
dependencies: dependencies:
'@mdi/font':
specifier: ^7.4.47
version: 7.4.47
electron-log: electron-log:
specifier: ^5.4.3 specifier: ^5.4.3
version: 5.4.3 version: 5.4.3
@@ -60,6 +63,9 @@ importers:
electron-builder: electron-builder:
specifier: ^26.0.14 specifier: ^26.0.14
version: 26.0.15(electron-builder-squirrel-windows@26.0.15) version: 26.0.15(electron-builder-squirrel-windows@26.0.15)
electron-devtools-installer:
specifier: ^4.0.0
version: 4.0.0
electron-extension-installer: electron-extension-installer:
specifier: ^2.0.0 specifier: ^2.0.0
version: 2.0.0(electron@38.3.0) version: 2.0.0(electron@38.3.0)
@@ -280,8 +286,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@https://codeload.github.com/electron/node-gyp/tar.gz/06b29aafb7708acef8b3669835c8a7857ebc92d2': '@electron/node-gyp@git+https://git@github.com:electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2':
resolution: {tarball: https://codeload.github.com/electron/node-gyp/tar.gz/06b29aafb7708acef8b3669835c8a7857ebc92d2} resolution: {commit: 06b29aafb7708acef8b3669835c8a7857ebc92d2, repo: git@github.com:electron/node-gyp.git, type: git}
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
@@ -569,6 +575,9 @@ packages:
resolution: {integrity: sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==} resolution: {integrity: sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==}
engines: {node: '>= 10.0.0'} engines: {node: '>= 10.0.0'}
'@mdi/font@7.4.47':
resolution: {integrity: sha512-43MtGpd585SNzHZPcYowu/84Vz2a2g31TvPMTm9uTiCSWzaheQySUcSyUH/46fPnuPQWof2yd0pGBtzee/IQWw==}
'@mdi/js@7.4.47': '@mdi/js@7.4.47':
resolution: {integrity: sha512-KPnNOtm5i2pMabqZxpUz7iQf+mfrYZyKCZ8QNz85czgEt7cuHcGorWfdzUMWYA0SD+a6Hn4FmJ+YhzzzjkTZrQ==} resolution: {integrity: sha512-KPnNOtm5i2pMabqZxpUz7iQf+mfrYZyKCZ8QNz85czgEt7cuHcGorWfdzUMWYA0SD+a6Hn4FmJ+YhzzzjkTZrQ==}
@@ -1387,6 +1396,9 @@ packages:
engines: {node: '>=14.0.0'} engines: {node: '>=14.0.0'}
hasBin: true hasBin: true
electron-devtools-installer@4.0.0:
resolution: {integrity: sha512-9Tntu/jtfSn0n6N/ZI6IdvRqXpDyLQiDuuIbsBI+dL+1Ef7C8J2JwByw58P3TJiNeuqyV3ZkphpNWuZK5iSY2w==}
electron-extension-installer@2.0.0: electron-extension-installer@2.0.0:
resolution: {integrity: sha512-dneHKI4zcKRiYJbdO/KP3XxzptlhUqQ/9h4WvIIg4I21rRKr7ZysoRU1vVd7Rgf695Rb5MUJXBwCY2dQm6qHQw==} resolution: {integrity: sha512-dneHKI4zcKRiYJbdO/KP3XxzptlhUqQ/9h4WvIIg4I21rRKr7ZysoRU1vVd7Rgf695Rb5MUJXBwCY2dQm6qHQw==}
peerDependencies: peerDependencies:
@@ -2945,6 +2957,9 @@ packages:
resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
engines: {node: '>= 10.0.0'} engines: {node: '>= 10.0.0'}
unzip-crx-3@0.2.0:
resolution: {integrity: sha512-0+JiUq/z7faJ6oifVB5nSwt589v1KCduqIJupNVDoWSXZtWDmjDGO3RAEOvwJ07w90aoXoP4enKsR7ecMrJtWQ==}
upath@2.0.1: upath@2.0.1:
resolution: {integrity: sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w==} resolution: {integrity: sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w==}
engines: {node: '>=4'} engines: {node: '>=4'}
@@ -3133,6 +3148,9 @@ packages:
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
engines: {node: '>=10'} engines: {node: '>=10'}
yaku@0.16.7:
resolution: {integrity: sha512-Syu3IB3rZvKvYk7yTiyl1bo/jiEFaaStrgv1V2TIJTqYPStSMQVO8EQjg/z+DRzLq/4LIIharNT3iH1hylEIRw==}
yallist@3.1.1: yallist@3.1.1:
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
@@ -3385,7 +3403,7 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@electron/node-gyp@https://codeload.github.com/electron/node-gyp/tar.gz/06b29aafb7708acef8b3669835c8a7857ebc92d2': '@electron/node-gyp@git+https://git@github.com:electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2':
dependencies: dependencies:
env-paths: 2.2.1 env-paths: 2.2.1
exponential-backoff: 3.1.2 exponential-backoff: 3.1.2
@@ -3422,7 +3440,7 @@ snapshots:
'@electron/rebuild@3.7.2': '@electron/rebuild@3.7.2':
dependencies: dependencies:
'@electron/node-gyp': https://codeload.github.com/electron/node-gyp/tar.gz/06b29aafb7708acef8b3669835c8a7857ebc92d2 '@electron/node-gyp': git+https://git@github.com:electron/node-gyp.git#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
@@ -3659,6 +3677,8 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@mdi/font@7.4.47': {}
'@mdi/js@7.4.47': {} '@mdi/js@7.4.47': {}
'@nodelib/fs.scandir@2.1.5': '@nodelib/fs.scandir@2.1.5':
@@ -4642,6 +4662,10 @@ snapshots:
- electron-builder-squirrel-windows - electron-builder-squirrel-windows
- supports-color - supports-color
electron-devtools-installer@4.0.0:
dependencies:
unzip-crx-3: 0.2.0
electron-extension-installer@2.0.0(electron@38.3.0): electron-extension-installer@2.0.0(electron@38.3.0):
dependencies: dependencies:
electron: 38.3.0 electron: 38.3.0
@@ -6481,6 +6505,12 @@ snapshots:
universalify@2.0.1: {} universalify@2.0.1: {}
unzip-crx-3@0.2.0:
dependencies:
jszip: 3.10.1
mkdirp: 0.5.6
yaku: 0.16.7
upath@2.0.1: {} upath@2.0.1: {}
update-browserslist-db@1.1.3(browserslist@4.24.5): update-browserslist-db@1.1.3(browserslist@4.24.5):
@@ -6662,6 +6692,8 @@ snapshots:
y18n@5.0.8: {} y18n@5.0.8: {}
yaku@0.16.7: {}
yallist@3.1.1: {} yallist@3.1.1: {}
yallist@4.0.0: {} yallist@4.0.0: {}
+12
View File
@@ -28,6 +28,18 @@ const initializeMainLogger = () => {
app.on('ready', async () => { app.on('ready', async () => {
if (Constants.IS_DEV_ENV) { if (Constants.IS_DEV_ENV) {
import('./index.dev') import('./index.dev')
// Install Vue DevTools
try {
const { default: installExtension, VUEJS_DEVTOOLS } = await import('electron-devtools-installer')
await installExtension(VUEJS_DEVTOOLS, {
loadExtensionOptions: {
allowFileAccess: true,
},
})
} catch (err) {
console.log('Failed to install Vue DevTools: ', err)
}
} }
// Disable special menus on macOS by uncommenting the following, if necessary // Disable special menus on macOS by uncommenting the following, if necessary
@@ -32,15 +32,12 @@ const headerMenus: {
text: 'title.second', text: 'title.second',
path: '/second' path: '/second'
} }
] ]
</script> </script>
<template> <template>
<v-app-bar <v-app-bar color="primary" density="compact">
color="primary" <v-app-bar-title @click="handleRoute('/')">{{ t(titleKey) }}</v-app-bar-title>
density="compact" <!-- <template #append>
>
<v-app-bar-title>{{ t(titleKey) }}</v-app-bar-title>
<template #append>
<v-btn <v-btn
v-for="menu in headerMenus" v-for="menu in headerMenus"
:key="menu.path" :key="menu.path"
@@ -51,13 +48,14 @@ const headerMenus: {
> >
{{ t(menu.text) }} {{ t(menu.text) }}
</v-btn> </v-btn>
</template> </template> -->
</v-app-bar> </v-app-bar>
</template> </template>
<style scoped> <style scoped>
.v-btn { .v-btn {
opacity: 0.4; opacity: 0.4;
} }
.active { .active {
opacity: 1 !important; opacity: 1 !important;
} }
+11 -10
View File
@@ -12,16 +12,17 @@ import {
pt, pt,
nl nl
} from 'vuetify/locale' } from 'vuetify/locale'
import { aliases, mdi } from 'vuetify/iconsets/mdi-svg' // import { aliases, mdi } from 'vuetify/iconsets/mdi-svg'
import 'vuetify/styles' import 'vuetify/styles'
import '@mdi/font/css/materialdesignicons.css'
import colors from 'vuetify/util/colors' import colors from 'vuetify/util/colors'
export default createVuetify({ export default createVuetify({
locale: { locale: {
messages: { ko, en, zhHans, zhHant, de, es, ja, fr, ru, pt, nl }, messages: { ko, en, zhHans, zhHant, de, es, ja, fr, ru, pt, nl },
locale: 'en', locale: 'zhHans',
fallback: 'en' fallback: 'zhHans'
}, },
defaults: { defaults: {
VBtn: { VBtn: {
@@ -33,13 +34,13 @@ export default createVuetify({
] ]
} }
}, },
icons: { // icons: {
defaultSet: 'mdi', // defaultSet: 'mdi',
aliases, // aliases,
sets: { // sets: {
mdi // mdi
} // }
}, // },
theme: { theme: {
themes: { themes: {
light: { light: {
+442 -219
View File
@@ -1,155 +1,233 @@
<template> <template>
<v-container> <v-container fluid class="pa-0 fill-height">
<v-row> <v-row class="fill-height">
<v-col cols="12"> <!-- Left Panel - Template Info -->
<h2 class="mb-4"> <v-col cols="12" md="4" class="pa-4 border-e">
{{ templateId ? '编辑模板' : '创建模板' }} <div class="d-flex align-center mb-4">
<h2 class="text-h5 mr-2">
{{ isEditMode ? '编辑模板' : '创建模板' }}
</h2> </h2>
<v-spacer />
<v-btn
icon
variant="text"
@click="goBack"
>
<v-icon>mdi-close</v-icon>
<v-tooltip
activator="parent"
location="bottom"
>
关闭
</v-tooltip>
</v-btn>
</div>
<v-form v-model="formValid"> <v-form v-model="formValid" class="mb-4">
<v-text-field <v-text-field
v-model="template.name" v-model="templateName"
label="模板名称 *" label="模板名称 *"
:rules="[(v) => !!v || '模板名称不能为空']" placeholder="输入模板名称"
:rules="[requiredRule]"
required required
data-testid="template-name-input" clearable
density="comfortable"
/> />
<v-textarea <v-textarea
v-model="template.description" v-model="templateDescription"
label="模板描述" label="模板描述"
placeholder="描述这个模板的用途..."
rows="2" rows="2"
data-testid="template-description-input" auto-grow
clearable
density="comfortable"
/> />
</v-form>
<!-- Template content --> <v-divider class="my-4" />
<v-card class="mb-4">
<v-card-title>模板内容</v-card-title>
<v-card-text>
<!-- Files section -->
<v-expansion-panels class="mb-4">
<v-expansion-panel>
<v-expansion-panel-title>文件</v-expansion-panel-title>
<v-expansion-panel-text>
<v-data-table
:headers="fileHeaders"
:items="template.files"
class="elevation-1"
>
<template v-slot:item.actions="{ item }">
<v-btn
icon
color="error"
@click="removeFile(item)"
>
<v-icon>mdi-delete</v-icon>
</v-btn>
</template>
</v-data-table>
<v-row class="mt-4"> <!-- File Operations -->
<v-col cols="12"> <div class="mb-4">
<h3 class="text-subtitle-1 mb-2">添加项</h3>
<div class="d-flex flex-column ga-2">
<v-btn <v-btn
color="primary" color="primary"
@click="showAddFileDialog = true" variant="outlined"
prepend-icon="mdi-file-plus"
>
添加文件
</v-btn>
</v-col>
</v-row>
</v-expansion-panel-text>
</v-expansion-panel>
<!-- Directories section -->
<v-expansion-panel>
<v-expansion-panel-title>目录</v-expansion-panel-title>
<v-expansion-panel-text>
<v-data-table
:headers="dirHeaders"
:items="template.directories"
class="elevation-1"
>
<template v-slot:item.actions="{ item }">
<v-btn
icon
color="error"
@click="removeDirectory(item)"
>
<v-icon>mdi-delete</v-icon>
</v-btn>
</template>
</v-data-table>
<v-row class="mt-4">
<v-col cols="12">
<v-btn
color="primary"
@click="showAddDirDialog = true"
prepend-icon="mdi-folder-plus" prepend-icon="mdi-folder-plus"
@click="showAddDirectoryDialog"
block
> >
添加目录 添加目录
</v-btn> </v-btn>
</v-col>
</v-row>
</v-expansion-panel-text>
</v-expansion-panel>
</v-expansion-panels>
</v-card-text>
</v-card>
<v-btn <v-btn
:color="templateId ? 'warning' : 'success'" color="success"
variant="outlined"
prepend-icon="mdi-file-plus"
@click="showAddFileDialog"
block
>
添加文件
</v-btn>
</div>
</div>
<v-divider class="my-4" />
<!-- Template Actions -->
<div class="d-flex ga-2">
<v-btn
color="primary"
@click="saveTemplate" @click="saveTemplate"
:disabled="!formValid" :disabled="!formValid || !hasChanges"
prepend-icon="mdi-content-save" prepend-icon="mdi-content-save"
block
> >
{{ templateId ? '更新模板' : '保存模板' }} 保存模板
</v-btn>
</div>
</v-col>
<!-- Right Panel - Tree Structure -->
<v-col cols="12" md="8" class="pa-4">
<div class="d-flex align-center mb-4">
<h3 class="text-h6 mr-2">模板结构</h3>
<v-spacer />
<v-btn
icon
variant="text"
@click="expandAll"
size="small"
>
<v-icon>mdi-arrow-expand-down</v-icon>
<v-tooltip
activator="parent"
location="bottom"
>
展开全部
</v-tooltip>
</v-btn> </v-btn>
<v-btn <v-btn
class="ml-2" icon
@click="cancel" variant="text"
prepend-icon="mdi-arrow-left" @click="collapseAll"
size="small"
> >
返回 <v-icon>mdi-arrow-collapse-up</v-icon>
<v-tooltip
activator="parent"
location="bottom"
>
收起全部
</v-tooltip>
</v-btn> </v-btn>
</v-form> </div>
<v-card
variant="outlined"
class="pa-2"
:height="treeHeight"
>
<v-treeview
v-model:expanded="expandedNodes"
:items="treeItems"
item-title="name"
item-key="id"
item-children="children"
activatable
open-on-click
:return-object="false"
>
<template v-slot: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 v-slot: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>
<template v-slot:no-data>
<div 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>
</template>
</v-treeview>
</v-card>
</v-col> </v-col>
</v-row> </v-row>
<!-- Add file dialog --> <!-- Add Directory Dialog -->
<v-dialog <v-dialog v-model="showDirectoryDialog" max-width="500">
v-model="showAddFileDialog"
max-width="600"
>
<v-card> <v-card>
<v-card-title>添加文件</v-card-title> <v-card-title>添加目录</v-card-title>
<v-card-text> <v-card-text>
<v-form v-model="fileFormValid"> <v-form v-model="directoryFormValid">
<v-text-field <v-text-field
v-model="newFile.path" v-model="newDirectoryPath"
label="文件路径 *" label="目录路径 *"
placeholder="例如: src/index.js" placeholder="例如: src/components"
:rules="[(v) => !!v || '文件路径不能为空']" :rules="[requiredRule]"
data-testid="add-file-path-input" required
/> density="comfortable"
<v-textarea
v-model="newFile.content"
label="文件内容"
placeholder="输入文件内容..."
rows="6"
data-testid="add-file-content-input"
/> />
</v-form> </v-form>
</v-card-text> </v-card-text>
<v-card-actions> <v-card-actions>
<v-spacer /> <v-spacer />
<v-btn @click="showAddFileDialog = false"> 取消 </v-btn> <v-btn @click="showDirectoryDialog = false">
取消
</v-btn>
<v-btn <v-btn
color="primary" color="primary"
@click="addFile" @click="addDirectory"
:disabled="!fileFormValid" :disabled="!directoryFormValid"
> >
添加 添加
</v-btn> </v-btn>
@@ -157,178 +235,323 @@
</v-card> </v-card>
</v-dialog> </v-dialog>
<!-- Add directory dialog --> <!-- Add File Dialog -->
<v-dialog <v-dialog v-model="showFileDialog" max-width="500">
v-model="showAddDirDialog"
max-width="600"
>
<v-card> <v-card>
<v-card-title>添加目录</v-card-title> <v-card-title>添加文件</v-card-title>
<v-card-text> <v-card-text>
<v-form v-model="dirFormValid"> <v-form v-model="fileFormValid">
<v-text-field <v-text-field
v-model="newDir.path" v-model="newFilePath"
label="目录路径 *" label="文件路径 *"
placeholder="例如: src/components" placeholder="例如: src/index.js"
:rules="[(v) => !!v || '目录路径不能为空']" :rules="[requiredRule]"
data-testid="add-dir-path-input" required
density="comfortable"
/> />
</v-form> </v-form>
</v-card-text> </v-card-text>
<v-card-actions> <v-card-actions>
<v-spacer /> <v-spacer />
<v-btn @click="showAddDirDialog = false"> 取消 </v-btn> <v-btn @click="showFileDialog = false">
取消
</v-btn>
<v-btn <v-btn
color="primary" color="primary"
@click="addDirectory" @click="addFile"
:disabled="!dirFormValid" :disabled="!fileFormValid"
> >
添加 添加并编辑
</v-btn> </v-btn>
</v-card-actions> </v-card-actions>
</v-card> </v-card>
</v-dialog> </v-dialog>
<!-- File Content Editor Dialog -->
<v-dialog v-model="showEditorDialog" fullscreen>
<v-card>
<v-toolbar dark color="primary">
<v-btn icon @click="showEditorDialog = false">
<v-icon>mdi-close</v-icon>
</v-btn>
<v-toolbar-title>
编辑文件: {{ editingFile?.path }}
</v-toolbar-title>
<v-spacer />
<v-btn
variant="text"
@click="saveFileContent"
>
保存
</v-btn>
</v-toolbar>
<v-card-text class="pa-0 fill-height">
<v-textarea
v-model="editingFileContent"
class="fill-height"
style="height: calc(100vh - 120px);"
no-resize
spellcheck="false"
placeholder="在此输入文件内容..."
variant="solo-filled"
density="comfortable"
/>
</v-card-text>
</v-card>
</v-dialog>
</v-container> </v-container>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue' import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
import { useRouter, useRoute } 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'
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
const templateStore = useTemplateStore() const templateStore = useTemplateStore()
// Form validation // Extract methods from store
const formValid = ref(false) const { getTemplateById } = templateStore
const fileFormValid = ref(false)
const dirFormValid = ref(false)
// Template data // Form state
const templateId = ref(route.params.id as string) const formValid = ref(false)
const template = ref({ const templateName = ref('')
id: '', const templateDescription = ref('')
name: '', const hasChanges = ref(false)
description: '',
files: [] as any[],
directories: [] as any[]
})
// Dialogs // Dialogs
const showAddFileDialog = ref(false) const showDirectoryDialog = ref(false)
const showAddDirDialog = ref(false) const showFileDialog = ref(false)
const showEditorDialog = ref(false)
const directoryFormValid = ref(false)
const fileFormValid = ref(false)
// New file data // New item state
const newFile = ref({ const newDirectoryPath = ref('')
path: '', const newFilePath = ref('')
content: '' const editingFile = ref<{ path: string; content: string } | null>(null)
const editingFileContent = ref('')
// Tree state
const expandedNodes = ref<string[]>([])
const treeHeight = ref(600)
// Item state
const directories = ref<{ path: string }[]>([])
const files = ref<{ path: string; content: string }[]>([])
// Computed
const isEditMode = computed(() => !!route.params.id)
const treeItems = computed(() => {
const items = []
// Add directories
directories.value.forEach(dir => {
items.push({
id: `dir-${dir.path}`,
name: dir.path,
type: 'directory',
children: []
})
})
// Add files
files.value.forEach(file => {
items.push({
id: `file-${file.path}`,
name: file.path,
type: 'file'
})
})
return items
}) })
// New directory data // Validation rules
const newDir = ref({ const requiredRule = (v: any) => !!v || '此字段为必填项'
path: ''
})
// Table headers // Watch for changes to detect unsaved changes
const fileHeaders = ref([ watch([templateName, templateDescription, directories, files], () => {
{ title: '文件路径', key: 'path' }, hasChanges.value = true
{ title: '操作', key: 'actions', sortable: false } }, { deep: true })
])
const dirHeaders = ref([
{ title: '目录路径', key: 'path' },
{ title: '操作', key: 'actions', sortable: false }
])
// Lifecycle
onMounted(async () => { onMounted(async () => {
await templateStore.loadTemplates() await templateStore.loadTemplates()
if (templateId.value) { // Set tree height based on window height
// Edit existing template updateTreeHeight()
const existingTemplate = templateStore.getTemplateById(templateId.value)
if (existingTemplate) { // Add resize listener
template.value = { ...existingTemplate } window.addEventListener('resize', updateTreeHeight)
// Handle edit mode
if (isEditMode.value) {
const templateId = route.params.id as string
const template = getTemplateById(templateId)
if (template) {
templateName.value = template.name
templateDescription.value = template.description
directories.value = [...template.directories]
files.value = [...template.files]
hasChanges.value = false
} else { } else {
// If template doesn't exist, redirect to template list router.push('/templates')
}
}
// Expand all nodes by default
expandAll()
})
onUnmounted(() => {
window.removeEventListener('resize', updateTreeHeight)
})
// Methods
const updateTreeHeight = () => {
treeHeight.value = window.innerHeight - 200
}
// Methods
const goBack = () => {
if (hasChanges.value) {
if (confirm('您有未保存的更改,确定要离开吗?')) {
router.push('/templates') router.push('/templates')
} }
} else { } else {
// New template router.push('/templates')
template.value = {
id: '',
name: '',
description: '',
files: [],
directories: []
} }
}
})
const addFile = () => {
if (!newFile.value.path.trim()) return
// Check for duplicate file paths
const exists = template.value.files.some((f) => f.path === newFile.value.path)
if (exists) {
alert('该文件路径已存在!')
return
}
template.value.files.push({
path: newFile.value.path,
content: newFile.value.content
})
newFile.value = { path: '', content: '' }
showAddFileDialog.value = false
} }
const removeFile = (file: any) => { const expandAll = () => {
template.value.files = template.value.files.filter((f) => f !== file) expandedNodes.value = treeItems.value.map(item => item.id)
}
const collapseAll = () => {
expandedNodes.value = []
}
const showAddDirectoryDialog = () => {
newDirectoryPath.value = ''
showDirectoryDialog.value = true
}
const showAddFileDialog = () => {
newFilePath.value = ''
showFileDialog.value = true
} }
const addDirectory = () => { const addDirectory = () => {
if (!newDir.value.path.trim()) return if (!newDirectoryPath.value.trim()) return
// Check for duplicate directory paths // Check for duplicates
const exists = template.value.directories.some( const exists = directories.value.some(d => d.path === newDirectoryPath.value)
(d) => d.path === newDir.value.path
)
if (exists) { if (exists) {
alert('该目录路径已存在!') alert('该目录路径已存在!')
return return
} }
template.value.directories.push({ directories.value.push({
path: newDir.value.path path: newDirectoryPath.value
}) })
newDir.value = { path: '' } showDirectoryDialog.value = false
showAddDirDialog.value = false newDirectoryPath.value = ''
} }
const removeDirectory = (dir: any) => { const addFile = () => {
template.value.directories = template.value.directories.filter( if (!newFilePath.value.trim()) return
(d) => d !== dir
) // Check for duplicates
const exists = files.value.some(f => f.path === newFilePath.value)
if (exists) {
alert('该文件路径已存在!')
return
}
const newFile = {
path: newFilePath.value,
content: ''
}
files.value.push(newFile)
// Open editor for the new file
editFileContent(newFile)
showFileDialog.value = false
newFilePath.value = ''
}
const editFile = (item: any) => {
const file = files.value.find(f => `file-${f.path}` === item.id)
if (file) {
editFileContent(file)
}
}
const editFileContent = (file: { path: string; content: string }) => {
editingFile.value = file
editingFileContent.value = file.content
showEditorDialog.value = true
}
const saveFileContent = () => {
if (editingFile.value) {
const index = files.value.findIndex(f => f.path === editingFile.value?.path)
if (index !== -1) {
files.value[index].content = editingFileContent.value
}
}
showEditorDialog.value = false
}
const deleteItem = (item: any) => {
if (!confirm('确定要删除此项吗?')) return
if (item.type === 'directory') {
directories.value = directories.value.filter(d => `dir-${d.path}` !== item.id)
} else {
files.value = files.value.filter(f => `file-${f.path}` !== item.id)
}
} }
const saveTemplate = () => { const saveTemplate = () => {
if (!formValid.value) return if (!formValid.value) return
if (templateId.value) { const templateData = {
// Update existing template name: templateName.value,
templateStore.updateTemplate(template.value) description: templateDescription.value,
} else { files: files.value,
// Create new template directories: directories.value
templateStore.addTemplate(template.value)
} }
router.push('/templates') if (isEditMode.value) {
} const templateId = route.params.id as string
templateStore.updateTemplate({
id: templateId,
...templateData
})
} else {
templateStore.addTemplate(templateData)
}
const cancel = () => { hasChanges.value = false
router.push('/templates') router.push('/templates')
} }
</script> </script>
<style scoped>
.border-e {
border-right: 1px solid rgba(0, 0, 0, 0.12);
}
.fill-height {
height: 100%;
}
</style>
+96 -108
View File
@@ -2,13 +2,15 @@
<v-container> <v-container>
<v-row> <v-row>
<v-col cols="12"> <v-col cols="12">
<h2 class="mb-4"> 模板列表 </h2> <div class="d-flex align-center mb-4 justify-space-between flex-wrap">
<h2 class="mr-2 mb-2"> 模板列表 </h2>
<v-btn @click="navigateToCreateTemplate" color="primary" prepend-icon="mdi-plus" class="mb-2">
创建模板
</v-btn>
</div>
<!-- No templates message --> <!-- No templates message -->
<v-card <v-card v-if="templates.length === 0" class="pa-6 mb-4 text-center">
v-if="templates.length === 0"
class="pa-6 mb-4 text-center"
>
<v-card-title class="text-h5 mb-4"> <v-card-title class="text-h5 mb-4">
暂无模板 暂无模板
</v-card-title> </v-card-title>
@@ -16,12 +18,7 @@
<p>还没有创建任何模板。创建您的第一个模板以开始批量生成文件和目录。</p> <p>还没有创建任何模板。创建您的第一个模板以开始批量生成文件和目录。</p>
</v-card-text> </v-card-text>
<v-card-actions class="justify-center"> <v-card-actions class="justify-center">
<v-btn <v-btn color="primary" size="default" @click="navigateToCreateTemplate" prepend-icon="mdi-plus">
color="primary"
size="x-large"
@click="navigateToCreateTemplate"
prepend-icon="mdi-plus"
>
创建模板 创建模板
</v-btn> </v-btn>
</v-card-actions> </v-card-actions>
@@ -30,38 +27,61 @@
<!-- Template list --> <!-- Template list -->
<div v-else> <div v-else>
<v-row> <v-row>
<v-col <v-col v-for="template in templates" :key="template.id" cols="12" sm="6" md="4" lg="3">
v-for="template in templates" <v-card class="d-flex flex-column" height="100%">
:key="template.id" <v-card-item>
cols="12"
md="6"
lg="4"
>
<v-card
class="d-flex flex-column"
height="100%"
>
<v-card-title>{{ template.name }}</v-card-title> <v-card-title>{{ template.name }}</v-card-title>
<v-card-subtitle>{{ template.description }}</v-card-subtitle> <v-card-subtitle>{{ template.description }}</v-card-subtitle>
<v-card-text class="flex-grow-1"> </v-card-item>
<p>文件数量: {{ template.files.length }}</p>
<p>目录数量: {{ template.directories.length }}</p> <v-card-text class="flex-grow-1 py-2">
<v-chip-group column>
<v-chip size="small" color="primary" variant="tonal">
文件: {{ template.files.length }}
</v-chip>
<v-chip size="small" color="success" variant="tonal">
目录: {{ template.directories.length }}
</v-chip>
</v-chip-group>
</v-card-text> </v-card-text>
<v-card-actions>
<v-btn <v-divider class="mt-auto"></v-divider>
color="primary"
variant="outlined" <v-card-actions class="pa-2 justify-space-between">
@click="viewTemplate(template)" <div>
> <v-btn color="primary" variant="text" size="small" @click="viewTemplate(template)">
查看 查看
</v-btn> </v-btn>
<v-btn <v-btn color="success" variant="text" size="small" @click="executeTemplate(template)">
color="success"
variant="outlined"
@click="executeTemplate(template)"
>
执行 执行
</v-btn> </v-btn>
</div>
<v-menu location="bottom end">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" icon variant="text" size="small" density="comfortable">
<v-icon>mdi-dots-vertical</v-icon>
</v-btn>
</template>
<v-list density="compact">
<v-list-item @click="editTemplate(template)">
<template v-slot:prepend>
<v-icon>mdi-pencil</v-icon>
</template>
<v-list-item-title>修改</v-list-item-title>
</v-list-item>
<v-divider></v-divider>
<v-list-item @click="deleteTemplate(template)">
<template v-slot:prepend>
<v-icon color="error">mdi-delete</v-icon>
</template>
<v-list-item-title class="text-error">删除</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</v-card-actions> </v-card-actions>
</v-card> </v-card>
</v-col> </v-col>
@@ -71,37 +91,20 @@
</v-row> </v-row>
<!-- Template detail dialog --> <!-- Template detail dialog -->
<v-dialog <v-dialog v-model="showTemplateDetail" max-width="800">
v-model="showTemplateDetail"
max-width="800"
>
<v-card> <v-card>
<v-card-title> <v-card-title>
<span>模板详情: {{ selectedTemplate?.name }}</span> <span>模板详情: {{ selectedTemplate?.name }}</span>
<v-spacer /> <v-spacer />
<v-btn
icon
@click="showTemplateDetail = false"
>
<v-icon>mdi-close</v-icon>
</v-btn>
</v-card-title> </v-card-title>
<v-card-text> <v-card-text>
<v-treeview <v-treeview :items="templateTree" item-title="name" item-key="id" item-children="children" activatable
:items="templateTree" open-all>
item-title="name" <template v-slot:prepend="{ item }">
item-key="id"
item-children="children"
activatable
open-all
>
<template v-slot:prepend="{ item, open }">
<v-icon> <v-icon>
{{ {{
item.type === 'directory' item.type === 'directory'
? open ? 'mdi-folder'
? 'mdi-folder-open'
: 'mdi-folder'
: 'mdi-file' : 'mdi-file'
}} }}
</v-icon> </v-icon>
@@ -112,63 +115,27 @@
</v-dialog> </v-dialog>
<!-- Execute template dialog --> <!-- Execute template dialog -->
<v-dialog <v-dialog v-model="showExecuteDialog" max-width="600">
v-model="showExecuteDialog"
max-width="600"
>
<v-card> <v-card>
<v-card-title>执行模板</v-card-title> <v-card-title>执行模板</v-card-title>
<v-card-text> <v-card-text>
<v-form v-model="executeFormValid"> <v-form v-model="executeFormValid">
<v-text-field <v-text-field v-model="executePath" label="选择目录" readonly @click="selectDirectory"
v-model="executePath" prepend-inner-icon="mdi-folder" :rules="[(v) => !!v || '请选择目录']" />
label="选择目录" <v-textarea v-model="baseDirNames" label="基础目录名 (每行一个,支持批量)" placeholder="输入基础目录名,每行一个"
readonly :rules="[(v) => !!v || '请输入基础目录名']" />
@click="selectDirectory"
prepend-inner-icon="mdi-folder"
:rules="[(v) => !!v || '请选择目录']"
/>
<v-textarea
v-model="baseDirNames"
label="基础目录名 (每行一个,支持批量)"
placeholder="输入基础目录名,每行一个"
:rules="[(v) => !!v || '请输入基础目录名']"
/>
</v-form> </v-form>
</v-card-text> </v-card-text>
<v-card-actions> <v-card-actions>
<v-spacer /> <v-spacer />
<v-btn @click="showExecuteDialog = false"> 取消 </v-btn> <v-btn @click="showExecuteDialog = false"> 取消 </v-btn>
<v-btn <v-btn color="success" @click="confirmExecuteTemplate" :disabled="!executeFormValid">
color="success"
@click="confirmExecuteTemplate"
:disabled="!executeFormValid"
>
确认执行 确认执行
</v-btn> </v-btn>
</v-card-actions> </v-card-actions>
</v-card> </v-card>
</v-dialog> </v-dialog>
<!-- Floating Action Button for creating templates -->
<v-btn
v-if="templates.length > 0"
color="primary"
size="x-large"
icon="mdi-plus"
fixed
location="bottom end"
class="mb-8 mr-8"
style="z-index: 1000;"
@click="navigateToCreateTemplate"
>
<v-tooltip
activator="parent"
location="top"
>
创建模板
</v-tooltip>
</v-btn>
</v-container> </v-container>
</template> </template>
@@ -176,6 +143,7 @@
import { ref, computed, onMounted } from 'vue' 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'
const router = useRouter() const router = useRouter()
const templateStore = useTemplateStore() const templateStore = useTemplateStore()
@@ -185,11 +153,13 @@ onMounted(async () => {
await templateStore.loadTemplates() await templateStore.loadTemplates()
}) })
const { deleteTemplate: storeDeleteTemplate } = templateStore
const templates = computed(() => templateStore.templates) const templates = computed(() => templateStore.templates)
// Template detail view // Template detail view
const showTemplateDetail = ref(false) const showTemplateDetail = ref(false)
const selectedTemplate = ref(null as any) const selectedTemplate = ref<Template | null>(null)
// Execute template // Execute template
const showExecuteDialog = ref(false) const showExecuteDialog = ref(false)
@@ -197,13 +167,21 @@ const executePath = ref('')
const baseDirNames = ref('') const baseDirNames = ref('')
const executeFormValid = ref(false) const executeFormValid = ref(false)
const templateTree = computed(() => { // Define item type for the tree view
interface TreeNode {
id: string
name: string
type: 'directory' | 'file'
children?: TreeNode[]
}
const templateTree = computed<TreeNode[]>(() => {
if (!selectedTemplate.value) return [] if (!selectedTemplate.value) return []
const items = [] const items: TreeNode[] = []
// Add directories // Add directories
selectedTemplate.value.directories.forEach((dir: any) => { selectedTemplate.value.directories.forEach((dir) => {
items.push({ items.push({
id: `dir-${dir.path}`, id: `dir-${dir.path}`,
name: dir.path, name: dir.path,
@@ -213,7 +191,7 @@ const templateTree = computed(() => {
}) })
// Add files // Add files
selectedTemplate.value.files.forEach((file: any) => { selectedTemplate.value.files.forEach((file) => {
items.push({ items.push({
id: `file-${file.path}`, id: `file-${file.path}`,
name: file.path, name: file.path,
@@ -228,12 +206,22 @@ const navigateToCreateTemplate = () => {
router.push('/create-template') router.push('/create-template')
} }
const viewTemplate = (template: any) => { const viewTemplate = (template: Template) => {
selectedTemplate.value = template selectedTemplate.value = template
showTemplateDetail.value = true showTemplateDetail.value = true
} }
const executeTemplate = (template: any) => { const editTemplate = (template: Template) => {
router.push(`/create-template/${template.id}`)
}
const deleteTemplate = (template: Template) => {
if (confirm(`确定要删除模板 "${template.name}" 吗?此操作不可撤销。`)) {
storeDeleteTemplate(template.id)
}
}
const executeTemplate = (template: Template) => {
selectedTemplate.value = template selectedTemplate.value = template
executePath.value = '' executePath.value = ''
baseDirNames.value = '' baseDirNames.value = ''
@@ -248,7 +236,7 @@ const selectDirectory = async () => {
} }
const confirmExecuteTemplate = () => { const confirmExecuteTemplate = () => {
if (!executePath.value || !baseDirNames.value.trim()) return if (!executePath.value || !baseDirNames.value.trim() || !selectedTemplate.value) return
const dirNames = baseDirNames.value.split('\n').filter((name) => name.trim()) const dirNames = baseDirNames.value.split('\n').filter((name) => name.trim())