🎨 更新用户版本
This commit is contained in:
		| @@ -8,7 +8,7 @@ | |||||||
|       content="Gin,Vue,Admin.Gin-Vue-Admin,GVA,gin-vue-admin,后台管理框架,vue后台管理框架,gin-vue-admin文档,gin-vue-admin首页,gin-vue-admin" |       content="Gin,Vue,Admin.Gin-Vue-Admin,GVA,gin-vue-admin,后台管理框架,vue后台管理框架,gin-vue-admin文档,gin-vue-admin首页,gin-vue-admin" | ||||||
|       name="keywords" |       name="keywords" | ||||||
|     /> |     /> | ||||||
|     <link rel="icon" href="favicon.ico" /> |     <link rel="icon" href="/favicon.ico" /> | ||||||
|     <title></title> |     <title></title> | ||||||
|     <style> |     <style> | ||||||
|       .transition-colors { |       .transition-colors { | ||||||
|   | |||||||
							
								
								
									
										4
									
								
								limit.js
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								limit.js
									
									
									
									
									
								
							| @@ -1,6 +1,6 @@ | |||||||
| // 运行项目前通过node执行此脚本 (此脚本与 node_modules 目录同级) | // 运行项目前通过node执行此脚本 (此脚本与 node_modules 目录同级) | ||||||
| const fs = require('fs') | import fs from 'fs' | ||||||
| const path = require('path') | import path from 'path' | ||||||
| const wfPath = path.resolve(__dirname, './node_modules/.bin') | const wfPath = path.resolve(__dirname, './node_modules/.bin') | ||||||
|  |  | ||||||
| fs.readdir(wfPath, (err, files) => { | fs.readdir(wfPath, (err, files) => { | ||||||
|   | |||||||
							
								
								
									
										33
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										33
									
								
								package.json
									
									
									
									
									
								
							| @@ -1,6 +1,6 @@ | |||||||
| { | { | ||||||
|   "name": "gin-vue-admin", |   "name": "gin-vue-admin", | ||||||
|   "version": "2.8.0", |   "version": "2.8.5", | ||||||
|   "private": true, |   "private": true, | ||||||
|   "scripts": { |   "scripts": { | ||||||
|     "serve": "vite --host --mode development", |     "serve": "vite --host --mode development", | ||||||
| @@ -13,6 +13,7 @@ | |||||||
|     "@element-plus/icons-vue": "^2.3.1", |     "@element-plus/icons-vue": "^2.3.1", | ||||||
|     "@form-create/designer": "^3.2.6", |     "@form-create/designer": "^3.2.6", | ||||||
|     "@form-create/element-ui": "^3.2.10", |     "@form-create/element-ui": "^3.2.10", | ||||||
|  |     "@unocss/transformer-directives": "^66.4.2", | ||||||
|     "@vue-office/docx": "^1.6.2", |     "@vue-office/docx": "^1.6.2", | ||||||
|     "@vue-office/excel": "^1.7.11", |     "@vue-office/excel": "^1.7.11", | ||||||
|     "@vue-office/pdf": "^2.0.2", |     "@vue-office/pdf": "^2.0.2", | ||||||
| @@ -21,15 +22,17 @@ | |||||||
|     "@wangeditor/editor": "^5.1.23", |     "@wangeditor/editor": "^5.1.23", | ||||||
|     "@wangeditor/editor-for-vue": "^5.1.12", |     "@wangeditor/editor-for-vue": "^5.1.12", | ||||||
|     "ace-builds": "^1.36.4", |     "ace-builds": "^1.36.4", | ||||||
|     "axios": "^1.7.7", |     "axios": "1.8.2", | ||||||
|     "chokidar": "^4.0.0", |     "chokidar": "^4.0.0", | ||||||
|     "core-js": "^3.38.1", |     "core-js": "^3.38.1", | ||||||
|     "echarts": "5.5.1", |     "echarts": "5.5.1", | ||||||
|     "element-plus": "^2.8.5", |     "element-plus": "^2.10.2", | ||||||
|     "highlight.js": "^11.10.0", |     "highlight.js": "^11.10.0", | ||||||
|  |     "install": "^0.13.0", | ||||||
|     "marked": "14.1.1", |     "marked": "14.1.1", | ||||||
|     "marked-highlight": "^2.1.4", |     "marked-highlight": "^2.1.4", | ||||||
|     "mitt": "^3.0.1", |     "mitt": "^3.0.1", | ||||||
|  |     "npm": "^11.3.0", | ||||||
|     "nprogress": "^0.2.0", |     "nprogress": "^0.2.0", | ||||||
|     "path": "^0.12.7", |     "path": "^0.12.7", | ||||||
|     "pinia": "^2.2.2", |     "pinia": "^2.2.2", | ||||||
| @@ -37,39 +40,45 @@ | |||||||
|     "screenfull": "^6.0.2", |     "screenfull": "^6.0.2", | ||||||
|     "sortablejs": "^1.15.3", |     "sortablejs": "^1.15.3", | ||||||
|     "spark-md5": "^3.0.2", |     "spark-md5": "^3.0.2", | ||||||
|     "tailwindcss": "^3.4.10", |  | ||||||
|     "universal-cookie": "^7", |     "universal-cookie": "^7", | ||||||
|     "vform3-builds": "^3.0.10", |     "vform3-builds": "^3.0.10", | ||||||
|     "vite-auto-import-svg": "^1.1.0", |     "vite-auto-import-svg": "^1.9.0", | ||||||
|     "vue": "^3.5.7", |     "vue": "^3.5.7", | ||||||
|     "vue-cropper": "^1.1.4", |     "vue-cropper": "^1.1.4", | ||||||
|     "vue-echarts": "^7.0.3", |     "vue-echarts": "^7.0.3", | ||||||
|     "vue-qr": "^4.0.9", |     "vue-qr": "^4.0.9", | ||||||
|     "vue-router": "^4.4.3", |     "vue-router": "^4.4.3", | ||||||
|     "vue3-ace-editor": "^2.2.4", |     "vue3-ace-editor": "^2.2.4", | ||||||
|  |     "vue3-sfc-loader": "^0.9.5", | ||||||
|     "vuedraggable": "^4.1.0" |     "vuedraggable": "^4.1.0" | ||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|     "@babel/eslint-parser": "^7.25.1", |     "@babel/eslint-parser": "^7.25.1", | ||||||
|     "@eslint/js": "^9.14.0", |     "@eslint/js": "^8.56.0", | ||||||
|     "@vitejs/plugin-legacy": "^5.4.2", |     "@unocss/extractor-svelte": "^66.4.2", | ||||||
|     "@vitejs/plugin-vue": "^5.1.4", |     "@unocss/preset-wind3": "^66.4.2", | ||||||
|  |     "@unocss/vite": "^66.5.0", | ||||||
|  |     "@vitejs/plugin-legacy": "^6.0.0", | ||||||
|  |     "@vitejs/plugin-vue": "^5.0.3", | ||||||
|     "@vue/cli-plugin-babel": "~5.0.8", |     "@vue/cli-plugin-babel": "~5.0.8", | ||||||
|     "@vue/cli-plugin-eslint": "~5.0.8", |     "@vue/cli-plugin-eslint": "~5.0.8", | ||||||
|     "@vue/cli-plugin-router": "~5.0.8", |     "@vue/cli-plugin-router": "~5.0.8", | ||||||
|     "@vue/cli-plugin-vuex": "~5.0.8", |     "@vue/cli-plugin-vuex": "~5.0.8", | ||||||
|     "@vue/cli-service": "~5.0.8", |     "@vue/cli-service": "~5.0.8", | ||||||
|     "@vue/compiler-sfc": "^3.5.1", |     "@vue/compiler-sfc": "^3.5.1", | ||||||
|  |     "autoprefixer": "^10.4.20", | ||||||
|     "babel-plugin-import": "^1.13.8", |     "babel-plugin-import": "^1.13.8", | ||||||
|     "chalk": "^5.3.0", |     "chalk": "^5.3.0", | ||||||
|     "dotenv": "^16.4.5", |     "dotenv": "^16.4.5", | ||||||
|     "eslint": "^9.14.0", |     "eslint": "^8.57.0", | ||||||
|     "eslint-plugin-vue": "^9.30.0", |     "eslint-plugin-vue": "^9.19.2", | ||||||
|  |     "globals": "^16.3.0", | ||||||
|     "sass": "^1.78.0", |     "sass": "^1.78.0", | ||||||
|     "terser": "^5.31.6", |     "terser": "^5.31.6", | ||||||
|     "vite": "^5.4.3", |     "unocss": "^66.4.2", | ||||||
|  |     "vite": "^6.2.3", | ||||||
|     "vite-plugin-banner": "^0.8.0", |     "vite-plugin-banner": "^0.8.0", | ||||||
|     "vite-plugin-importer": "^0.2.5", |     "vite-plugin-importer": "^0.2.5", | ||||||
|     "vite-plugin-vue-devtools": "^7.4.4" |     "vite-plugin-vue-devtools": "^7.0.16" | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,6 +0,0 @@ | |||||||
| module.exports = { |  | ||||||
|   plugins: { |  | ||||||
|     tailwindcss: {}, |  | ||||||
|     autoprefixer: {} |  | ||||||
|   } |  | ||||||
| } |  | ||||||
							
								
								
									
										58
									
								
								src/App.vue
									
									
									
									
									
								
							
							
						
						
									
										58
									
								
								src/App.vue
									
									
									
									
									
								
							| @@ -1,41 +1,43 @@ | |||||||
| <template> | <template> | ||||||
|   <div |   <div id="app" class="bg-gray-50 text-slate-700 dark:text-slate-500 dark:bg-slate-800"> | ||||||
|     id="app" |  | ||||||
|     class="bg-gray-50 text-slate-700 dark:text-slate-500 dark:bg-slate-800" |  | ||||||
|   > |  | ||||||
|     <el-config-provider :locale="zhCn"> |     <el-config-provider :locale="zhCn"> | ||||||
|       <router-view /> |       <router-view /> | ||||||
|  |       <Application /> | ||||||
|     </el-config-provider> |     </el-config-provider> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script setup> | <script setup> | ||||||
|   import zhCn from 'element-plus/dist/locale/zh-cn.mjs' | import zhCn from 'element-plus/dist/locale/zh-cn.mjs' | ||||||
|   import { useAppStore } from '@/pinia' | import Application from '@/components/application/index.vue' | ||||||
|   useAppStore() | import { useAppStore } from '@/pinia' | ||||||
|   defineOptions({ |  | ||||||
|     name: 'App' | useAppStore() | ||||||
|   }) | defineOptions({ | ||||||
|  |   name: 'App' | ||||||
|  | }) | ||||||
| </script> | </script> | ||||||
| <style lang="scss"> | <style lang="scss"> | ||||||
|   // 引入初始化样式 | // 引入初始化样式 | ||||||
|   #app { | #app { | ||||||
|     height: 100vh; |   height: 100vh; | ||||||
|     overflow: hidden; |   overflow: hidden; | ||||||
|     font-weight: 400 !important; |   font-weight: 400 !important; | ||||||
|   } | } | ||||||
|   .el-button { |  | ||||||
|     font-weight: 400 !important; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   .gva-body-h { | .el-button { | ||||||
|     min-height: calc(100% - 3rem); |   font-weight: 400 !important; | ||||||
|   } | } | ||||||
|  |  | ||||||
|   .gva-container { | .gva-body-h { | ||||||
|     height: calc(100% - 2.5rem); |   min-height: calc(100% - 3rem); | ||||||
|   } | } | ||||||
|   .gva-container2 { |  | ||||||
|     height: calc(100% - 4.5rem); | .gva-container { | ||||||
|   } |   height: calc(100% - 2.5rem); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .gva-container2 { | ||||||
|  |   height: calc(100% - 4.5rem); | ||||||
|  | } | ||||||
| </style> | </style> | ||||||
|   | |||||||
| @@ -173,6 +173,17 @@ export const eye = (data) => { | |||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | export const createWeb = (data) => { | ||||||
|  |   return service({ | ||||||
|  |     url: '/autoCode/llmAuto', | ||||||
|  |     method: 'post', | ||||||
|  |     data: { ...data, mode: 'painter' }, | ||||||
|  |     timeout: 1000 * 60 * 10 | ||||||
|  |   }) | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
| export const addFunc = (data) => { | export const addFunc = (data) => { | ||||||
|   return service({ |   return service({ | ||||||
|     url: '/autoCode/addFunc', |     url: '/autoCode/addFunc', | ||||||
| @@ -196,3 +207,29 @@ export const initAPI = (data) => { | |||||||
|     data |     data | ||||||
|   }) |   }) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | export const mcp = (data) => { | ||||||
|  |   return service({ | ||||||
|  |     url: '/autoCode/mcp', | ||||||
|  |     method: 'post', | ||||||
|  |     data | ||||||
|  |   }) | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | export const mcpList = (data) => { | ||||||
|  |   return service({ | ||||||
|  |     url: '/autoCode/mcpList', | ||||||
|  |     method: 'post', | ||||||
|  |     data | ||||||
|  |   }) | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | export const mcpTest = (data) => { | ||||||
|  |   return service({ | ||||||
|  |     url: '/autoCode/mcpTest', | ||||||
|  |     method: 'post', | ||||||
|  |     data | ||||||
|  |   }) | ||||||
|  | } | ||||||
|   | |||||||
| @@ -42,7 +42,7 @@ export const getSystemState = () => { | |||||||
| } | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * 重启服务 |  * 重载服务 | ||||||
|  * @param data |  * @param data | ||||||
|  * @returns {*} |  * @returns {*} | ||||||
|  */ |  */ | ||||||
|   | |||||||
							
								
								
									
										114
									
								
								src/api/version.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								src/api/version.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,114 @@ | |||||||
|  | import service from '@/utils/request' | ||||||
|  |  | ||||||
|  | // @Tags SysVersion | ||||||
|  | // @Summary 删除版本管理 | ||||||
|  | // @Security ApiKeyAuth | ||||||
|  | // @Accept application/json | ||||||
|  | // @Produce application/json | ||||||
|  | // @Param data body model.SysVersion true "删除版本管理" | ||||||
|  | // @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}" | ||||||
|  | // @Router /sysVersion/deleteSysVersion [delete] | ||||||
|  | export const deleteSysVersion = (params) => { | ||||||
|  |   return service({ | ||||||
|  |     url: '/sysVersion/deleteSysVersion', | ||||||
|  |     method: 'delete', | ||||||
|  |     params | ||||||
|  |   }) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // @Tags SysVersion | ||||||
|  | // @Summary 批量删除版本管理 | ||||||
|  | // @Security ApiKeyAuth | ||||||
|  | // @Accept application/json | ||||||
|  | // @Produce application/json | ||||||
|  | // @Param data body request.IdsReq true "批量删除版本管理" | ||||||
|  | // @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}" | ||||||
|  | // @Router /sysVersion/deleteSysVersion [delete] | ||||||
|  | export const deleteSysVersionByIds = (params) => { | ||||||
|  |   return service({ | ||||||
|  |     url: '/sysVersion/deleteSysVersionByIds', | ||||||
|  |     method: 'delete', | ||||||
|  |     params | ||||||
|  |   }) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // @Tags SysVersion | ||||||
|  | // @Summary 用id查询版本管理 | ||||||
|  | // @Security ApiKeyAuth | ||||||
|  | // @Accept application/json | ||||||
|  | // @Produce application/json | ||||||
|  | // @Param data query model.SysVersion true "用id查询版本管理" | ||||||
|  | // @Success 200 {string} string "{"success":true,"data":{},"msg":"查询成功"}" | ||||||
|  | // @Router /sysVersion/findSysVersion [get] | ||||||
|  | export const findSysVersion = (params) => { | ||||||
|  |   return service({ | ||||||
|  |     url: '/sysVersion/findSysVersion', | ||||||
|  |     method: 'get', | ||||||
|  |     params | ||||||
|  |   }) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // @Tags SysVersion | ||||||
|  | // @Summary 分页获取版本管理列表 | ||||||
|  | // @Security ApiKeyAuth | ||||||
|  | // @Accept application/json | ||||||
|  | // @Produce application/json | ||||||
|  | // @Param data query request.PageInfo true "分页获取版本管理列表" | ||||||
|  | // @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" | ||||||
|  | // @Router /sysVersion/getSysVersionList [get] | ||||||
|  | export const getSysVersionList = (params) => { | ||||||
|  |   return service({ | ||||||
|  |     url: '/sysVersion/getSysVersionList', | ||||||
|  |     method: 'get', | ||||||
|  |     params | ||||||
|  |   }) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // @Tags SysVersion | ||||||
|  | // @Summary 导出版本数据 | ||||||
|  | // @Security ApiKeyAuth | ||||||
|  | // @Accept application/json | ||||||
|  | // @Produce application/json | ||||||
|  | // @Param data body object true "导出版本数据" | ||||||
|  | // @Success 200 {string} string "{\"success\":true,\"data\":{},\"msg\":\"导出成功\"}" | ||||||
|  | // @Router /sysVersion/exportVersion [post] | ||||||
|  | export const exportVersion = (data) => { | ||||||
|  |   return service({ | ||||||
|  |     url: '/sysVersion/exportVersion', | ||||||
|  |     method: 'post', | ||||||
|  |     data | ||||||
|  |   }) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // @Tags SysVersion | ||||||
|  | // @Summary 下载版本JSON数据 | ||||||
|  | // @Security ApiKeyAuth | ||||||
|  | // @Accept application/json | ||||||
|  | // @Produce application/json | ||||||
|  | // @Param ID query string true "版本ID" | ||||||
|  | // @Success 200 {string} string "{\"success\":true,\"data\":{},\"msg\":\"下载成功\"}" | ||||||
|  | // @Router /sysVersion/downloadVersionJson [get] | ||||||
|  | export const downloadVersionJson = (params) => { | ||||||
|  |   return service({ | ||||||
|  |     url: '/sysVersion/downloadVersionJson', | ||||||
|  |     method: 'get', | ||||||
|  |     params, | ||||||
|  |     responseType: 'blob' | ||||||
|  |   }) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // @Tags SysVersion | ||||||
|  | // @Summary 导入版本数据 | ||||||
|  | // @Security ApiKeyAuth | ||||||
|  | // @Accept application/json | ||||||
|  | // @Produce application/json | ||||||
|  | // @Param data body object true "版本JSON数据" | ||||||
|  | // @Success 200 {string} string "{\"success\":true,\"data\":{},\"msg\":\"导入成功\"}" | ||||||
|  | // @Router /sysVersion/importVersion [post] | ||||||
|  | export const importVersion = (data) => { | ||||||
|  |   return service({ | ||||||
|  |     url: '/sysVersion/importVersion', | ||||||
|  |     method: 'post', | ||||||
|  |     data | ||||||
|  |   }) | ||||||
|  | } | ||||||
							
								
								
									
										1
									
								
								src/assets/icons/close.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/assets/icons/close.svg
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | <svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024"><path fill="currentColor" d="M195.2 195.2a64 64 0 0 1 90.496 0L512 421.504L738.304 195.2a64 64 0 0 1 90.496 90.496L602.496 512L828.8 738.304a64 64 0 0 1-90.496 90.496L512 602.496L285.696 828.8a64 64 0 0 1-90.496-90.496L421.504 512L195.2 285.696a64 64 0 0 1 0-90.496"/></svg> | ||||||
| After Width: | Height: | Size: 366 B | 
							
								
								
									
										1
									
								
								src/assets/icons/idea.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/assets/icons/idea.svg
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M6.09 14.999a6.9 6.9 0 0 1-.59-2.794C5.5 8.5 8.41 5.499 12 5.499s6.5 3.002 6.5 6.706a6.9 6.9 0 0 1-.59 2.794m-5.91-13v1m10 9h-1m-18 0H2m17.07-7.071l-.707.707m-12.726.001l-.707-.707m9.587 14.377c1.01-.327 1.416-1.252 1.53-2.182c.034-.278-.195-.509-.475-.509H8.477a.483.483 0 0 0-.488.534c.112.928.394 1.606 1.464 2.156m5.064 0H9.453m5.064 0c-.121 1.945-.683 2.716-2.51 2.694c-1.954.036-2.404-.916-2.554-2.693"/></svg> | ||||||
| After Width: | Height: | Size: 609 B | 
							
								
								
									
										1
									
								
								src/assets/icons/lock.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/assets/icons/lock.svg
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M12 2a5 5 0 0 1 5 5v3a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H7a3 3 0 0 1-3-3v-6a3 3 0 0 1 3-3V7a5 5 0 0 1 5-5m0 12a2 2 0 0 0-1.995 1.85L10 16a2 2 0 1 0 2-2m0-10a3 3 0 0 0-3 3v3h6V7a3 3 0 0 0-3-3"/></svg> | ||||||
| After Width: | Height: | Size: 307 B | 
							
								
								
									
										1
									
								
								src/assets/icons/server.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/assets/icons/server.svg
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"><path d="M12.5.5h-11a1 1 0 0 0-1 1v3a1 1 0 0 0 1 1h11a1 1 0 0 0 1-1v-3a1 1 0 0 0-1-1M7.5 3H11M1.5 5.5a1 1 0 0 0-1 1v3a1 1 0 0 0 1 1h11a1 1 0 0 0 1-1v-3a1 1 0 0 0-1-1"/><path d="M3.25 8.25a.25.25 0 0 1 0-.5m0 .5a.25.25 0 0 0 0-.5m0-4.5a.25.25 0 0 1 0-.5m0 .5a.25.25 0 0 0 0-.5M7.5 8H11m-4 2.5v3m-5 0h10"/></g></svg> | ||||||
| After Width: | Height: | Size: 499 B | 
							
								
								
									
										1
									
								
								src/assets/icons/warn.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/assets/icons/warn.svg
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | <svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024"><path fill="currentColor" d="M928.99 755.83L574.6 203.25c-12.89-20.16-36.76-32.58-62.6-32.58s-49.71 12.43-62.6 32.58L95.01 755.83c-12.91 20.12-12.9 44.91.01 65.03c12.92 20.12 36.78 32.51 62.59 32.49h708.78c25.82.01 49.68-12.37 62.59-32.49s12.92-44.91.01-65.03M554.67 768h-85.33v-85.33h85.33zm0-426.67v298.66h-85.33V341.32z"/></svg> | ||||||
| After Width: | Height: | Size: 423 B | 
							
								
								
									
										39
									
								
								src/components/application/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								src/components/application/index.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,39 @@ | |||||||
|  | <template> | ||||||
|  |   <error-preview v-if="showError" :error-data="errorInfo" @close="handleClose" @confirm="handleConfirm" /> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup> | ||||||
|  | import { ref, onUnmounted } from 'vue' | ||||||
|  | import { emitter } from '@/utils/bus' | ||||||
|  | import ErrorPreview from '@/components/errorPreview/index.vue' | ||||||
|  |  | ||||||
|  | const showError = ref(false) | ||||||
|  | const errorInfo = ref(null) | ||||||
|  | let cb = null | ||||||
|  |  | ||||||
|  | const showErrorDialog = (data) => { | ||||||
|  |   // 这玩意同时只允许存在一个 | ||||||
|  |   if(showError.value) return | ||||||
|  |  | ||||||
|  |   errorInfo.value = data | ||||||
|  |   showError.value = true | ||||||
|  |   cb = data?.fn || null | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const handleClose = () => { | ||||||
|  |   showError.value = false | ||||||
|  |   errorInfo.value = null | ||||||
|  |   cb = null | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const handleConfirm = (code) => { | ||||||
|  |   cb && cb(code) | ||||||
|  |   handleClose() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | emitter.on('show-error', showErrorDialog) | ||||||
|  |  | ||||||
|  | onUnmounted(() => { | ||||||
|  |   emitter.off('show-error', showErrorDialog) | ||||||
|  | }) | ||||||
|  | </script> | ||||||
| @@ -11,12 +11,22 @@ | |||||||
|       <span> |       <span> | ||||||
|         <a |         <a | ||||||
|           class="font-bold text-active" |           class="font-bold text-active" | ||||||
|           href="https://echol.cn" |           href="https://github.com/flipped-aurora/gin-vue-admin" | ||||||
|           >Echo</a |           >Gin-Vue-Admin</a | ||||||
|  |         > | ||||||
|  |       </span> | ||||||
|  |     </div> | ||||||
|  |     <slot /> | ||||||
|  |     <div class="text-center"> | ||||||
|  |       <span class="mr-1">Copyright</span> | ||||||
|  |       <span> | ||||||
|  |         <a | ||||||
|  |           class="font-bold text-active" | ||||||
|  |           href="https://github.com/flipped-aurora" | ||||||
|  |           >flipped-aurora团队</a | ||||||
|         > |         > | ||||||
|       </span> |       </span> | ||||||
|     </div> |     </div> | ||||||
|  |  | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| @@ -25,4 +35,10 @@ | |||||||
|     name: 'BottomInfo' |     name: 'BottomInfo' | ||||||
|   }) |   }) | ||||||
|  |  | ||||||
|  |   console.log( | ||||||
|  |     `%c powered by %c flipped-aurorae %c`, | ||||||
|  |     'background:#0081ff; padding: 1px; border-radius: 3px 0 0 3px; color: #fff', | ||||||
|  |     'background:#354855; padding: 1px 5px; border-radius: 0 3px 3px 0; color: #fff; font-weight: bold;', | ||||||
|  |     'background:transparent' | ||||||
|  |   ) | ||||||
| </script> | </script> | ||||||
|   | |||||||
| @@ -55,7 +55,6 @@ | |||||||
|   const deepMenus = (menus) => { |   const deepMenus = (menus) => { | ||||||
|     const arr = [] |     const arr = [] | ||||||
|     menus?.forEach((menu) => { |     menus?.forEach((menu) => { | ||||||
|       if (!menu?.children) return |  | ||||||
|       if (menu.children && menu.children.length > 0) { |       if (menu.children && menu.children.length > 0) { | ||||||
|         arr.push(...deepMenus(menu.children)) |         arr.push(...deepMenus(menu.children)) | ||||||
|       } else { |       } else { | ||||||
|   | |||||||
							
								
								
									
										126
									
								
								src/components/errorPreview/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										126
									
								
								src/components/errorPreview/index.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,126 @@ | |||||||
|  | <template> | ||||||
|  |   <div  | ||||||
|  |     class="fixed inset-0 bg-black/40 flex items-center justify-center z-[999]" | ||||||
|  |     @click.self="closeModal" | ||||||
|  |   > | ||||||
|  |     <div class="bg-white rounded-xl shadow-dialog w-full max-w-md mx-4 transform transition-all duration-300 ease-in-out"> | ||||||
|  |       <!-- 弹窗头部 --> | ||||||
|  |       <div class="p-5 border-b border-gray-100 flex justify-between items-center"> | ||||||
|  |         <h3 class="text-lg font-semibold text-gray-800">{{ displayData.title }}</h3> | ||||||
|  |         <div class="text-gray-400 hover:text-gray-600 transition-colors cursor-pointer" @click="closeModal"> | ||||||
|  |           <close class="h-6 w-6" /> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |        | ||||||
|  |       <!-- 弹窗内容 --> | ||||||
|  |       <div class="p-6 pt-0"> | ||||||
|  |         <!-- 错误类型 --> | ||||||
|  |         <div class="mb-4"> | ||||||
|  |           <div class="text-xs font-medium text-gray-500 uppercase mb-2">错误类型</div> | ||||||
|  |           <div class="flex items-center gap-2"> | ||||||
|  |             <lock v-if="displayData.icon === 'lock'" class="text-red-500 w-5 h-5" /> | ||||||
|  |             <warn v-if="displayData.icon === 'warn'" class="text-red-500 w-5 h-5" /> | ||||||
|  |             <server v-if="displayData.icon === 'server'" class="text-red-500 w-5 h-5" /> | ||||||
|  |             <span class="font-medium text-gray-800">{{ displayData.type }}</span> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |          | ||||||
|  |         <!-- 具体错误 --> | ||||||
|  |         <div class="mb-6"> | ||||||
|  |           <div class="text-xs font-medium text-gray-500 uppercase mb-2">具体错误</div> | ||||||
|  |           <div class="bg-gray-100 rounded-lg p-3 text-sm text-gray-700 leading-relaxed"> | ||||||
|  |             {{ displayData.message }} | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |          | ||||||
|  |         <!-- 提示信息 --> | ||||||
|  |         <div v-if="displayData.tips"> | ||||||
|  |           <div class="text-xs font-medium text-gray-500 uppercase mb-2">提示</div> | ||||||
|  |           <div class="flex items-center gap-2"> | ||||||
|  |             <idea class="text-blue-500 w-5 h-5" /> | ||||||
|  |             <p class="text-sm text-gray-600">{{ displayData.tips }}</p> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |        | ||||||
|  |       <!-- 弹窗底部 --> | ||||||
|  |       <div class="py-2 px-4 border-t border-gray-100 flex justify-end"> | ||||||
|  |         <div class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors font-medium text-sm shadow-sm cursor-pointer" @click="handleConfirm"> | ||||||
|  |           确定 | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup> | ||||||
|  | import { defineProps, defineEmits, computed } from 'vue'; | ||||||
|  |  | ||||||
|  | const props = defineProps({ | ||||||
|  |   errorData: { | ||||||
|  |     type: Object, | ||||||
|  |     required: true | ||||||
|  |   } | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | const emits = defineEmits(['close', 'confirm']); | ||||||
|  |  | ||||||
|  | const presetErrors = { | ||||||
|  |   500: { | ||||||
|  |     title: '检测到接口错误', | ||||||
|  |     type: '服务器发生内部错误', | ||||||
|  |     icon: 'server', | ||||||
|  |     color: 'text-red-500', | ||||||
|  |     tips: '此类错误内容常见于后台panic,请先查看后台日志,如果影响您正常使用可强制登出清理缓存' | ||||||
|  |   }, | ||||||
|  |   404: { | ||||||
|  |     title: '资源未找到', | ||||||
|  |     type: 'Not Found', | ||||||
|  |     icon: 'warn', | ||||||
|  |     color: 'text-orange-500', | ||||||
|  |     tips: '此类错误多为接口未注册(或未重启)或者请求路径(方法)与api路径(方法)不符--如果为自动化代码请检查是否存在空格' | ||||||
|  |   }, | ||||||
|  |   401: { | ||||||
|  |     title: '身份认证失败', | ||||||
|  |     type: '身份令牌无效', | ||||||
|  |     icon: 'lock', | ||||||
|  |     color: 'text-purple-500', | ||||||
|  |     tips: '您的身份认证已过期或无效,请重新登录。' | ||||||
|  |   }, | ||||||
|  |   'network': { | ||||||
|  |     title: '网络错误', | ||||||
|  |     type: 'Network Error', | ||||||
|  |     icon: 'fa-wifi-slash', | ||||||
|  |     color: 'text-gray-500', | ||||||
|  |     tips: '无法连接到服务器,请检查您的网络连接。' | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const displayData = computed(() => { | ||||||
|  |   const preset = presetErrors[props.errorData.code]; | ||||||
|  |   if (preset) { | ||||||
|  |     return { | ||||||
|  |       ...preset, | ||||||
|  |       message: props.errorData.message || '没有提供额外信息。' | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return { | ||||||
|  |     title: '未知错误', | ||||||
|  |     type: '检测到请求错误', | ||||||
|  |     icon: 'fa-question-circle', | ||||||
|  |     color: 'text-gray-400', | ||||||
|  |     message: props.errorData.message || '发生了一个未知错误。', | ||||||
|  |     tips: '请检查控制台获取更多信息。' | ||||||
|  |   }; | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | const closeModal = () => { | ||||||
|  |    emits('close') | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const handleConfirm = () => { | ||||||
|  |   emits('confirm', props.errorData.code); | ||||||
|  |   closeModal(); | ||||||
|  | }; | ||||||
|  | </script> | ||||||
| @@ -9,6 +9,10 @@ | |||||||
| import { exportExcel } from '@/api/exportTemplate' | import { exportExcel } from '@/api/exportTemplate' | ||||||
|  |  | ||||||
|   const props = defineProps({ |   const props = defineProps({ | ||||||
|  |     filterDeleted: { | ||||||
|  |       type: Boolean, | ||||||
|  |       default: true | ||||||
|  |     }, | ||||||
|     templateId: { |     templateId: { | ||||||
|       type: String, |       type: String, | ||||||
|       required: true |       required: true | ||||||
| @@ -43,6 +47,11 @@ import { exportExcel } from '@/api/exportTemplate' | |||||||
|       baseUrl = "" |       baseUrl = "" | ||||||
|     } |     } | ||||||
|     const paramsCopy = JSON.parse(JSON.stringify(props.condition)) |     const paramsCopy = JSON.parse(JSON.stringify(props.condition)) | ||||||
|  |  | ||||||
|  |     if (props.filterDeleted) { | ||||||
|  |       paramsCopy.filterDeleted = 'true' | ||||||
|  |     } | ||||||
|  |  | ||||||
|     if (props.limit) { |     if (props.limit) { | ||||||
|       paramsCopy.limit = props.limit |       paramsCopy.limit = props.limit | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| <template> | <template> | ||||||
|   <div class="border border-solid border-gray-100 h-full"> |   <div class="border border-solid border-gray-100 h-full z-10"> | ||||||
|     <Toolbar |     <Toolbar | ||||||
|       :editor="editorRef" |       :editor="editorRef" | ||||||
|       :default-config="toolbarConfig" |       :default-config="toolbarConfig" | ||||||
| @@ -8,7 +8,7 @@ | |||||||
|     <Editor |     <Editor | ||||||
|       v-model="valueHtml" |       v-model="valueHtml" | ||||||
|       class="overflow-y-hidden mt-0.5" |       class="overflow-y-hidden mt-0.5" | ||||||
|       style="min-height: 18rem" |       style="height: 18rem" | ||||||
|       :default-config="editorConfig" |       :default-config="editorConfig" | ||||||
|       mode="default" |       mode="default" | ||||||
|       @onCreated="handleCreated" |       @onCreated="handleCreated" | ||||||
| @@ -27,7 +27,7 @@ | |||||||
|  |  | ||||||
|   import { ElMessage } from 'element-plus' |   import { ElMessage } from 'element-plus' | ||||||
|   import { getUrl } from '@/utils/image' |   import { getUrl } from '@/utils/image' | ||||||
|   import botLogo from '@/assets/bot_logo.png' |   import { useUserStore } from '@/pinia/modules/user' | ||||||
|  |  | ||||||
|   const emits = defineEmits(['change', 'update:modelValue']) |   const emits = defineEmits(['change', 'update:modelValue']) | ||||||
|  |  | ||||||
| @@ -36,14 +36,11 @@ | |||||||
|     emits('update:modelValue', valueHtml.value) |     emits('update:modelValue', valueHtml.value) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   const userStore = useUserStore() | ||||||
|   const props = defineProps({ |   const props = defineProps({ | ||||||
|     modelValue: { |     modelValue: { | ||||||
|       type: String, |       type: String, | ||||||
|       default: '' |       default: '' | ||||||
|     }, |  | ||||||
|     useWatermark: { |  | ||||||
|       type: Boolean, |  | ||||||
|       default: false |  | ||||||
|     } |     } | ||||||
|   }) |   }) | ||||||
|  |  | ||||||
| @@ -53,94 +50,14 @@ | |||||||
|   const toolbarConfig = {} |   const toolbarConfig = {} | ||||||
|   const editorConfig = { |   const editorConfig = { | ||||||
|     placeholder: '请输入内容...', |     placeholder: '请输入内容...', | ||||||
|     MENU_CONF: { |     MENU_CONF: {} | ||||||
|       uploadImage: { |  | ||||||
|         server: basePath + '/fileUploadAndDownload/upload?noSave=1', |  | ||||||
|         fieldName: 'file', |  | ||||||
|         maxFileSize: 1024 * 1024 * 10, // 限制图片大小为10MB |  | ||||||
|         maxNumberOfFiles: 1, |  | ||||||
|         async customUpload(file, insertFn) { |  | ||||||
|           // 未开启水印则直接上传原图 |  | ||||||
|           if (!props.useWatermark) { |  | ||||||
|             const formData = new FormData() |  | ||||||
|             formData.append('file', file) |  | ||||||
|             const resp = await fetch(basePath + '/fileUploadAndDownload/upload?noSave=1', { method: 'POST', body: formData }) |  | ||||||
|             const res = await resp.json() |  | ||||||
|             if (res.code === 0) { |  | ||||||
|               const urlPath = getUrl(res.data.file.url) |  | ||||||
|               insertFn(urlPath, res.data.file.name) |  | ||||||
|             } else { |  | ||||||
|               ElMessage.error(res.msg || '上传失败') |  | ||||||
|             } |  | ||||||
|             return |  | ||||||
|           } |  | ||||||
|           try { |  | ||||||
|             const watermarkedBlob = await addBottomWatermark(file, { |  | ||||||
|               stripRatio: 0.18, // 水印条高度占原图高度比例 |  | ||||||
|               background: 'rgba(255,255,255,0.96)', |  | ||||||
|               text: '老陈机器人', |  | ||||||
|               textColor: '#333', |  | ||||||
|               fontFamily: 'PingFang SC, Microsoft YaHei, Arial', |  | ||||||
|               logo: botLogo |  | ||||||
|             }) |  | ||||||
|  |  | ||||||
|             const newFile = new File([watermarkedBlob], file.name, { type: watermarkedBlob.type || file.type }) |  | ||||||
|             const formData = new FormData() |  | ||||||
|             formData.append('file', newFile) |  | ||||||
|             const resp = await fetch(basePath + '/fileUploadAndDownload/upload?noSave=1', { |  | ||||||
|               method: 'POST', |  | ||||||
|               body: formData |  | ||||||
|             }) |  | ||||||
|             const res = await resp.json() |  | ||||||
|             if (res.code === 0) { |  | ||||||
|               const urlPath = getUrl(res.data.file.url) |  | ||||||
|               insertFn(urlPath, res.data.file.name) |  | ||||||
|             } else { |  | ||||||
|               ElMessage.error(res.msg || '上传失败') |  | ||||||
|             } |  | ||||||
|           } catch { |  | ||||||
|             ElMessage.error('处理水印失败') |  | ||||||
|             // 降级:直接走原图上传 |  | ||||||
|             const formData = new FormData() |  | ||||||
|             formData.append('file', file) |  | ||||||
|             const resp = await fetch(basePath + '/fileUploadAndDownload/upload?noSave=1', { method: 'POST', body: formData }) |  | ||||||
|             const res = await resp.json() |  | ||||||
|             if (res.code === 0) { |  | ||||||
|               const urlPath = getUrl(res.data.file.url) |  | ||||||
|               insertFn(urlPath, res.data.file.name) |  | ||||||
|             } else { |  | ||||||
|               ElMessage.error(res.msg || '上传失败') |  | ||||||
|             } |  | ||||||
|           } |  | ||||||
|         }, |  | ||||||
|         customInsert(res, insertFn) { |  | ||||||
|           if (res.code === 0) { |  | ||||||
|             const urlPath = getUrl(res.data.file.url) |  | ||||||
|             insertFn(urlPath, res.data.file.name) |  | ||||||
|             return |  | ||||||
|           } |  | ||||||
|           ElMessage.error(res.msg) |  | ||||||
|         } |  | ||||||
|       }, |  | ||||||
|       uploadVideo: { |  | ||||||
|         server: basePath + '/fileUploadAndDownload/upload?noSave=1', |  | ||||||
|         fieldName: 'file', |  | ||||||
|         maxFileSize: 1024 * 1024 * 100, // 限制视频大小为100MB |  | ||||||
|         maxNumberOfFiles: 1, |  | ||||||
|         customInsert(res, insertFn) { |  | ||||||
|           if (res.code === 0) { |  | ||||||
|             const urlPath = getUrl(res.data.file.url) |  | ||||||
|             insertFn(urlPath, res.data.file.name) |  | ||||||
|             return |  | ||||||
|           } |  | ||||||
|           ElMessage.error(res.msg) |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |   } | ||||||
|   editorConfig.MENU_CONF['uploadImage','uploadVideo'] = { |   editorConfig.MENU_CONF['uploadImage'] = { | ||||||
|     fieldName: 'file', |     fieldName: 'file', | ||||||
|     server: basePath + '/fileUploadAndDownload/upload?noSave=1', |     server: basePath + '/fileUploadAndDownload/upload?noSave=1', | ||||||
|  |     headers: { | ||||||
|  |       'x-token': userStore.token, | ||||||
|  |     }, | ||||||
|     customInsert(res, insertFn) { |     customInsert(res, insertFn) { | ||||||
|       if (res.code === 0) { |       if (res.code === 0) { | ||||||
|         const urlPath = getUrl(res.data.file.url) |         const urlPath = getUrl(res.data.file.url) | ||||||
| @@ -169,94 +86,8 @@ | |||||||
|       valueHtml.value = props.modelValue |       valueHtml.value = props.modelValue | ||||||
|     } |     } | ||||||
|   ) |   ) | ||||||
|  |  | ||||||
|   async function addBottomWatermark(file, options) { |  | ||||||
|     const { stripRatio = 0.18, background = 'rgba(255,255,255,0.96)', text = '', textColor = '#333', fontFamily = 'Arial', logo } = options || {} |  | ||||||
|  |  | ||||||
|     const img = await fileToImage(file) |  | ||||||
|     const width = img.naturalWidth || img.width |  | ||||||
|     const height = img.naturalHeight || img.height |  | ||||||
|     const stripHeight = Math.max(60, Math.floor(height * stripRatio)) |  | ||||||
|  |  | ||||||
|     const canvas = document.createElement('canvas') |  | ||||||
|     canvas.width = width |  | ||||||
|     canvas.height = height + stripHeight |  | ||||||
|     const ctx = canvas.getContext('2d') |  | ||||||
|  |  | ||||||
|     // 原图 |  | ||||||
|     ctx.drawImage(img, 0, 0, width, height) |  | ||||||
|  |  | ||||||
|     // 底部水印条背景 |  | ||||||
|     ctx.fillStyle = background |  | ||||||
|     ctx.fillRect(0, height, width, stripHeight) |  | ||||||
|  |  | ||||||
|     // 左侧 Logo(可选) |  | ||||||
|     let logoSize = Math.floor(stripHeight * 0.6) |  | ||||||
|     let logoPadding = Math.floor(stripHeight * 0.2) |  | ||||||
|     if (logo) { |  | ||||||
|       try { |  | ||||||
|         const logoImg = await srcToImage(logo) |  | ||||||
|         const ratio = logoImg.width / logoImg.height |  | ||||||
|         const drawW = logoSize |  | ||||||
|         const drawH = Math.floor(drawW / ratio) |  | ||||||
|         const y = height + Math.floor((stripHeight - drawH) / 2) |  | ||||||
|         ctx.drawImage(logoImg, logoPadding, y, drawW, drawH) |  | ||||||
|       } catch { void 0 } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     // 右侧文字 |  | ||||||
|     ctx.fillStyle = textColor |  | ||||||
|     const fontSize = Math.floor(stripHeight * 0.38) |  | ||||||
|     ctx.font = `${fontSize}px ${fontFamily}` |  | ||||||
|     ctx.textBaseline = 'middle' |  | ||||||
|     ctx.textAlign = 'right' |  | ||||||
|     const textPadding = Math.floor(stripHeight * 0.25) |  | ||||||
|     ctx.fillText(text, width - textPadding, height + Math.floor(stripHeight / 2)) |  | ||||||
|  |  | ||||||
|     const blob = await canvasToBlob(canvas, file.type || 'image/png') |  | ||||||
|     return blob |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   function fileToImage(file) { |  | ||||||
|     return new Promise((resolve, reject) => { |  | ||||||
|       const reader = new FileReader() |  | ||||||
|       reader.onload = () => { |  | ||||||
|         const img = new Image() |  | ||||||
|         img.onload = () => resolve(img) |  | ||||||
|         img.onerror = reject |  | ||||||
|         img.src = reader.result |  | ||||||
|       } |  | ||||||
|       reader.onerror = reject |  | ||||||
|       reader.readAsDataURL(file) |  | ||||||
|     }) |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   function srcToImage(src) { |  | ||||||
|     return new Promise((resolve, reject) => { |  | ||||||
|       const img = new Image() |  | ||||||
|       img.crossOrigin = 'anonymous' |  | ||||||
|       img.onload = () => resolve(img) |  | ||||||
|       img.onerror = reject |  | ||||||
|       img.src = src |  | ||||||
|     }) |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   function canvasToBlob(canvas, mime) { |  | ||||||
|     return new Promise((resolve) => { |  | ||||||
|       if (canvas.toBlob) { |  | ||||||
|         canvas.toBlob((blob) => resolve(blob), mime, 0.92) |  | ||||||
|       } else { |  | ||||||
|         // 兼容处理 |  | ||||||
|         const dataURL = canvas.toDataURL(mime) |  | ||||||
|         const arr = dataURL.split(',') |  | ||||||
|         const bstr = atob(arr[1]) |  | ||||||
|         let n = bstr.length |  | ||||||
|         const u8arr = new Uint8Array(n) |  | ||||||
|         while (n--) u8arr[n] = bstr.charCodeAt(n) |  | ||||||
|         resolve(new Blob([u8arr], { type: mime })) |  | ||||||
|       } |  | ||||||
|     }) |  | ||||||
|   } |  | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style scoped lang="scss"></style> | <style scoped lang="scss"> | ||||||
|  |  | ||||||
|  | </style> | ||||||
|   | |||||||
| @@ -47,7 +47,7 @@ | |||||||
|  |  | ||||||
|   const model = defineModel({ type: Array }) |   const model = defineModel({ type: Array }) | ||||||
|  |  | ||||||
|   const fileList = ref(model.value) |   const fileList = ref(model.value || []) | ||||||
|  |  | ||||||
|   const emits = defineEmits(['on-success', 'on-error']) |   const emits = defineEmits(['on-success', 'on-error']) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -2,9 +2,27 @@ | |||||||
|   <div> |   <div> | ||||||
|     <selectComponent :rounded="rounded" v-if="!props.multiple" :model="model" @chooseItem="openChooseImg" @deleteItem="openChooseImg" /> |     <selectComponent :rounded="rounded" v-if="!props.multiple" :model="model" @chooseItem="openChooseImg" @deleteItem="openChooseImg" /> | ||||||
|     <div v-else class="w-full gap-4 flex flex-wrap"> |     <div v-else class="w-full gap-4 flex flex-wrap"> | ||||||
|       <selectComponent :rounded="rounded" v-for="(item, index) in model" :key="index" :model="item" @chooseItem="openChooseImg" |       <draggable  | ||||||
|                        @deleteItem="deleteImg(index)" |         v-model="model"  | ||||||
|       /> |         class="flex flex-wrap gap-4"  | ||||||
|  |         item-key="url"  | ||||||
|  |         ghost-class="ghost-item"  | ||||||
|  |         handle=".drag-handle" | ||||||
|  |         animation="300" | ||||||
|  |         @start="onDragStart" | ||||||
|  |         @end="onDragEnd" | ||||||
|  |       > | ||||||
|  |         <template #item="{element, index}"> | ||||||
|  |           <div class="relative group"> | ||||||
|  |             <div class="drag-handle absolute left-2 top-2 w-8 h-8 flex items-center justify-center cursor-move z-10 opacity-0 group-hover:opacity-100 rounded-full"> | ||||||
|  |               <el-icon :size="18"><Menu /></el-icon> | ||||||
|  |             </div> | ||||||
|  |             <selectComponent :rounded="rounded" :model="element" @chooseItem="openChooseImg" | ||||||
|  |                            @deleteItem="deleteImg(index)" | ||||||
|  |             /> | ||||||
|  |           </div> | ||||||
|  |         </template> | ||||||
|  |       </draggable> | ||||||
|       <selectComponent :rounded="rounded" v-if="model?.length < props.maxUpdateCount || props.maxUpdateCount === 0" |       <selectComponent :rounded="rounded" v-if="model?.length < props.maxUpdateCount || props.maxUpdateCount === 0" | ||||||
|                        @chooseItem="openChooseImg" @deleteItem="openChooseImg" |                        @chooseItem="openChooseImg" @deleteItem="openChooseImg" | ||||||
|       /> |       /> | ||||||
| @@ -140,6 +158,7 @@ import { ElMessage, ElMessageBox } from 'element-plus' | |||||||
| import { | import { | ||||||
|   ArrowLeftBold, |   ArrowLeftBold, | ||||||
|   CloseBold, |   CloseBold, | ||||||
|  |   Menu, | ||||||
|   MoreFilled, |   MoreFilled, | ||||||
|   Picture as IconPicture, |   Picture as IconPicture, | ||||||
|   Plus, |   Plus, | ||||||
| @@ -149,6 +168,7 @@ import selectComponent from '@/components/selectImage/selectComponent.vue' | |||||||
| import { addCategory, deleteCategory, getCategoryList } from '@/api/attachmentCategory' | import { addCategory, deleteCategory, getCategoryList } from '@/api/attachmentCategory' | ||||||
| import CropperImage from "@/components/upload/cropper.vue"; | import CropperImage from "@/components/upload/cropper.vue"; | ||||||
| import QRCodeUpload from "@/components/upload/QR-code.vue"; | import QRCodeUpload from "@/components/upload/QR-code.vue"; | ||||||
|  | import draggable from 'vuedraggable' | ||||||
|  |  | ||||||
| const imageUrl = ref('') | const imageUrl = ref('') | ||||||
| const imageCommon = ref('') | const imageCommon = ref('') | ||||||
| @@ -424,6 +444,20 @@ const useSelectedImages = () => { | |||||||
|   selectedImages.value = [] |   selectedImages.value = [] | ||||||
| } | } | ||||||
|  |  | ||||||
|  | const onDragStart = () => { | ||||||
|  |   // 拖拽开始时的处理 | ||||||
|  |   document.body.style.cursor = 'grabbing' | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const onDragEnd = () => { | ||||||
|  |   // 拖拽结束时的处理 | ||||||
|  |   document.body.style.cursor = 'default' | ||||||
|  |   // 确保model是数组类型 | ||||||
|  |   if (!Array.isArray(model.value)) { | ||||||
|  |     model.value = [] | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
| </script> | </script> | ||||||
| <style scoped> | <style scoped> | ||||||
| .selected { | .selected { | ||||||
| @@ -450,4 +484,20 @@ const useSelectedImages = () => { | |||||||
|   border-left-color: transparent; |   border-left-color: transparent; | ||||||
|   transform: rotate(45deg); |   transform: rotate(45deg); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .ghost-item { | ||||||
|  |   opacity: 0.5; | ||||||
|  |   background: #c8ebfb; | ||||||
|  |   border: 1px dashed #409eff; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .drag-handle { | ||||||
|  |   background-color: rgba(64, 158, 255, 0.1); | ||||||
|  |   border-radius: 4px; | ||||||
|  |   transition: opacity 0.3s; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .drag-handle:hover { | ||||||
|  |   background-color: rgba(64, 158, 255, 0.2); | ||||||
|  | } | ||||||
| </style> | </style> | ||||||
|   | |||||||
| @@ -1,10 +1,12 @@ | |||||||
| /** | /** | ||||||
|  * 网站配置文件 |  * 网站配置文件 | ||||||
|  */ |  */ | ||||||
|  | import packageInfo from '../../package.json' | ||||||
|  |  | ||||||
| const greenText = (text) => `\x1b[32m${text}\x1b[0m` | const greenText = (text) => `\x1b[32m${text}\x1b[0m` | ||||||
|  |  | ||||||
| const config = { | const config = { | ||||||
|   appName: 'Lckt-Admin', |   appName: 'Gin-Vue-Admin', | ||||||
|   appLogo: 'logo.png', |   appLogo: 'logo.png', | ||||||
|   showViteLogo: true, |   showViteLogo: true, | ||||||
|   logs: [] |   logs: [] | ||||||
| @@ -12,20 +14,7 @@ const config = { | |||||||
|  |  | ||||||
| export const viteLogo = (env) => { | export const viteLogo = (env) => { | ||||||
|   if (config.showViteLogo) { |   if (config.showViteLogo) { | ||||||
|     console.log( |     console.log(greenText(`> 当前版本:v${packageInfo.version}`)) | ||||||
|       greenText( |  | ||||||
|         `> 欢迎使用Gin-Vue-Admin,开源地址:https://github.com/flipped-aurora/gin-vue-admin` |  | ||||||
|       ) |  | ||||||
|     ) |  | ||||||
|     console.log(greenText(`> 当前版本:v2.8.0`)) |  | ||||||
|     console.log(greenText(`> 加群方式:微信:shouzi_1994 QQ群:470239250`)) |  | ||||||
|     console.log( |  | ||||||
|       greenText(`> 项目地址:https://github.com/flipped-aurora/gin-vue-admin`) |  | ||||||
|     ) |  | ||||||
|     console.log(greenText(`> 插件市场:https://plugin.gin-vue-admin.com`)) |  | ||||||
|     console.log( |  | ||||||
|       greenText(`> GVA讨论社区:https://support.qq.com/products/371961`) |  | ||||||
|     ) |  | ||||||
|     console.log( |     console.log( | ||||||
|       greenText( |       greenText( | ||||||
|         `> 默认自动化文档地址:http://127.0.0.1:${env.VITE_SERVER_PORT}/swagger/index.html` |         `> 默认自动化文档地址:http://127.0.0.1:${env.VITE_SERVER_PORT}/swagger/index.html` | ||||||
| @@ -34,18 +23,6 @@ export const viteLogo = (env) => { | |||||||
|     console.log( |     console.log( | ||||||
|       greenText(`> 默认前端文件运行地址:http://127.0.0.1:${env.VITE_CLI_PORT}`) |       greenText(`> 默认前端文件运行地址:http://127.0.0.1:${env.VITE_CLI_PORT}`) | ||||||
|     ) |     ) | ||||||
|     console.log( |  | ||||||
|       greenText( |  | ||||||
|         `--------------------------------------版权声明--------------------------------------` |  | ||||||
|       ) |  | ||||||
|     ) |  | ||||||
|     console.log(greenText(`** 版权所有方:flipped-aurora开源团队 **`)) |  | ||||||
|     console.log(greenText(`** 版权持有公司:北京翻转极光科技有限责任公司 **`)) |  | ||||||
|     console.log( |  | ||||||
|       greenText( |  | ||||||
|         `** 剔除授权标识需购买商用授权:https://gin-vue-admin.com/empower/index.html **` |  | ||||||
|       ) |  | ||||||
|     ) |  | ||||||
|     console.log('\n') |     console.log('\n') | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -4,24 +4,10 @@ | |||||||
|  * */ |  * */ | ||||||
| // 加载网站配置文件夹 | // 加载网站配置文件夹 | ||||||
| import { register } from './global' | import { register } from './global' | ||||||
|  | import packageInfo from '../../package.json' | ||||||
|  |  | ||||||
| export default { | export default { | ||||||
|   install: (app) => { |   install: (app) => { | ||||||
|     register(app) |     register(app) | ||||||
|     console.log(` |  | ||||||
|        欢迎使用 Gin-Vue-Admin |  | ||||||
|        当前版本:v2.8.0 |  | ||||||
|        加群方式:微信:shouzi_1994 QQ群:622360840 |  | ||||||
|        项目地址:https://github.com/flipped-aurora/gin-vue-admin |  | ||||||
|        插件市场:https://plugin.gin-vue-admin.com |  | ||||||
|        GVA讨论社区:https://support.qq.com/products/371961 |  | ||||||
|        默认自动化文档地址:http://127.0.0.1:${import.meta.env.VITE_SERVER_PORT}/swagger/index.html |  | ||||||
|        默认前端文件运行地址:http://127.0.0.1:${import.meta.env.VITE_CLI_PORT} |  | ||||||
|        如果项目让您获得了收益,希望您能请团队喝杯可乐:https://www.gin-vue-admin.com/coffee/index.html |  | ||||||
|        --------------------------------------版权声明-------------------------------------- |  | ||||||
|        ** 版权所有方:flipped-aurora开源团队 ** |  | ||||||
|        ** 版权持有公司:北京翻转极光科技有限责任公司 ** |  | ||||||
|        ** 剔除授权标识需购买商用授权:https://gin-vue-admin.com/empower/index.html ** |  | ||||||
|     `) |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -7,22 +7,7 @@ export default { | |||||||
|       // 当被绑定的元素插入到 DOM 中时…… |       // 当被绑定的元素插入到 DOM 中时…… | ||||||
|       mounted: function (el, binding) { |       mounted: function (el, binding) { | ||||||
|         const userInfo = userStore.userInfo |         const userInfo = userStore.userInfo | ||||||
|         let type = '' |         if (!binding.value){ | ||||||
|         switch (Object.prototype.toString.call(binding.value)) { |  | ||||||
|           case '[object Array]': |  | ||||||
|             type = 'Array' |  | ||||||
|             break |  | ||||||
|           case '[object String]': |  | ||||||
|             type = 'String' |  | ||||||
|             break |  | ||||||
|           case '[object Number]': |  | ||||||
|             type = 'Number' |  | ||||||
|             break |  | ||||||
|           default: |  | ||||||
|             type = '' |  | ||||||
|             break |  | ||||||
|         } |  | ||||||
|         if (type === '') { |  | ||||||
|           el.parentNode.removeChild(el) |           el.parentNode.removeChild(el) | ||||||
|           return |           return | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -1,5 +1,6 @@ | |||||||
| import './style/element_visiable.scss' | import './style/element_visiable.scss' | ||||||
| import 'element-plus/theme-chalk/dark/css-vars.css' | import 'element-plus/theme-chalk/dark/css-vars.css' | ||||||
|  | import 'uno.css'; | ||||||
| import { createApp } from 'vue' | import { createApp } from 'vue' | ||||||
| import ElementPlus from 'element-plus' | import ElementPlus from 'element-plus' | ||||||
|  |  | ||||||
|   | |||||||
| @@ -35,13 +35,21 @@ | |||||||
|   "/src/view/layout/aside/headMode.vue": "GvaAside", |   "/src/view/layout/aside/headMode.vue": "GvaAside", | ||||||
|   "/src/view/layout/aside/index.vue": "Index", |   "/src/view/layout/aside/index.vue": "Index", | ||||||
|   "/src/view/layout/aside/normalMode.vue": "GvaAside", |   "/src/view/layout/aside/normalMode.vue": "GvaAside", | ||||||
|  |   "/src/view/layout/aside/sidebarMode.vue": "SidebarMode", | ||||||
|   "/src/view/layout/header/index.vue": "Index", |   "/src/view/layout/header/index.vue": "Index", | ||||||
|   "/src/view/layout/header/tools.vue": "Tools", |   "/src/view/layout/header/tools.vue": "Tools", | ||||||
|   "/src/view/layout/iframe.vue": "GvaLayoutIframe", |   "/src/view/layout/iframe.vue": "GvaLayoutIframe", | ||||||
|   "/src/view/layout/index.vue": "GvaLayout", |   "/src/view/layout/index.vue": "GvaLayout", | ||||||
|   "/src/view/layout/screenfull/index.vue": "Screenfull", |   "/src/view/layout/screenfull/index.vue": "Screenfull", | ||||||
|   "/src/view/layout/search/search.vue": "BtnBox", |   "/src/view/layout/search/search.vue": "BtnBox", | ||||||
|  |   "/src/view/layout/setting/components/layoutModeCard.vue": "LayoutModeCard", | ||||||
|  |   "/src/view/layout/setting/components/settingItem.vue": "SettingItem", | ||||||
|  |   "/src/view/layout/setting/components/themeColorPicker.vue": "ThemeColorPicker", | ||||||
|  |   "/src/view/layout/setting/components/themeModeSelector.vue": "ThemeModeSelector", | ||||||
|   "/src/view/layout/setting/index.vue": "GvaSetting", |   "/src/view/layout/setting/index.vue": "GvaSetting", | ||||||
|  |   "/src/view/layout/setting/modules/appearance/index.vue": "AppearanceSettings", | ||||||
|  |   "/src/view/layout/setting/modules/general/index.vue": "GeneralSettings", | ||||||
|  |   "/src/view/layout/setting/modules/layout/index.vue": "LayoutSettings", | ||||||
|   "/src/view/layout/setting/title.vue": "layoutSettingTitle", |   "/src/view/layout/setting/title.vue": "layoutSettingTitle", | ||||||
|   "/src/view/layout/tabs/index.vue": "HistoryComponent", |   "/src/view/layout/tabs/index.vue": "HistoryComponent", | ||||||
|   "/src/view/login/index.vue": "Login", |   "/src/view/login/index.vue": "Login", | ||||||
| @@ -69,6 +77,9 @@ | |||||||
|   "/src/view/systemTools/autoCode/component/fieldDialog.vue": "FieldDialog", |   "/src/view/systemTools/autoCode/component/fieldDialog.vue": "FieldDialog", | ||||||
|   "/src/view/systemTools/autoCode/component/previewCodeDialog.vue": "PreviewCodeDialog", |   "/src/view/systemTools/autoCode/component/previewCodeDialog.vue": "PreviewCodeDialog", | ||||||
|   "/src/view/systemTools/autoCode/index.vue": "AutoCode", |   "/src/view/systemTools/autoCode/index.vue": "AutoCode", | ||||||
|  |   "/src/view/systemTools/autoCode/mcp.vue": "MCP", | ||||||
|  |   "/src/view/systemTools/autoCode/mcpTest.vue": "MCPTest", | ||||||
|  |   "/src/view/systemTools/autoCode/picture.vue": "Picture", | ||||||
|   "/src/view/systemTools/autoCodeAdmin/index.vue": "AutoCodeAdmin", |   "/src/view/systemTools/autoCodeAdmin/index.vue": "AutoCodeAdmin", | ||||||
|   "/src/view/systemTools/autoPkg/autoPkg.vue": "AutoPkg", |   "/src/view/systemTools/autoPkg/autoPkg.vue": "AutoPkg", | ||||||
|   "/src/view/systemTools/exportTemplate/exportTemplate.vue": "ExportTemplate", |   "/src/view/systemTools/exportTemplate/exportTemplate.vue": "ExportTemplate", | ||||||
| @@ -77,6 +88,7 @@ | |||||||
|   "/src/view/systemTools/installPlugin/index.vue": "Index", |   "/src/view/systemTools/installPlugin/index.vue": "Index", | ||||||
|   "/src/view/systemTools/pubPlug/pubPlug.vue": "PubPlug", |   "/src/view/systemTools/pubPlug/pubPlug.vue": "PubPlug", | ||||||
|   "/src/view/systemTools/system/system.vue": "Config", |   "/src/view/systemTools/system/system.vue": "Config", | ||||||
|  |   "/src/view/systemTools/version/version.vue": "SysVersion", | ||||||
|   "/src/view/user/index.vue": "UserManage", |   "/src/view/user/index.vue": "UserManage", | ||||||
|   "/src/view/user/user/index.vue": "Index", |   "/src/view/user/user/index.vue": "Index", | ||||||
|   "/src/view/user/user/teacherApply.vue": "TeacherApply", |   "/src/view/user/user/teacherApply.vue": "TeacherApply", | ||||||
|   | |||||||
| @@ -115,7 +115,7 @@ router.beforeEach(async (to, from) => { | |||||||
|  |  | ||||||
|       return { |       return { | ||||||
|         name: 'Login', |         name: 'Login', | ||||||
|         query: { redirect: to.href } |         query: { redirect: to.fullPath } | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -126,7 +126,7 @@ router.beforeEach(async (to, from) => { | |||||||
|   return { |   return { | ||||||
|     name: 'Login', |     name: 'Login', | ||||||
|     query: { |     query: { | ||||||
|       redirect: document.location.hash |       redirect: to.fullPath | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| }) | }) | ||||||
|   | |||||||
| @@ -99,6 +99,27 @@ export const useAppStore = defineStore('app', () => { | |||||||
|     config.transition_type = e |     config.transition_type = e | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   const baseCoinfg = { | ||||||
|  |     weakness: false, | ||||||
|  |     grey: false, | ||||||
|  |     primaryColor: '#3b82f6', | ||||||
|  |     showTabs: true, | ||||||
|  |     darkMode: 'auto', | ||||||
|  |     layout_side_width: 256, | ||||||
|  |     layout_side_collapsed_width: 80, | ||||||
|  |     layout_side_item_height: 48, | ||||||
|  |     show_watermark: true, | ||||||
|  |     side_mode: 'normal', | ||||||
|  |     // 页面过渡动画配置 | ||||||
|  |     transition_type: 'slide' | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const resetConfig = () => { | ||||||
|  |     for (let baseCoinfgKey in baseCoinfg) { | ||||||
|  |       config[baseCoinfgKey] = baseCoinfg[baseCoinfgKey] | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|   // 监听色弱模式和灰色模式 |   // 监听色弱模式和灰色模式 | ||||||
|   watchEffect(() => { |   watchEffect(() => { | ||||||
|     document.documentElement.classList.toggle('html-weakenss', config.weakness) |     document.documentElement.classList.toggle('html-weakenss', config.weakness) | ||||||
| @@ -128,6 +149,7 @@ export const useAppStore = defineStore('app', () => { | |||||||
|     toggleConfigSideItemHeight, |     toggleConfigSideItemHeight, | ||||||
|     toggleConfigWatermark, |     toggleConfigWatermark, | ||||||
|     toggleSideMode, |     toggleSideMode, | ||||||
|     toggleTransition |     toggleTransition, | ||||||
|  |     resetConfig | ||||||
|   } |   } | ||||||
| }) | }) | ||||||
|   | |||||||
| @@ -1,9 +1,10 @@ | |||||||
| import { asyncRouterHandle } from '@/utils/asyncRouter' | import { asyncRouterHandle } from '@/utils/asyncRouter' | ||||||
| import { emitter } from '@/utils/bus.js' | import { emitter } from '@/utils/bus.js' | ||||||
| import { asyncMenu, getMenuList } from '@/api/menu' | import { asyncMenu } from '@/api/menu' | ||||||
| import { defineStore } from 'pinia' | import { defineStore } from 'pinia' | ||||||
| import { ref, watchEffect } from 'vue' | import { ref, watchEffect } from 'vue' | ||||||
| import pathInfo from '@/pathInfo.json' | import pathInfo from '@/pathInfo.json' | ||||||
|  | import {useRoute} from "vue-router"; | ||||||
|  |  | ||||||
| const notLayoutRouterArr = [] | const notLayoutRouterArr = [] | ||||||
| const keepAliveRoutersArr = [] | const keepAliveRoutersArr = [] | ||||||
| @@ -51,13 +52,33 @@ export const useRouterStore = defineStore('router', () => { | |||||||
|   const asyncRouterFlag = ref(0) |   const asyncRouterFlag = ref(0) | ||||||
|   const setKeepAliveRouters = (history) => { |   const setKeepAliveRouters = (history) => { | ||||||
|     const keepArrTemp = [] |     const keepArrTemp = [] | ||||||
|  |      | ||||||
|  |     // 1. 首先添加原有的keepAlive配置 | ||||||
|  |     keepArrTemp.push(...keepAliveRoutersArr) | ||||||
|  |      | ||||||
|     history.forEach((item) => { |     history.forEach((item) => { | ||||||
|  |       // 2. 为所有history中的路由强制启用keep-alive | ||||||
|  |       // 通过routeMap获取路由信息,然后通过pathInfo获取组件名 | ||||||
|  |       const routeInfo = routeMap[item.name] | ||||||
|  |       if (routeInfo && routeInfo.meta && routeInfo.meta.path) { | ||||||
|  |         const componentName = pathInfo[routeInfo.meta.path] | ||||||
|  |         if (componentName) { | ||||||
|  |           keepArrTemp.push(componentName) | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       // 3. 如果子路由在tabs中打开,父路由也需要keepAlive | ||||||
|       if (nameMap[item.name]) { |       if (nameMap[item.name]) { | ||||||
|         keepArrTemp.push(nameMap[item.name]) |         keepArrTemp.push(nameMap[item.name]) | ||||||
|       } |       } | ||||||
|     }) |     }) | ||||||
|  |      | ||||||
|     keepAliveRouters.value = Array.from(new Set(keepArrTemp)) |     keepAliveRouters.value = Array.from(new Set(keepArrTemp)) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |   const route = useRoute() | ||||||
|  |  | ||||||
|   emitter.on('setKeepAlive', setKeepAliveRouters) |   emitter.on('setKeepAlive', setKeepAliveRouters) | ||||||
|  |  | ||||||
|   const asyncRouters = ref([]) |   const asyncRouters = ref([]) | ||||||
| @@ -80,14 +101,32 @@ export const useRouterStore = defineStore('router', () => { | |||||||
|     return menuMap[name]?.children |     return menuMap[name]?.children | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   const findTopActive = (menuMap, routeName) => { | ||||||
|  |     for (let topName in menuMap) { | ||||||
|  |       const topItem = menuMap[topName]; | ||||||
|  |       if (topItem.children?.some(item => item.name === routeName)) { | ||||||
|  |         return topName; | ||||||
|  |       } | ||||||
|  |       const foundName = findTopActive(topItem.children || {}, routeName); | ||||||
|  |       if (foundName) { | ||||||
|  |         return topName; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     return null; | ||||||
|  |   }; | ||||||
|  |  | ||||||
|   watchEffect(() => { |   watchEffect(() => { | ||||||
|     let topActive = sessionStorage.getItem('topActive') |     let topActive = sessionStorage.getItem('topActive') | ||||||
|  |     // 初始化菜单内容,防止重复添加 | ||||||
|  |     topMenu.value = []; | ||||||
|     asyncRouters.value[0]?.children.forEach((item) => { |     asyncRouters.value[0]?.children.forEach((item) => { | ||||||
|       if (item.hidden) return |       if (item.hidden) return | ||||||
|       menuMap[item.name] = item |       menuMap[item.name] = item | ||||||
|       topMenu.value.push({ ...item, children: [] }) |       topMenu.value.push({ ...item, children: [] }) | ||||||
|     }) |     }) | ||||||
|  |     if (!topActive || topActive === 'undefined' || topActive === 'null') { | ||||||
|  |       topActive = findTopActive(menuMap, route.name); | ||||||
|  |     } | ||||||
|     setLeftMenu(topActive) |     setLeftMenu(topActive) | ||||||
|   }) |   }) | ||||||
|  |  | ||||||
| @@ -106,10 +145,8 @@ export const useRouterStore = defineStore('router', () => { | |||||||
|         children: [] |         children: [] | ||||||
|       } |       } | ||||||
|     ] |     ] | ||||||
|     // const asyncRouterRes = await asyncMenu() |     const asyncRouterRes = await asyncMenu() | ||||||
|     const asyncRouterRes = await getMenuList() |     const asyncRouter = asyncRouterRes.data.menus | ||||||
|     // const asyncRouter = asyncRouterRes.data.menus |  | ||||||
|     const asyncRouter = asyncRouterRes.data |  | ||||||
|     asyncRouter && |     asyncRouter && | ||||||
|       asyncRouter.push({ |       asyncRouter.push({ | ||||||
|         path: 'reload', |         path: 'reload', | ||||||
|   | |||||||
| @@ -71,7 +71,6 @@ export const useUserStore = defineStore('user', () => { | |||||||
|       const res = await login(loginInfo) |       const res = await login(loginInfo) | ||||||
|  |  | ||||||
|       if (res.code !== 0) { |       if (res.code !== 0) { | ||||||
|         ElMessage.error(res.message || '登录失败') |  | ||||||
|         return false |         return false | ||||||
|       } |       } | ||||||
|       // 登陆成功,设置用户信息和权限相关信息 |       // 登陆成功,设置用户信息和权限相关信息 | ||||||
| @@ -88,8 +87,13 @@ export const useUserStore = defineStore('user', () => { | |||||||
|         router.addRoute(asyncRouter) |         router.addRoute(asyncRouter) | ||||||
|       }) |       }) | ||||||
|  |  | ||||||
|  |       if(router.currentRoute.value.query.redirect) { | ||||||
|  |         await router.replace(router.currentRoute.value.query.redirect) | ||||||
|  |         return true | ||||||
|  |       } | ||||||
|  |  | ||||||
|       if (!router.hasRoute(userInfo.value.authority.defaultRouter)) { |       if (!router.hasRoute(userInfo.value.authority.defaultRouter)) { | ||||||
|         ElMessage.error('请联系管理员进行授权') |         ElMessage.error('不存在可以登陆的首页,请联系管理员进行配置') | ||||||
|       } else { |       } else { | ||||||
|         await router.replace({ name: userInfo.value.authority.defaultRouter }) |         await router.replace({ name: userInfo.value.authority.defaultRouter }) | ||||||
|       } |       } | ||||||
| @@ -124,9 +128,12 @@ export const useUserStore = defineStore('user', () => { | |||||||
|   /* 清理数据 */ |   /* 清理数据 */ | ||||||
|   const ClearStorage = async () => { |   const ClearStorage = async () => { | ||||||
|     token.value = '' |     token.value = '' | ||||||
|     xToken.value = '' |     // 使用remove方法正确删除cookie | ||||||
|  |     xToken.remove() | ||||||
|     sessionStorage.clear() |     sessionStorage.clear() | ||||||
|  |     // 清理所有相关的localStorage项 | ||||||
|     localStorage.removeItem('originSetting') |     localStorage.removeItem('originSetting') | ||||||
|  |     localStorage.removeItem('token') | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   return { |   return { | ||||||
|   | |||||||
| @@ -99,7 +99,7 @@ | |||||||
|       > |       > | ||||||
|         <el-table-column type="selection" width="55" /> |         <el-table-column type="selection" width="55" /> | ||||||
|  |  | ||||||
|         <el-table-column align="left" label="日期" prop="createdAt" width="180"> |         <el-table-column align="left" label="日期" prop="CreatedAt" width="180"> | ||||||
|           <template #default="scope"> |           <template #default="scope"> | ||||||
|             {{ formatDate(scope.row.CreatedAt) }} |             {{ formatDate(scope.row.CreatedAt) }} | ||||||
|           </template> |           </template> | ||||||
|   | |||||||
| @@ -31,30 +31,6 @@ const routes = [ | |||||||
|     }, |     }, | ||||||
|     component: () => import('@/view/error/index.vue') |     component: () => import('@/view/error/index.vue') | ||||||
|   }, |   }, | ||||||
|   { |  | ||||||
|     path: '/kefu/login', |  | ||||||
|     name: 'ServiceLogin', |  | ||||||
|     component: () => import('@/plugin/customerservice/view/login/index.vue'), |  | ||||||
|     meta:{ |  | ||||||
|       client:true |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     path: '/kefu/main', |  | ||||||
|     name: 'ServiceMain', |  | ||||||
|     component: () => import('@/plugin/customerservice/view/chat/index.vue'), |  | ||||||
|     meta:{ |  | ||||||
|       client:true |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     path: '/kefu/test', |  | ||||||
|     name: 'ServiceUserTest', |  | ||||||
|     component: () => import('@/plugin/customerservice/view/chat/test.vue'), |  | ||||||
|     meta:{ |  | ||||||
|       client:true |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
| ] | ] | ||||||
|  |  | ||||||
| const router = createRouter({ | const router = createRouter({ | ||||||
|   | |||||||
| @@ -1,9 +1,6 @@ | |||||||
| @use '@/style/main.scss'; | @use '@/style/main.scss'; | ||||||
| @use '@/style/reset'; | @use '@/style/reset'; | ||||||
|  |  | ||||||
| @tailwind base; |  | ||||||
| @tailwind components; |  | ||||||
| @tailwind utilities; |  | ||||||
|  |  | ||||||
| .el-button { | .el-button { | ||||||
|   font-weight: 400; |   font-weight: 400; | ||||||
| @@ -118,6 +115,10 @@ | |||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .el-menu-item.is-active{ | ||||||
|  |   color: var(--el-color-primary)!important; | ||||||
|  | } | ||||||
|  |  | ||||||
| .el-sub-menu__title.el-tooltip__trigger, | .el-sub-menu__title.el-tooltip__trigger, | ||||||
| .el-menu-item .el-menu-tooltip__trigger { | .el-menu-item .el-menu-tooltip__trigger { | ||||||
|   justify-content: center; |   justify-content: center; | ||||||
|   | |||||||
| @@ -17,7 +17,15 @@ | |||||||
| } | } | ||||||
|  |  | ||||||
| .gva-btn-list { | .gva-btn-list { | ||||||
|   @apply mb-3 flex items-center; |   @apply mb-3 flex items-center flex-wrap gap-2; | ||||||
|  |   .el-button+.el-button{ | ||||||
|  |     @apply ml-0 !important; | ||||||
|  |   } | ||||||
|  |   .el-upload{ | ||||||
|  |     .el-button{ | ||||||
|  |       @apply ml-0 !important; | ||||||
|  |     } | ||||||
|  |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| #nprogress .bar { | #nprogress .bar { | ||||||
| @@ -34,9 +42,7 @@ | |||||||
| .gva-search-box { | .gva-search-box { | ||||||
|   @apply p-4 bg-white text-slate-700 dark:text-slate-400 dark:bg-slate-900 rounded my-2; |   @apply p-4 bg-white text-slate-700 dark:text-slate-400 dark:bg-slate-900 rounded my-2; | ||||||
| } | } | ||||||
| .search-form{ |  | ||||||
|   @apply p-4 bg-white dark:text-slate-400 dark:bg-slate-900 |  | ||||||
| } |  | ||||||
| .gva-form-box { | .gva-form-box { | ||||||
|   @apply p-4 bg-white text-slate-700 dark:text-slate-400 dark:bg-slate-900 rounded my-2; |   @apply p-4 bg-white text-slate-700 dark:text-slate-400 dark:bg-slate-900 rounded my-2; | ||||||
| } | } | ||||||
| @@ -51,18 +57,3 @@ | |||||||
|     outline: none; |     outline: none; | ||||||
|   } |   } | ||||||
| } | } | ||||||
| .container-wrapper{ |  | ||||||
|   min-height: calc(100vh - 12rem); |  | ||||||
|   display: flex; |  | ||||||
|   flex-direction: column; |  | ||||||
|   justify-content: space-between; |  | ||||||
|   position: relative; |  | ||||||
| } |  | ||||||
| .footer-box{ |  | ||||||
|   display: flex; |  | ||||||
|   justify-content: center; |  | ||||||
|   padding: 1rem 0; |  | ||||||
|   position: sticky; |  | ||||||
|   bottom: 0; |  | ||||||
|   //border-top: 1px solid #ededed; |  | ||||||
| } |  | ||||||
|   | |||||||
| @@ -393,7 +393,7 @@ fieldset, | |||||||
| table, | table, | ||||||
| th, | th, | ||||||
| td { | td { | ||||||
|   border: none; |   // border: none; | ||||||
|   font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', |   font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', | ||||||
|     'Microsoft YaHei', '微软雅黑', Arial, sans-serif; |     'Microsoft YaHei', '微软雅黑', Arial, sans-serif; | ||||||
|   font-size: 14px; |   font-size: 14px; | ||||||
|   | |||||||
| @@ -1,8 +1,8 @@ | |||||||
| import axios from 'axios' // 引入axios | import axios from 'axios' // 引入axios | ||||||
| import { ElMessage, ElMessageBox } from 'element-plus' |  | ||||||
| import { useUserStore } from '@/pinia/modules/user' | import { useUserStore } from '@/pinia/modules/user' | ||||||
|  | import { ElLoading, ElMessage } from 'element-plus' | ||||||
|  | import { emitter } from '@/utils/bus' | ||||||
| import router from '@/router/index' | import router from '@/router/index' | ||||||
| import { ElLoading } from 'element-plus' |  | ||||||
|  |  | ||||||
| const service = axios.create({ | const service = axios.create({ | ||||||
|   baseURL: import.meta.env.VITE_BASE_API, |   baseURL: import.meta.env.VITE_BASE_API, | ||||||
| @@ -11,6 +11,9 @@ const service = axios.create({ | |||||||
| let activeAxios = 0 | let activeAxios = 0 | ||||||
| let timer | let timer | ||||||
| let loadingInstance | let loadingInstance | ||||||
|  | let isLoadingVisible = false | ||||||
|  | let forceCloseTimer | ||||||
|  |  | ||||||
| const showLoading = ( | const showLoading = ( | ||||||
|   option = { |   option = { | ||||||
|     target: null |     target: null | ||||||
| @@ -18,13 +21,33 @@ const showLoading = ( | |||||||
| ) => { | ) => { | ||||||
|   const loadDom = document.getElementById('gva-base-load-dom') |   const loadDom = document.getElementById('gva-base-load-dom') | ||||||
|   activeAxios++ |   activeAxios++ | ||||||
|  |  | ||||||
|  |   // 清除之前的定时器 | ||||||
|   if (timer) { |   if (timer) { | ||||||
|     clearTimeout(timer) |     clearTimeout(timer) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   // 清除强制关闭定时器 | ||||||
|  |   if (forceCloseTimer) { | ||||||
|  |     clearTimeout(forceCloseTimer) | ||||||
|  |   } | ||||||
|  |  | ||||||
|   timer = setTimeout(() => { |   timer = setTimeout(() => { | ||||||
|     if (activeAxios > 0) { |     // 再次检查activeAxios状态,防止竞态条件 | ||||||
|  |     if (activeAxios > 0 && !isLoadingVisible) { | ||||||
|       if (!option.target) option.target = loadDom |       if (!option.target) option.target = loadDom | ||||||
|       loadingInstance = ElLoading.service(option) |       loadingInstance = ElLoading.service(option) | ||||||
|  |       isLoadingVisible = true | ||||||
|  |  | ||||||
|  |       // 设置强制关闭定时器,防止loading永远不关闭(30秒超时) | ||||||
|  |       forceCloseTimer = setTimeout(() => { | ||||||
|  |         if (isLoadingVisible && loadingInstance) { | ||||||
|  |           console.warn('Loading强制关闭:超时30秒') | ||||||
|  |           loadingInstance.close() | ||||||
|  |           isLoadingVisible = false | ||||||
|  |           activeAxios = 0 // 重置计数器 | ||||||
|  |         } | ||||||
|  |       }, 30000) | ||||||
|     } |     } | ||||||
|   }, 400) |   }, 400) | ||||||
| } | } | ||||||
| @@ -32,10 +55,47 @@ const showLoading = ( | |||||||
| const closeLoading = () => { | const closeLoading = () => { | ||||||
|   activeAxios-- |   activeAxios-- | ||||||
|   if (activeAxios <= 0) { |   if (activeAxios <= 0) { | ||||||
|  |     activeAxios = 0 // 确保不会变成负数 | ||||||
|     clearTimeout(timer) |     clearTimeout(timer) | ||||||
|     loadingInstance && loadingInstance.close() |  | ||||||
|  |     if (forceCloseTimer) { | ||||||
|  |       clearTimeout(forceCloseTimer) | ||||||
|  |       forceCloseTimer = null | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (isLoadingVisible && loadingInstance) { | ||||||
|  |       loadingInstance.close() | ||||||
|  |       isLoadingVisible = false | ||||||
|  |     } | ||||||
|  |     loadingInstance = null | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // 全局重置loading状态的函数,用于异常情况 | ||||||
|  | const resetLoading = () => { | ||||||
|  |   activeAxios = 0 | ||||||
|  |   isLoadingVisible = false | ||||||
|  |  | ||||||
|  |   if (timer) { | ||||||
|  |     clearTimeout(timer) | ||||||
|  |     timer = null | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (forceCloseTimer) { | ||||||
|  |     clearTimeout(forceCloseTimer) | ||||||
|  |     forceCloseTimer = null | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (loadingInstance) { | ||||||
|  |     try { | ||||||
|  |       loadingInstance.close() | ||||||
|  |     } catch (e) { | ||||||
|  |       console.warn('关闭loading时出错:', e) | ||||||
|  |     } | ||||||
|  |     loadingInstance = null | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
| // http request 拦截器 | // http request 拦截器 | ||||||
| service.interceptors.request.use( | service.interceptors.request.use( | ||||||
|   (config) => { |   (config) => { | ||||||
| @@ -55,15 +115,18 @@ service.interceptors.request.use( | |||||||
|     if (!error.config.donNotShowLoading) { |     if (!error.config.donNotShowLoading) { | ||||||
|       closeLoading() |       closeLoading() | ||||||
|     } |     } | ||||||
|     ElMessage({ |     emitter.emit('show-error', { | ||||||
|       showClose: true, |       code: 'request', | ||||||
|       message: error, |       message: error.message || '请求发送失败' | ||||||
|       type: 'error' |  | ||||||
|     }) |     }) | ||||||
|     return error |     return error | ||||||
|   } |   } | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | function getErrorMessage(error) { | ||||||
|  |   return error.response?.data?.msg || '请求失败' | ||||||
|  | } | ||||||
|  |  | ||||||
| // http response 拦截器 | // http response 拦截器 | ||||||
| service.interceptors.response.use( | service.interceptors.response.use( | ||||||
|   (response) => { |   (response) => { | ||||||
| @@ -74,6 +137,9 @@ service.interceptors.response.use( | |||||||
|     if (response.headers['new-token']) { |     if (response.headers['new-token']) { | ||||||
|       userStore.setToken(response.headers['new-token']) |       userStore.setToken(response.headers['new-token']) | ||||||
|     } |     } | ||||||
|  |     if (typeof response.data.code === 'undefined') { | ||||||
|  |       return response | ||||||
|  |     } | ||||||
|     if (response.data.code === 0 || response.headers.success === 'true') { |     if (response.data.code === 0 || response.headers.success === 'true') { | ||||||
|       if (response.headers.msg) { |       if (response.headers.msg) { | ||||||
|         response.data.msg = decodeURI(response.headers.msg) |         response.data.msg = decodeURI(response.headers.msg) | ||||||
| @@ -94,79 +160,43 @@ service.interceptors.response.use( | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     if (!error.response) { |     if (!error.response) { | ||||||
|       ElMessageBox.confirm( |       // 网络错误 | ||||||
|         ` |       resetLoading() | ||||||
|         <p>检测到请求错误</p> |       emitter.emit('show-error', { | ||||||
|         <p>${error}</p> |         code: 'network', | ||||||
|         `, |         message: getErrorMessage(error) | ||||||
|         '请求报错', |       }) | ||||||
|         { |       return Promise.reject(error) | ||||||
|           dangerouslyUseHTMLString: true, |     } | ||||||
|           distinguishCancelAndClose: true, |  | ||||||
|           confirmButtonText: '稍后重试', |     // HTTP 状态码错误 | ||||||
|           cancelButtonText: '取消' |     if (error.response.status === 401) { | ||||||
|  |       emitter.emit('show-error', { | ||||||
|  |         code: '401', | ||||||
|  |         message: getErrorMessage(error), | ||||||
|  |         fn: () => { | ||||||
|  |           const userStore = useUserStore() | ||||||
|  |           userStore.ClearStorage() | ||||||
|  |           router.push({ name: 'Login', replace: true }) | ||||||
|         } |         } | ||||||
|       ) |       }) | ||||||
|       return |       return Promise.reject(error) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     switch (error.response.status) { |     emitter.emit('show-error', { | ||||||
|       case 500: |       code: error.response.status, | ||||||
|         ElMessageBox.confirm( |       message: getErrorMessage(error) | ||||||
|           ` |     }) | ||||||
|         <p>检测到接口错误${error}</p> |     return Promise.reject(error) | ||||||
|         <p>错误码<span style="color:red"> 500 </span>:此类错误内容常见于后台panic,请先查看后台日志,如果影响您正常使用可强制登出清理缓存</p> |  | ||||||
|         `, |  | ||||||
|           '接口报错', |  | ||||||
|           { |  | ||||||
|             dangerouslyUseHTMLString: true, |  | ||||||
|             distinguishCancelAndClose: true, |  | ||||||
|             confirmButtonText: '清理缓存', |  | ||||||
|             cancelButtonText: '取消' |  | ||||||
|           } |  | ||||||
|         ).then(() => { |  | ||||||
|           const userStore = useUserStore() |  | ||||||
|           userStore.ClearStorage() |  | ||||||
|           router.push({ name: 'Login', replace: true }) |  | ||||||
|         }) |  | ||||||
|         break |  | ||||||
|       case 404: |  | ||||||
|         ElMessageBox.confirm( |  | ||||||
|           ` |  | ||||||
|           <p>检测到接口错误${error}</p> |  | ||||||
|           <p>错误码<span style="color:red"> 404 </span>:此类错误多为接口未注册(或未重启)或者请求路径(方法)与api路径(方法)不符--如果为自动化代码请检查是否存在空格</p> |  | ||||||
|           `, |  | ||||||
|           '接口报错', |  | ||||||
|           { |  | ||||||
|             dangerouslyUseHTMLString: true, |  | ||||||
|             distinguishCancelAndClose: true, |  | ||||||
|             confirmButtonText: '我知道了', |  | ||||||
|             cancelButtonText: '取消' |  | ||||||
|           } |  | ||||||
|         ) |  | ||||||
|         break |  | ||||||
|       case 401: |  | ||||||
|         ElMessageBox.confirm( |  | ||||||
|           ` |  | ||||||
|           <p>无效的令牌</p> |  | ||||||
|           <p>错误码:<span style="color:red"> 401 </span>错误信息:${error}</p> |  | ||||||
|           `, |  | ||||||
|           '身份信息', |  | ||||||
|           { |  | ||||||
|             dangerouslyUseHTMLString: true, |  | ||||||
|             distinguishCancelAndClose: true, |  | ||||||
|             confirmButtonText: '重新登录', |  | ||||||
|             cancelButtonText: '取消' |  | ||||||
|           } |  | ||||||
|         ).then(() => { |  | ||||||
|           const userStore = useUserStore() |  | ||||||
|           userStore.ClearStorage() |  | ||||||
|           router.push({ name: 'Login', replace: true }) |  | ||||||
|         }) |  | ||||||
|         break |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return error |  | ||||||
|   } |   } | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | // 监听页面卸载事件,确保loading被正确清理 | ||||||
|  | if (typeof window !== 'undefined') { | ||||||
|  |   window.addEventListener('beforeunload', resetLoading) | ||||||
|  |   window.addEventListener('unload', resetLoading) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // 导出service和resetLoading函数 | ||||||
|  | export { resetLoading } | ||||||
| export default service | export default service | ||||||
|   | |||||||
| @@ -24,6 +24,7 @@ | |||||||
| <script setup> | <script setup> | ||||||
|   import { useUserStore } from '@/pinia/modules/user' |   import { useUserStore } from '@/pinia/modules/user' | ||||||
|   import { useRouter } from 'vue-router' |   import { useRouter } from 'vue-router' | ||||||
|  |   import { emitter } from '@/utils/bus' | ||||||
|  |  | ||||||
|   defineOptions({ |   defineOptions({ | ||||||
|     name: 'Error' |     name: 'Error' | ||||||
| @@ -32,6 +33,17 @@ | |||||||
|   const userStore = useUserStore() |   const userStore = useUserStore() | ||||||
|   const router = useRouter() |   const router = useRouter() | ||||||
|   const toDashboard = () => { |   const toDashboard = () => { | ||||||
|     router.push({ name: userStore.userInfo.authority.defaultRouter }) |     try { | ||||||
|  |       router.push({ name: userStore.userInfo.authority.defaultRouter }) | ||||||
|  |     } catch (error) { | ||||||
|  |         emitter.emit('show-error', { | ||||||
|  |         code: '401', | ||||||
|  |         message: "检测到其他用户修改了路由权限,请重新登录", | ||||||
|  |         fn: () => { | ||||||
|  |           userStore.ClearStorage() | ||||||
|  |           router.push({ name: 'Login', replace: true }) | ||||||
|  |         } | ||||||
|  |       }) | ||||||
|  |     } | ||||||
|   } |   } | ||||||
| </script> | </script> | ||||||
|   | |||||||
| @@ -136,7 +136,7 @@ | |||||||
|   // @ts-ignore |   // @ts-ignore | ||||||
|   import { initDB } from '@/api/initdb' |   import { initDB } from '@/api/initdb' | ||||||
|   import { reactive, ref } from 'vue' |   import { reactive, ref } from 'vue' | ||||||
|   import { ElLoading, ElMessage } from 'element-plus' |   import { ElLoading, ElMessage, ElMessageBox } from 'element-plus' | ||||||
|   import { useRouter } from 'vue-router' |   import { useRouter } from 'vue-router' | ||||||
|  |  | ||||||
|   defineOptions({ |   defineOptions({ | ||||||
| @@ -274,7 +274,25 @@ | |||||||
|           type: 'success', |           type: 'success', | ||||||
|           message: res.msg |           message: res.msg | ||||||
|         }) |         }) | ||||||
|         router.push({ name: 'Login' }) |          | ||||||
|  |         // 显示AI助手配置提示弹窗 | ||||||
|  |         ElMessageBox.confirm( | ||||||
|  |           '已经完成基础数据库初始化!建议先进行编辑器AI助手配置,以获得更好的开发体验。', | ||||||
|  |           '配置完成', | ||||||
|  |           { | ||||||
|  |             confirmButtonText: '查看AI配置文档', | ||||||
|  |             cancelButtonText: '稍后配置', | ||||||
|  |             type: 'success', | ||||||
|  |             center: true | ||||||
|  |           } | ||||||
|  |         ).then(() => { | ||||||
|  |           // 点击确认按钮,打开AI配置文档 | ||||||
|  |           window.open('https://www.gin-vue-admin.com/guide/server/mcp.html', '_blank') | ||||||
|  |           router.push({ name: 'Login' }) | ||||||
|  |         }).catch(() => { | ||||||
|  |           // 点击取消按钮或关闭弹窗,直接跳转到登录页 | ||||||
|  |           router.push({ name: 'Login' }) | ||||||
|  |         }) | ||||||
|       } |       } | ||||||
|       loading.close() |       loading.close() | ||||||
|     } catch (_) { |     } catch (_) { | ||||||
|   | |||||||
| @@ -37,7 +37,7 @@ | |||||||
|   const menuComponent = computed(() => { |   const menuComponent = computed(() => { | ||||||
|     if ( |     if ( | ||||||
|       props.routerInfo.children && |       props.routerInfo.children && | ||||||
|       props.routerInfo.children.filter((item) => !item.hidden).length |       props.routerInfo.children?.filter((item) => !item.hidden).length | ||||||
|     ) { |     ) { | ||||||
|       return AsyncSubmenu |       return AsyncSubmenu | ||||||
|     } else { |     } else { | ||||||
|   | |||||||
| @@ -1,14 +1,16 @@ | |||||||
| <template> | <template> | ||||||
|   <el-menu-item |   <el-menu-item | ||||||
|     :index="routerInfo.name" |     :index="routerInfo.name" | ||||||
|     class="dark:text-slate-300 overflow-hidden" |  | ||||||
|     :style="{ |     :style="{ | ||||||
|       height: sideHeight |           height: sideHeight | ||||||
|     }" |         }" | ||||||
|   > |   > | ||||||
|     <el-icon v-if="routerInfo.meta.icon"> |     <el-icon v-if="routerInfo.meta.icon"> | ||||||
|       <component :is="routerInfo.meta.icon" /> |       <component :is="routerInfo.meta.icon" /> | ||||||
|     </el-icon> |     </el-icon> | ||||||
|  |     <template v-else> | ||||||
|  |       {{ isCollapse ? routerInfo.meta.title[0] : "" }} | ||||||
|  |     </template> | ||||||
|     <template #title> |     <template #title> | ||||||
|       {{ routerInfo.meta.title }} |       {{ routerInfo.meta.title }} | ||||||
|     </template> |     </template> | ||||||
| @@ -16,7 +18,7 @@ | |||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script setup> | <script setup> | ||||||
|   import { computed } from 'vue' | import {computed, inject} from 'vue' | ||||||
|   import { useAppStore } from '@/pinia' |   import { useAppStore } from '@/pinia' | ||||||
|   import { storeToRefs } from 'pinia' |   import { storeToRefs } from 'pinia' | ||||||
|   const appStore = useAppStore() |   const appStore = useAppStore() | ||||||
| @@ -35,6 +37,10 @@ | |||||||
|     } |     } | ||||||
|   }) |   }) | ||||||
|  |  | ||||||
|  | const isCollapse = inject('isCollapse', { | ||||||
|  |   default: false | ||||||
|  | }) | ||||||
|  |  | ||||||
|   const sideHeight = computed(() => { |   const sideHeight = computed(() => { | ||||||
|     return config.value.layout_side_item_height + 'px' |     return config.value.layout_side_item_height + 'px' | ||||||
|   }) |   }) | ||||||
|   | |||||||
| @@ -7,7 +7,7 @@ | |||||||
|       <el-menu |       <el-menu | ||||||
|         :default-active="routerStore.topActive" |         :default-active="routerStore.topActive" | ||||||
|         mode="horizontal" |         mode="horizontal" | ||||||
|         class="border-r-0 border-b-0 w-full flex gap-1 items-center box-border h-[calc(100%-1px)]" |         class="!border-r-0 border-b-0 w-full flex gap-1 items-center box-border h-[calc(100%-1px)]" | ||||||
|         unique-opened |         unique-opened | ||||||
|         @select="(index, _, ele) => selectMenuItem(index, _, ele, true)" |         @select="(index, _, ele) => selectMenuItem(index, _, ele, true)" | ||||||
|       > |       > | ||||||
| @@ -34,7 +34,7 @@ | |||||||
|           :collapse="isCollapse" |           :collapse="isCollapse" | ||||||
|           :collapse-transition="false" |           :collapse-transition="false" | ||||||
|           :default-active="active" |           :default-active="active" | ||||||
|           class="border-r-0 w-full" |           class="!border-r-0 w-full" | ||||||
|           unique-opened |           unique-opened | ||||||
|           @select="(index, _, ele) => selectMenuItem(index, _, ele, false)" |           @select="(index, _, ele) => selectMenuItem(index, _, ele, false)" | ||||||
|         > |         > | ||||||
|   | |||||||
| @@ -1,13 +1,16 @@ | |||||||
| <template> | <template> | ||||||
|   <div |   <div | ||||||
|     class="bg-white h-[calc(100%-4px)] text-slate-700 dark:text-slate-300 mx-2 dark:bg-slate-900 flex items-center w-[calc(100vw-600px)] overflow-auto" |     class="bg-white h-[calc(100%-4px)] text-slate-700 dark:text-slate-300 mx-2 dark:bg-slate-900 flex items-center w-[calc(100vw-600px)] overflow-auto" | ||||||
|  |     ref="menuContainer" | ||||||
|   > |   > | ||||||
|     <el-menu |     <el-menu | ||||||
|       :default-active="active" |       :default-active="active" | ||||||
|       mode="horizontal" |       mode="horizontal" | ||||||
|       class="border-r-0 w-full flex gap-1 items-center box-border h-[calc(100%-1px)]" |       class="!border-r-0 w-full flex gap-1 items-center box-border h-[calc(100%-1px)]" | ||||||
|       unique-opened |       unique-opened | ||||||
|  |       :ellipsis="shouldEllipsis" | ||||||
|       @select="selectMenuItem" |       @select="selectMenuItem" | ||||||
|  |       ref="menuRef" | ||||||
|     > |     > | ||||||
|       <template v-for="item in routerStore.asyncRouters[0].children"> |       <template v-for="item in routerStore.asyncRouters[0].children"> | ||||||
|         <aside-component |         <aside-component | ||||||
| @@ -23,7 +26,7 @@ | |||||||
|  |  | ||||||
| <script setup> | <script setup> | ||||||
|   import AsideComponent from '@/view/layout/aside/asideComponent/index.vue' |   import AsideComponent from '@/view/layout/aside/asideComponent/index.vue' | ||||||
|   import { ref, provide, watchEffect } from 'vue' |   import { ref, provide, watchEffect, onMounted, nextTick } from 'vue' | ||||||
|   import { useRoute, useRouter } from 'vue-router' |   import { useRoute, useRouter } from 'vue-router' | ||||||
|   import { useRouterStore } from '@/pinia/modules/router' |   import { useRouterStore } from '@/pinia/modules/router' | ||||||
|   import { useAppStore } from '@/pinia' |   import { useAppStore } from '@/pinia' | ||||||
| @@ -39,6 +42,26 @@ | |||||||
|   const routerStore = useRouterStore() |   const routerStore = useRouterStore() | ||||||
|   const isCollapse = ref(false) |   const isCollapse = ref(false) | ||||||
|   const active = ref('') |   const active = ref('') | ||||||
|  |   const menuRef = ref(null) | ||||||
|  |   const menuContainer = ref(null) | ||||||
|  |   const shouldEllipsis = ref(false) | ||||||
|  |  | ||||||
|  |   // 计算是否需要启用省略功能 | ||||||
|  |   const calculateEllipsis = async () => { | ||||||
|  |     await nextTick() | ||||||
|  |     if (!menuRef.value || !menuContainer.value) return | ||||||
|  |  | ||||||
|  |     const menuItems = menuRef.value.$el.querySelectorAll('.el-menu-item, .el-sub-menu') | ||||||
|  |     let totalWidth = 0 | ||||||
|  |  | ||||||
|  |     menuItems.forEach(item => { | ||||||
|  |       totalWidth += item.offsetWidth | ||||||
|  |     }) | ||||||
|  |  | ||||||
|  |     const containerWidth = menuContainer.value.offsetWidth | ||||||
|  |     shouldEllipsis.value = totalWidth > containerWidth | ||||||
|  |   } | ||||||
|  |  | ||||||
|   watchEffect(() => { |   watchEffect(() => { | ||||||
|     if (route.name === 'Iframe') { |     if (route.name === 'Iframe') { | ||||||
|       active.value = decodeURIComponent(route.query.url) |       active.value = decodeURIComponent(route.query.url) | ||||||
| @@ -53,10 +76,24 @@ | |||||||
|     } else { |     } else { | ||||||
|       isCollapse.value = false |       isCollapse.value = false | ||||||
|     } |     } | ||||||
|  |     // 设备变化时重新计算 | ||||||
|  |     calculateEllipsis() | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   // 当路由变化时重新计算 | ||||||
|  |   watchEffect(() => { | ||||||
|  |     if (route.name) { | ||||||
|  |       nextTick(calculateEllipsis) | ||||||
|  |     } | ||||||
|   }) |   }) | ||||||
|  |  | ||||||
|   provide('isCollapse', isCollapse) |   provide('isCollapse', isCollapse) | ||||||
|  |  | ||||||
|  |   onMounted(() => { | ||||||
|  |     calculateEllipsis() | ||||||
|  |     window.addEventListener('resize', calculateEllipsis) | ||||||
|  |   }) | ||||||
|  |  | ||||||
|   const selectMenuItem = (index) => { |   const selectMenuItem = (index) => { | ||||||
|     const query = {} |     const query = {} | ||||||
|     const params = {} |     const params = {} | ||||||
| @@ -70,30 +107,26 @@ | |||||||
|       }) |       }) | ||||||
|     if (index === route.name) return |     if (index === route.name) return | ||||||
|     if (index.indexOf('http://') > -1 || index.indexOf('https://') > -1) { |     if (index.indexOf('http://') > -1 || index.indexOf('https://') > -1) { | ||||||
|       if (index === 'Iframe') { |  | ||||||
|         query.url = decodeURIComponent(index) |  | ||||||
|         router.push({ |  | ||||||
|           name: 'Iframe', |  | ||||||
|           query, |  | ||||||
|           params |  | ||||||
|         }) |  | ||||||
|         return |  | ||||||
|       } else { |  | ||||||
|         window.open(index, '_blank') |         window.open(index, '_blank') | ||||||
|         return |         return | ||||||
|       } |  | ||||||
|     } else { |  | ||||||
|       router.push({ name: index, query, params }) |  | ||||||
|     } |     } | ||||||
|  |     if (index === 'Iframe') { | ||||||
|  |       query.url = decodeURIComponent(index) | ||||||
|  |     } | ||||||
|  |     router.push({ name: index, query, params }) | ||||||
|   } |   } | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style lang="scss" scoped> | <style lang="scss"> | ||||||
|   .el-menu--horizontal.el-menu, |   .el-menu--horizontal.el-menu, | ||||||
|   .el-menu--horizontal > .el-menu-item.is-active { |   .el-menu--horizontal > .el-menu-item.is-active { | ||||||
|     border-bottom: none !important; |     border-bottom: none !important; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   .el-menu--horizontal>.el-sub-menu.is-active .el-sub-menu__title { | ||||||
|  |     border-bottom: none !important; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   .el-menu-item.is-active { |   .el-menu-item.is-active { | ||||||
|     background-color: var(--el-color-primary-light-8) !important; |     background-color: var(--el-color-primary-light-8) !important; | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -4,7 +4,8 @@ | |||||||
|       v-if=" |       v-if=" | ||||||
|         config.side_mode === 'normal' || |         config.side_mode === 'normal' || | ||||||
|         (device === 'mobile' && config.side_mode == 'head') || |         (device === 'mobile' && config.side_mode == 'head') || | ||||||
|         (device === 'mobile' && config.side_mode == 'combination') |         (device === 'mobile' && config.side_mode == 'combination') || | ||||||
|  |         (device === 'mobile' && config.side_mode == 'sidebar') | ||||||
|       " |       " | ||||||
|     /> |     /> | ||||||
|     <head-mode v-if="config.side_mode === 'head' && device !== 'mobile'" /> |     <head-mode v-if="config.side_mode === 'head' && device !== 'mobile'" /> | ||||||
| @@ -12,6 +13,9 @@ | |||||||
|       v-if="config.side_mode === 'combination' && device !== 'mobile'" |       v-if="config.side_mode === 'combination' && device !== 'mobile'" | ||||||
|       :mode="mode" |       :mode="mode" | ||||||
|     /> |     /> | ||||||
|  |     <sidebar-mode | ||||||
|  |       v-if="config.side_mode === 'sidebar' && device !== 'mobile'" | ||||||
|  |     /> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| @@ -19,6 +23,7 @@ | |||||||
|   import NormalMode from './normalMode.vue' |   import NormalMode from './normalMode.vue' | ||||||
|   import HeadMode from './headMode.vue' |   import HeadMode from './headMode.vue' | ||||||
|   import CombinationMode from './combinationMode.vue' |   import CombinationMode from './combinationMode.vue' | ||||||
|  |   import SidebarMode from './sidebarMode.vue' | ||||||
|  |  | ||||||
|   defineProps({ |   defineProps({ | ||||||
|     mode: { |     mode: { | ||||||
|   | |||||||
| @@ -11,7 +11,7 @@ | |||||||
|         :collapse="isCollapse" |         :collapse="isCollapse" | ||||||
|         :collapse-transition="false" |         :collapse-transition="false" | ||||||
|         :default-active="active" |         :default-active="active" | ||||||
|         class="border-r-0 w-full" |         class="!border-r-0 w-full" | ||||||
|         unique-opened |         unique-opened | ||||||
|         @select="selectMenuItem" |         @select="selectMenuItem" | ||||||
|       > |       > | ||||||
|   | |||||||
							
								
								
									
										300
									
								
								src/view/layout/aside/sidebarMode.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										300
									
								
								src/view/layout/aside/sidebarMode.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,300 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="flex h-full"> | ||||||
|  |     <!-- 一级菜单常驻侧边栏 --> | ||||||
|  |     <div | ||||||
|  |       class="relative !h-full bg-white text-slate-700 dark:text-slate-300 dark:bg-slate-900 border-r shadow dark:shadow-gray-700" | ||||||
|  |       :style="{ | ||||||
|  |         width: config.layout_side_collapsed_width + 'px' | ||||||
|  |       }" | ||||||
|  |     > | ||||||
|  |       <el-scrollbar> | ||||||
|  |         <el-menu | ||||||
|  |           :collapse="true" | ||||||
|  |           :collapse-transition="false" | ||||||
|  |           :default-active="topActive" | ||||||
|  |           class="!border-r-0 w-full" | ||||||
|  |           unique-opened | ||||||
|  |           @select="selectTopMenuItem" | ||||||
|  |         > | ||||||
|  |           <template v-for="item in routerStore.asyncRouters[0]?.children || []"> | ||||||
|  |             <el-menu-item | ||||||
|  |               v-if="!item.hidden && (!item.children || item.children.length === 0)" | ||||||
|  |               :key="item.name" | ||||||
|  |               :index="item.name" | ||||||
|  |               class="dark:text-slate-300 overflow-hidden" | ||||||
|  |               :style="{ | ||||||
|  |                 height: config.layout_side_item_height + 'px' | ||||||
|  |               }" | ||||||
|  |             > | ||||||
|  |               <el-icon v-if="item.meta.icon"> | ||||||
|  |                 <component :is="item.meta.icon" /> | ||||||
|  |               </el-icon> | ||||||
|  |               <template v-else> | ||||||
|  |                 {{ item.meta.title[0] }} | ||||||
|  |               </template> | ||||||
|  |               <template #title> | ||||||
|  |                 {{ item.meta.title }} | ||||||
|  |               </template> | ||||||
|  |             </el-menu-item> | ||||||
|  |             <template v-else-if="!item.hidden" > | ||||||
|  |              <el-menu-item | ||||||
|  |               :key="item.name" | ||||||
|  |               :index="item.name" | ||||||
|  |               :class="{'is-active': topActive === item.name}" | ||||||
|  |               class="dark:text-slate-300 overflow-hidden" | ||||||
|  |               :style="{ | ||||||
|  |                 height: config.layout_side_item_height + 'px' | ||||||
|  |               }" | ||||||
|  |             > | ||||||
|  |               <el-icon v-if="item.meta.icon"> | ||||||
|  |                 <component :is="item.meta.icon" /> | ||||||
|  |               </el-icon> | ||||||
|  |               <template v-else> | ||||||
|  |                 {{ item.meta.title[0] }} | ||||||
|  |                 </template> | ||||||
|  |               <template #title> | ||||||
|  |                 {{ item.meta.title }} | ||||||
|  |               </template> | ||||||
|  |             </el-menu-item> | ||||||
|  |             </template> | ||||||
|  |           </template> | ||||||
|  |         </el-menu> | ||||||
|  |       </el-scrollbar> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <!-- 二级菜单并列显示 --> | ||||||
|  |     <div | ||||||
|  |       class="relative h-full bg-white text-slate-700 dark:text-slate-300 dark:bg-slate-900 border-r shadow dark:shadow-gray-700 px-2" | ||||||
|  |       :style="{ | ||||||
|  |         width: layoutSideWidth + 'px' | ||||||
|  |       }" | ||||||
|  |     > | ||||||
|  |       <el-scrollbar> | ||||||
|  |         <el-menu | ||||||
|  |           :collapse="isCollapse" | ||||||
|  |           :collapse-transition="false" | ||||||
|  |           :default-active="active" | ||||||
|  |           class="!border-r-0 w-full" | ||||||
|  |           unique-opened | ||||||
|  |           @select="selectMenuItem" | ||||||
|  |         > | ||||||
|  |           <template v-for="item in secondLevelMenus"> | ||||||
|  |             <aside-component | ||||||
|  |               v-if="!item.hidden" | ||||||
|  |               :key="item.name" | ||||||
|  |               :router-info="item" | ||||||
|  |             /> | ||||||
|  |           </template> | ||||||
|  |         </el-menu> | ||||||
|  |       </el-scrollbar> | ||||||
|  |       <div | ||||||
|  |         class="absolute bottom-8 right-2 w-8 h-8 bg-gray-50 dark:bg-slate-800 flex items-center justify-center rounded cursor-pointer" | ||||||
|  |         :class="isCollapse ? 'right-0 left-0 mx-auto' : 'right-2'" | ||||||
|  |         @click="toggleCollapse" | ||||||
|  |       > | ||||||
|  |         <el-icon v-if="!isCollapse"> | ||||||
|  |           <DArrowLeft /> | ||||||
|  |         </el-icon> | ||||||
|  |         <el-icon v-else> | ||||||
|  |           <DArrowRight /> | ||||||
|  |         </el-icon> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup> | ||||||
|  |   import AsideComponent from '@/view/layout/aside/asideComponent/index.vue' | ||||||
|  |   import { ref, provide, watchEffect, computed } from 'vue' | ||||||
|  |   import { useRoute, useRouter } from 'vue-router' | ||||||
|  |   import { useRouterStore } from '@/pinia/modules/router' | ||||||
|  |   import { useAppStore } from '@/pinia' | ||||||
|  |   import { storeToRefs } from 'pinia' | ||||||
|  |  | ||||||
|  |   const appStore = useAppStore() | ||||||
|  |   const { device, config } = storeToRefs(appStore) | ||||||
|  |  | ||||||
|  |   defineOptions({ | ||||||
|  |     name: 'SidebarMode' | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   const route = useRoute() | ||||||
|  |   const router = useRouter() | ||||||
|  |   const routerStore = useRouterStore() | ||||||
|  |   const isCollapse = ref(false) | ||||||
|  |   const active = ref('') | ||||||
|  |   const topActive = ref('') | ||||||
|  |   const secondLevelMenus = ref([]) | ||||||
|  |  | ||||||
|  |   const layoutSideWidth = computed(() => { | ||||||
|  |     if (!isCollapse.value) { | ||||||
|  |       return config.value.layout_side_width | ||||||
|  |     } else { | ||||||
|  |       return config.value.layout_side_collapsed_width | ||||||
|  |     } | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |  | ||||||
|  |   provide('isCollapse', isCollapse) | ||||||
|  |  | ||||||
|  |   // 更新二级菜单 | ||||||
|  |   const updateSecondLevelMenus = (menuName) => { | ||||||
|  |     const menu = routerStore.asyncRouters[0]?.children.find(item => item.name === menuName) | ||||||
|  |     if (menu && menu.children && menu.children.length > 0) { | ||||||
|  |       secondLevelMenus.value = menu.children | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // 选择一级菜单 | ||||||
|  |   const selectTopMenuItem = (index) => { | ||||||
|  |     topActive.value = index | ||||||
|  |  | ||||||
|  |     // 获取选中的菜单项 | ||||||
|  |     const menu = routerStore.asyncRouters[0]?.children.find(item => item.name === index) | ||||||
|  |  | ||||||
|  |     // 只有当选中的菜单有子菜单时,才更新二级菜单区域 | ||||||
|  |     if (menu && menu.children && menu.children.length > 0) { | ||||||
|  |       updateSecondLevelMenus(index) | ||||||
|  |  | ||||||
|  |       // 导航到第一个可见的子菜单 | ||||||
|  |       const firstVisibleChild = menu.children.find(child => !child.hidden) | ||||||
|  |       if (firstVisibleChild) { | ||||||
|  |         navigateToMenuItem(firstVisibleChild.name) | ||||||
|  |       } | ||||||
|  |     } else { | ||||||
|  |       // 如果没有子菜单,直接导航到该菜单,但不更新二级菜单区域 | ||||||
|  |       navigateToMenuItem(index) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // 选择二级或更深层级的菜单 | ||||||
|  |   const selectMenuItem = (index) => { | ||||||
|  |     navigateToMenuItem(index) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // 导航到指定菜单 | ||||||
|  |   const navigateToMenuItem = (index) => { | ||||||
|  |     const query = {} | ||||||
|  |     const params = {} | ||||||
|  |     routerStore.routeMap[index]?.parameters && | ||||||
|  |       routerStore.routeMap[index]?.parameters.forEach((item) => { | ||||||
|  |         if (item.type === 'query') { | ||||||
|  |           query[item.key] = item.value | ||||||
|  |         } else { | ||||||
|  |           params[item.key] = item.value | ||||||
|  |         } | ||||||
|  |       }) | ||||||
|  |     if (index === route.name) return | ||||||
|  |     if (index.indexOf('http://') > -1 || index.indexOf('https://') > -1) { | ||||||
|  |       if (index === 'Iframe') { | ||||||
|  |         query.url = decodeURIComponent(index) | ||||||
|  |         router.push({ | ||||||
|  |           name: 'Iframe', | ||||||
|  |           query, | ||||||
|  |           params | ||||||
|  |         }) | ||||||
|  |         return | ||||||
|  |       } else { | ||||||
|  |         window.open(index, '_blank') | ||||||
|  |         return | ||||||
|  |       } | ||||||
|  |     } else { | ||||||
|  |       router.push({ name: index, query, params }) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const toggleCollapse = () => { | ||||||
|  |     isCollapse.value = !isCollapse.value | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |   watchEffect(() => { | ||||||
|  |     if (route.name === 'Iframe') { | ||||||
|  |       active.value = decodeURIComponent(route.query.url) | ||||||
|  |       return | ||||||
|  |     } | ||||||
|  |     active.value = route.meta.activeName || route.name | ||||||
|  |  | ||||||
|  |     // 找到当前路由所属的一级菜单 | ||||||
|  |     const findParentMenu = () => { | ||||||
|  |       // 首先检查当前路由是否就是一级菜单 | ||||||
|  |       const isTopMenu = routerStore.asyncRouters[0]?.children.some( | ||||||
|  |         item => !item.hidden && item.name === route.name | ||||||
|  |       ) | ||||||
|  |  | ||||||
|  |       if (isTopMenu) { | ||||||
|  |         return route.name | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       for (const topMenu of routerStore.asyncRouters[0]?.children || []) { | ||||||
|  |         if (topMenu.hidden) continue | ||||||
|  |  | ||||||
|  |         // 检查当前路由是否是这个一级菜单的子菜单 | ||||||
|  |         if (topMenu.children && topMenu.children.some(child => child.name === route.name)) { | ||||||
|  |           return topMenu.name | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // 递归检查更深层级 | ||||||
|  |         const checkChildren = (items) => { | ||||||
|  |           for (const item of items || []) { | ||||||
|  |             if (item.name === route.name) { | ||||||
|  |               return true | ||||||
|  |             } | ||||||
|  |             if (item.children && checkChildren(item.children)) { | ||||||
|  |               return true | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |           return false | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (topMenu.children && checkChildren(topMenu.children)) { | ||||||
|  |           return topMenu.name | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       return null | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const parentMenu = findParentMenu() | ||||||
|  |     if (parentMenu) { | ||||||
|  |       topActive.value = parentMenu | ||||||
|  |  | ||||||
|  |       // 只有当父菜单有子菜单时,才更新二级菜单区域 | ||||||
|  |       const menu = routerStore.asyncRouters[0]?.children.find(item => item.name === parentMenu) | ||||||
|  |       if (menu && menu.children && menu.children.length > 0) { | ||||||
|  |         updateSecondLevelMenus(parentMenu) | ||||||
|  |       } else { | ||||||
|  |         // 如果找到的父菜单没有子菜单,保持当前一级菜单高亮,但需要显示一些二级菜单 | ||||||
|  |         // 寻找第一个有子菜单的一级菜单来显示其子菜单 | ||||||
|  |         const firstMenuWithChildren = routerStore.asyncRouters[0].children.find( | ||||||
|  |           item => !item.hidden && item.children && item.children.length > 0 | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         if (firstMenuWithChildren) { | ||||||
|  |           // 只更新二级菜单区域,但保持当前一级菜单的高亮状态 | ||||||
|  |           updateSecondLevelMenus(firstMenuWithChildren.name) | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } else if (routerStore.asyncRouters[0]?.children?.length > 0) { | ||||||
|  |       // 如果没有找到父菜单,保持当前路由名称作为高亮,但需要显示一些二级菜单 | ||||||
|  |       // 寻找第一个有子菜单的一级菜单来显示其子菜单 | ||||||
|  |       const firstMenuWithChildren = routerStore.asyncRouters[0].children.find( | ||||||
|  |         item => !item.hidden && item.children && item.children.length > 0 | ||||||
|  |       ) | ||||||
|  |  | ||||||
|  |       if (firstMenuWithChildren) { | ||||||
|  |         // 只更新二级菜单区域,高亮状态保持为当前路由 | ||||||
|  |         topActive.value = route.name | ||||||
|  |         secondLevelMenus.value = firstMenuWithChildren.children | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   watchEffect(() => { | ||||||
|  |     if (device.value === 'mobile') { | ||||||
|  |       isCollapse.value = true | ||||||
|  |     } else { | ||||||
|  |       isCollapse.value = false | ||||||
|  |     } | ||||||
|  |   }) | ||||||
|  | </script> | ||||||
| @@ -8,7 +8,7 @@ | |||||||
|     <el-tooltip class="" effect="dark" content="视频教程" placement="bottom"> |     <el-tooltip class="" effect="dark" content="视频教程" placement="bottom"> | ||||||
|       <el-dropdown @command="toDoc"> |       <el-dropdown @command="toDoc"> | ||||||
|         <el-icon |         <el-icon | ||||||
|           class="w-8 h-8 shadow rounded-full border border-gray-200 dark:border-gray-600 cursor-pointer border-solid" |           class="w-8 h-8 p-2 shadow rounded-full border border-gray-200 dark:border-gray-600 cursor-pointer border-solid" | ||||||
|         > |         > | ||||||
|           <Film /> |           <Film /> | ||||||
|         </el-icon> |         </el-icon> | ||||||
| @@ -29,7 +29,7 @@ | |||||||
|     <el-tooltip class="" effect="dark" content="搜索" placement="bottom"> |     <el-tooltip class="" effect="dark" content="搜索" placement="bottom"> | ||||||
|       <el-icon |       <el-icon | ||||||
|         @click="handleCommand" |         @click="handleCommand" | ||||||
|         class="w-8 h-8 shadow rounded-full border border-gray-200 dark:border-gray-600 cursor-pointer border-solid" |         class="w-8 h-8 p-2 shadow rounded-full border border-gray-200 dark:border-gray-600 cursor-pointer border-solid" | ||||||
|       > |       > | ||||||
|         <Search /> |         <Search /> | ||||||
|       </el-icon> |       </el-icon> | ||||||
| @@ -37,7 +37,7 @@ | |||||||
|  |  | ||||||
|     <el-tooltip class="" effect="dark" content="系统设置" placement="bottom"> |     <el-tooltip class="" effect="dark" content="系统设置" placement="bottom"> | ||||||
|       <el-icon |       <el-icon | ||||||
|         class="w-8 h-8 shadow rounded-full border border-gray-200 dark:border-gray-600 cursor-pointer border-solid" |         class="w-8 h-8 p-2 shadow rounded-full border border-gray-200 dark:border-gray-600 cursor-pointer border-solid" | ||||||
|         @click="toggleSetting" |         @click="toggleSetting" | ||||||
|       > |       > | ||||||
|         <Setting /> |         <Setting /> | ||||||
| @@ -46,7 +46,7 @@ | |||||||
|  |  | ||||||
|     <el-tooltip class="" effect="dark" content="刷新" placement="bottom"> |     <el-tooltip class="" effect="dark" content="刷新" placement="bottom"> | ||||||
|       <el-icon |       <el-icon | ||||||
|         class="w-8 h-8 shadow rounded-full border border-gray-200 dark:border-gray-600 cursor-pointer border-solid" |         class="w-8 h-8 p-2 shadow rounded-full border border-gray-200 dark:border-gray-600 cursor-pointer border-solid" | ||||||
|         :class="showRefreshAnmite ? 'animate-spin' : ''" |         :class="showRefreshAnmite ? 'animate-spin' : ''" | ||||||
|         @click="toggleRefresh" |         @click="toggleRefresh" | ||||||
|       > |       > | ||||||
| @@ -61,14 +61,14 @@ | |||||||
|     > |     > | ||||||
|       <el-icon |       <el-icon | ||||||
|         v-if="appStore.isDark" |         v-if="appStore.isDark" | ||||||
|         class="w-8 h-8 shadow rounded-full border border-gray-600 cursor-pointer border-solid" |         class="w-8 h-8 p-2 shadow rounded-full border border-gray-600 cursor-pointer border-solid" | ||||||
|         @click="appStore.toggleTheme(false)" |         @click="appStore.toggleTheme(false)" | ||||||
|       > |       > | ||||||
|         <Sunny /> |         <Sunny /> | ||||||
|       </el-icon> |       </el-icon> | ||||||
|       <el-icon |       <el-icon | ||||||
|         v-else |         v-else | ||||||
|         class="w-8 h-8 shadow rounded-full border border-gray-200 cursor-pointer border-solid" |         class="w-8 h-8 p-2 shadow rounded-full border border-gray-200 cursor-pointer border-solid" | ||||||
|         @click="appStore.toggleTheme(true)" |         @click="appStore.toggleTheme(true)" | ||||||
|       > |       > | ||||||
|         <Moon /> |         <Moon /> | ||||||
|   | |||||||
| @@ -11,10 +11,10 @@ | |||||||
|       :content="userStore.userInfo.nickName" |       :content="userStore.userInfo.nickName" | ||||||
|     /> |     /> | ||||||
|     <gva-header /> |     <gva-header /> | ||||||
|     <div class="flex flex-row w-full gva-container pt-16 box-border h-full"> |     <div class="flex flex-row w-full gva-container pt-16 box-border !h-full"> | ||||||
|       <gva-aside |       <gva-aside | ||||||
|         v-if=" |         v-if=" | ||||||
|           config.side_mode === 'normal' || |           config.side_mode === 'normal' || config.side_mode === 'sidebar' || | ||||||
|           (device === 'mobile' && config.side_mode == 'head') || |           (device === 'mobile' && config.side_mode == 'head') || | ||||||
|           (device === 'mobile' && config.side_mode == 'combination') |           (device === 'mobile' && config.side_mode == 'combination') | ||||||
|         " |         " | ||||||
| @@ -34,7 +34,7 @@ | |||||||
|               id="gva-base-load-dom" |               id="gva-base-load-dom" | ||||||
|               class="gva-body-h bg-gray-50 dark:bg-slate-800" |               class="gva-body-h bg-gray-50 dark:bg-slate-800" | ||||||
|             > |             > | ||||||
|               <transition mode="out-in" :name="config.transition_type"> |               <transition mode="out-in" :name="route.meta.transitionType || config.transition_type"> | ||||||
|                 <keep-alive :include="routerStore.keepAliveRouters"> |                 <keep-alive :include="routerStore.keepAliveRouters"> | ||||||
|                   <component :is="Component" :key="route.fullPath" /> |                   <component :is="Component" :key="route.fullPath" /> | ||||||
|                 </keep-alive> |                 </keep-alive> | ||||||
|   | |||||||
							
								
								
									
										219
									
								
								src/view/layout/setting/components/layoutModeCard.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										219
									
								
								src/view/layout/setting/components/layoutModeCard.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,219 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="grid grid-cols-2 gap-6 font-inter px-6"> | ||||||
|  |     <div | ||||||
|  |       v-for="layout in layoutModes" | ||||||
|  |       :key="layout.value" | ||||||
|  |       class="bg-white dark:bg-gray-700 border-2 border-gray-200 dark:border-gray-600 rounded-xl p-6 cursor-pointer transition-all duration-150 ease-in-out hover:transform hover:-translate-y-1 hover:shadow-xl" | ||||||
|  |       :class="{ | ||||||
|  |         'ring-2 ring-offset-2 ring-offset-gray-50 dark:ring-offset-gray-900 transform -translate-y-1 shadow-xl': modelValue === layout.value | ||||||
|  |       }" | ||||||
|  |       :style="modelValue === layout.value ? { | ||||||
|  |         borderColor: primaryColor, | ||||||
|  |         ringColor: primaryColor + '40' | ||||||
|  |       } : {}" | ||||||
|  |       @click="handleLayoutChange(layout.value)" | ||||||
|  |     > | ||||||
|  |       <div class="flex justify-center mb-5"> | ||||||
|  |         <div | ||||||
|  |           class="w-28 h-20 bg-gray-50 dark:bg-gray-600 border border-gray-200 dark:border-gray-500 rounded-lg p-2 flex gap-1.5 shadow-inner" | ||||||
|  |           :class="layout.containerClass" | ||||||
|  |         > | ||||||
|  |           <div | ||||||
|  |             v-if="layout.showSidebar" | ||||||
|  |             class="rounded-sm" | ||||||
|  |             :class="[layout.sidebarClass]" | ||||||
|  |             :style="getSidebarStyle(layout)" | ||||||
|  |           ></div> | ||||||
|  |  | ||||||
|  |           <div class="flex-1 flex flex-col gap-1.5"> | ||||||
|  |             <div | ||||||
|  |               v-if="layout.showHeader" | ||||||
|  |               class="rounded-sm" | ||||||
|  |               :class="layout.headerClass" | ||||||
|  |               :style="getHeaderStyle(layout)" | ||||||
|  |             ></div> | ||||||
|  |  | ||||||
|  |             <div | ||||||
|  |               class="flex-1 rounded-sm" | ||||||
|  |               :class="layout.contentClass" | ||||||
|  |               :style="getContentStyle(layout)" | ||||||
|  |             ></div> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |       <div class="text-center"> | ||||||
|  |         <span class="block text-base font-semibold text-gray-900 dark:text-white mb-2" :class="{ 'text-current': modelValue === layout.value }" :style="modelValue === layout.value ? { color: primaryColor } : {}">{{ layout.label }}</span> | ||||||
|  |         <span class="block text-sm text-gray-500 dark:text-gray-400">{{ layout.description }}</span> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup> | ||||||
|  | import { computed } from 'vue' | ||||||
|  | import { storeToRefs } from 'pinia' | ||||||
|  | import { useAppStore } from '@/pinia' | ||||||
|  |  | ||||||
|  | defineOptions({ | ||||||
|  |   name: 'LayoutModeCard' | ||||||
|  | }) | ||||||
|  |  | ||||||
|  | const props = defineProps({ | ||||||
|  |   modelValue: { | ||||||
|  |     type: String, | ||||||
|  |     default: 'normal' | ||||||
|  |   } | ||||||
|  | }) | ||||||
|  |  | ||||||
|  | const emit = defineEmits(['update:modelValue']) | ||||||
|  |  | ||||||
|  | const appStore = useAppStore() | ||||||
|  | const { config } = storeToRefs(appStore) | ||||||
|  |  | ||||||
|  | const primaryColor = computed(() => config.value.primaryColor) | ||||||
|  | const lighterPrimaryColor = computed(() => { | ||||||
|  |   const hex = config.value.primaryColor.replace('#', '') | ||||||
|  |   const r = parseInt(hex.substr(0, 2), 16) | ||||||
|  |   const g = parseInt(hex.substr(2, 2), 16) | ||||||
|  |   const b = parseInt(hex.substr(4, 2), 16) | ||||||
|  |   return `rgba(${r}, ${g}, ${b}, 0.7)` | ||||||
|  | }) | ||||||
|  | const lightestPrimaryColor = computed(() => { | ||||||
|  |   const hex = config.value.primaryColor.replace('#', '') | ||||||
|  |   const r = parseInt(hex.substr(0, 2), 16) | ||||||
|  |   const g = parseInt(hex.substr(2, 2), 16) | ||||||
|  |   const b = parseInt(hex.substr(4, 2), 16) | ||||||
|  |   return `rgba(${r}, ${g}, ${b}, 0.4)` | ||||||
|  | }) | ||||||
|  |  | ||||||
|  | const layoutModes = [ | ||||||
|  |   { | ||||||
|  |     value: 'normal', | ||||||
|  |     label: '经典布局', | ||||||
|  |     description: '左侧导航,顶部标题栏', | ||||||
|  |     containerClass: '', | ||||||
|  |     showSidebar: true, | ||||||
|  |     sidebarClass: 'w-1/4', | ||||||
|  |     showHeader: true, | ||||||
|  |     headerClass: 'h-1/4', | ||||||
|  |     contentClass: '', | ||||||
|  |     showRightSidebar: false, | ||||||
|  |     primaryElement: 'sidebar' | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     value: 'head', | ||||||
|  |     label: '顶部导航', | ||||||
|  |     description: '水平导航栏布局', | ||||||
|  |     containerClass: 'flex-col', | ||||||
|  |     showSidebar: false, | ||||||
|  |     showHeader: true, | ||||||
|  |     headerClass: 'h-1/3', | ||||||
|  |     contentClass: '', | ||||||
|  |     showRightSidebar: false, | ||||||
|  |     primaryElement: 'header' | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     value: 'combination', | ||||||
|  |     label: '混合布局', | ||||||
|  |     description: '多级导航组合模式', | ||||||
|  |     containerClass: '', | ||||||
|  |     showSidebar: true, | ||||||
|  |     sidebarClass: 'w-1/5', | ||||||
|  |     showHeader: true, | ||||||
|  |     headerClass: 'h-1/4', | ||||||
|  |     contentClass: '', | ||||||
|  |     showRightSidebar: true, | ||||||
|  |     rightSidebarClass: 'w-1/5', | ||||||
|  |     primaryElement: 'header', | ||||||
|  |     secondaryElement: 'sidebar' | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     value: 'sidebar', | ||||||
|  |     label: '侧栏常驻', | ||||||
|  |     description: '二级菜单会始终打开', | ||||||
|  |     containerClass: '', | ||||||
|  |     showSidebar: true, | ||||||
|  |     sidebarClass: 'w-1/3', | ||||||
|  |     showHeader: true, | ||||||
|  |     headerClass: 'h-1/4', | ||||||
|  |     contentClass: '', | ||||||
|  |     showRightSidebar: false, | ||||||
|  |     primaryElement: 'sidebar' | ||||||
|  |   } | ||||||
|  | ] | ||||||
|  |  | ||||||
|  | const getSidebarStyle = (layout) => { | ||||||
|  |   if (layout.primaryElement === 'sidebar') { | ||||||
|  |     return { backgroundColor: primaryColor.value, opacity: '0.95' } | ||||||
|  |   } else if (layout.secondaryElement === 'sidebar') { | ||||||
|  |     return { backgroundColor: lighterPrimaryColor.value, opacity: '0.85' } | ||||||
|  |   } else { | ||||||
|  |     return { backgroundColor: lightestPrimaryColor.value, opacity: '0.6' } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const getHeaderStyle = (layout) => { | ||||||
|  |   if (layout.primaryElement === 'header') { | ||||||
|  |     return { backgroundColor: primaryColor.value, opacity: '0.95' } | ||||||
|  |   } else if (layout.secondaryElement === 'header') { | ||||||
|  |     return { backgroundColor: lighterPrimaryColor.value, opacity: '0.85' } | ||||||
|  |   } else { | ||||||
|  |     return { backgroundColor: lightestPrimaryColor.value, opacity: '0.6' } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const getContentStyle = (layout) => { | ||||||
|  |   return { backgroundColor: lightestPrimaryColor.value, opacity: '0.5' } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const getRightSidebarStyle = (layout) => { | ||||||
|  |   if (layout.primaryElement === 'rightSidebar') { | ||||||
|  |     return { backgroundColor: primaryColor.value, opacity: '0.95' } | ||||||
|  |   } else if (layout.secondaryElement === 'rightSidebar') { | ||||||
|  |     return { backgroundColor: lighterPrimaryColor.value, opacity: '0.85' } | ||||||
|  |   } else { | ||||||
|  |     return { backgroundColor: lightestPrimaryColor.value, opacity: '0.6' } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const handleLayoutChange = (layout) => { | ||||||
|  |   emit('update:modelValue', layout) | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style scoped> | ||||||
|  | .font-inter { | ||||||
|  |   font-family: 'Inter', sans-serif; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .flex-col { | ||||||
|  |   flex-direction: column; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .w-1\/4 { | ||||||
|  |   width: 25%; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .w-1\/3 { | ||||||
|  |   width: 33.333333%; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .w-1\/5 { | ||||||
|  |   width: 20%; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .h-1\/4 { | ||||||
|  |   height: 25%; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .h-1\/3 { | ||||||
|  |   height: 33.333333%; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @media (max-width: 480px) { | ||||||
|  |   .grid-cols-2 { | ||||||
|  |     grid-template-columns: repeat(1, minmax(0, 1fr)); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										113
									
								
								src/view/layout/setting/components/settingItem.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								src/view/layout/setting/components/settingItem.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,113 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="flex items-center justify-between py-4 font-inter border-b border-gray-100 dark:border-gray-700 last:border-b-0"> | ||||||
|  |     <div class="flex items-center gap-2"> | ||||||
|  |       <span class="text-sm font-medium text-gray-900 dark:text-white">{{ label }}</span> | ||||||
|  |       <slot name="suffix"></slot> | ||||||
|  |     </div> | ||||||
|  |     <div class="flex items-center setting-controls"> | ||||||
|  |       <slot></slot> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup> | ||||||
|  | import { computed } from 'vue' | ||||||
|  | import { storeToRefs } from 'pinia' | ||||||
|  | import { useAppStore } from '@/pinia' | ||||||
|  |  | ||||||
|  | defineOptions({ | ||||||
|  |   name: 'SettingItem' | ||||||
|  | }) | ||||||
|  |  | ||||||
|  | defineProps({ | ||||||
|  |   label: { | ||||||
|  |     type: String, | ||||||
|  |     required: true | ||||||
|  |   } | ||||||
|  | }) | ||||||
|  |  | ||||||
|  | const appStore = useAppStore() | ||||||
|  | const { config } = storeToRefs(appStore) | ||||||
|  |  | ||||||
|  | const primaryColor = computed(() => config.value.primaryColor) | ||||||
|  | const primaryColorWithOpacity = computed(() => config.value.primaryColor + '40') | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style scoped> | ||||||
|  | .font-inter { | ||||||
|  |   font-family: 'Inter', sans-serif; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .setting-controls { | ||||||
|  |   ::v-deep(.el-switch) { | ||||||
|  |     --el-switch-on-color: v-bind(primaryColor); | ||||||
|  |     --el-switch-off-color: #d1d5db; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   ::v-deep(.el-select) { | ||||||
|  |     .el-input__wrapper { | ||||||
|  |       border: 1px solid #e5e7eb; | ||||||
|  |       border-radius: 6px; | ||||||
|  |       transition: all 150ms ease-in-out; | ||||||
|  |  | ||||||
|  |       &:hover { | ||||||
|  |         border-color: v-bind(primaryColor); | ||||||
|  |         transform: translateY(-1px); | ||||||
|  |         box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       &.is-focus { | ||||||
|  |         border-color: v-bind(primaryColor); | ||||||
|  |         box-shadow: 0 0 0 2px v-bind(primaryColorWithOpacity); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   ::v-deep(.el-input-number) { | ||||||
|  |     .el-input__wrapper { | ||||||
|  |       border: 1px solid #e5e7eb; | ||||||
|  |       border-radius: 6px; | ||||||
|  |       transition: all 150ms ease-in-out; | ||||||
|  |  | ||||||
|  |       &:hover { | ||||||
|  |         border-color: v-bind(primaryColor); | ||||||
|  |         transform: translateY(-1px); | ||||||
|  |         box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       &.is-focus { | ||||||
|  |         border-color: v-bind(primaryColor); | ||||||
|  |         box-shadow: 0 0 0 2px v-bind(primaryColorWithOpacity); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .dark .setting-controls { | ||||||
|  |   ::v-deep(.el-switch) { | ||||||
|  |     --el-switch-off-color: #4b5563; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   ::v-deep(.el-select) { | ||||||
|  |     .el-input__wrapper { | ||||||
|  |       border-color: #4b5563; | ||||||
|  |       background-color: #374151; | ||||||
|  |  | ||||||
|  |       &:hover { | ||||||
|  |         border-color: v-bind(primaryColor); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   ::v-deep(.el-input-number) { | ||||||
|  |     .el-input__wrapper { | ||||||
|  |       border-color: #4b5563; | ||||||
|  |       background-color: #374151; | ||||||
|  |  | ||||||
|  |       &:hover { | ||||||
|  |         border-color: v-bind(primaryColor); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										152
									
								
								src/view/layout/setting/components/themeColorPicker.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										152
									
								
								src/view/layout/setting/components/themeColorPicker.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,152 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="font-inter"> | ||||||
|  |     <div class="bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-8 shadow-sm"> | ||||||
|  |       <div class="mb-8"> | ||||||
|  |         <p class="text-base font-semibold text-gray-700 dark:text-gray-300 mb-5">精选色彩</p> | ||||||
|  |         <div class="grid grid-cols-3 gap-4"> | ||||||
|  |           <div | ||||||
|  |             v-for="colorItem in presetColors" | ||||||
|  |             :key="colorItem.color" | ||||||
|  |             class="flex items-center gap-4 p-4 bg-white dark:bg-gray-700 border-2 border-gray-200 dark:border-gray-600 rounded-xl cursor-pointer transition-all duration-150 ease-in-out hover:transform hover:-translate-y-1 hover:shadow-lg" | ||||||
|  |             :class="{ | ||||||
|  |               'ring-2 ring-offset-2 ring-offset-gray-50 dark:ring-offset-gray-800 transform -translate-y-1 shadow-lg': modelValue === colorItem.color | ||||||
|  |             }" | ||||||
|  |             :style="modelValue === colorItem.color ? { | ||||||
|  |               borderColor: colorItem.color, | ||||||
|  |               ringColor: colorItem.color + '40' | ||||||
|  |             } : {}" | ||||||
|  |             @click="handleColorChange(colorItem.color)" | ||||||
|  |           > | ||||||
|  |             <div | ||||||
|  |               class="relative w-10 h-10 rounded-lg border border-gray-300 dark:border-gray-500 flex-shrink-0 shadow-sm" | ||||||
|  |               :style="{ backgroundColor: colorItem.color }" | ||||||
|  |             > | ||||||
|  |               <div | ||||||
|  |                 v-if="modelValue === colorItem.color" | ||||||
|  |                 class="absolute inset-0 flex items-center justify-center text-white text-base" | ||||||
|  |                 style="text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);" | ||||||
|  |               > | ||||||
|  |                 <el-icon> | ||||||
|  |                   <Check /> | ||||||
|  |                 </el-icon> | ||||||
|  |               </div> | ||||||
|  |             </div> | ||||||
|  |             <div class="min-w-0 flex-1"> | ||||||
|  |               <span class="block text-sm font-semibold text-gray-900 dark:text-white">{{ colorItem.name }}</span> | ||||||
|  |             </div> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |       <div class="flex items-center justify-between p-5 bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-xl mb-6 shadow-sm"> | ||||||
|  |         <div class="flex-1"> | ||||||
|  |           <h4 class="text-base font-semibold text-gray-900 dark:text-white">自定义颜色</h4> | ||||||
|  |           <p class="text-sm text-gray-500 dark:text-gray-400 mt-1">选择任意颜色作为主题色</p> | ||||||
|  |         </div> | ||||||
|  |         <el-color-picker | ||||||
|  |           v-model="customColor" | ||||||
|  |           size="large" | ||||||
|  |           :predefine="presetColors.map(item => item.color)" | ||||||
|  |           @change="handleCustomColorChange" | ||||||
|  |           class="custom-color-picker" | ||||||
|  |         /> | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |       <div class="bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-xl p-5 shadow-sm"> | ||||||
|  |         <div class="flex items-center justify-between"> | ||||||
|  |           <span class="text-base font-semibold text-gray-700 dark:text-gray-300">当前主题色</span> | ||||||
|  |           <div class="flex items-center gap-3"> | ||||||
|  |             <div | ||||||
|  |               class="w-6 h-6 rounded-lg border border-gray-300 dark:border-gray-500 shadow-sm" | ||||||
|  |               :style="{ backgroundColor: modelValue }" | ||||||
|  |             ></div> | ||||||
|  |             <code class="text-sm font-mono bg-gray-100 dark:bg-gray-600 text-gray-700 dark:text-gray-300 px-3 py-2 rounded-lg border border-gray-200 dark:border-gray-500"> | ||||||
|  |               {{ modelValue }} | ||||||
|  |             </code> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup> | ||||||
|  | import { ref, watch } from 'vue' | ||||||
|  | import { Check } from '@element-plus/icons-vue' | ||||||
|  |  | ||||||
|  | defineOptions({ | ||||||
|  |   name: 'ThemeColorPicker' | ||||||
|  | }) | ||||||
|  |  | ||||||
|  | const props = defineProps({ | ||||||
|  |   modelValue: { | ||||||
|  |     type: String, | ||||||
|  |     default: '#3b82f6' | ||||||
|  |   } | ||||||
|  | }) | ||||||
|  |  | ||||||
|  | const emit = defineEmits(['update:modelValue']) | ||||||
|  |  | ||||||
|  | const customColor = ref(props.modelValue) | ||||||
|  |  | ||||||
|  | const presetColors = [ | ||||||
|  |   { color: '#4E80EE', name: '默认' }, | ||||||
|  |   { color: '#8bb5d1', name: '晨雾蓝' }, | ||||||
|  |   { color: '#a8c8a8', name: '薄荷绿' }, | ||||||
|  |   { color: '#d4a5a5', name: '玫瑰粉' }, | ||||||
|  |   { color: '#c8a8d8', name: '薰衣草' }, | ||||||
|  |   { color: '#f0c674', name: '暖阳黄' }, | ||||||
|  |   { color: '#b8b8b8', name: '月光银' }, | ||||||
|  |   { color: '#d8a8a8', name: '珊瑚橙' }, | ||||||
|  |   { color: '#a8d8d8', name: '海雾青' }, | ||||||
|  |   { color: '#c8c8a8', name: '橄榄绿' }, | ||||||
|  |   { color: '#d8c8a8', name: '奶茶棕' }, | ||||||
|  |   { color: '#a8a8d8', name: '梦幻紫' }, | ||||||
|  |   { color: '#c8d8a8', name: '抹茶绿' } | ||||||
|  | ] | ||||||
|  |  | ||||||
|  | const handleColorChange = (color) => { | ||||||
|  |   customColor.value = color | ||||||
|  |   emit('update:modelValue', color) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const handleCustomColorChange = (color) => { | ||||||
|  |   if (color) { | ||||||
|  |     emit('update:modelValue', color) | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | watch(() => props.modelValue, (newValue) => { | ||||||
|  |   customColor.value = newValue | ||||||
|  | }) | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style scoped> | ||||||
|  | .font-inter { | ||||||
|  |   font-family: 'Inter', sans-serif; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .custom-color-picker { | ||||||
|  |   ::v-deep(.el-color-picker__trigger) { | ||||||
|  |     border: 1px solid #e5e7eb; | ||||||
|  |     border-radius: 6px; | ||||||
|  |     transition: all 150ms ease-in-out; | ||||||
|  |  | ||||||
|  |     &:hover { | ||||||
|  |       border-color: #9ca3af; | ||||||
|  |       transform: translateY(-1px); | ||||||
|  |       box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .dark .custom-color-picker { | ||||||
|  |   ::v-deep(.el-color-picker__trigger) { | ||||||
|  |     border-color: #4b5563; | ||||||
|  |  | ||||||
|  |     &:hover { | ||||||
|  |       border-color: #6b7280; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										70
									
								
								src/view/layout/setting/components/themeModeSelector.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								src/view/layout/setting/components/themeModeSelector.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,70 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="flex justify-center"> | ||||||
|  |     <div class="inline-flex bg-gray-100 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-1 gap-1"> | ||||||
|  |       <div | ||||||
|  |         v-for="mode in themeModes" | ||||||
|  |         :key="mode.value" | ||||||
|  |         class="flex flex-col items-center justify-center px-4 py-3 rounded-md cursor-pointer transition-all duration-150 ease-in-out min-w-[64px]" | ||||||
|  |         :class="[ | ||||||
|  |           modelValue === mode.value | ||||||
|  |             ? 'text-white shadow-sm transform -translate-y-0.5' | ||||||
|  |             : 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700' | ||||||
|  |         ]" | ||||||
|  |         :style="modelValue === mode.value ? { backgroundColor: primaryColor } : {}" | ||||||
|  |         @click="handleModeChange(mode.value)" | ||||||
|  |       > | ||||||
|  |         <el-icon class="text-lg mb-1"> | ||||||
|  |           <component :is="mode.icon" /> | ||||||
|  |         </el-icon> | ||||||
|  |         <span class="text-xs font-medium">{{ mode.label }}</span> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup> | ||||||
|  | import { computed } from 'vue' | ||||||
|  | import { storeToRefs } from 'pinia' | ||||||
|  | import { Sunny, Moon, Monitor } from '@element-plus/icons-vue' | ||||||
|  | import { useAppStore } from '@/pinia' | ||||||
|  |  | ||||||
|  | defineOptions({ | ||||||
|  |   name: 'ThemeModeSelector' | ||||||
|  | }) | ||||||
|  |  | ||||||
|  | const props = defineProps({ | ||||||
|  |   modelValue: { | ||||||
|  |     type: String, | ||||||
|  |     default: 'auto' | ||||||
|  |   } | ||||||
|  | }) | ||||||
|  |  | ||||||
|  | const emit = defineEmits(['update:modelValue']) | ||||||
|  |  | ||||||
|  | const appStore = useAppStore() | ||||||
|  | const { config } = storeToRefs(appStore) | ||||||
|  |  | ||||||
|  | const primaryColor = computed(() => config.value.primaryColor) | ||||||
|  |  | ||||||
|  | const themeModes = [ | ||||||
|  |   { | ||||||
|  |     value: 'light', | ||||||
|  |     label: '浅色', | ||||||
|  |     icon: Sunny | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     value: 'dark', | ||||||
|  |     label: '深色', | ||||||
|  |     icon: Moon | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     value: 'auto', | ||||||
|  |     label: '跟随系统', | ||||||
|  |     icon: Monitor | ||||||
|  |   } | ||||||
|  | ] | ||||||
|  |  | ||||||
|  | const handleModeChange = (mode) => { | ||||||
|  |   emit('update:modelValue', mode) | ||||||
|  | } | ||||||
|  | </script> | ||||||
| @@ -5,208 +5,175 @@ | |||||||
|     direction="rtl" |     direction="rtl" | ||||||
|     :size="width" |     :size="width" | ||||||
|     :show-close="false" |     :show-close="false" | ||||||
|  |     class="theme-config-drawer" | ||||||
|   > |   > | ||||||
|     <template #header> |     <template #header> | ||||||
|       <div class="flex justify-between items-center"> |       <div class="flex items-center justify-between w-full px-6 py-4 bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-700"> | ||||||
|         <span class="text-lg">系统配置</span> |         <h2 class="text-xl font-semibold text-gray-900 dark:text-white font-inter">系统配置</h2> | ||||||
|         <el-button type="primary" @click="saveConfig">保存配置</el-button> |         <el-button | ||||||
|  |           type="primary" | ||||||
|  |           size="small" | ||||||
|  |           class="reset-btn" | ||||||
|  |           :style="{ backgroundColor: config.primaryColor, borderColor: config.primaryColor }" | ||||||
|  |           @click="resetConfig" | ||||||
|  |         > | ||||||
|  |           重置配置 | ||||||
|  |         </el-button> | ||||||
|       </div> |       </div> | ||||||
|     </template> |     </template> | ||||||
|     <div class="flex flex-col"> |  | ||||||
|       <div class="mb-8"> |  | ||||||
|         <Title title="默认主题"></Title> |  | ||||||
|         <div class="mt-2 text-sm p-2 flex items-center justify-center gap-2"> |  | ||||||
|           <el-segmented |  | ||||||
|             v-model="config.darkMode" |  | ||||||
|             :options="options" |  | ||||||
|             size="default" |  | ||||||
|             @change="appStore.toggleDarkMode" |  | ||||||
|           /> |  | ||||||
|         </div> |  | ||||||
|       </div> |  | ||||||
|       <div class="mb-8"> |  | ||||||
|         <Title title="主题色"></Title> |  | ||||||
|         <div class="mt-2 text-sm p-2 flex items-center gap-2 justify-center"> |  | ||||||
|           <div |  | ||||||
|             v-for="item in colors" |  | ||||||
|             :key="item" |  | ||||||
|             class="w-5 h-5 rounded cursor-pointer flex items-center justify-center" |  | ||||||
|             :style="`background:${item}`" |  | ||||||
|             @click="appStore.togglePrimaryColor(item)" |  | ||||||
|           > |  | ||||||
|             <el-icon v-if="config.primaryColor === item"> |  | ||||||
|               <Select /> |  | ||||||
|             </el-icon> |  | ||||||
|           </div> |  | ||||||
|           <el-color-picker |  | ||||||
|             v-model="customColor" |  | ||||||
|             @change="appStore.togglePrimaryColor" |  | ||||||
|           /> |  | ||||||
|         </div> |  | ||||||
|       </div> |  | ||||||
|       <div class="mb-8"> |  | ||||||
|         <Title title="主题配置"></Title> |  | ||||||
|         <div class="mt-2 text-md p-2 flex flex-col gap-2"> |  | ||||||
|           <div class="flex items-center justify-between"> |  | ||||||
|             <div>展示水印</div> |  | ||||||
|             <el-switch |  | ||||||
|               v-model="config.show_watermark" |  | ||||||
|               @change="appStore.toggleConfigWatermark" |  | ||||||
|             /> |  | ||||||
|           </div> |  | ||||||
|           <div class="flex items-center justify-between"> |  | ||||||
|             <div>灰色模式</div> |  | ||||||
|             <el-switch v-model="config.grey" @change="appStore.toggleGrey" /> |  | ||||||
|           </div> |  | ||||||
|           <div class="flex items-center justify-between"> |  | ||||||
|             <div>色弱模式</div> |  | ||||||
|             <el-switch |  | ||||||
|               v-model="config.weakness" |  | ||||||
|               @change="appStore.toggleWeakness" |  | ||||||
|             /> |  | ||||||
|           </div> |  | ||||||
|           <div class="flex items-center justify-between"> |  | ||||||
|             <div>菜单模式</div> |  | ||||||
|             <el-segmented |  | ||||||
|               v-model="config.side_mode" |  | ||||||
|               :options="sideModes" |  | ||||||
|               size="default" |  | ||||||
|               @change="appStore.toggleSideMode" |  | ||||||
|             /> |  | ||||||
|           </div> |  | ||||||
|  |  | ||||||
|           <div class="flex items-center justify-between"> |     <div class="bg-white dark:bg-gray-900"> | ||||||
|             <div>显示标签页</div> |       <div class="px-8 pt-4 pb-6 border-b border-gray-200 dark:border-gray-700"> | ||||||
|             <el-switch |         <div class="flex justify-center"> | ||||||
|               v-model="config.showTabs" |           <div class="inline-flex bg-gray-100 dark:bg-gray-800 rounded-xl p-1.5 border border-gray-200 dark:border-gray-700 shadow-sm"> | ||||||
|               @change="appStore.toggleTabs" |             <div | ||||||
|             /> |               v-for="tab in tabs" | ||||||
|           </div> |               :key="tab.key" | ||||||
|           <div class="flex items-center justify-between gap-2"> |               class="px-6 py-3 text-base text-center cursor-pointer font-medium rounded-lg transition-all duration-150 ease-in-out min-w-[80px]" | ||||||
|             <div class="flex-shrink-0">页面切换动画</div> |               :class="[ | ||||||
|             <el-select |                 activeTab === tab.key | ||||||
|               v-model="config.transition_type" |                   ? 'text-white shadow-md transform -translate-y-0.5' | ||||||
|               @change="appStore.toggleTransition" |                   : 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700' | ||||||
|               class="w-40" |               ]" | ||||||
|  |               :style="activeTab === tab.key ? { backgroundColor: config.primaryColor } : {}" | ||||||
|  |               @click="activeTab = tab.key" | ||||||
|             > |             > | ||||||
|               <el-option value="fade" label="淡入淡出" /> |               {{ tab.label }} | ||||||
|               <el-option value="slide" label="滑动" /> |             </div> | ||||||
|               <el-option value="zoom" label="缩放" /> |  | ||||||
|               <el-option value="none" label="无动画" /> |  | ||||||
|             </el-select> |  | ||||||
|           </div> |           </div> | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
|  |  | ||||||
|       <div class="mb-8"> |       <div class="pb-8 h-full overflow-y-auto"> | ||||||
|         <Title title="layout 大小配置"></Title> |         <div class="transition-all duration-300 ease-in-out"> | ||||||
|         <div class="mt-2 text-md p-2 flex flex-col gap-2"> |           <AppearanceSettings v-if="activeTab === 'appearance'" /> | ||||||
|           <div class="flex items-center justify-between mb-2"> |           <LayoutSettings v-else-if="activeTab === 'layout'" /> | ||||||
|             <div>侧边栏展开宽度</div> |           <GeneralSettings v-else-if="activeTab === 'general'" /> | ||||||
|             <el-input-number |  | ||||||
|               v-model="config.layout_side_width" |  | ||||||
|               :min="150" |  | ||||||
|               :max="400" |  | ||||||
|               :step="10" |  | ||||||
|             ></el-input-number> |  | ||||||
|           </div> |  | ||||||
|           <div class="flex items-center justify-between mb-2"> |  | ||||||
|             <div>侧边栏收缩宽度</div> |  | ||||||
|             <el-input-number |  | ||||||
|               v-model="config.layout_side_collapsed_width" |  | ||||||
|               :min="60" |  | ||||||
|               :max="100" |  | ||||||
|             ></el-input-number> |  | ||||||
|           </div> |  | ||||||
|           <div class="flex items-center justify-between mb-2"> |  | ||||||
|             <div>侧边栏子项高度</div> |  | ||||||
|             <el-input-number |  | ||||||
|               v-model="config.layout_side_item_height" |  | ||||||
|               :min="30" |  | ||||||
|               :max="50" |  | ||||||
|             ></el-input-number> |  | ||||||
|           </div> |  | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
|  |  | ||||||
|       <!--      <el-alert type="warning" :closable="false"> |  | ||||||
|         请注意,所有配置请保存到本地文件的 |  | ||||||
|         <el-tag>config.json</el-tag> 文件中,否则刷新页面后会丢失配置 |  | ||||||
|       </el-alert>--> |  | ||||||
|     </div> |     </div> | ||||||
|   </el-drawer> |   </el-drawer> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script setup> | <script setup> | ||||||
|   import { useAppStore } from '@/pinia' |   import { ref, computed, watch } from 'vue' | ||||||
|   import { storeToRefs } from 'pinia' |   import { storeToRefs } from 'pinia' | ||||||
|   import { ref, computed } from 'vue' |  | ||||||
|   import { ElMessage } from 'element-plus' |   import { ElMessage } from 'element-plus' | ||||||
|  |   import { useAppStore } from '@/pinia' | ||||||
|   import { setSelfSetting } from '@/api/user' |   import { setSelfSetting } from '@/api/user' | ||||||
|   import Title from './title.vue' |   import AppearanceSettings from './modules/appearance/index.vue' | ||||||
|   const appStore = useAppStore() |   import LayoutSettings from './modules/layout/index.vue' | ||||||
|   const { config, device } = storeToRefs(appStore) |   import GeneralSettings from './modules/general/index.vue' | ||||||
|  |  | ||||||
|   defineOptions({ |   defineOptions({ | ||||||
|     name: 'GvaSetting' |     name: 'GvaSetting' | ||||||
|   }) |   }) | ||||||
|  |  | ||||||
|  |   const appStore = useAppStore() | ||||||
|  |   const { config, device } = storeToRefs(appStore) | ||||||
|  |  | ||||||
|  |   const activeTab = ref('appearance') | ||||||
|  |  | ||||||
|  |   const tabs = [ | ||||||
|  |     { key: 'appearance', label: '外观' }, | ||||||
|  |     { key: 'layout', label: '布局' }, | ||||||
|  |     { key: 'general', label: '通用' } | ||||||
|  |   ] | ||||||
|  |  | ||||||
|   const width = computed(() => { |   const width = computed(() => { | ||||||
|     return device.value === 'mobile' ? '100%' : '500px' |     return device.value === 'mobile' ? '100%' : '500px' | ||||||
|   }) |   }) | ||||||
|  |  | ||||||
|   const colors = [ |  | ||||||
|     '#EB2F96', |  | ||||||
|     '#3b82f6', |  | ||||||
|     '#2FEB54', |  | ||||||
|     '#EBEB2F', |  | ||||||
|     '#EB2F2F', |  | ||||||
|     '#2FEBEB' |  | ||||||
|   ] |  | ||||||
|  |  | ||||||
|   const drawer = defineModel('drawer', { |   const drawer = defineModel('drawer', { | ||||||
|     default: true, |     default: true, | ||||||
|     type: Boolean |     type: Boolean | ||||||
|   }) |   }) | ||||||
|  |  | ||||||
|   const options = ['dark', 'light', 'auto'] |  | ||||||
|   const sideModes = [ |  | ||||||
|     { |  | ||||||
|       label: '正常模式', |  | ||||||
|       value: 'normal' |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|       label: '顶部菜单栏模式', |  | ||||||
|       value: 'head' |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|       label: '组合模式', |  | ||||||
|       value: 'combination' |  | ||||||
|     } |  | ||||||
|   ] |  | ||||||
|  |  | ||||||
|   const saveConfig = async () => { |   const saveConfig = async () => { | ||||||
|     /*const input = document.createElement("textarea"); |  | ||||||
|   input.value = JSON.stringify(config.value); |  | ||||||
|   // 添加回车 |  | ||||||
|   input.value = input.value.replace(/,/g, ",\n"); |  | ||||||
|   document.body.appendChild(input); |  | ||||||
|   input.select(); |  | ||||||
|   document.execCommand("copy"); |  | ||||||
|   document.body.removeChild(input); |  | ||||||
|   ElMessage.success("复制成功, 请自行保存到本地文件中");*/ |  | ||||||
|     const res = await setSelfSetting(config.value) |     const res = await setSelfSetting(config.value) | ||||||
|     if (res.code === 0) { |     if (res.code === 0) { | ||||||
|       localStorage.setItem('originSetting', JSON.stringify(config.value)) |       localStorage.setItem('originSetting', JSON.stringify(config.value)) | ||||||
|       ElMessage.success('保存成功') |       ElMessage.success('保存成功') | ||||||
|       drawer.value = false |  | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   const customColor = ref('') |   const resetConfig = () => { | ||||||
|  |     appStore.resetConfig() | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   watch(config, async () => { | ||||||
|  |     await saveConfig(); | ||||||
|  |   }, { deep: true }); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
|   ::v-deep(.el-drawer__header) { | .theme-config-drawer { | ||||||
|     @apply border-gray-400 dark:border-gray-600; |   font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; | ||||||
|  |  | ||||||
|  |   ::v-deep(.el-drawer) { | ||||||
|  |     background: white; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   ::v-deep(.el-drawer__header) { | ||||||
|  |     padding: 0; | ||||||
|  |     border: 0; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   ::v-deep(.el-drawer__body) { | ||||||
|  |     padding: 0; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .dark .theme-config-drawer { | ||||||
|  |   ::v-deep(.el-drawer) { | ||||||
|  |     background: #111827; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .font-inter { | ||||||
|  |   font-family: 'Inter', sans-serif; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .reset-btn { | ||||||
|  |   border-radius: 0.5rem; | ||||||
|  |   font-weight: 500; | ||||||
|  |   transition: all 150ms ease-in-out; | ||||||
|  |  | ||||||
|  |   &:hover { | ||||||
|  |     transform: translateY(-2px); | ||||||
|  |     filter: brightness(0.9); | ||||||
|  |     box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /* Custom scrollbar for webkit browsers */ | ||||||
|  | ::-webkit-scrollbar { | ||||||
|  |   width: 6px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | ::-webkit-scrollbar-track { | ||||||
|  |   background: #f3f4f6; | ||||||
|  |   border-radius: 3px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | ::-webkit-scrollbar-thumb { | ||||||
|  |   background: #d1d5db; | ||||||
|  |   border-radius: 3px; | ||||||
|  |  | ||||||
|  |   &:hover { | ||||||
|  |     background: #9ca3af; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .dark ::-webkit-scrollbar-track { | ||||||
|  |   background: #1f2937; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .dark ::-webkit-scrollbar-thumb { | ||||||
|  |   background: #4b5563; | ||||||
|  |  | ||||||
|  |   &:hover { | ||||||
|  |     background: #6b7280; | ||||||
|  |   } | ||||||
|  | } | ||||||
| </style> | </style> | ||||||
|   | |||||||
							
								
								
									
										114
									
								
								src/view/layout/setting/modules/appearance/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								src/view/layout/setting/modules/appearance/index.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,114 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="font-inter"> | ||||||
|  |     <!-- Theme Mode Section --> | ||||||
|  |     <div class="mb-10"> | ||||||
|  |       <div class="flex items-center justify-center mb-6"> | ||||||
|  |         <div class="h-px bg-gray-200 dark:bg-gray-700 flex-1"></div> | ||||||
|  |         <span class="px-6 text-lg font-semibold text-gray-700 dark:text-gray-300">主题模式</span> | ||||||
|  |         <div class="h-px bg-gray-200 dark:bg-gray-700 flex-1"></div> | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |       <div class="section-content"> | ||||||
|  |         <ThemeModeSelector | ||||||
|  |           v-model="config.darkMode" | ||||||
|  |           @update:modelValue="appStore.toggleDarkMode" | ||||||
|  |         /> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <!-- Theme Color Section --> | ||||||
|  |     <div class="mb-10"> | ||||||
|  |       <div class="flex items-center justify-center mb-6"> | ||||||
|  |         <div class="h-px bg-gray-200 dark:bg-gray-700 flex-1"></div> | ||||||
|  |         <span class="px-6 text-lg font-semibold text-gray-700 dark:text-gray-300">主题颜色</span> | ||||||
|  |         <div class="h-px bg-gray-200 dark:bg-gray-700 flex-1"></div> | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |       <div class="section-content"> | ||||||
|  |         <ThemeColorPicker | ||||||
|  |           v-model="config.primaryColor" | ||||||
|  |           @update:modelValue="appStore.togglePrimaryColor" | ||||||
|  |         /> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <!-- Visual Accessibility Section --> | ||||||
|  |     <div class="mb-10"> | ||||||
|  |       <div class="flex items-center justify-center mb-6"> | ||||||
|  |         <div class="h-px bg-gray-200 dark:bg-gray-700 flex-1"></div> | ||||||
|  |         <span class="px-6 text-lg font-semibold text-gray-700 dark:text-gray-300">视觉辅助</span> | ||||||
|  |         <div class="h-px bg-gray-200 dark:bg-gray-700 flex-1"></div> | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |       <div class="section-content"> | ||||||
|  |         <div class="bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6 shadow-sm"> | ||||||
|  |           <SettingItem label="灰色模式"> | ||||||
|  |             <template #suffix> | ||||||
|  |               <span class="text-xs text-gray-400 dark:text-gray-500 ml-2">降低色彩饱和度</span> | ||||||
|  |             </template> | ||||||
|  |             <el-switch | ||||||
|  |               v-model="config.grey" | ||||||
|  |               @change="appStore.toggleGrey" | ||||||
|  |             /> | ||||||
|  |           </SettingItem> | ||||||
|  |  | ||||||
|  |           <SettingItem label="色弱模式"> | ||||||
|  |             <template #suffix> | ||||||
|  |               <span class="text-xs text-gray-400 dark:text-gray-500 ml-2">优化色彩对比度</span> | ||||||
|  |             </template> | ||||||
|  |             <el-switch | ||||||
|  |               v-model="config.weakness" | ||||||
|  |               @change="appStore.toggleWeakness" | ||||||
|  |             /> | ||||||
|  |           </SettingItem> | ||||||
|  |  | ||||||
|  |           <SettingItem label="显示水印"> | ||||||
|  |             <template #suffix> | ||||||
|  |               <span class="text-xs text-gray-400 dark:text-gray-500 ml-2">在页面显示水印标识</span> | ||||||
|  |             </template> | ||||||
|  |             <el-switch | ||||||
|  |               v-model="config.show_watermark" | ||||||
|  |               @change="appStore.toggleConfigWatermark" | ||||||
|  |             /> | ||||||
|  |           </SettingItem> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup> | ||||||
|  | import { storeToRefs } from 'pinia' | ||||||
|  | import { useAppStore } from '@/pinia' | ||||||
|  | import ThemeModeSelector from '../../components/themeModeSelector.vue' | ||||||
|  | import ThemeColorPicker from '../../components/themeColorPicker.vue' | ||||||
|  | import SettingItem from '../../components/settingItem.vue' | ||||||
|  |  | ||||||
|  | defineOptions({ | ||||||
|  |   name: 'AppearanceSettings' | ||||||
|  | }) | ||||||
|  |  | ||||||
|  | const appStore = useAppStore() | ||||||
|  | const { config } = storeToRefs(appStore) | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style scoped> | ||||||
|  | .font-inter { | ||||||
|  |   font-family: 'Inter', sans-serif; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .section-content { | ||||||
|  |   animation: fadeInUp 0.3s ease; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @keyframes fadeInUp { | ||||||
|  |   from { | ||||||
|  |     opacity: 0; | ||||||
|  |     transform: translateY(12px); | ||||||
|  |   } | ||||||
|  |   to { | ||||||
|  |     opacity: 1; | ||||||
|  |     transform: translateY(0); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										288
									
								
								src/view/layout/setting/modules/general/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										288
									
								
								src/view/layout/setting/modules/general/index.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,288 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="font-inter"> | ||||||
|  |     <div class="mb-10"> | ||||||
|  |       <div class="flex items-center justify-center mb-6"> | ||||||
|  |         <div class="h-px bg-gray-200 dark:bg-gray-700 flex-1"></div> | ||||||
|  |         <span class="px-6 text-lg font-semibold text-gray-700 dark:text-gray-300">系统信息</span> | ||||||
|  |         <div class="h-px bg-gray-200 dark:bg-gray-700 flex-1"></div> | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |       <div class="section-content"> | ||||||
|  |         <div class="bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6 shadow-sm"> | ||||||
|  |           <div class="grid grid-cols-2 gap-4 text-sm"> | ||||||
|  |             <div class="flex justify-between items-center py-3 border-b border-gray-200 dark:border-gray-600"> | ||||||
|  |               <span class="text-gray-600 dark:text-gray-400 font-medium">版本</span> | ||||||
|  |               <span class="font-mono text-gray-900 dark:text-white font-semibold">v2.7.4</span> | ||||||
|  |             </div> | ||||||
|  |             <div class="flex justify-between items-center py-3 border-b border-gray-200 dark:border-gray-600"> | ||||||
|  |               <span class="text-gray-600 dark:text-gray-400 font-medium">前端框架</span> | ||||||
|  |               <span class="font-mono text-gray-900 dark:text-white font-semibold">Vue 3</span> | ||||||
|  |             </div> | ||||||
|  |             <div class="flex justify-between items-center py-3 border-b border-gray-200 dark:border-gray-600"> | ||||||
|  |               <span class="text-gray-600 dark:text-gray-400 font-medium">UI 组件库</span> | ||||||
|  |               <span class="font-mono text-gray-900 dark:text-white font-semibold">Element Plus</span> | ||||||
|  |             </div> | ||||||
|  |             <div class="flex justify-between items-center py-3 border-b border-gray-200 dark:border-gray-600"> | ||||||
|  |               <span class="text-gray-600 dark:text-gray-400 font-medium">构建工具</span> | ||||||
|  |               <span class="font-mono text-gray-900 dark:text-white font-semibold">Vite</span> | ||||||
|  |             </div> | ||||||
|  |             <div class="flex justify-between items-center py-3"> | ||||||
|  |               <span class="text-gray-600 dark:text-gray-400 font-medium">浏览器</span> | ||||||
|  |               <span class="font-mono text-gray-900 dark:text-white font-semibold">{{ browserInfo }}</span> | ||||||
|  |             </div> | ||||||
|  |             <div class="flex justify-between items-center py-3"> | ||||||
|  |               <span class="text-gray-600 dark:text-gray-400 font-medium">屏幕分辨率</span> | ||||||
|  |               <span class="font-mono text-gray-900 dark:text-white font-semibold">{{ screenResolution }}</span> | ||||||
|  |             </div> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <div class="mb-10"> | ||||||
|  |       <div class="flex items-center justify-center mb-6"> | ||||||
|  |         <div class="h-px bg-gray-200 dark:bg-gray-700 flex-1"></div> | ||||||
|  |         <span class="px-6 text-lg font-semibold text-gray-700 dark:text-gray-300">配置管理</span> | ||||||
|  |         <div class="h-px bg-gray-200 dark:bg-gray-700 flex-1"></div> | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |       <div class="section-content"> | ||||||
|  |         <div class="bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6 shadow-sm"> | ||||||
|  |           <div class="space-y-5"> | ||||||
|  |             <div class="bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg p-5 flex items-center justify-between hover:shadow-md transition-all duration-150 ease-in-out hover:-translate-y-0.5"> | ||||||
|  |               <div class="flex items-center gap-4"> | ||||||
|  |                 <div class="w-12 h-12 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-xl flex items-center justify-center text-red-600 dark:text-red-400 text-xl"> | ||||||
|  |                   🔄 | ||||||
|  |                 </div> | ||||||
|  |                 <div> | ||||||
|  |                   <h4 class="text-sm font-semibold text-gray-900 dark:text-white">重置配置</h4> | ||||||
|  |                   <p class="text-xs text-gray-500 dark:text-gray-400 mt-1">将所有设置恢复为默认值</p> | ||||||
|  |                 </div> | ||||||
|  |               </div> | ||||||
|  |               <el-button | ||||||
|  |                 type="danger" | ||||||
|  |                 size="small" | ||||||
|  |                 class="rounded-lg font-medium transition-all duration-150 ease-in-out hover:-translate-y-0.5" | ||||||
|  |                 @click="handleResetConfig" | ||||||
|  |               > | ||||||
|  |                 重置配置 | ||||||
|  |               </el-button> | ||||||
|  |             </div> | ||||||
|  |  | ||||||
|  |             <div class="bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg p-5 flex items-center justify-between hover:shadow-md transition-all duration-150 ease-in-out hover:-translate-y-0.5"> | ||||||
|  |               <div class="flex items-center gap-4"> | ||||||
|  |                 <div class="w-12 h-12 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-xl flex items-center justify-center text-blue-600 dark:text-blue-400 text-xl"> | ||||||
|  |                   📤 | ||||||
|  |                 </div> | ||||||
|  |                 <div> | ||||||
|  |                   <h4 class="text-sm font-semibold text-gray-900 dark:text-white">导出配置</h4> | ||||||
|  |                   <p class="text-xs text-gray-500 dark:text-gray-400 mt-1">导出当前配置为 JSON 文件</p> | ||||||
|  |                 </div> | ||||||
|  |               </div> | ||||||
|  |               <el-button | ||||||
|  |                 type="primary" | ||||||
|  |                 size="small" | ||||||
|  |                 class="rounded-lg font-medium transition-all duration-150 ease-in-out hover:-translate-y-0.5" | ||||||
|  |                 :style="{ backgroundColor: config.primaryColor, borderColor: config.primaryColor }" | ||||||
|  |                 @click="handleExportConfig" | ||||||
|  |               > | ||||||
|  |                 导出配置 | ||||||
|  |               </el-button> | ||||||
|  |             </div> | ||||||
|  |  | ||||||
|  |             <div class="bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg p-5 flex items-center justify-between hover:shadow-md transition-all duration-150 ease-in-out hover:-translate-y-0.5"> | ||||||
|  |               <div class="flex items-center gap-4"> | ||||||
|  |                 <div class="w-12 h-12 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-xl flex items-center justify-center text-green-600 dark:text-green-400 text-xl"> | ||||||
|  |                   📥 | ||||||
|  |                 </div> | ||||||
|  |                 <div> | ||||||
|  |                   <h4 class="text-sm font-semibold text-gray-900 dark:text-white">导入配置</h4> | ||||||
|  |                   <p class="text-xs text-gray-500 dark:text-gray-400 mt-1">从 JSON 文件导入配置</p> | ||||||
|  |                 </div> | ||||||
|  |               </div> | ||||||
|  |               <el-upload | ||||||
|  |                 ref="uploadRef" | ||||||
|  |                 :auto-upload="false" | ||||||
|  |                 :show-file-list="false" | ||||||
|  |                 accept=".json" | ||||||
|  |                 @change="handleImportConfig" | ||||||
|  |               > | ||||||
|  |                 <el-button | ||||||
|  |                   type="success" | ||||||
|  |                   size="small" | ||||||
|  |                   class="rounded-lg font-medium transition-all duration-150 ease-in-out hover:-translate-y-0.5" | ||||||
|  |                 > | ||||||
|  |                   导入配置 | ||||||
|  |                 </el-button> | ||||||
|  |               </el-upload> | ||||||
|  |             </div> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <div class="mb-10"> | ||||||
|  |       <div class="flex items-center justify-center mb-6"> | ||||||
|  |         <div class="h-px bg-gray-200 dark:bg-gray-700 flex-1"></div> | ||||||
|  |         <span class="px-6 text-lg font-semibold text-gray-700 dark:text-gray-300">关于项目</span> | ||||||
|  |         <div class="h-px bg-gray-200 dark:bg-gray-700 flex-1"></div> | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |       <div class="section-content"> | ||||||
|  |         <div class="bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6 shadow-sm"> | ||||||
|  |           <div class="flex items-start gap-5"> | ||||||
|  |             <div class="w-16 h-16 bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-xl flex items-center justify-center flex-shrink-0 shadow-sm"> | ||||||
|  |               <img | ||||||
|  |                 src="/logo.png" | ||||||
|  |                 alt="Gin-Vue-Admin Logo" | ||||||
|  |                 class="w-10 h-10 object-contain" | ||||||
|  |                 @error="handleLogoError" | ||||||
|  |               /> | ||||||
|  |             </div> | ||||||
|  |             <div class="flex-1"> | ||||||
|  |               <h4 class="text-xl font-semibold text-gray-900 dark:text-white mb-3">Gin-Vue-Admin</h4> | ||||||
|  |               <p class="text-sm text-gray-600 dark:text-gray-400 mb-5 leading-relaxed"> | ||||||
|  |                 基于 Vue3 + Gin 的全栈开发基础平台,提供完整的后台管理解决方案 | ||||||
|  |               </p> | ||||||
|  |               <div class="flex items-center gap-3 text-sm"> | ||||||
|  |                 <a | ||||||
|  |                   href="https://github.com/flipped-aurora/gin-vue-admin" | ||||||
|  |                   target="_blank" | ||||||
|  |                   class="font-medium transition-colors duration-150 hover:underline" | ||||||
|  |                   :style="{ color: config.primaryColor }" | ||||||
|  |                 > | ||||||
|  |                   GitHub 仓库 | ||||||
|  |                 </a> | ||||||
|  |                 <span class="text-gray-400 dark:text-gray-500">·</span> | ||||||
|  |                 <a | ||||||
|  |                   href="https://www.gin-vue-admin.com/" | ||||||
|  |                   target="_blank" | ||||||
|  |                   class="font-medium transition-colors duration-150 hover:underline" | ||||||
|  |                   :style="{ color: config.primaryColor }" | ||||||
|  |                 > | ||||||
|  |                   官方文档 | ||||||
|  |                 </a> | ||||||
|  |               </div> | ||||||
|  |             </div> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup> | ||||||
|  | import { ref, computed, onMounted } from 'vue' | ||||||
|  | import { ElMessage, ElMessageBox } from 'element-plus' | ||||||
|  | import { storeToRefs } from 'pinia' | ||||||
|  | import { useAppStore } from '@/pinia' | ||||||
|  |  | ||||||
|  | defineOptions({ | ||||||
|  |   name: 'GeneralSettings' | ||||||
|  | }) | ||||||
|  |  | ||||||
|  | const appStore = useAppStore() | ||||||
|  | const { config } = storeToRefs(appStore) | ||||||
|  | const uploadRef = ref() | ||||||
|  |  | ||||||
|  | const browserInfo = ref('') | ||||||
|  | const screenResolution = ref('') | ||||||
|  | const logoUrl = ref('') | ||||||
|  |  | ||||||
|  | onMounted(() => { | ||||||
|  |   const userAgent = navigator.userAgent | ||||||
|  |   if (userAgent.includes('Chrome')) { | ||||||
|  |     browserInfo.value = 'Chrome' | ||||||
|  |   } else if (userAgent.includes('Firefox')) { | ||||||
|  |     browserInfo.value = 'Firefox' | ||||||
|  |   } else if (userAgent.includes('Safari')) { | ||||||
|  |     browserInfo.value = 'Safari' | ||||||
|  |   } else if (userAgent.includes('Edge')) { | ||||||
|  |     browserInfo.value = 'Edge' | ||||||
|  |   } else { | ||||||
|  |     browserInfo.value = 'Unknown' | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   screenResolution.value = `${screen.width}×${screen.height}` | ||||||
|  | }) | ||||||
|  |  | ||||||
|  | const handleLogoError = () => { | ||||||
|  |   logoUrl.value = '' | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const handleResetConfig = async () => { | ||||||
|  |   try { | ||||||
|  |     await ElMessageBox.confirm( | ||||||
|  |       '确定要重置所有配置吗?此操作不可撤销。', | ||||||
|  |       '重置配置', | ||||||
|  |       { | ||||||
|  |         confirmButtonText: '确定', | ||||||
|  |         cancelButtonText: '取消', | ||||||
|  |         type: 'warning' | ||||||
|  |       } | ||||||
|  |     ) | ||||||
|  |      | ||||||
|  |     appStore.resetConfig() | ||||||
|  |     ElMessage.success('配置已重置') | ||||||
|  |   } catch { | ||||||
|  |     // User cancelled | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const handleExportConfig = () => { | ||||||
|  |   const configData = JSON.stringify(config.value, null, 2) | ||||||
|  |   const blob = new Blob([configData], { type: 'application/json' }) | ||||||
|  |   const url = URL.createObjectURL(blob) | ||||||
|  |    | ||||||
|  |   const link = document.createElement('a') | ||||||
|  |   link.href = url | ||||||
|  |   link.download = `gin-vue-admin-config-${new Date().toISOString().split('T')[0]}.json` | ||||||
|  |   document.body.appendChild(link) | ||||||
|  |   link.click() | ||||||
|  |   document.body.removeChild(link) | ||||||
|  |   URL.revokeObjectURL(url) | ||||||
|  |    | ||||||
|  |   ElMessage.success('配置已导出') | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const handleImportConfig = (file) => { | ||||||
|  |   const reader = new FileReader() | ||||||
|  |   reader.onload = (e) => { | ||||||
|  |     try { | ||||||
|  |       const importedConfig = JSON.parse(e.target.result) | ||||||
|  |        | ||||||
|  |       Object.keys(importedConfig).forEach(key => { | ||||||
|  |         if (key in config.value) { | ||||||
|  |           config.value[key] = importedConfig[key] | ||||||
|  |         } | ||||||
|  |       }) | ||||||
|  |        | ||||||
|  |       ElMessage.success('配置已导入') | ||||||
|  |     } catch (error) { | ||||||
|  |       ElMessage.error('配置文件格式错误') | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   reader.readAsText(file.raw) | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style scoped> | ||||||
|  | .font-inter { | ||||||
|  |   font-family: 'Inter', sans-serif; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .section-content { | ||||||
|  |   animation: fadeInUp 0.3s ease; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @keyframes fadeInUp { | ||||||
|  |   from { | ||||||
|  |     opacity: 0; | ||||||
|  |     transform: translateY(12px); | ||||||
|  |   } | ||||||
|  |   to { | ||||||
|  |     opacity: 1; | ||||||
|  |     transform: translateY(0); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										164
									
								
								src/view/layout/setting/modules/layout/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										164
									
								
								src/view/layout/setting/modules/layout/index.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,164 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="font-inter"> | ||||||
|  |     <div class="mb-10"> | ||||||
|  |       <div class="flex items-center justify-center mb-6"> | ||||||
|  |         <div class="h-px bg-gray-200 dark:bg-gray-700 flex-1"></div> | ||||||
|  |         <span class="px-6 text-lg font-semibold text-gray-700 dark:text-gray-300">布局模式</span> | ||||||
|  |         <div class="h-px bg-gray-200 dark:bg-gray-700 flex-1"></div> | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |       <div class="section-content"> | ||||||
|  |         <LayoutModeCard | ||||||
|  |           v-model="config.side_mode" | ||||||
|  |           @update:modelValue="appStore.toggleSideMode" | ||||||
|  |         /> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <div class="mb-10"> | ||||||
|  |       <div class="flex items-center justify-center mb-6"> | ||||||
|  |         <div class="h-px bg-gray-200 dark:bg-gray-700 flex-1"></div> | ||||||
|  |         <span class="px-6 text-lg font-semibold text-gray-700 dark:text-gray-300">界面配置</span> | ||||||
|  |         <div class="h-px bg-gray-200 dark:bg-gray-700 flex-1"></div> | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |       <div class="section-content"> | ||||||
|  |         <div class="bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6 shadow-sm"> | ||||||
|  |           <SettingItem label="显示标签页"> | ||||||
|  |             <template #suffix> | ||||||
|  |               <span class="text-xs text-gray-400 dark:text-gray-500 ml-2">页面标签导航</span> | ||||||
|  |             </template> | ||||||
|  |             <el-switch | ||||||
|  |               v-model="config.showTabs" | ||||||
|  |               @change="appStore.toggleTabs" | ||||||
|  |             /> | ||||||
|  |           </SettingItem> | ||||||
|  |  | ||||||
|  |           <SettingItem label="页面切换动画"> | ||||||
|  |             <template #suffix> | ||||||
|  |               <span class="text-xs text-gray-400 dark:text-gray-500 ml-2">页面过渡效果</span> | ||||||
|  |             </template> | ||||||
|  |             <el-select | ||||||
|  |               v-model="config.transition_type" | ||||||
|  |               @change="appStore.toggleTransition" | ||||||
|  |               class="w-32" | ||||||
|  |               size="small" | ||||||
|  |             > | ||||||
|  |               <el-option value="fade" label="淡入淡出" /> | ||||||
|  |               <el-option value="slide" label="滑动" /> | ||||||
|  |               <el-option value="zoom" label="缩放" /> | ||||||
|  |               <el-option value="none" label="无动画" /> | ||||||
|  |             </el-select> | ||||||
|  |           </SettingItem> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <div class="mb-10"> | ||||||
|  |       <div class="flex items-center justify-center mb-6"> | ||||||
|  |         <div class="h-px bg-gray-200 dark:bg-gray-700 flex-1"></div> | ||||||
|  |         <span class="px-6 text-lg font-semibold text-gray-700 dark:text-gray-300">尺寸配置</span> | ||||||
|  |         <div class="h-px bg-gray-200 dark:bg-gray-700 flex-1"></div> | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |       <div class="section-content"> | ||||||
|  |         <div class="bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6 shadow-sm"> | ||||||
|  |           <div class="space-y-6"> | ||||||
|  |             <div class="bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg p-5 hover:shadow-md transition-all duration-150 ease-in-out hover:-translate-y-0.5"> | ||||||
|  |               <div class="flex items-center justify-between"> | ||||||
|  |                 <div> | ||||||
|  |                   <h4 class="text-sm font-medium text-gray-900 dark:text-white">侧边栏展开宽度</h4> | ||||||
|  |                   <p class="text-xs text-gray-500 dark:text-gray-400 mt-1">侧边栏完全展开时的宽度</p> | ||||||
|  |                 </div> | ||||||
|  |                 <div class="flex items-center gap-2"> | ||||||
|  |                   <el-input-number | ||||||
|  |                     v-model="config.layout_side_width" | ||||||
|  |                     :min="150" | ||||||
|  |                     :max="400" | ||||||
|  |                     :step="10" | ||||||
|  |                     size="small" | ||||||
|  |                     class="w-24" | ||||||
|  |                   /> | ||||||
|  |                   <span class="text-xs font-medium text-gray-500 dark:text-gray-400">px</span> | ||||||
|  |                 </div> | ||||||
|  |               </div> | ||||||
|  |             </div> | ||||||
|  |  | ||||||
|  |             <div class="bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg p-5 hover:shadow-md transition-all duration-150 ease-in-out hover:-translate-y-0.5"> | ||||||
|  |               <div class="flex items-center justify-between"> | ||||||
|  |                 <div> | ||||||
|  |                   <h4 class="text-sm font-medium text-gray-900 dark:text-white">侧边栏收缩宽度</h4> | ||||||
|  |                   <p class="text-xs text-gray-500 dark:text-gray-400 mt-1">侧边栏收缩时的最小宽度</p> | ||||||
|  |                 </div> | ||||||
|  |                 <div class="flex items-center gap-2"> | ||||||
|  |                   <el-input-number | ||||||
|  |                     v-model="config.layout_side_collapsed_width" | ||||||
|  |                     :min="60" | ||||||
|  |                     :max="100" | ||||||
|  |                     size="small" | ||||||
|  |                     class="w-24" | ||||||
|  |                   /> | ||||||
|  |                   <span class="text-xs font-medium text-gray-500 dark:text-gray-400">px</span> | ||||||
|  |                 </div> | ||||||
|  |               </div> | ||||||
|  |             </div> | ||||||
|  |  | ||||||
|  |             <div class="bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg p-5 hover:shadow-md transition-all duration-150 ease-in-out hover:-translate-y-0.5"> | ||||||
|  |               <div class="flex items-center justify-between"> | ||||||
|  |                 <div> | ||||||
|  |                   <h4 class="text-sm font-medium text-gray-900 dark:text-white">菜单项高度</h4> | ||||||
|  |                   <p class="text-xs text-gray-500 dark:text-gray-400 mt-1">侧边栏菜单项的行高</p> | ||||||
|  |                 </div> | ||||||
|  |                 <div class="flex items-center gap-2"> | ||||||
|  |                   <el-input-number | ||||||
|  |                     v-model="config.layout_side_item_height" | ||||||
|  |                     :min="30" | ||||||
|  |                     :max="50" | ||||||
|  |                     size="small" | ||||||
|  |                     class="w-24" | ||||||
|  |                   /> | ||||||
|  |                   <span class="text-xs font-medium text-gray-500 dark:text-gray-400">px</span> | ||||||
|  |                 </div> | ||||||
|  |               </div> | ||||||
|  |             </div> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup> | ||||||
|  | import { storeToRefs } from 'pinia' | ||||||
|  | import { useAppStore } from '@/pinia' | ||||||
|  | import LayoutModeCard from '../../components/layoutModeCard.vue' | ||||||
|  | import SettingItem from '../../components/settingItem.vue' | ||||||
|  |  | ||||||
|  | defineOptions({ | ||||||
|  |   name: 'LayoutSettings' | ||||||
|  | }) | ||||||
|  |  | ||||||
|  | const appStore = useAppStore() | ||||||
|  | const { config } = storeToRefs(appStore) | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style scoped> | ||||||
|  | .font-inter { | ||||||
|  |   font-family: 'Inter', sans-serif; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .section-content { | ||||||
|  |   animation: fadeInUp 0.3s ease; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @keyframes fadeInUp { | ||||||
|  |   from { | ||||||
|  |     opacity: 0; | ||||||
|  |     transform: translateY(12px); | ||||||
|  |   } | ||||||
|  |   to { | ||||||
|  |     opacity: 1; | ||||||
|  |     transform: translateY(0); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </style> | ||||||
| @@ -8,7 +8,7 @@ | |||||||
|       v-model="activeValue" |       v-model="activeValue" | ||||||
|       :closable="!(historys.length === 1 && $route.name === defaultRouter)" |       :closable="!(historys.length === 1 && $route.name === defaultRouter)" | ||||||
|       type="card" |       type="card" | ||||||
|       class="bg-white text-slate-700 dark:text-slate-500 dark:bg-slate-900" |       class="bg-white text-slate-700 dark:text-slate-500 dark:bg-slate-900 pt-1" | ||||||
|       @contextmenu.prevent="openContextMenu($event)" |       @contextmenu.prevent="openContextMenu($event)" | ||||||
|       @tab-click="changeTab" |       @tab-click="changeTab" | ||||||
|       @tab-remove="removeTab" |       @tab-remove="removeTab" | ||||||
|   | |||||||
| @@ -531,7 +531,7 @@ | |||||||
|     if (res.code === 0) { |     if (res.code === 0) { | ||||||
|       ElMessage({ |       ElMessage({ | ||||||
|         type: 'success', |         type: 'success', | ||||||
|         message: '添加成功', |         message: '添加成功,请到角色管理页面分配权限', | ||||||
|         showClose: true |         showClose: true | ||||||
|       }) |       }) | ||||||
|       syncApiData.value.newApis = syncApiData.value.newApis.filter( |       syncApiData.value.newApis = syncApiData.value.newApis.filter( | ||||||
|   | |||||||
| @@ -23,7 +23,7 @@ | |||||||
|           <template #default="{ node, data }"> |           <template #default="{ node, data }"> | ||||||
|             <span class="custom-tree-node"> |             <span class="custom-tree-node"> | ||||||
|               <span>{{ node.label }}</span> |               <span>{{ node.label }}</span> | ||||||
|               <span v-if="node.checked"> |               <span v-if="node.checked && !data.name?.startsWith('http://') && !data.name?.startsWith('https://')"> | ||||||
|                 <el-button |                 <el-button | ||||||
|                   type="primary" |                   type="primary" | ||||||
|                   link |                   link | ||||||
| @@ -31,13 +31,13 @@ | |||||||
|                     color: |                     color: | ||||||
|                       row.defaultRouter === data.name ? '#E6A23C' : '#85ce61' |                       row.defaultRouter === data.name ? '#E6A23C' : '#85ce61' | ||||||
|                   }" |                   }" | ||||||
|                   @click="() => setDefault(data)" |                   @click.stop="() => setDefault(data)" | ||||||
|                 > |                 > | ||||||
|                   {{ row.defaultRouter === data.name ? '首页' : '设为首页' }} |                   {{ row.defaultRouter === data.name ? '首页' : '设为首页' }} | ||||||
|                 </el-button> |                 </el-button> | ||||||
|               </span> |               </span> | ||||||
|               <span v-if="data.menuBtn.length"> |               <span v-if="data.menuBtn.length"> | ||||||
|                 <el-button type="primary" link @click="() => OpenBtn(data)"> |                 <el-button type="primary" link @click.stop="() => OpenBtn(data)"> | ||||||
|                   分配按钮 |                   分配按钮 | ||||||
|                 </el-button> |                 </el-button> | ||||||
|               </span> |               </span> | ||||||
| @@ -132,7 +132,7 @@ | |||||||
|       defaultRouter: data.name |       defaultRouter: data.name | ||||||
|     }) |     }) | ||||||
|     if (res.code === 0) { |     if (res.code === 0) { | ||||||
|       ElMessage({ type: 'success', message: '设置成功' }) |       relation() | ||||||
|       emit('changeRow', 'defaultRouter', res.data.authority.defaultRouter) |       emit('changeRow', 'defaultRouter', res.data.authority.defaultRouter) | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -119,287 +119,394 @@ | |||||||
|       </template> |       </template> | ||||||
|  |  | ||||||
|       <warning-bar title="新增菜单,需要在角色管理内配置权限才可使用" /> |       <warning-bar title="新增菜单,需要在角色管理内配置权限才可使用" /> | ||||||
|       <el-form |  | ||||||
|         v-if="dialogFormVisible" |  | ||||||
|         ref="menuForm" |  | ||||||
|         :inline="true" |  | ||||||
|         :model="form" |  | ||||||
|         :rules="rules" |  | ||||||
|         label-position="top" |  | ||||||
|       > |  | ||||||
|         <el-row class="w-full"> |  | ||||||
|           <el-col :span="16"> |  | ||||||
|             <el-form-item label="文件路径" prop="component"> |  | ||||||
|               <components-cascader |  | ||||||
|                 :component="form.component" |  | ||||||
|                 @change="fmtComponent" |  | ||||||
|               /> |  | ||||||
|               <span style="font-size: 12px; margin-right: 12px" |  | ||||||
|                 >如果菜单包含子菜单,请创建router-view二级路由页面或者</span |  | ||||||
|               > |  | ||||||
|               <el-button |  | ||||||
|                 style="margin-top: 4px" |  | ||||||
|                 @click="form.component = 'view/routerHolder.vue'" |  | ||||||
|               > |  | ||||||
|                 点我设置 |  | ||||||
|               </el-button> |  | ||||||
|             </el-form-item> |  | ||||||
|           </el-col> |  | ||||||
|           <el-col :span="8"> |  | ||||||
|             <el-form-item label="展示名称" prop="meta.title"> |  | ||||||
|               <el-input v-model="form.meta.title" autocomplete="off" /> |  | ||||||
|             </el-form-item> |  | ||||||
|           </el-col> |  | ||||||
|         </el-row> |  | ||||||
|         <el-row class="w-full"> |  | ||||||
|           <el-col :span="8"> |  | ||||||
|             <el-form-item label="路由Name" prop="path"> |  | ||||||
|               <el-input |  | ||||||
|                 v-model="form.name" |  | ||||||
|                 autocomplete="off" |  | ||||||
|                 placeholder="唯一英文字符串" |  | ||||||
|                 @change="changeName" |  | ||||||
|               /> |  | ||||||
|             </el-form-item> |  | ||||||
|           </el-col> |  | ||||||
|           <el-col :span="8"> |  | ||||||
|             <el-form-item prop="path"> |  | ||||||
|               <template #label> |  | ||||||
|                 <span style="display: inline-flex; align-items: center"> |  | ||||||
|                   <span>路由Path</span> |  | ||||||
|                   <el-checkbox |  | ||||||
|                     v-model="checkFlag" |  | ||||||
|                     style="margin-left: 12px; height: auto" |  | ||||||
|                     >添加参数</el-checkbox |  | ||||||
|                   > |  | ||||||
|                 </span> |  | ||||||
|               </template> |  | ||||||
|        |        | ||||||
|               <el-input |       <!-- 基础信息区域 --> | ||||||
|                 v-model="form.path" |       <div class="border-b border-gray-200"> | ||||||
|                 :disabled="!checkFlag" |         <h3 class="font-semibold text-gray-700 mb-4">基础信息</h3> | ||||||
|                 autocomplete="off" |         <el-form | ||||||
|                 placeholder="建议只在后方拼接参数" |           v-if="dialogFormVisible" | ||||||
|               /> |           ref="menuForm" | ||||||
|             </el-form-item> |           :inline="true" | ||||||
|           </el-col> |           :model="form" | ||||||
|           <el-col :span="8"> |           :rules="rules" | ||||||
|             <el-form-item label="是否隐藏"> |           label-position="top" | ||||||
|               <el-select |         > | ||||||
|                 v-model="form.hidden" |           <el-row class="w-full"> | ||||||
|                 style="width: 100%" |             <el-col :span="24"> | ||||||
|                 placeholder="是否在列表隐藏" |               <el-form-item label="文件路径" prop="component"> | ||||||
|               > |                 <components-cascader | ||||||
|                 <el-option :value="false" label="否" /> |                   :component="form.component" | ||||||
|                 <el-option :value="true" label="是" /> |                   @change="fmtComponent" | ||||||
|               </el-select> |                 /> | ||||||
|             </el-form-item> |                 <div class="form-tip"> | ||||||
|           </el-col> |                   <el-icon><InfoFilled /></el-icon> | ||||||
|         </el-row> |                   <span>如果菜单包含子菜单,请创建router-view二级路由页面或者</span> | ||||||
|         <el-row class="w-full"> |                   <el-button | ||||||
|           <el-col :span="8"> |                     size="small" | ||||||
|             <el-form-item label="父节点ID"> |                     type="text" | ||||||
|               <el-cascader |                     @click="form.component = 'view/routerHolder.vue'" | ||||||
|                 v-model="form.parentId" |  | ||||||
|                 style="width: 100%" |  | ||||||
|                 :disabled="!isEdit" |  | ||||||
|                 :options="menuOption" |  | ||||||
|                 :props="{ |  | ||||||
|                   checkStrictly: true, |  | ||||||
|                   label: 'title', |  | ||||||
|                   value: 'ID', |  | ||||||
|                   disabled: 'disabled', |  | ||||||
|                   emitPath: false |  | ||||||
|                 }" |  | ||||||
|                 :show-all-levels="false" |  | ||||||
|                 filterable |  | ||||||
|               /> |  | ||||||
|             </el-form-item> |  | ||||||
|           </el-col> |  | ||||||
|           <el-col :span="8"> |  | ||||||
|             <el-form-item label="图标" prop="meta.icon"> |  | ||||||
|               <icon v-model="form.meta.icon" /> |  | ||||||
|             </el-form-item> |  | ||||||
|           </el-col> |  | ||||||
|           <el-col :span="8"> |  | ||||||
|             <el-form-item label="排序标记" prop="sort"> |  | ||||||
|               <el-input v-model.number="form.sort" autocomplete="off" /> |  | ||||||
|             </el-form-item> |  | ||||||
|           </el-col> |  | ||||||
|         </el-row> |  | ||||||
|         <el-row class="w-full"> |  | ||||||
|           <el-col :span="8"> |  | ||||||
|             <el-form-item prop="meta.activeName"> |  | ||||||
|               <template #label> |  | ||||||
|                 <div> |  | ||||||
|                   <span> 高亮菜单 </span> |  | ||||||
|                   <el-tooltip |  | ||||||
|                     content="注:当到达此路由时候,指定左侧菜单指定name会处于活跃状态(亮起),可为空,为空则为本路由Name。" |  | ||||||
|                     placement="top" |  | ||||||
|                     effect="light" |  | ||||||
|                   > |                   > | ||||||
|                     <el-icon><QuestionFilled /></el-icon> |                     点我设置 | ||||||
|                   </el-tooltip> |                   </el-button> | ||||||
|                 </div> |                 </div> | ||||||
|               </template> |               </el-form-item> | ||||||
|               <el-input |             </el-col> | ||||||
|                 v-model="form.meta.activeName" |           </el-row> | ||||||
|                 :placeholder="form.name" |           <el-row class="w-full"> | ||||||
|                 autocomplete="off" |             <el-col :span="12"> | ||||||
|               /> |               <el-form-item label="展示名称" prop="meta.title"> | ||||||
|             </el-form-item> |                 <el-input  | ||||||
|           </el-col> |                   v-model="form.meta.title"  | ||||||
|           <el-col :span="8"> |                   autocomplete="off"  | ||||||
|             <el-form-item label="KeepAlive" prop="meta.keepAlive"> |                   placeholder="请输入菜单展示名称" | ||||||
|               <el-select |                 /> | ||||||
|                 v-model="form.meta.keepAlive" |               </el-form-item> | ||||||
|                 style="width: 100%" |             </el-col> | ||||||
|                 placeholder="是否keepAlive缓存页面" |             <el-col :span="12"> | ||||||
|               > |               <el-form-item label="路由Name" prop="path"> | ||||||
|                 <el-option :value="false" label="否" /> |                 <el-input | ||||||
|                 <el-option :value="true" label="是" /> |                   v-model="form.name" | ||||||
|               </el-select> |                   autocomplete="off" | ||||||
|             </el-form-item> |                   placeholder="唯一英文字符串" | ||||||
|           </el-col> |                   @change="changeName" | ||||||
|           <el-col :span="8"> |                 /> | ||||||
|             <el-form-item label="CloseTab" prop="meta.closeTab"> |               </el-form-item> | ||||||
|               <el-select |             </el-col> | ||||||
|                 v-model="form.meta.closeTab" |           </el-row> | ||||||
|                 style="width: 100%" |         </el-form> | ||||||
|                 placeholder="是否自动关闭tab" |       </div> | ||||||
|               > |  | ||||||
|                 <el-option :value="false" label="否" /> |  | ||||||
|                 <el-option :value="true" label="是" /> |  | ||||||
|               </el-select> |  | ||||||
|             </el-form-item> |  | ||||||
|           </el-col> |  | ||||||
|         </el-row> |  | ||||||
|         <el-row class="w-full"> |  | ||||||
|           <el-col :span="8"> |  | ||||||
|             <el-form-item> |  | ||||||
|               <template #label> |  | ||||||
|                 <div> |  | ||||||
|                   <span> 是否为基础页面 </span> |  | ||||||
|                   <el-tooltip |  | ||||||
|                     content="此项选择为是,则不会展示左侧菜单以及顶部信息。" |  | ||||||
|                     placement="top" |  | ||||||
|                     effect="light" |  | ||||||
|                   > |  | ||||||
|                     <el-icon><QuestionFilled /></el-icon> |  | ||||||
|                   </el-tooltip> |  | ||||||
|                 </div> |  | ||||||
|               </template> |  | ||||||
|         |         | ||||||
|               <el-select |       <!-- 路由配置区域 --> | ||||||
|                 v-model="form.meta.defaultMenu" |       <div class="border-b border-gray-200"> | ||||||
|                 style="width: 100%" |         <h3 class="font-semibold text-gray-700 mb-4">路由配置</h3> | ||||||
|                 placeholder="是否为基础页面" |         <el-form | ||||||
|               > |           :inline="true" | ||||||
|                 <el-option :value="false" label="否" /> |           :model="form" | ||||||
|                 <el-option :value="true" label="是" /> |           :rules="rules" | ||||||
|               </el-select> |           label-position="top" | ||||||
|             </el-form-item> |         > | ||||||
|           </el-col> |            <el-row class="w-full"> | ||||||
|         </el-row> |              <el-col :span="12"> | ||||||
|       </el-form> |                <el-form-item label="父节点ID"> | ||||||
|       <div> |                  <el-cascader | ||||||
|         <div class="flex items-center gap-2"> |                    v-model="form.parentId" | ||||||
|           <el-button type="primary" icon="edit" @click="addParameter(form)"> |                    style="width: 100%" | ||||||
|  |                    :disabled="!isEdit" | ||||||
|  |                    :options="menuOption" | ||||||
|  |                    :props="{ | ||||||
|  |                      checkStrictly: true, | ||||||
|  |                      label: 'title', | ||||||
|  |                      value: 'ID', | ||||||
|  |                      disabled: 'disabled', | ||||||
|  |                      emitPath: false | ||||||
|  |                    }" | ||||||
|  |                    :show-all-levels="false" | ||||||
|  |                    filterable | ||||||
|  |                    placeholder="请选择父节点" | ||||||
|  |                  /> | ||||||
|  |                </el-form-item> | ||||||
|  |              </el-col> | ||||||
|  |              <el-col :span="12"> | ||||||
|  |                <el-form-item prop="path"> | ||||||
|  |                  <template #label> | ||||||
|  |                   <div class="inline-flex items-center h-4"> | ||||||
|  |                      <span>路由Path</span> | ||||||
|  |                      <el-checkbox | ||||||
|  |                        class="ml-2" | ||||||
|  |                        v-model="checkFlag" | ||||||
|  |                        >添加参数</el-checkbox | ||||||
|  |                      > | ||||||
|  |                     </div> | ||||||
|  |                  </template> | ||||||
|  |                  <el-input | ||||||
|  |                    v-model="form.path" | ||||||
|  |                    :disabled="!checkFlag" | ||||||
|  |                    autocomplete="off" | ||||||
|  |                    placeholder="建议只在后方拼接参数" | ||||||
|  |                  /> | ||||||
|  |                </el-form-item> | ||||||
|  |              </el-col> | ||||||
|  |            </el-row> | ||||||
|  |         </el-form> | ||||||
|  |       </div> | ||||||
|  |         | ||||||
|  |       <!-- 显示设置区域 --> | ||||||
|  |       <div class="border-b border-gray-200"> | ||||||
|  |         <h3 class="font-semibold text-gray-700 mb-4">显示设置</h3> | ||||||
|  |         <el-form | ||||||
|  |           :inline="true" | ||||||
|  |           :model="form" | ||||||
|  |           :rules="rules" | ||||||
|  |           label-position="top" | ||||||
|  |         > | ||||||
|  |            <el-row class="w-full"> | ||||||
|  |               <el-col :span="8"> | ||||||
|  |                 <el-form-item label="图标" prop="meta.icon"> | ||||||
|  |                   <icon v-model="form.meta.icon" /> | ||||||
|  |                 </el-form-item> | ||||||
|  |               </el-col> | ||||||
|  |               <el-col :span="8"> | ||||||
|  |                 <el-form-item label="排序标记" prop="sort"> | ||||||
|  |                   <el-input  | ||||||
|  |                     v-model.number="form.sort"  | ||||||
|  |                     autocomplete="off"  | ||||||
|  |                     placeholder="请输入排序数字" | ||||||
|  |                   /> | ||||||
|  |                 </el-form-item> | ||||||
|  |               </el-col> | ||||||
|  |               <el-col :span="8"> | ||||||
|  |                 <el-form-item label="是否隐藏"> | ||||||
|  |                   <el-select | ||||||
|  |                     v-model="form.hidden" | ||||||
|  |                     style="width: 100%" | ||||||
|  |                     placeholder="是否在列表隐藏" | ||||||
|  |                   > | ||||||
|  |                     <el-option :value="false" label="否" /> | ||||||
|  |                     <el-option :value="true" label="是" /> | ||||||
|  |                   </el-select> | ||||||
|  |                 </el-form-item> | ||||||
|  |               </el-col> | ||||||
|  |             </el-row> | ||||||
|  |         </el-form> | ||||||
|  |       </div> | ||||||
|  |          | ||||||
|  |       <!-- 高级配置区域 --> | ||||||
|  |       <div class="border-b border-gray-200"> | ||||||
|  |         <h3 class="font-semibold text-gray-700 mb-4">高级配置</h3> | ||||||
|  |         <el-form | ||||||
|  |           :inline="true" | ||||||
|  |           :model="form" | ||||||
|  |           :rules="rules" | ||||||
|  |           label-position="top" | ||||||
|  |         > | ||||||
|  |             <el-row class="w-full"> | ||||||
|  |               <el-col :span="12"> | ||||||
|  |                 <el-form-item prop="meta.activeName"> | ||||||
|  |                   <template #label> | ||||||
|  |                     <div class="label-with-tooltip"> | ||||||
|  |                       <span>高亮菜单</span> | ||||||
|  |                       <el-tooltip | ||||||
|  |                         content="注:当到达此路由时候,指定左侧菜单指定name会处于活跃状态(亮起),可为空,为空则为本路由Name。" | ||||||
|  |                         placement="top" | ||||||
|  |                         effect="light" | ||||||
|  |                       > | ||||||
|  |                         <el-icon><QuestionFilled /></el-icon> | ||||||
|  |                       </el-tooltip> | ||||||
|  |                     </div> | ||||||
|  |                   </template> | ||||||
|  |                   <el-input | ||||||
|  |                     v-model="form.meta.activeName" | ||||||
|  |                     :placeholder="form.name || '请输入高亮菜单名称'" | ||||||
|  |                     autocomplete="off" | ||||||
|  |                   /> | ||||||
|  |                 </el-form-item> | ||||||
|  |               </el-col> | ||||||
|  |               <el-col :span="12"> | ||||||
|  |                 <el-form-item label="KeepAlive" prop="meta.keepAlive"> | ||||||
|  |                   <el-select | ||||||
|  |                     v-model="form.meta.keepAlive" | ||||||
|  |                     style="width: 100%" | ||||||
|  |                     placeholder="是否keepAlive缓存页面" | ||||||
|  |                   > | ||||||
|  |                     <el-option :value="false" label="否" /> | ||||||
|  |                     <el-option :value="true" label="是" /> | ||||||
|  |                   </el-select> | ||||||
|  |                 </el-form-item> | ||||||
|  |               </el-col> | ||||||
|  |             </el-row> | ||||||
|  |              <el-row class="w-full"> | ||||||
|  |                <el-col :span="8"> | ||||||
|  |                  <el-form-item label="CloseTab" prop="meta.closeTab"> | ||||||
|  |                    <el-select | ||||||
|  |                      v-model="form.meta.closeTab" | ||||||
|  |                      style="width: 100%" | ||||||
|  |                      placeholder="是否自动关闭tab" | ||||||
|  |                    > | ||||||
|  |                      <el-option :value="false" label="否" /> | ||||||
|  |                      <el-option :value="true" label="是" /> | ||||||
|  |                    </el-select> | ||||||
|  |                  </el-form-item> | ||||||
|  |                </el-col> | ||||||
|  |                <el-col :span="8"> | ||||||
|  |                  <el-form-item> | ||||||
|  |                    <template #label> | ||||||
|  |                      <div class="label-with-tooltip"> | ||||||
|  |                        <span>是否为基础页面</span> | ||||||
|  |                        <el-tooltip | ||||||
|  |                          content="此项选择为是,则不会展示左侧菜单以及顶部信息。" | ||||||
|  |                          placement="top" | ||||||
|  |                          effect="light" | ||||||
|  |                        > | ||||||
|  |                          <el-icon><QuestionFilled /></el-icon> | ||||||
|  |                        </el-tooltip> | ||||||
|  |                      </div> | ||||||
|  |                    </template> | ||||||
|  |                    <el-select | ||||||
|  |                      v-model="form.meta.defaultMenu" | ||||||
|  |                      style="width: 100%" | ||||||
|  |                      placeholder="是否为基础页面" | ||||||
|  |                    > | ||||||
|  |                      <el-option :value="false" label="否" /> | ||||||
|  |                      <el-option :value="true" label="是" /> | ||||||
|  |                    </el-select> | ||||||
|  |                  </el-form-item> | ||||||
|  |                </el-col> | ||||||
|  |                <el-col :span="8"> | ||||||
|  |                  <el-form-item> | ||||||
|  |                    <template #label> | ||||||
|  |                      <div class="label-with-tooltip"> | ||||||
|  |                        <span>路由切换动画</span> | ||||||
|  |                        <el-tooltip | ||||||
|  |                          content="如果设置了路由切换动画,在本路由下的动画优先级高于全局动画切换优先级。" | ||||||
|  |                          placement="top" | ||||||
|  |                          effect="light" | ||||||
|  |                        > | ||||||
|  |                          <el-icon><QuestionFilled /></el-icon> | ||||||
|  |                        </el-tooltip> | ||||||
|  |                      </div> | ||||||
|  |                    </template> | ||||||
|  |                    <el-select | ||||||
|  |                      v-model="form.meta.transitionType" | ||||||
|  |                      style="width: 100%" | ||||||
|  |                      placeholder="跟随全局" | ||||||
|  |                      clearable | ||||||
|  |                    > | ||||||
|  |                      <el-option value="fade" label="淡入淡出" /> | ||||||
|  |                      <el-option value="slide" label="滑动" /> | ||||||
|  |                      <el-option value="zoom" label="缩放" /> | ||||||
|  |                      <el-option value="none" label="无动画" /> | ||||||
|  |                    </el-select> | ||||||
|  |                  </el-form-item> | ||||||
|  |                </el-col> | ||||||
|  |              </el-row> | ||||||
|  |         </el-form> | ||||||
|  |       </div> | ||||||
|  |            | ||||||
|  |       <!-- 菜单参数配置区域 --> | ||||||
|  |       <div class="border-b border-gray-200"> | ||||||
|  |         <div class="flex justify-between items-center mb-4"> | ||||||
|  |           <h3 class="font-semibold text-gray-700">菜单参数配置</h3> | ||||||
|  |           <el-button type="primary" size="small" @click="addParameter(form)"> | ||||||
|             新增菜单参数 |             新增菜单参数 | ||||||
|           </el-button> |           </el-button> | ||||||
|         </div> |         </div> | ||||||
|         <el-table :data="form.parameters" style="width: 100%; margin-top: 12px"> |             <el-table  | ||||||
|           <el-table-column |               :data="form.parameters"  | ||||||
|             align="left" |               style="width: 100%" | ||||||
|             prop="type" |               class="parameter-table" | ||||||
|             label="参数类型" |             > | ||||||
|             width="180" |               <el-table-column | ||||||
|           > |                 align="center" | ||||||
|             <template #default="scope"> |                 prop="type" | ||||||
|               <el-select v-model="scope.row.type" placeholder="请选择"> |                 label="参数类型" | ||||||
|                 <el-option key="query" value="query" label="query" /> |                 width="150" | ||||||
|                 <el-option key="params" value="params" label="params" /> |               > | ||||||
|               </el-select> |                 <template #default="scope"> | ||||||
|             </template> |                   <el-select  | ||||||
|           </el-table-column> |                     v-model="scope.row.type"  | ||||||
|           <el-table-column align="left" prop="key" label="参数key" width="180"> |                     placeholder="请选择" | ||||||
|             <template #default="scope"> |                     size="small" | ||||||
|               <div> |                   > | ||||||
|                 <el-input v-model="scope.row.key" /> |                     <el-option key="query" value="query" label="query" /> | ||||||
|               </div> |                     <el-option key="params" value="params" label="params" /> | ||||||
|             </template> |                   </el-select> | ||||||
|           </el-table-column> |                 </template> | ||||||
|           <el-table-column align="left" prop="value" label="参数值"> |               </el-table-column> | ||||||
|             <template #default="scope"> |               <el-table-column align="center" prop="key" label="参数key" width="150"> | ||||||
|               <div> |                 <template #default="scope"> | ||||||
|                 <el-input v-model="scope.row.value" /> |                   <el-input  | ||||||
|               </div> |                     v-model="scope.row.key"  | ||||||
|             </template> |                     size="small" | ||||||
|           </el-table-column> |                     placeholder="请输入参数key" | ||||||
|           <el-table-column align="left"> |                   /> | ||||||
|             <template #default="scope"> |                 </template> | ||||||
|               <div> |               </el-table-column> | ||||||
|                 <el-button |               <el-table-column align="center" prop="value" label="参数值"> | ||||||
|                   type="danger" |                 <template #default="scope"> | ||||||
|                   icon="delete" |                   <el-input  | ||||||
|                   @click="deleteParameter(form.parameters, scope.$index)" |                     v-model="scope.row.value"  | ||||||
|                 > |                     size="small" | ||||||
|                   删除 |                     placeholder="请输入参数值" | ||||||
|                 </el-button> |                   /> | ||||||
|               </div> |                 </template> | ||||||
|             </template> |               </el-table-column> | ||||||
|           </el-table-column> |               <el-table-column align="center" label="操作" width="100"> | ||||||
|         </el-table> |                 <template #default="scope"> | ||||||
|  |                   <el-button | ||||||
|         <div class="flex items-center gap-2 mt-3"> |                     type="danger" | ||||||
|           <el-button type="primary" icon="edit" @click="addBtn(form)"> |                     size="small" | ||||||
|             新增可控按钮 |                     @click="deleteParameter(form.parameters, scope.$index)" | ||||||
|           </el-button> |                   > | ||||||
|           <el-icon |                     <el-icon><Delete /></el-icon> | ||||||
|             class="cursor-pointer" |                   </el-button> | ||||||
|             @click=" |                 </template> | ||||||
|               toDoc('https://www.gin-vue-admin.com/guide/web/button-auth.html') |               </el-table-column> | ||||||
|             " |             </el-table> | ||||||
|           > |  | ||||||
|             <QuestionFilled /> |  | ||||||
|           </el-icon> |  | ||||||
|         </div> |  | ||||||
|  |  | ||||||
|         <el-table :data="form.menuBtn" style="width: 100%; margin-top: 12px"> |  | ||||||
|           <el-table-column |  | ||||||
|             align="left" |  | ||||||
|             prop="name" |  | ||||||
|             label="按钮名称" |  | ||||||
|             width="180" |  | ||||||
|           > |  | ||||||
|             <template #default="scope"> |  | ||||||
|               <div> |  | ||||||
|                 <el-input v-model="scope.row.name" /> |  | ||||||
|               </div> |  | ||||||
|             </template> |  | ||||||
|           </el-table-column> |  | ||||||
|           <el-table-column align="left" prop="name" label="备注" width="180"> |  | ||||||
|             <template #default="scope"> |  | ||||||
|               <div> |  | ||||||
|                 <el-input v-model="scope.row.desc" /> |  | ||||||
|               </div> |  | ||||||
|             </template> |  | ||||||
|           </el-table-column> |  | ||||||
|           <el-table-column align="left"> |  | ||||||
|             <template #default="scope"> |  | ||||||
|               <div> |  | ||||||
|                 <el-button |  | ||||||
|                   type="danger" |  | ||||||
|                   icon="delete" |  | ||||||
|                   @click="deleteBtn(form.menuBtn, scope.$index)" |  | ||||||
|                 > |  | ||||||
|                   删除 |  | ||||||
|                 </el-button> |  | ||||||
|               </div> |  | ||||||
|             </template> |  | ||||||
|           </el-table-column> |  | ||||||
|         </el-table> |  | ||||||
|       </div> |       </div> | ||||||
|  |             | ||||||
|  |       <!-- 可控按钮配置区域 --> | ||||||
|  |       <div class="mb-2 mt-2"> | ||||||
|  |         <div class="flex justify-between items-center mb-4"> | ||||||
|  |           <h3 class="font-semibold text-gray-700">可控按钮配置</h3> | ||||||
|  |           <div class="flex items-center gap-2"> | ||||||
|  |             <el-button type="primary" size="small" @click="addBtn(form)"> | ||||||
|  |               新增可控按钮 | ||||||
|  |             </el-button> | ||||||
|  |             <el-tooltip | ||||||
|  |               content="点击查看按钮权限配置文档" | ||||||
|  |               placement="top" | ||||||
|  |               effect="light" | ||||||
|  |             > | ||||||
|  |               <el-icon | ||||||
|  |                 class="cursor-pointer text-blue-500 hover:text-blue-700" | ||||||
|  |                 @click="toDoc('https://www.gin-vue-admin.com/guide/web/button-auth.html')" | ||||||
|  |               > | ||||||
|  |                 <QuestionFilled /> | ||||||
|  |               </el-icon> | ||||||
|  |             </el-tooltip> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |              <el-table  | ||||||
|  |                :data="form.menuBtn"  | ||||||
|  |                style="width: 100%" | ||||||
|  |                class="button-table" | ||||||
|  |              > | ||||||
|  |                <el-table-column | ||||||
|  |                  align="center" | ||||||
|  |                  prop="name" | ||||||
|  |                  label="按钮名称" | ||||||
|  |                  width="150" | ||||||
|  |                > | ||||||
|  |                  <template #default="scope"> | ||||||
|  |                    <el-input  | ||||||
|  |                      v-model="scope.row.name"  | ||||||
|  |                      size="small" | ||||||
|  |                      placeholder="请输入按钮名称" | ||||||
|  |                    /> | ||||||
|  |                  </template> | ||||||
|  |                </el-table-column> | ||||||
|  |                <el-table-column align="center" prop="desc" label="备注"> | ||||||
|  |                  <template #default="scope"> | ||||||
|  |                    <el-input  | ||||||
|  |                      v-model="scope.row.desc"  | ||||||
|  |                      size="small" | ||||||
|  |                      placeholder="请输入按钮备注" | ||||||
|  |                    /> | ||||||
|  |                  </template> | ||||||
|  |                </el-table-column> | ||||||
|  |                <el-table-column align="center" label="操作" width="100"> | ||||||
|  |                  <template #default="scope"> | ||||||
|  |                    <el-button | ||||||
|  |                      type="danger" | ||||||
|  |                      size="small" | ||||||
|  |                      @click="deleteBtn(form.menuBtn, scope.$index)" | ||||||
|  |                    > | ||||||
|  |                      <el-icon><Delete /></el-icon> | ||||||
|  |                    </el-button> | ||||||
|  |                  </template> | ||||||
|  |                </el-table-column> | ||||||
|  |              </el-table> | ||||||
|  |        </div> | ||||||
|     </el-drawer> |     </el-drawer> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
| @@ -417,7 +524,7 @@ | |||||||
|   import { canRemoveAuthorityBtnApi } from '@/api/authorityBtn' |   import { canRemoveAuthorityBtnApi } from '@/api/authorityBtn' | ||||||
|   import { reactive, ref } from 'vue' |   import { reactive, ref } from 'vue' | ||||||
|   import { ElMessage, ElMessageBox } from 'element-plus' |   import { ElMessage, ElMessageBox } from 'element-plus' | ||||||
|   import { QuestionFilled } from '@element-plus/icons-vue' |   import { QuestionFilled, InfoFilled, Delete } from '@element-plus/icons-vue' | ||||||
|   import { toDoc } from '@/utils/doc' |   import { toDoc } from '@/utils/doc' | ||||||
|   import { toLowerCase } from '@/utils/stringFun' |   import { toLowerCase } from '@/utils/stringFun' | ||||||
|   import ComponentsCascader from '@/view/superAdmin/menu/components/components-cascader.vue' |   import ComponentsCascader from '@/view/superAdmin/menu/components/components-cascader.vue' | ||||||
| @@ -593,7 +700,7 @@ | |||||||
|         if (res.code === 0) { |         if (res.code === 0) { | ||||||
|           ElMessage({ |           ElMessage({ | ||||||
|             type: 'success', |             type: 'success', | ||||||
|             message: isEdit.value ? '编辑成功' : '添加成功!' |             message: isEdit.value ? '编辑成功' : '添加成功,请到角色管理页面分配权限' | ||||||
|           }) |           }) | ||||||
|           getTableData() |           getTableData() | ||||||
|         } |         } | ||||||
| @@ -677,4 +784,52 @@ | |||||||
|       margin-right: 8px; |       margin-right: 8px; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |    | ||||||
|  |   .form-tip { | ||||||
|  |     margin-top: 8px; | ||||||
|  |     font-size: 12px; | ||||||
|  |     color: #909399; | ||||||
|  |     display: flex; | ||||||
|  |     align-items: center; | ||||||
|  |     gap: 8px; | ||||||
|  |      | ||||||
|  |     .el-icon { | ||||||
|  |       color: #409eff; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   .label-with-tooltip { | ||||||
|  |     display: flex; | ||||||
|  |     align-items: center; | ||||||
|  |     gap: 6px; | ||||||
|  |      | ||||||
|  |     .el-icon { | ||||||
|  |       color: #909399; | ||||||
|  |       cursor: help; | ||||||
|  |        | ||||||
|  |       &:hover { | ||||||
|  |         color: #409eff; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   .parameter-table, | ||||||
|  |   .button-table { | ||||||
|  |     border: 1px solid #ebeef5; | ||||||
|  |     border-radius: 6px; | ||||||
|  |      | ||||||
|  |     :deep(.el-table__header) { | ||||||
|  |       background-color: #fafafa; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     :deep(.el-table__body) { | ||||||
|  |       .el-table__row { | ||||||
|  |         &:hover { | ||||||
|  |           background-color: #f5f7fa; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
| </style> | </style> | ||||||
|   | |||||||
| @@ -268,16 +268,16 @@ | |||||||
|     > |     > | ||||||
|       <el-descriptions :column="1" border> |       <el-descriptions :column="1" border> | ||||||
|         <el-descriptions-item label="参数名称"> |         <el-descriptions-item label="参数名称"> | ||||||
|           {{ detailFrom.name }} |           {{ detailForm.name }} | ||||||
|         </el-descriptions-item> |         </el-descriptions-item> | ||||||
|         <el-descriptions-item label="参数键"> |         <el-descriptions-item label="参数键"> | ||||||
|           {{ detailFrom.key }} |           {{ detailForm.key }} | ||||||
|         </el-descriptions-item> |         </el-descriptions-item> | ||||||
|         <el-descriptions-item label="参数值"> |         <el-descriptions-item label="参数值"> | ||||||
|           {{ detailFrom.value }} |           {{ detailForm.value }} | ||||||
|         </el-descriptions-item> |         </el-descriptions-item> | ||||||
|         <el-descriptions-item label="参数说明"> |         <el-descriptions-item label="参数说明"> | ||||||
|           {{ detailFrom.desc }} |           {{ detailForm.desc }} | ||||||
|         </el-descriptions-item> |         </el-descriptions-item> | ||||||
|       </el-descriptions> |       </el-descriptions> | ||||||
|     </el-drawer> |     </el-drawer> | ||||||
| @@ -574,7 +574,7 @@ | |||||||
|     }) |     }) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   const detailFrom = ref({}) |   const detailForm = ref({}) | ||||||
|  |  | ||||||
|   // 查看详情控制标记 |   // 查看详情控制标记 | ||||||
|   const detailShow = ref(false) |   const detailShow = ref(false) | ||||||
| @@ -589,7 +589,7 @@ | |||||||
|     // 打开弹窗 |     // 打开弹窗 | ||||||
|     const res = await findSysParams({ ID: row.ID }) |     const res = await findSysParams({ ID: row.ID }) | ||||||
|     if (res.code === 0) { |     if (res.code === 0) { | ||||||
|       detailFrom.value = res.data |       detailForm.value = res.data | ||||||
|       openDetailShow() |       openDetailShow() | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| @@ -597,7 +597,7 @@ | |||||||
|   // 关闭详情弹窗 |   // 关闭详情弹窗 | ||||||
|   const closeDetailShow = () => { |   const closeDetailShow = () => { | ||||||
|     detailShow.value = false |     detailShow.value = false | ||||||
|     detailFrom.value = {} |     detailForm.value = {} | ||||||
|   } |   } | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|   | |||||||
| @@ -143,6 +143,38 @@ | |||||||
|         /> |         /> | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
|  |     <!-- 重置密码对话框 --> | ||||||
|  |     <el-dialog | ||||||
|  |       v-model="resetPwdDialog" | ||||||
|  |       title="重置密码" | ||||||
|  |       width="500px" | ||||||
|  |       :close-on-click-modal="false" | ||||||
|  |       :close-on-press-escape="false" | ||||||
|  |     > | ||||||
|  |       <el-form :model="resetPwdInfo" ref="resetPwdForm" label-width="100px"> | ||||||
|  |         <el-form-item label="用户账号"> | ||||||
|  |           <el-input v-model="resetPwdInfo.userName" disabled /> | ||||||
|  |         </el-form-item> | ||||||
|  |         <el-form-item label="用户昵称"> | ||||||
|  |           <el-input v-model="resetPwdInfo.nickName" disabled /> | ||||||
|  |         </el-form-item> | ||||||
|  |         <el-form-item label="新密码"> | ||||||
|  |           <div class="flex w-full"> | ||||||
|  |             <el-input class="flex-1" v-model="resetPwdInfo.password" placeholder="请输入新密码" show-password /> | ||||||
|  |             <el-button type="primary" @click="generateRandomPassword" style="margin-left: 10px"> | ||||||
|  |               生成随机密码 | ||||||
|  |             </el-button> | ||||||
|  |           </div> | ||||||
|  |         </el-form-item> | ||||||
|  |       </el-form> | ||||||
|  |       <template #footer> | ||||||
|  |         <div class="dialog-footer"> | ||||||
|  |           <el-button @click="closeResetPwdDialog">取 消</el-button> | ||||||
|  |           <el-button type="primary" @click="confirmResetPassword">确 定</el-button> | ||||||
|  |         </div> | ||||||
|  |       </template> | ||||||
|  |     </el-dialog> | ||||||
|  |      | ||||||
|     <el-drawer |     <el-drawer | ||||||
|       v-model="addUserDialog" |       v-model="addUserDialog" | ||||||
|       :size="appStore.drawerSize" |       :size="appStore.drawerSize" | ||||||
| @@ -332,28 +364,81 @@ | |||||||
|  |  | ||||||
|   initPage() |   initPage() | ||||||
|  |  | ||||||
|   const resetPasswordFunc = (row) => { |   // 重置密码对话框相关 | ||||||
|     ElMessageBox.confirm('是否将此用户密码重置为123456?', '警告', { |   const resetPwdDialog = ref(false) | ||||||
|       confirmButtonText: '确定', |   const resetPwdForm = ref(null) | ||||||
|       cancelButtonText: '取消', |   const resetPwdInfo = ref({ | ||||||
|       type: 'warning' |     ID: '', | ||||||
|     }).then(async () => { |     userName: '', | ||||||
|       const res = await resetPassword({ |     nickName: '', | ||||||
|         ID: row.ID |     password: '' | ||||||
|  |   }) | ||||||
|  |    | ||||||
|  |   // 生成随机密码 | ||||||
|  |   const generateRandomPassword = () => { | ||||||
|  |     const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*' | ||||||
|  |     let password = '' | ||||||
|  |     for (let i = 0; i < 12; i++) { | ||||||
|  |       password += chars.charAt(Math.floor(Math.random() * chars.length)) | ||||||
|  |     } | ||||||
|  |     resetPwdInfo.value.password = password | ||||||
|  |     // 复制到剪贴板 | ||||||
|  |     navigator.clipboard.writeText(password).then(() => { | ||||||
|  |       ElMessage({ | ||||||
|  |         type: 'success', | ||||||
|  |         message: '密码已复制到剪贴板' | ||||||
|  |       }) | ||||||
|  |     }).catch(() => { | ||||||
|  |       ElMessage({ | ||||||
|  |         type: 'error', | ||||||
|  |         message: '复制失败,请手动复制' | ||||||
|       }) |       }) | ||||||
|       if (res.code === 0) { |  | ||||||
|         ElMessage({ |  | ||||||
|           type: 'success', |  | ||||||
|           message: res.msg |  | ||||||
|         }) |  | ||||||
|       } else { |  | ||||||
|         ElMessage({ |  | ||||||
|           type: 'error', |  | ||||||
|           message: res.msg |  | ||||||
|         }) |  | ||||||
|       } |  | ||||||
|     }) |     }) | ||||||
|   } |   } | ||||||
|  |    | ||||||
|  |   // 打开重置密码对话框 | ||||||
|  |   const resetPasswordFunc = (row) => { | ||||||
|  |     resetPwdInfo.value.ID = row.ID | ||||||
|  |     resetPwdInfo.value.userName = row.userName | ||||||
|  |     resetPwdInfo.value.nickName = row.nickName | ||||||
|  |     resetPwdInfo.value.password = '' | ||||||
|  |     resetPwdDialog.value = true | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   // 确认重置密码 | ||||||
|  |   const confirmResetPassword = async () => { | ||||||
|  |     if (!resetPwdInfo.value.password) { | ||||||
|  |       ElMessage({ | ||||||
|  |         type: 'warning', | ||||||
|  |         message: '请输入或生成密码' | ||||||
|  |       }) | ||||||
|  |       return | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     const res = await resetPassword({ | ||||||
|  |       ID: resetPwdInfo.value.ID, | ||||||
|  |       password: resetPwdInfo.value.password | ||||||
|  |     }) | ||||||
|  |      | ||||||
|  |     if (res.code === 0) { | ||||||
|  |       ElMessage({ | ||||||
|  |         type: 'success', | ||||||
|  |         message: res.msg || '密码重置成功' | ||||||
|  |       }) | ||||||
|  |       resetPwdDialog.value = false | ||||||
|  |     } else { | ||||||
|  |       ElMessage({ | ||||||
|  |         type: 'error', | ||||||
|  |         message: res.msg || '密码重置失败' | ||||||
|  |       }) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   // 关闭重置密码对话框 | ||||||
|  |   const closeResetPwdDialog = () => { | ||||||
|  |     resetPwdInfo.value.password = '' | ||||||
|  |     resetPwdDialog.value = false | ||||||
|  |   } | ||||||
|   const setAuthorityIds = () => { |   const setAuthorityIds = () => { | ||||||
|     tableData.value && |     tableData.value && | ||||||
|       tableData.value.forEach((user) => { |       tableData.value.forEach((user) => { | ||||||
|   | |||||||
| @@ -206,7 +206,7 @@ | |||||||
|             </el-form-item> |             </el-form-item> | ||||||
|           </el-col> |           </el-col> | ||||||
|           <el-col :span="6"> |           <el-col :span="6"> | ||||||
|             <el-form-item label="TableName" class="w-full"> |             <el-form-item label="abbreviation" prop="abbreviation" class="w-full"> | ||||||
|               <template #label> |               <template #label> | ||||||
|                 <el-tooltip |                 <el-tooltip | ||||||
|                   content="简称会作为入参对象名和路由group" |                   content="简称会作为入参对象名和路由group" | ||||||
| @@ -268,7 +268,7 @@ | |||||||
|               prop="package" |               prop="package" | ||||||
|               class="w-full relative" |               class="w-full relative" | ||||||
|             > |             > | ||||||
|               <el-select v-model="form.package" class="w-full pr-12"> |               <el-select v-model="form.package" class="w-full pr-12" filterable> | ||||||
|                 <el-option |                 <el-option | ||||||
|                   v-for="item in pkgs" |                   v-for="item in pkgs" | ||||||
|                   :key="item.ID" |                   :key="item.ID" | ||||||
| @@ -307,6 +307,7 @@ | |||||||
|               </template> |               </template> | ||||||
|               <el-select |               <el-select | ||||||
|                 v-model="form.businessDB" |                 v-model="form.businessDB" | ||||||
|  |                 clearable | ||||||
|                 placeholder="选择业务库" |                 placeholder="选择业务库" | ||||||
|                 class="w-full" |                 class="w-full" | ||||||
|               > |               > | ||||||
| @@ -534,7 +535,7 @@ | |||||||
|             width="160" |             width="160" | ||||||
|           > |           > | ||||||
|             <template #default="{ row }"> |             <template #default="{ row }"> | ||||||
|               <el-input :disabled="row.disabled" v-model="row.fieldName" /> |               <el-input disabled v-model="row.fieldName" /> | ||||||
|             </template> |             </template> | ||||||
|           </el-table-column> |           </el-table-column> | ||||||
|           <el-table-column |           <el-table-column | ||||||
| @@ -696,7 +697,7 @@ | |||||||
|                 style="width: 100%" |                 style="width: 100%" | ||||||
|                 placeholder="请选择字段查询条件" |                 placeholder="请选择字段查询条件" | ||||||
|                 clearable |                 clearable | ||||||
|                 :disabled="row.fieldType !== 'json' || row.disabled" |                 :disabled="row.fieldType === 'json' || row.disabled" | ||||||
|               > |               > | ||||||
|                 <el-option |                 <el-option | ||||||
|                   v-for="item in typeSearchOptions" |                   v-for="item in typeSearchOptions" | ||||||
| @@ -1541,15 +1542,6 @@ | |||||||
|   } |   } | ||||||
|   init() |   init() | ||||||
|  |  | ||||||
|   watch( |  | ||||||
|     () => route.params.id, |  | ||||||
|     () => { |  | ||||||
|       if (route.name === 'autoCodeEdit') { |  | ||||||
|         init() |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   ) |  | ||||||
|  |  | ||||||
|   watch(()=>form.value.generateServer,()=>{ |   watch(()=>form.value.generateServer,()=>{ | ||||||
|     if(!form.value.generateServer){ |     if(!form.value.generateServer){ | ||||||
|       form.value.autoCreateApiToSql = false |       form.value.autoCreateApiToSql = false | ||||||
| @@ -1566,6 +1558,7 @@ | |||||||
|  |  | ||||||
|   const catchData = () => { |   const catchData = () => { | ||||||
|     window.sessionStorage.setItem('autoCode', JSON.stringify(form.value)) |     window.sessionStorage.setItem('autoCode', JSON.stringify(form.value)) | ||||||
|  |     ElMessage.success('暂存成功') | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   const getCatch = () => { |   const getCatch = () => { | ||||||
| @@ -1619,6 +1612,8 @@ | |||||||
|     reader.onload = (e) => { |     reader.onload = (e) => { | ||||||
|       try { |       try { | ||||||
|         form.value = JSON.parse(e.target.result) |         form.value = JSON.parse(e.target.result) | ||||||
|  |         form.value.generateServer = true | ||||||
|  |         form.value.generateWeb = true | ||||||
|         ElMessage.success('JSON 文件导入成功') |         ElMessage.success('JSON 文件导入成功') | ||||||
|       } catch (_) { |       } catch (_) { | ||||||
|         ElMessage.error('无效的 JSON 文件') |         ElMessage.error('无效的 JSON 文件') | ||||||
|   | |||||||
							
								
								
									
										151
									
								
								src/view/systemTools/autoCode/mcp.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										151
									
								
								src/view/systemTools/autoCode/mcp.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,151 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="gva-form-box"> | ||||||
|  |     <el-form :model="form" ref="formRef" label-width="100px" :rules="rules"> | ||||||
|  |       <el-form-item label="工具名称" prop="name"> | ||||||
|  |         <el-input v-model="form.name" placeholder="例:CurrentTime" /> | ||||||
|  |       </el-form-item> | ||||||
|  |       <el-form-item label="工具描述" prop="description"> | ||||||
|  |         <el-input type="textarea" v-model="form.description" placeholder="请输入工具描述" /> | ||||||
|  |       </el-form-item> | ||||||
|  |       <el-form-item label="参数列表"> | ||||||
|  |         <el-table :data="form.params"  style="width: 100%"> | ||||||
|  |           <el-table-column prop="name" label="参数名" width="120"> | ||||||
|  |             <template #default="scope"> | ||||||
|  |               <el-input v-model="scope.row.name" placeholder="参数名" /> | ||||||
|  |             </template> | ||||||
|  |           </el-table-column> | ||||||
|  |           <el-table-column prop="description" label="描述" min-width="180"> | ||||||
|  |             <template #default="scope"> | ||||||
|  |               <el-input v-model="scope.row.description" placeholder="描述" /> | ||||||
|  |             </template> | ||||||
|  |           </el-table-column> | ||||||
|  |           <el-table-column prop="type" label="类型" width="120"> | ||||||
|  |             <template #default="scope"> | ||||||
|  |               <el-select v-model="scope.row.type" placeholder="类型"> | ||||||
|  |                 <el-option label="string" value="string" /> | ||||||
|  |                 <el-option label="number" value="number" /> | ||||||
|  |                 <el-option label="boolean" value="boolean" /> | ||||||
|  |                 <el-option label="object" value="object" /> | ||||||
|  |                 <el-option label="array" value="array" /> | ||||||
|  |               </el-select> | ||||||
|  |             </template> | ||||||
|  |           </el-table-column> | ||||||
|  |           <el-table-column label="默认值" width="300"> | ||||||
|  |             <template #default="scope"> | ||||||
|  |               <el-input :disabled="scope.row.type === 'object'" v-model="scope.row.default" /> | ||||||
|  |             </template> | ||||||
|  |           </el-table-column> | ||||||
|  |           <el-table-column prop="required" label="必填" width="80"> | ||||||
|  |             <template #default="scope"> | ||||||
|  |               <el-checkbox v-model="scope.row.required" /> | ||||||
|  |             </template> | ||||||
|  |           </el-table-column> | ||||||
|  |           <el-table-column label="操作" width="80"> | ||||||
|  |             <template #default="scope"> | ||||||
|  |               <el-button type="text" @click="removeParam(scope.$index)">删除</el-button> | ||||||
|  |             </template> | ||||||
|  |           </el-table-column> | ||||||
|  |         </el-table> | ||||||
|  |       </el-form-item> | ||||||
|  |       <div class="flex justify-end"> | ||||||
|  |         <el-button type="primary" icon="plus" @click="addParam" style="margin-top: 10px;">添加参数</el-button> | ||||||
|  |       </div> | ||||||
|  |       <el-form-item label="返回参数"> | ||||||
|  |         <el-table :data="form.response" style="width: 100%"> | ||||||
|  |           <el-table-column prop="type" label="类型" min-width="120"> | ||||||
|  |             <template #default="scope"> | ||||||
|  |               <el-select v-model="scope.row.type" placeholder="类型"> | ||||||
|  |                 <el-option label="text" value="text" /> | ||||||
|  |                 <el-option label="image" value="image" /> | ||||||
|  |               </el-select> | ||||||
|  |             </template> | ||||||
|  |           </el-table-column> | ||||||
|  |           <el-table-column label="操作" width="80"> | ||||||
|  |             <template #default="scope"> | ||||||
|  |               <el-button type="text" @click="removeResponse(scope.$index)">删除</el-button> | ||||||
|  |             </template> | ||||||
|  |           </el-table-column> | ||||||
|  |         </el-table> | ||||||
|  |       </el-form-item> | ||||||
|  |       <div class="flex justify-end"> | ||||||
|  |         <el-button type="primary" icon="plus" @click="addResponse" style="margin-top: 10px;">添加返回参数</el-button> | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |       <div class="flex justify-end mt-8"> | ||||||
|  |         <el-button type="primary" @click="submit">生成</el-button> | ||||||
|  |       </div> | ||||||
|  |     </el-form> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup> | ||||||
|  | import { ref, reactive } from 'vue' | ||||||
|  | import { ElMessage } from 'element-plus' | ||||||
|  | import { mcp } from '@/api/autoCode' | ||||||
|  |  | ||||||
|  | defineOptions({ | ||||||
|  |   name: 'MCP' | ||||||
|  | }) | ||||||
|  |  | ||||||
|  | const formRef = ref(null) | ||||||
|  | const form = reactive({ | ||||||
|  |   name: '', | ||||||
|  |   description: '', | ||||||
|  |   type: '', | ||||||
|  |   params: [], | ||||||
|  |   response: [] | ||||||
|  | }) | ||||||
|  |  | ||||||
|  | const rules = { | ||||||
|  |   name: [{ required: true, message: '请输入工具名称', trigger: 'blur' }], | ||||||
|  |   description: [{ required: true, message: '请输入工具描述', trigger: 'blur' }], | ||||||
|  |   type: [{ required: true, message: '请选择类型', trigger: 'change' }] | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function addParam() { | ||||||
|  |   form.params.push({ | ||||||
|  |     name: '', | ||||||
|  |     description: '', | ||||||
|  |     type: '', | ||||||
|  |     required: false | ||||||
|  |   }) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function removeParam(index) { | ||||||
|  |   form.params.splice(index, 1) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function addResponse() { | ||||||
|  |   form.response.push({ | ||||||
|  |     type: '' | ||||||
|  |   }) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function removeResponse(index) { | ||||||
|  |   form.response.splice(index, 1) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function submit() { | ||||||
|  |   formRef.value.validate(async (valid) => { | ||||||
|  |     if (!valid) return | ||||||
|  |     // 简单校验参数 | ||||||
|  |     for (const p of form.params) { | ||||||
|  |       if (!p.name || !p.description || !p.type) { | ||||||
|  |         ElMessage.error('请完善所有参数信息') | ||||||
|  |         return | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     // 校验返回参数 | ||||||
|  |     for (const r of form.response) { | ||||||
|  |       if (!r.type) { | ||||||
|  |         ElMessage.error('请完善所有返回参数类型') | ||||||
|  |         return | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |       const res = await mcp(form) | ||||||
|  |       if (res.code === 0) { | ||||||
|  |         ElMessage.success(res.msg) | ||||||
|  |       } | ||||||
|  |   }) | ||||||
|  | } | ||||||
|  | </script> | ||||||
							
								
								
									
										261
									
								
								src/view/systemTools/autoCode/mcpTest.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										261
									
								
								src/view/systemTools/autoCode/mcpTest.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,261 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="p-2"> | ||||||
|  |  | ||||||
|  |     <el-card class="mb-2"> | ||||||
|  |       <template #header> | ||||||
|  |         <div class="flex justify-between items-center font-bold"> | ||||||
|  |           <span>MCP 服务器配置示例</span> | ||||||
|  |           <el-tooltip content="复制配置" placement="top"> | ||||||
|  |             <el-button :icon="DocumentCopy" circle @click="copyMcpConfig" /> | ||||||
|  |           </el-tooltip> | ||||||
|  |         </div> | ||||||
|  |       </template> | ||||||
|  |       <pre class="font-mono whitespace-pre-wrap break-words bg-gray-100 p-2.5 rounded text-gray-700">{{ mcpServerConfig }}</pre> | ||||||
|  |     </el-card> | ||||||
|  |  | ||||||
|  |      | ||||||
|  |     <el-row :gutter="8"> | ||||||
|  |       <el-col v-for="tool in mcpTools" :key="tool.name" :xs="24" :sm="12" :md="12" :lg="8"> | ||||||
|  |         <el-card class="mb-5 min-h-[150px] flex flex-col overflow-hidden"> | ||||||
|  |           <template #header> | ||||||
|  |             <div class="flex justify-between items-center font-bold"> | ||||||
|  |               <span>{{ tool.name }}</span> | ||||||
|  |               <el-tooltip content="测试工具" placement="top"> | ||||||
|  |                 <el-button :icon="VideoPlay" circle @click="openTestDialog(tool)" /> | ||||||
|  |               </el-tooltip> | ||||||
|  |             </div> | ||||||
|  |           </template> | ||||||
|  |           <div class="text-sm mb-1">{{ tool.description }}</div> | ||||||
|  |           <div v-if="tool.inputSchema && tool.inputSchema.properties && Object.keys(tool.inputSchema.properties).length > 0" class="mt-1 text-xs overflow-y-auto max-h-[100px] p-2 border-t border-gray-200 bg-gray-50 rounded-b"> | ||||||
|  |             <p class="font-semibold mb-1 text-gray-700 flex items-center"> | ||||||
|  |               <span class="mr-1 my-2">参数列表</span> | ||||||
|  |               <span class="text-xs text-gray-500">({{ Object.keys(tool.inputSchema.properties).length }})</span> | ||||||
|  |             </p> | ||||||
|  |             <div class="space-y-2"> | ||||||
|  |               <div v-for="(propDetails, propName) in tool.inputSchema.properties" :key="propName" class="flex flex-col p-1.5 bg-white rounded border border-gray-100 hover:border-gray-300 transition-colors"> | ||||||
|  |                 <div class="flex items-center justify-between"> | ||||||
|  |                   <div class="flex items-center"> | ||||||
|  |                     <span class="font-medium text-gray-800">{{ propName }}</span> | ||||||
|  |                     <span v-if="tool.inputSchema.required && tool.inputSchema.required.includes(propName)" class="ml-1 text-red-500 text-xs">*</span> | ||||||
|  |                   </div> | ||||||
|  |                   <span class="text-xs px-1.5 py-0.5 bg-blue-100 text-blue-700 rounded">{{ propDetails.type }}</span> | ||||||
|  |                 </div> | ||||||
|  |                 <div class="text-gray-500 mt-0.5 text-xs line-clamp-2" :title="propDetails.description || '无描述'"> | ||||||
|  |                   {{ propDetails.description || '无描述' }} | ||||||
|  |                 </div> | ||||||
|  |               </div> | ||||||
|  |             </div> | ||||||
|  |           </div> | ||||||
|  |           <div v-else class="mt-1 text-xs p-2 border-t border-gray-200 bg-gray-50 rounded-b flex items-center justify-center"> | ||||||
|  |             <span class="text-gray-500 italic py-3">无输入参数</span> | ||||||
|  |           </div> | ||||||
|  |         </el-card> | ||||||
|  |       </el-col> | ||||||
|  |     </el-row> | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     <el-dialog | ||||||
|  |       v-model="testDialogVisible" | ||||||
|  |       :title="currentTestingTool ? `${currentTestingTool.name} - 参数测试` : '参数测试'" | ||||||
|  |       width="60%" | ||||||
|  |       :before-close="handleCloseDialog" | ||||||
|  |     > | ||||||
|  |       <el-form v-if="currentTestingTool" :model="testParamsForm" ref="testParamsFormRef" label-width="120px" label-position="top" class="max-h-[calc(60vh-120px)] overflow-y-auto"> | ||||||
|  |         <el-form-item | ||||||
|  |           v-for="(propDetails, propName) in currentTestingTool.inputSchema.properties" | ||||||
|  |           :key="propName" | ||||||
|  |           :label="propDetails.description || propName" | ||||||
|  |           :prop="propName" | ||||||
|  |           :rules="currentTestingTool.inputSchema.required && currentTestingTool.inputSchema.required.includes(propName) ? [{ required: true, message: '请输入 ' + (propDetails.description || propName), trigger: 'blur' }] : []" | ||||||
|  |         > | ||||||
|  |           <el-input  | ||||||
|  |             v-if="propDetails.type === 'string' && !propDetails.enum"  | ||||||
|  |             v-model="testParamsForm[propName]"  | ||||||
|  |             :placeholder="propDetails.description || '请输入' + propName"  | ||||||
|  |           /> | ||||||
|  |           <el-input  | ||||||
|  |             v-else-if="propDetails.type === 'number'"  | ||||||
|  |             v-model.number="testParamsForm[propName]"  | ||||||
|  |             type="number"  | ||||||
|  |             :placeholder="propDetails.description || '请输入数字' + propName"  | ||||||
|  |           /> | ||||||
|  |           <el-select  | ||||||
|  |             v-else-if="propDetails.type === 'boolean'"  | ||||||
|  |             v-model="testParamsForm[propName]"  | ||||||
|  |             :placeholder="propDetails.description || '请选择'" | ||||||
|  |           > | ||||||
|  |             <el-option label="True" :value="true" /> | ||||||
|  |             <el-option label="False" :value="false" /> | ||||||
|  |           </el-select> | ||||||
|  |           <el-select  | ||||||
|  |             v-else-if="propDetails.type === 'string' && propDetails.enum"  | ||||||
|  |             v-model="testParamsForm[propName]"  | ||||||
|  |             :placeholder="propDetails.description || '请选择' + propName" | ||||||
|  |           > | ||||||
|  |             <el-option v-for="enumValue in propDetails.enum" :key="enumValue" :label="enumValue" :value="enumValue" /> | ||||||
|  |           </el-select> | ||||||
|  |           <el-input  | ||||||
|  |             v-else  | ||||||
|  |             type="textarea"  | ||||||
|  |             v-model="testParamsForm[propName]"  | ||||||
|  |             :placeholder="(propDetails.description || propName) + ' (请输入JSON格式)'"  | ||||||
|  |             :autosize="{ minRows: 2, maxRows: 6 }" | ||||||
|  |           /> | ||||||
|  |         </el-form-item> | ||||||
|  |       </el-form> | ||||||
|  |       <div v-if="apiDialogResponse" class="mt-5 p-[15px] border border-gray-200 rounded bg-gray-50"> | ||||||
|  |         <h4 class="mt-0 mb-2.5 text-base">API 返回结果:</h4> | ||||||
|  |         <div v-if="typeof apiDialogResponse === 'string'"> | ||||||
|  |           <pre class="bg-gray-100 p-2.5 rounded whitespace-pre-wrap break-words overflow-y-auto">{{ apiDialogResponse }}</pre> | ||||||
|  |         </div> | ||||||
|  |         <div v-else-if="apiDialogResponse.type === 'image' && apiDialogResponse.content"> | ||||||
|  |            <el-image | ||||||
|  |               class="max-w-full max-h-[300px]" | ||||||
|  |               :src="apiDialogResponse.content" | ||||||
|  |               :preview-src-list="[apiDialogResponse.content]" | ||||||
|  |               fit="contain" | ||||||
|  |             /> | ||||||
|  |         </div> | ||||||
|  |          <div v-else-if="apiDialogResponse.type === 'text' && apiDialogResponse.content"> | ||||||
|  |           <pre class="bg-gray-100 p-2.5 rounded whitespace-pre-wrap break-words overflow-y-auto">{{ apiDialogResponse.content }}</pre> | ||||||
|  |         </div> | ||||||
|  |         <div v-else> | ||||||
|  |           <pre class="bg-gray-100 p-2.5 rounded whitespace-pre-wrap break-words overflow-y-auto">{{ JSON.stringify(apiDialogResponse, null, 2) }}</pre> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |       <template #footer> | ||||||
|  |         <span class="dialog-footer"> | ||||||
|  |           <el-button @click="testDialogVisible = false">取消</el-button> | ||||||
|  |           <el-button type="primary" @click="handleDialogTestTool">测试</el-button> | ||||||
|  |         </span> | ||||||
|  |       </template> | ||||||
|  |     </el-dialog> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup> | ||||||
|  | import { ref, reactive, onMounted } from 'vue' | ||||||
|  | import { ElMessage } from 'element-plus' | ||||||
|  | import { VideoPlay, DocumentCopy } from '@element-plus/icons-vue' // Added DocumentCopy | ||||||
|  | import { mcpList, mcpTest } from '@/api/autoCode' | ||||||
|  |  | ||||||
|  | defineOptions({ | ||||||
|  |   name: 'MCPTest' | ||||||
|  | }) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | const mcpTools = ref([]) | ||||||
|  | const testDialogVisible = ref(false) | ||||||
|  | const currentTestingTool = ref(null) | ||||||
|  | const testParamsForm = reactive({}) | ||||||
|  | const testParamsFormRef = ref(null) | ||||||
|  | const apiDialogResponse = ref(null) | ||||||
|  |  | ||||||
|  | const mcpServerConfig = ref(JSON.stringify({ | ||||||
|  |   "mcpServers": { | ||||||
|  |     "gva": { | ||||||
|  |       "url": "https://127.0.0.1/sse" | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | }, null, 2)) | ||||||
|  |  | ||||||
|  | const fetchMcpTools = async () => { | ||||||
|  |     const res = await mcpList() | ||||||
|  |     if (res.code === 0 && res.data && res.data.list.tools) { | ||||||
|  |       mcpTools.value = res.data.list.tools | ||||||
|  |       mcpServerConfig.value = JSON.stringify(res.data.mcpServerConfig, null, 2) | ||||||
|  |     } else { | ||||||
|  |       ElMessage.error(res.msg || '获取工具列表失败或数据格式不正确') | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | onMounted(() => { | ||||||
|  |   fetchMcpTools() | ||||||
|  | }) | ||||||
|  |  | ||||||
|  | const copyMcpConfig = async () => { | ||||||
|  |   try { | ||||||
|  |     await navigator.clipboard.writeText(mcpServerConfig.value) | ||||||
|  |     ElMessage.success('配置已复制到剪贴板') | ||||||
|  |   } catch (err) { | ||||||
|  |     ElMessage.error('复制失败: ' + err) | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const openTestDialog = (tool) => { | ||||||
|  |   currentTestingTool.value = tool | ||||||
|  |   apiDialogResponse.value = null // 清空之前的API响应 | ||||||
|  |  | ||||||
|  |   // 重置并初始化表单数据 | ||||||
|  |   for (const key in testParamsForm) { | ||||||
|  |     delete testParamsForm[key] | ||||||
|  |   } | ||||||
|  |   if (tool.inputSchema && tool.inputSchema.properties) { | ||||||
|  |     Object.keys(tool.inputSchema.properties).forEach(propName => { | ||||||
|  |       const propDetails = tool.inputSchema.properties[propName] | ||||||
|  |       // 设置默认值,优先使用 schema 中的 default,否则根据类型给初始值 | ||||||
|  |       if (propDetails.default !== undefined) { | ||||||
|  |         testParamsForm[propName] = propDetails.default | ||||||
|  |       } else if (propDetails.type === 'boolean') { | ||||||
|  |         testParamsForm[propName] = false | ||||||
|  |       } else if (propDetails.type === 'number') { | ||||||
|  |         testParamsForm[propName] = null // 或者 0 | ||||||
|  |       } else if (propDetails.type === 'object' || propDetails.type === 'array') { | ||||||
|  |         testParamsForm[propName] = '' // 对象和数组类型,默认为空字符串,提示用户输入JSON | ||||||
|  |       } else { | ||||||
|  |         testParamsForm[propName] = '' | ||||||
|  |       } | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  |   testDialogVisible.value = true | ||||||
|  |   // 清除表单校验状态 | ||||||
|  |   if (testParamsFormRef.value) { | ||||||
|  |     testParamsFormRef.value.clearValidate() | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const handleCloseDialog = (done) => { | ||||||
|  |   apiDialogResponse.value = null | ||||||
|  |   done() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const handleDialogTestTool = async () => { | ||||||
|  |   if (!currentTestingTool.value) { | ||||||
|  |     ElMessage.warning('没有选中的测试工具') | ||||||
|  |     return | ||||||
|  |   } | ||||||
|  |   if (testParamsFormRef.value) { | ||||||
|  |     testParamsFormRef.value.validate(async (valid) => { | ||||||
|  |       if (valid) { | ||||||
|  |       | ||||||
|  |           const toolName = currentTestingTool.value.name | ||||||
|  |           const payload = { ...testParamsForm } | ||||||
|  |            | ||||||
|  |           if (currentTestingTool.value.inputSchema && currentTestingTool.value.inputSchema.properties) { | ||||||
|  |             Object.keys(currentTestingTool.value.inputSchema.properties).forEach(propName => { | ||||||
|  |               const propDetails = currentTestingTool.value.inputSchema.properties[propName] | ||||||
|  |               if ((propDetails.type === 'object' || propDetails.type === 'array') && payload[propName] && typeof payload[propName] === 'string') { | ||||||
|  |                 try { | ||||||
|  |                   payload[propName] = JSON.parse(payload[propName]) | ||||||
|  |                 } catch (e) { | ||||||
|  |                   ElMessage.error(`参数 ${propName} 的JSON格式无效: ${e.message}`) | ||||||
|  |                   throw new Error(`参数 ${propName} JSON无效`);  | ||||||
|  |                 } | ||||||
|  |               } | ||||||
|  |             }) | ||||||
|  |           } | ||||||
|  |  | ||||||
|  |           const res = await mcpTest({ | ||||||
|  |             name:toolName, | ||||||
|  |             arguments:payload | ||||||
|  |           }) | ||||||
|  |           apiDialogResponse.value = res.data | ||||||
|  |           if (res.code === 0) { | ||||||
|  |             ElMessage.success('API调用成功') | ||||||
|  |           } | ||||||
|  |       } | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | </script> | ||||||
							
								
								
									
										426
									
								
								src/view/systemTools/autoCode/picture.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										426
									
								
								src/view/systemTools/autoCode/picture.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,426 @@ | |||||||
|  | <template> | ||||||
|  |   <div> | ||||||
|  |     <warning-bar | ||||||
|  |         href="https://www.gin-vue-admin.com/empower/" | ||||||
|  |         title="此功能只针对授权用户开放,点我【购买授权】" | ||||||
|  |     /> | ||||||
|  |     <div class="gva-search-box"> | ||||||
|  |       <div class="text-xl mb-2 text-gray-600"> | ||||||
|  |         AI前端工程师<a | ||||||
|  |           class="text-blue-600 text-sm ml-4" | ||||||
|  |           href="https://plugin.gin-vue-admin.com/#/layout/userInfo/center" | ||||||
|  |           target="_blank" | ||||||
|  |       >获取AiPath</a | ||||||
|  |       > | ||||||
|  |       </div> | ||||||
|  |        | ||||||
|  |       <!-- 选项模式 --> | ||||||
|  |       <div class="mb-4"> | ||||||
|  |         <div class="mb-3"> | ||||||
|  |           <div class="text-base font-medium mb-2">页面用途</div> | ||||||
|  |           <el-radio-group v-model="pageType" class="mb-2" @change="handlePageTypeChange"> | ||||||
|  |             <el-radio label="企业官网">企业官网</el-radio> | ||||||
|  |             <el-radio label="电商页面">电商页面</el-radio> | ||||||
|  |             <el-radio label="个人博客">个人博客</el-radio> | ||||||
|  |             <el-radio label="产品介绍">产品介绍</el-radio> | ||||||
|  |             <el-radio label="活动落地页">活动落地页</el-radio> | ||||||
|  |             <el-radio label="其他">其他</el-radio> | ||||||
|  |           </el-radio-group> | ||||||
|  |           <el-input v-if="pageType === '其他'" v-model="pageTypeCustom" placeholder="请输入页面用途" class="w-full" /> | ||||||
|  |         </div> | ||||||
|  |          | ||||||
|  |         <div class="mb-3"> | ||||||
|  |           <div class="text-base font-medium mb-2">主要内容板块</div> | ||||||
|  |           <el-checkbox-group v-model="contentBlocks" class="flex flex-wrap gap-2 mb-2"> | ||||||
|  |             <el-checkbox label="Banner轮播图">Banner轮播图</el-checkbox> | ||||||
|  |             <el-checkbox label="产品/服务介绍">产品/服务介绍</el-checkbox> | ||||||
|  |             <el-checkbox label="功能特点展示">功能特点展示</el-checkbox> | ||||||
|  |             <el-checkbox label="客户案例">客户案例</el-checkbox> | ||||||
|  |             <el-checkbox label="团队介绍">团队介绍</el-checkbox> | ||||||
|  |             <el-checkbox label="联系表单">联系表单</el-checkbox> | ||||||
|  |             <el-checkbox label="新闻/博客列表">新闻/博客列表</el-checkbox> | ||||||
|  |             <el-checkbox label="价格表">价格表</el-checkbox> | ||||||
|  |             <el-checkbox label="FAQ/常见问题">FAQ/常见问题</el-checkbox> | ||||||
|  |             <el-checkbox label="用户评价">用户评价</el-checkbox> | ||||||
|  |             <el-checkbox label="数据统计">数据统计</el-checkbox> | ||||||
|  |             <el-checkbox label="商品列表">商品列表</el-checkbox> | ||||||
|  |             <el-checkbox label="商品卡片">商品卡片</el-checkbox> | ||||||
|  |             <el-checkbox label="购物车">购物车</el-checkbox> | ||||||
|  |             <el-checkbox label="结算页面">结算页面</el-checkbox> | ||||||
|  |             <el-checkbox label="订单跟踪">订单跟踪</el-checkbox> | ||||||
|  |             <el-checkbox label="商品分类">商品分类</el-checkbox> | ||||||
|  |             <el-checkbox label="热门推荐">热门推荐</el-checkbox> | ||||||
|  |             <el-checkbox label="限时特惠">限时特惠</el-checkbox> | ||||||
|  |             <el-checkbox label="其他">其他</el-checkbox> | ||||||
|  |           </el-checkbox-group> | ||||||
|  |           <el-input v-if="contentBlocks.includes('其他')" v-model="contentBlocksCustom" placeholder="请输入其他内容板块" class="w-full" /> | ||||||
|  |         </div> | ||||||
|  |          | ||||||
|  |         <div class="mb-3"> | ||||||
|  |           <div class="text-base font-medium mb-2">风格偏好</div> | ||||||
|  |           <el-radio-group v-model="stylePreference" class="mb-2"> | ||||||
|  |             <el-radio label="简约">简约</el-radio> | ||||||
|  |             <el-radio label="科技感">科技感</el-radio> | ||||||
|  |             <el-radio label="温馨">温馨</el-radio> | ||||||
|  |             <el-radio label="专业">专业</el-radio> | ||||||
|  |             <el-radio label="创意">创意</el-radio> | ||||||
|  |             <el-radio label="复古">复古</el-radio> | ||||||
|  |             <el-radio label="奢华">奢华</el-radio> | ||||||
|  |             <el-radio label="其他">其他</el-radio> | ||||||
|  |           </el-radio-group> | ||||||
|  |           <el-input v-if="stylePreference === '其他'" v-model="stylePreferenceCustom" placeholder="请输入风格偏好" class="w-full" /> | ||||||
|  |         </div> | ||||||
|  |          | ||||||
|  |         <div class="mb-3"> | ||||||
|  |           <div class="text-base font-medium mb-2">设计布局</div> | ||||||
|  |           <el-radio-group v-model="layoutDesign" class="mb-2"> | ||||||
|  |             <el-radio label="单栏布局">单栏布局</el-radio> | ||||||
|  |             <el-radio label="双栏布局">双栏布局</el-radio> | ||||||
|  |             <el-radio label="三栏布局">三栏布局</el-radio> | ||||||
|  |             <el-radio label="网格布局">网格布局</el-radio> | ||||||
|  |             <el-radio label="画廊布局">画廊布局</el-radio> | ||||||
|  |             <el-radio label="瀑布流">瀑布流</el-radio> | ||||||
|  |             <el-radio label="卡片式">卡片式</el-radio> | ||||||
|  |             <el-radio label="侧边栏+内容布局">侧边栏+内容布局</el-radio> | ||||||
|  |             <el-radio label="分屏布局">分屏布局</el-radio> | ||||||
|  |             <el-radio label="全屏滚动布局">全屏滚动布局</el-radio> | ||||||
|  |             <el-radio label="混合布局">混合布局</el-radio> | ||||||
|  |             <el-radio label="响应式">响应式</el-radio> | ||||||
|  |             <el-radio label="其他">其他</el-radio> | ||||||
|  |           </el-radio-group> | ||||||
|  |           <el-input v-if="layoutDesign === '其他'" v-model="layoutDesignCustom" placeholder="请输入设计布局" class="w-full" /> | ||||||
|  |         </div> | ||||||
|  |          | ||||||
|  |         <div class="mb-3"> | ||||||
|  |           <div class="text-base font-medium mb-2">配色方案</div> | ||||||
|  |           <el-radio-group v-model="colorScheme" class="mb-2"> | ||||||
|  |             <el-radio label="蓝色系">蓝色系</el-radio> | ||||||
|  |             <el-radio label="绿色系">绿色系</el-radio> | ||||||
|  |             <el-radio label="红色系">红色系</el-radio> | ||||||
|  |             <el-radio label="黑白灰">黑白灰</el-radio> | ||||||
|  |             <el-radio label="纯黑白">纯黑白</el-radio> | ||||||
|  |             <el-radio label="暖色调">暖色调</el-radio> | ||||||
|  |             <el-radio label="冷色调">冷色调</el-radio> | ||||||
|  |             <el-radio label="其他">其他</el-radio> | ||||||
|  |           </el-radio-group> | ||||||
|  |           <el-input v-if="colorScheme === '其他'" v-model="colorSchemeCustom" placeholder="请输入配色方案" class="w-full" /> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |        | ||||||
|  |       <!-- 详细描述输入框 --> | ||||||
|  |       <div class="relative"> | ||||||
|  |         <div class="text-base font-medium mb-2">详细描述(可选)</div> | ||||||
|  |         <el-input | ||||||
|  |             v-model="prompt" | ||||||
|  |             :maxlength="2000" | ||||||
|  |             :placeholder="placeholder" | ||||||
|  |             :rows="5" | ||||||
|  |             resize="none" | ||||||
|  |             type="textarea" | ||||||
|  |             @blur="handleBlur" | ||||||
|  |             @focus="handleFocus" | ||||||
|  |         /> | ||||||
|  |         <div class="flex absolute right-2 bottom-2"> | ||||||
|  |           <el-tooltip effect="light"> | ||||||
|  |             <template #content> | ||||||
|  |               <div> | ||||||
|  |                 此功能仅针对授权用户开放,前往<a | ||||||
|  |                   class="text-blue-600" | ||||||
|  |                   href="https://www.gin-vue-admin.com/empower/" | ||||||
|  |                   target="_blank" | ||||||
|  |               >购买授权</a | ||||||
|  |               > | ||||||
|  |               </div> | ||||||
|  |             </template> | ||||||
|  |             <el-button | ||||||
|  |                 type="primary" | ||||||
|  |                 @click="llmAutoFunc()" | ||||||
|  |             > | ||||||
|  |               <el-icon size="18"> | ||||||
|  |                 <ai-gva/> | ||||||
|  |               </el-icon> | ||||||
|  |               生成 | ||||||
|  |             </el-button> | ||||||
|  |           </el-tooltip> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |     <div> | ||||||
|  |       <div v-if="!outPut"> | ||||||
|  |         <el-empty :image-size="200"/> | ||||||
|  |       </div> | ||||||
|  |       <div v-if="outPut && htmlFromLLM"> | ||||||
|  |         <el-tabs type="border-card"> | ||||||
|  |           <el-tab-pane label="页面预览"> | ||||||
|  |             <div class="h-[500px] overflow-auto bg-gray-50 p-4 rounded"> | ||||||
|  |               <div v-if="!loadedComponents" class="text-gray-500 text-center py-4"> | ||||||
|  |                 组件加载中... | ||||||
|  |               </div> | ||||||
|  |               <component | ||||||
|  |                 v-else | ||||||
|  |                 :is="loadedComponents"  | ||||||
|  |                 class="vue-component-container w-full" | ||||||
|  |               /> | ||||||
|  |             </div> | ||||||
|  |           </el-tab-pane> | ||||||
|  |           <el-tab-pane label="源代码"> | ||||||
|  |             <div class="relative h-[500px] overflow-auto bg-gray-50 p-4 rounded"> | ||||||
|  |               <el-button  | ||||||
|  |                 type="primary"  | ||||||
|  |                 :icon="DocumentCopy"  | ||||||
|  |                 class="absolute top-2 right-2 px-2 py-1"  | ||||||
|  |                 @click="copySnippet(htmlFromLLM)"  | ||||||
|  |                 plain | ||||||
|  |               > | ||||||
|  |                 复制 | ||||||
|  |               </el-button> | ||||||
|  |               <pre class="mt-10 whitespace-pre-wrap">{{ htmlFromLLM }}</pre> | ||||||
|  |             </div> | ||||||
|  |           </el-tab-pane> | ||||||
|  |         </el-tabs> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup> | ||||||
|  | import { createWeb } from '@/api/autoCode' | ||||||
|  | import { ref, reactive, markRaw } from 'vue' | ||||||
|  | import * as Vue from "vue"; | ||||||
|  | import WarningBar from '@/components/warningBar/warningBar.vue' | ||||||
|  | import { ElMessage } from 'element-plus' | ||||||
|  | import { defineAsyncComponent } from 'vue' | ||||||
|  | import { DocumentCopy } from '@element-plus/icons-vue' | ||||||
|  | import { loadModule } from "vue3-sfc-loader"; | ||||||
|  |  | ||||||
|  | defineOptions({ | ||||||
|  |   name: 'Picture' | ||||||
|  | }) | ||||||
|  |  | ||||||
|  | const handleFocus = () => { | ||||||
|  |   document.addEventListener('keydown', handleKeydown); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const handleBlur = () => { | ||||||
|  |   document.removeEventListener('keydown', handleKeydown); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const handleKeydown = (event) => { | ||||||
|  |   if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') { | ||||||
|  |     llmAutoFunc() | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // 复制方法:把某个字符串写进剪贴板 | ||||||
|  | const copySnippet = (vueString) => { | ||||||
|  |   navigator.clipboard.writeText(vueString) | ||||||
|  |       .then(() => { | ||||||
|  |         ElMessage({ | ||||||
|  |           message: '复制成功', | ||||||
|  |           type: 'success', | ||||||
|  |         }) | ||||||
|  |       }) | ||||||
|  |       .catch(err => { | ||||||
|  |         ElMessage({ | ||||||
|  |           message: '复制失败', | ||||||
|  |           type: 'warning', | ||||||
|  |         }) | ||||||
|  |       }) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // 选项模式相关变量 | ||||||
|  | const pageType = ref('企业官网') | ||||||
|  | const pageTypeCustom = ref('') | ||||||
|  | const contentBlocks = ref(['Banner轮播图', '产品/服务介绍']) | ||||||
|  | const contentBlocksCustom = ref('') | ||||||
|  | const stylePreference = ref('简约') | ||||||
|  | const stylePreferenceCustom = ref('') | ||||||
|  | const layoutDesign = ref('响应式') | ||||||
|  | const layoutDesignCustom = ref('') | ||||||
|  | const colorScheme = ref('蓝色系') | ||||||
|  | const colorSchemeCustom = ref('') | ||||||
|  |  | ||||||
|  | // 页面用途与内容板块的推荐映射关系 | ||||||
|  | const pageTypeContentMap = { | ||||||
|  |   '企业官网': ['Banner轮播图', '产品/服务介绍', '功能特点展示', '客户案例', '联系表单'], | ||||||
|  |   '电商页面': ['Banner轮播图', '商品列表', '商品卡片', '购物车', '商品分类', '热门推荐', '限时特惠', '结算页面', '用户评价'], | ||||||
|  |   '个人博客': ['Banner轮播图', '新闻/博客列表', '用户评价', '联系表单'], | ||||||
|  |   '产品介绍': ['Banner轮播图', '产品/服务介绍', '功能特点展示', '价格表', 'FAQ/常见问题'], | ||||||
|  |   '活动落地页': ['Banner轮播图', '功能特点展示', '联系表单', '数据统计'] | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const prompt = ref('') | ||||||
|  |  | ||||||
|  | // 判断是否返回的标志 | ||||||
|  | const outPut = ref(false) | ||||||
|  | // 容纳llm返回的vue组件代码 | ||||||
|  | const htmlFromLLM = ref("") | ||||||
|  |  | ||||||
|  | // 存储加载的组件 | ||||||
|  | const loadedComponents = ref(null) | ||||||
|  |  | ||||||
|  | const loadVueComponent = async (vueCode) => { | ||||||
|  |   try { | ||||||
|  |     // 使用内存中的虚拟路径 | ||||||
|  |     const fakePath = `virtual:component-0.vue` | ||||||
|  |      | ||||||
|  |     const component = defineAsyncComponent({ | ||||||
|  |       loader: async () => { | ||||||
|  |         try { | ||||||
|  |           const options = { | ||||||
|  |             moduleCache: { | ||||||
|  |               vue: Vue, | ||||||
|  |             }, | ||||||
|  |             getFile(url) { | ||||||
|  |               // 处理所有可能的URL格式,包括相对路径、绝对路径等 | ||||||
|  |               // 提取路径的最后部分,忽略查询参数 | ||||||
|  |               const fileName = url.split('/').pop().split('?')[0] | ||||||
|  |               const componentFileName = fakePath.split('/').pop() | ||||||
|  |                | ||||||
|  |               // 如果文件名包含我们的组件名称,或者url完全匹配fakePath | ||||||
|  |               if (fileName === componentFileName || url === fakePath ||  | ||||||
|  |                   url === `./component/0.vue`) { | ||||||
|  |                 return Promise.resolve({ | ||||||
|  |                   type: '.vue', | ||||||
|  |                   getContentData: () => vueCode | ||||||
|  |                 }) | ||||||
|  |               } | ||||||
|  |                | ||||||
|  |               console.warn('请求未知文件:', url) | ||||||
|  |               return Promise.reject(new Error(`找不到文件: ${url}`)) | ||||||
|  |             }, | ||||||
|  |             addStyle(textContent) { | ||||||
|  |               // 不再将样式添加到document.head,而是返回样式内容 | ||||||
|  |               // 稍后会将样式添加到Shadow DOM中 | ||||||
|  |               return textContent | ||||||
|  |             }, | ||||||
|  |             handleModule(type, source, path, options) { | ||||||
|  |               // 默认处理器 | ||||||
|  |               return undefined | ||||||
|  |             }, | ||||||
|  |             log(type, ...args) { | ||||||
|  |               console.log(`[vue3-sfc-loader] [${type}]`, ...args) | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |            | ||||||
|  |           // 尝试加载组件 | ||||||
|  |           const comp = await loadModule(fakePath, options) | ||||||
|  |           return comp.default || comp | ||||||
|  |         } catch (error) { | ||||||
|  |           console.error('组件加载详细错误:', error) | ||||||
|  |           throw error | ||||||
|  |         } | ||||||
|  |       }, | ||||||
|  |       loadingComponent: { | ||||||
|  |         template: '<div>加载中...</div>' | ||||||
|  |       }, | ||||||
|  |       errorComponent: { | ||||||
|  |         props: ['error'], | ||||||
|  |         template: '<div>组件加载失败: {{ error && error.message }}</div>', | ||||||
|  |         setup(props) { | ||||||
|  |           console.error('错误组件收到的错误:', props.error) | ||||||
|  |           return {} | ||||||
|  |         } | ||||||
|  |       }, | ||||||
|  |       // 添加超时和重试选项 | ||||||
|  |       timeout: 30000, | ||||||
|  |       delay: 200, | ||||||
|  |       suspensible: false, | ||||||
|  |       onError(error, retry, fail) { | ||||||
|  |         console.error('加载错误,细节:', error) | ||||||
|  |         fail() | ||||||
|  |       } | ||||||
|  |     }) | ||||||
|  |  | ||||||
|  |     // 创建一个包装组件,使用Shadow DOM隔离样式 | ||||||
|  |     const ShadowWrapper = { | ||||||
|  |       name: 'ShadowWrapper', | ||||||
|  |       setup() { | ||||||
|  |         return {} | ||||||
|  |       }, | ||||||
|  |       render() { | ||||||
|  |         return Vue.h('div', { class: 'shadow-wrapper' }) | ||||||
|  |       }, | ||||||
|  |       mounted() { | ||||||
|  |         // 创建Shadow DOM | ||||||
|  |         const shadowRoot = this.$el.attachShadow({ mode: 'open' }) | ||||||
|  |          | ||||||
|  |         // 创建一个容器元素 | ||||||
|  |         const container = document.createElement('div') | ||||||
|  |         container.className = 'shadow-container' | ||||||
|  |         shadowRoot.appendChild(container) | ||||||
|  |          | ||||||
|  |         // 提取组件中的样式 | ||||||
|  |         const styleContent = vueCode.match(/<style[^>]*>([\s\S]*?)<\/style>/i)?.[1] || '' | ||||||
|  |          | ||||||
|  |         // 创建样式元素并添加到Shadow DOM | ||||||
|  |         if (styleContent) { | ||||||
|  |           const style = document.createElement('style') | ||||||
|  |           style.textContent = styleContent | ||||||
|  |           shadowRoot.appendChild(style) | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         // 创建Vue应用并挂载到Shadow DOM容器中 | ||||||
|  |         const app = Vue.createApp({ | ||||||
|  |           render: () => Vue.h(component) | ||||||
|  |         }) | ||||||
|  |         app.mount(container) | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     loadedComponents.value = markRaw(ShadowWrapper) | ||||||
|  |     return ShadowWrapper | ||||||
|  |   } catch (error) { | ||||||
|  |     console.error('组件创建总错误:', error) | ||||||
|  |     return null | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // 当页面用途改变时,更新内容板块的选择 | ||||||
|  | const handlePageTypeChange = (value) => { | ||||||
|  |   if (value !== '其他' && pageTypeContentMap[value]) { | ||||||
|  |     contentBlocks.value = [...pageTypeContentMap[value]] | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const llmAutoFunc = async () => { | ||||||
|  |   // 构建完整的描述,包含选项模式的选择 | ||||||
|  |   let fullPrompt = '' | ||||||
|  |    | ||||||
|  |   // 添加页面用途 | ||||||
|  |   fullPrompt += `页面用途: ${pageType.value === '其他' ? pageTypeCustom.value : pageType.value}\n` | ||||||
|  |    | ||||||
|  |   // 添加内容板块 | ||||||
|  |   fullPrompt += '主要内容板块: ' | ||||||
|  |   const blocks = contentBlocks.value.filter(block => block !== '其他') | ||||||
|  |   if (contentBlocksCustom.value) { | ||||||
|  |     blocks.push(contentBlocksCustom.value) | ||||||
|  |   } | ||||||
|  |   fullPrompt += blocks.join(', ') + '\n' | ||||||
|  |    | ||||||
|  |   // 添加风格偏好 | ||||||
|  |   fullPrompt += `风格偏好: ${stylePreference.value === '其他' ? stylePreferenceCustom.value : stylePreference.value}\n` | ||||||
|  |    | ||||||
|  |   // 添加设计布局 | ||||||
|  |   fullPrompt += `设计布局: ${layoutDesign.value === '其他' ? layoutDesignCustom.value : layoutDesign.value}\n` | ||||||
|  |    | ||||||
|  |   // 添加配色方案 | ||||||
|  |   fullPrompt += `配色方案: ${colorScheme.value === '其他' ? colorSchemeCustom.value : colorScheme.value}\n` | ||||||
|  |    | ||||||
|  |   // 添加用户的详细描述 | ||||||
|  |   if (prompt.value) { | ||||||
|  |     fullPrompt += `\n详细描述: ${prompt.value}` | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   const res = await createWeb({web: fullPrompt, command: 'createWeb'}) | ||||||
|  |   if (res.code === 0) { | ||||||
|  |     outPut.value = true | ||||||
|  |     // 添加返回的Vue组件代码到数组 | ||||||
|  |     htmlFromLLM.value = res.data | ||||||
|  |     // 加载新生成的组件 | ||||||
|  |     await loadVueComponent(res.data) | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const placeholder = ref(`补充您对页面的其他要求或特殊需求,例如:特别强调的元素、参考网站、交互效果等。`) | ||||||
|  | </script> | ||||||
| @@ -123,6 +123,8 @@ | |||||||
|       callback(new Error('不能为中文')) |       callback(new Error('不能为中文')) | ||||||
|     } else if (/^\d+$/.test(value[0])) { |     } else if (/^\d+$/.test(value[0])) { | ||||||
|       callback(new Error('不能够以数字开头')) |       callback(new Error('不能够以数字开头')) | ||||||
|  |     } else if (!/^[a-zA-Z0-9_]+$/.test(value)) { | ||||||
|  |       callback(new Error('只能包含英文字母、数字和下划线')) | ||||||
|     } else { |     } else { | ||||||
|       callback() |       callback() | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -509,7 +509,15 @@ JOINS模式下不支持导入 | |||||||
|     { |     { | ||||||
|       label: 'NOT BETWEEN', |       label: 'NOT BETWEEN', | ||||||
|       value: 'NOT BETWEEN' |       value: 'NOT BETWEEN' | ||||||
|     } |     }, | ||||||
|  |     { | ||||||
|  |       label: 'IN', | ||||||
|  |       value: 'IN' | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       label: 'NOT IN', | ||||||
|  |       value: 'NOT IN' | ||||||
|  |     }, | ||||||
|   ]) |   ]) | ||||||
|  |  | ||||||
|   const addCondition = () => { |   const addCondition = () => { | ||||||
|   | |||||||
| @@ -202,6 +202,9 @@ | |||||||
|           <el-form-item label="是否为ssl"> |           <el-form-item label="是否为ssl"> | ||||||
|             <el-switch v-model="config.email['is-ssl']" /> |             <el-switch v-model="config.email['is-ssl']" /> | ||||||
|           </el-form-item> |           </el-form-item> | ||||||
|  |           <el-form-item label="是否LoginAuth认证"> | ||||||
|  |             <el-switch v-model="config.email['is-loginauth']" /> | ||||||
|  |           </el-form-item> | ||||||
|           <el-form-item label="secret"> |           <el-form-item label="secret"> | ||||||
|             <el-input |             <el-input | ||||||
|               v-model.trim="config.email.secret" |               v-model.trim="config.email.secret" | ||||||
| @@ -938,7 +941,7 @@ | |||||||
|     </el-form> |     </el-form> | ||||||
|     <div class="mt-4"> |     <div class="mt-4"> | ||||||
|       <el-button type="primary" @click="update">立即更新 </el-button> |       <el-button type="primary" @click="update">立即更新 </el-button> | ||||||
|       <el-button type="primary" @click="reload">重启服务 </el-button> |       <el-button type="primary" @click="reload">重载服务 </el-button> | ||||||
|     </div> |     </div> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
| @@ -1010,7 +1013,7 @@ | |||||||
|   } |   } | ||||||
|   initForm() |   initForm() | ||||||
|   const reload = () => { |   const reload = () => { | ||||||
|     ElMessageBox.confirm('确定要重启服务?', '警告', { |     ElMessageBox.confirm('确定要重载服务?', '警告', { | ||||||
|       confirmButtonText: '确定', |       confirmButtonText: '确定', | ||||||
|       cancelButtonText: '取消', |       cancelButtonText: '取消', | ||||||
|       type: 'warning' |       type: 'warning' | ||||||
|   | |||||||
							
								
								
									
										991
									
								
								src/view/systemTools/version/version.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										991
									
								
								src/view/systemTools/version/version.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,991 @@ | |||||||
|  | <template> | ||||||
|  |   <div> | ||||||
|  |     <div class="gva-search-box"> | ||||||
|  |       <el-form ref="elSearchFormRef" :inline="true" :model="searchInfo" class="demo-form-inline" | ||||||
|  |         @keyup.enter="onSubmit"> | ||||||
|  |         <el-form-item label="创建日期" prop="createdAtRange"> | ||||||
|  |           <template #label> | ||||||
|  |             <span> | ||||||
|  |               创建日期 | ||||||
|  |               <el-tooltip content="搜索范围是开始日期(包含)至结束日期(不包含)"> | ||||||
|  |                 <el-icon> | ||||||
|  |                   <QuestionFilled /> | ||||||
|  |                 </el-icon> | ||||||
|  |               </el-tooltip> | ||||||
|  |             </span> | ||||||
|  |           </template> | ||||||
|  |  | ||||||
|  |           <el-date-picker v-model="searchInfo.createdAtRange" class="w-[380px]" type="datetimerange" range-separator="至" | ||||||
|  |             start-placeholder="开始时间" end-placeholder="结束时间" /> | ||||||
|  |         </el-form-item> | ||||||
|  |  | ||||||
|  |         <el-form-item label="版本名称" prop="versionName"> | ||||||
|  |           <el-input v-model="searchInfo.versionName" placeholder="搜索条件" /> | ||||||
|  |         </el-form-item> | ||||||
|  |  | ||||||
|  |         <el-form-item label="版本号" prop="versionCode"> | ||||||
|  |           <el-input v-model="searchInfo.versionCode" placeholder="搜索条件" /> | ||||||
|  |         </el-form-item> | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |         <template v-if="showAllQuery"> | ||||||
|  |           <!-- 将需要控制显示状态的查询条件添加到此范围内 --> | ||||||
|  |         </template> | ||||||
|  |  | ||||||
|  |         <el-form-item> | ||||||
|  |           <el-button type="primary" icon="search" @click="onSubmit">查询</el-button> | ||||||
|  |           <el-button icon="refresh" @click="onReset">重置</el-button> | ||||||
|  |           <el-button link type="primary" icon="arrow-down" @click="showAllQuery = true" | ||||||
|  |             v-if="!showAllQuery">展开</el-button> | ||||||
|  |           <el-button link type="primary" icon="arrow-up" @click="showAllQuery = false" v-else>收起</el-button> | ||||||
|  |         </el-form-item> | ||||||
|  |       </el-form> | ||||||
|  |     </div> | ||||||
|  |     <div class="gva-table-box"> | ||||||
|  |       <div class="gva-btn-list"> | ||||||
|  |         <el-button type="success" icon="download" @click="openExportDialog">创建发版</el-button> | ||||||
|  |         <el-button type="warning" icon="upload" @click="openImportDialog">导入版本</el-button> | ||||||
|  |         <el-button icon="delete" style="margin-left: 10px;" :disabled="!multipleSelection.length" | ||||||
|  |           @click="onDelete">删除</el-button> | ||||||
|  |       </div> | ||||||
|  |       <el-table ref="multipleTable" style="width: 100%" tooltip-effect="dark" :data="tableData" row-key="ID" | ||||||
|  |         @selection-change="handleSelectionChange"> | ||||||
|  |         <el-table-column type="selection" width="55" /> | ||||||
|  |  | ||||||
|  |         <el-table-column sortable align="left" label="日期" prop="CreatedAt" width="180"> | ||||||
|  |           <template #default="scope">{{ formatDate(scope.row.CreatedAt) }}</template> | ||||||
|  |         </el-table-column> | ||||||
|  |  | ||||||
|  |         <el-table-column align="left" label="版本名称" prop="versionName" width="120" /> | ||||||
|  |  | ||||||
|  |         <el-table-column align="left" label="版本号" prop="versionCode" width="120" /> | ||||||
|  |  | ||||||
|  |         <el-table-column align="left" label="操作" fixed="right" min-width="320"> | ||||||
|  |           <template #default="scope"> | ||||||
|  |             <el-button type="primary" link class="table-button" @click="getDetails(scope.row)"><el-icon | ||||||
|  |                 style="margin-right: 5px"> | ||||||
|  |                 <InfoFilled /> | ||||||
|  |               </el-icon>查看</el-button> | ||||||
|  |             <el-button type="success" link icon="download" class="table-button" | ||||||
|  |               @click="downloadJson(scope.row)">下载发版包</el-button> | ||||||
|  |             <el-button type="primary" link icon="delete" @click="deleteRow(scope.row)">删除</el-button> | ||||||
|  |           </template> | ||||||
|  |         </el-table-column> | ||||||
|  |       </el-table> | ||||||
|  |       <div class="gva-pagination"> | ||||||
|  |         <el-pagination layout="total, sizes, prev, pager, next, jumper" :current-page="page" :page-size="pageSize" | ||||||
|  |           :page-sizes="[10, 30, 50, 100]" :total="total" @current-change="handleCurrentChange" | ||||||
|  |           @size-change="handleSizeChange" /> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <el-drawer destroy-on-close :size="appStore.drawerSize" v-model="detailShow" :show-close="true" | ||||||
|  |       :before-close="closeDetailShow" title="查看"> | ||||||
|  |       <el-descriptions :column="1" border> | ||||||
|  |         <el-descriptions-item label="版本名称"> | ||||||
|  |           {{ detailForm.versionName }} | ||||||
|  |         </el-descriptions-item> | ||||||
|  |         <el-descriptions-item label="版本号"> | ||||||
|  |           {{ detailForm.versionCode }} | ||||||
|  |         </el-descriptions-item> | ||||||
|  |         <el-descriptions-item label="版本描述"> | ||||||
|  |           {{ detailForm.description }} | ||||||
|  |         </el-descriptions-item> | ||||||
|  |       </el-descriptions> | ||||||
|  |     </el-drawer> | ||||||
|  |  | ||||||
|  |     <!-- 导出版本抽屉 --> | ||||||
|  |     <el-drawer v-model="exportDialogVisible" title="创建发版" direction="rtl" size="80%" :before-close="closeExportDialog" :show-close="false"> | ||||||
|  |       <template #header> | ||||||
|  |         <div class="flex justify-between items-center"> | ||||||
|  |           <span class="text-lg">创建发版</span> | ||||||
|  |           <div> | ||||||
|  |             <el-button @click="closeExportDialog">取消</el-button> | ||||||
|  |             <el-button type="primary" @click="handleExport" :loading="exportLoading">创建发版</el-button> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |       </template> | ||||||
|  |       <el-form :model="exportForm" label-width="100px"> | ||||||
|  |         <el-form-item label="版本名称" required> | ||||||
|  |           <el-input v-model="exportForm.versionName" placeholder="请输入版本名称" /> | ||||||
|  |         </el-form-item> | ||||||
|  |         <el-form-item label="版本号" required> | ||||||
|  |           <el-input v-model="exportForm.versionCode" placeholder="请输入版本号" /> | ||||||
|  |         </el-form-item> | ||||||
|  |         <el-form-item label="版本描述"> | ||||||
|  |           <el-input v-model="exportForm.description" type="textarea" placeholder="请输入版本描述" /> | ||||||
|  |         </el-form-item> | ||||||
|  |         <el-form-item label="发版信息"> | ||||||
|  |           <div class="flex gap-3 w-full"> | ||||||
|  |             <!-- 菜单选择 --> | ||||||
|  |             <div class="flex flex-col border border-gray-300 rounded overflow-hidden h-full flex-1 w-1/3"> | ||||||
|  |               <div class="flex justify-between items-center px-4 py-3 bg-gray-50 border-b border-gray-300"> | ||||||
|  |                 <span class="m-0 text-gray-800 text-base font-medium">选择菜单</span> | ||||||
|  |               </div> | ||||||
|  |               <div class="px-4 py-3 border-b border-gray-300 bg-gray-50"> | ||||||
|  |                 <el-input v-model="menuFilterText" placeholder="输入关键字进行过滤" clearable size="small" /> | ||||||
|  |               </div> | ||||||
|  |               <div class="flex-1 p-2 min-h-[300px] max-h-[400px] overflow-y-auto"> | ||||||
|  |                 <el-tree ref="menuTreeRef" :data="menuTreeData" :default-checked-keys="selectedMenuIds" | ||||||
|  |                   :props="menuTreeProps" default-expand-all highlight-current node-key="ID" show-checkbox | ||||||
|  |                   :filter-node-method="filterMenuNode" @check="onMenuCheck" class="menu-tree"> | ||||||
|  |                   <template #default="{ node }"> | ||||||
|  |                     <span class="flex-1 flex items-center justify-between text-sm pr-2"> | ||||||
|  |                       <span>{{ node.label }}</span> | ||||||
|  |                     </span> | ||||||
|  |                   </template> | ||||||
|  |                 </el-tree> | ||||||
|  |               </div> | ||||||
|  |             </div> | ||||||
|  |  | ||||||
|  |             <!-- API选择 --> | ||||||
|  |             <div class="flex flex-col border border-gray-300 rounded overflow-hidden h-full flex-1 w-1/3"> | ||||||
|  |               <div class="flex justify-between items-center px-4 py-3 bg-gray-50 border-b border-gray-300"> | ||||||
|  |                 <span class="m-0 text-gray-800 text-base font-medium">选择API</span> | ||||||
|  |               </div> | ||||||
|  |               <div class="px-4 py-3 border-b border-gray-300 bg-gray-50"> | ||||||
|  |                 <el-input v-model="apiFilterTextName" placeholder="按名称过滤" clearable size="small" | ||||||
|  |                   style="margin-bottom: 8px" /> | ||||||
|  |                 <el-input v-model="apiFilterTextPath" placeholder="按路径过滤" clearable size="small" /> | ||||||
|  |               </div> | ||||||
|  |               <div class="flex-1 p-2 min-h-[300px] max-h-[400px] overflow-y-auto"> | ||||||
|  |                 <el-tree ref="apiTreeRef" :data="apiTreeData" :default-checked-keys="selectedApiIds" | ||||||
|  |                   :props="apiTreeProps" default-expand-all highlight-current node-key="onlyId" show-checkbox | ||||||
|  |                   :filter-node-method="filterApiNode" @check="onApiCheck" class="api-tree"> | ||||||
|  |                   <template #default="{ data }"> | ||||||
|  |                     <div class="flex items-center justify-between w-full pr-1"> | ||||||
|  |                       <span>{{ data.description }}</span> | ||||||
|  |                       <el-tooltip :content="data.path"> | ||||||
|  |                         <span class="max-w-[240px] break-all overflow-ellipsis overflow-hidden"> | ||||||
|  |                           {{ data.path }} | ||||||
|  |                         </span> | ||||||
|  |                       </el-tooltip> | ||||||
|  |                     </div> | ||||||
|  |                   </template> | ||||||
|  |                 </el-tree> | ||||||
|  |               </div> | ||||||
|  |             </div> | ||||||
|  |  | ||||||
|  |             <!-- 字典选择 --> | ||||||
|  |             <div class="flex flex-col border border-gray-300 rounded overflow-hidden h-full flex-1 w-1/3"> | ||||||
|  |               <div class="flex justify-between items-center px-4 py-3 bg-gray-50 border-b border-gray-300"> | ||||||
|  |                 <span class="m-0 text-gray-800 text-base font-medium">选择字典</span> | ||||||
|  |               </div> | ||||||
|  |               <div class="px-4 py-3 border-b border-gray-300 bg-gray-50"> | ||||||
|  |                 <el-input v-model="dictFilterText" placeholder="输入关键字进行过滤" clearable size="small" /> | ||||||
|  |               </div> | ||||||
|  |               <div class="flex-1 p-2 min-h-[300px] max-h-[400px] overflow-y-auto"> | ||||||
|  |                 <el-tree ref="dictTreeRef" :data="dictTreeData" :default-checked-keys="selectedDictIds" | ||||||
|  |                   :props="dictTreeProps" default-expand-all highlight-current node-key="ID" show-checkbox | ||||||
|  |                   :filter-node-method="filterDictNode" @check="onDictCheck" class="dict-tree"> | ||||||
|  |                   <template #default="{ data }"> | ||||||
|  |                     <div class="flex items-center justify-between w-full pr-1"> | ||||||
|  |                       <span>{{ data.name || data.label }}</span> | ||||||
|  |                       <el-tooltip :content="data.desc || (data.value ? `值: ${data.value}` : '')"> | ||||||
|  |                         <span class="text-gray-500 text-xs ml-2"> | ||||||
|  |                           {{ data.type || (data.value ? `值: ${data.value}` : '') }} | ||||||
|  |                         </span> | ||||||
|  |                       </el-tooltip> | ||||||
|  |                     </div> | ||||||
|  |                   </template> | ||||||
|  |                 </el-tree> | ||||||
|  |               </div> | ||||||
|  |             </div> | ||||||
|  |           </div> | ||||||
|  |         </el-form-item> | ||||||
|  |       </el-form> | ||||||
|  |     </el-drawer> | ||||||
|  |  | ||||||
|  |     <!-- 导入版本抽屉 --> | ||||||
|  |     <el-drawer v-model="importDialogVisible" title="导入版本" direction="rtl" size="80%" :before-close="closeImportDialog" :show-close="false"> | ||||||
|  |       <template #header> | ||||||
|  |         <div class="flex justify-between items-center"> | ||||||
|  |           <span class="text-lg">导入版本</span> | ||||||
|  |           <div> | ||||||
|  |             <el-button @click="closeImportDialog">取消</el-button> | ||||||
|  |             <el-button type="primary" @click="handleImport" :loading="importLoading" | ||||||
|  |               :disabled="!importJsonContent.trim()">导入</el-button> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |       </template> | ||||||
|  |       <el-form label-width="100px"> | ||||||
|  |         <el-form-item label="上传文件"> | ||||||
|  |           <el-upload | ||||||
|  |             ref="uploadRef" | ||||||
|  |             :auto-upload="false" | ||||||
|  |             :show-file-list="true" | ||||||
|  |             :limit="1" | ||||||
|  |             accept=".json" | ||||||
|  |             :on-change="handleFileChange" | ||||||
|  |             :on-remove="handleFileRemove" | ||||||
|  |             drag | ||||||
|  |           > | ||||||
|  |             <el-icon class="el-icon--upload"><upload-filled /></el-icon> | ||||||
|  |             <div class="el-upload__text"> | ||||||
|  |               将JSON文件拖到此处,或<em>点击上传</em> | ||||||
|  |             </div> | ||||||
|  |             <template #tip> | ||||||
|  |               <div class="el-upload__tip"> | ||||||
|  |                 只能上传JSON文件 | ||||||
|  |               </div> | ||||||
|  |             </template> | ||||||
|  |           </el-upload> | ||||||
|  |         </el-form-item> | ||||||
|  |         <el-form-item label="版本JSON"> | ||||||
|  |           <el-input v-model="importJsonContent" type="textarea" :rows="20" placeholder="请粘贴版本JSON" | ||||||
|  |             @input="handleJsonContentChange" /> | ||||||
|  |         </el-form-item> | ||||||
|  |         <el-form-item label="预览内容" v-if="importPreviewData"> | ||||||
|  |           <div class="flex flex-col flex-1 gap-4 border border-gray-300 rounded p-4 bg-gray-50"> | ||||||
|  |             <div class="flex gap-3 w-full"> | ||||||
|  |               <div class="border border-gray-300 rounded overflow-hidden flex-1 w-1/3"> | ||||||
|  |                 <div class="flex flex-col border border-gray-300 rounded overflow-hidden h-full"> | ||||||
|  |                   <div class="flex justify-between items-center px-4 py-3 bg-gray-50 border-b border-gray-300"> | ||||||
|  |                     <h3 class="m-0 text-gray-800 text-base font-medium">菜单 ({{ getTotalMenuCount() }}项)</h3> | ||||||
|  |                   </div> | ||||||
|  |                   <div class="flex-1 p-2 min-h-[300px] max-h-[400px] overflow-y-auto"> | ||||||
|  |                     <el-tree | ||||||
|  |                       :data="previewMenuTreeData" | ||||||
|  |                       :props="menuTreeProps" | ||||||
|  |                       node-key="name" | ||||||
|  |                       :expand-on-click-node="false" | ||||||
|  |                       :check-on-click-node="false" | ||||||
|  |                       :show-checkbox="false" | ||||||
|  |                       default-expand-all | ||||||
|  |                     > | ||||||
|  |                       <template #default="{ data }"> | ||||||
|  |                         <div class="flex-1 flex items-center justify-between text-sm pr-2"> | ||||||
|  |                           <span>{{ data.meta?.title || data.title }}</span> | ||||||
|  |                           <span class="text-gray-500 text-xs ml-2">{{ data.path }}</span> | ||||||
|  |                         </div> | ||||||
|  |                       </template> | ||||||
|  |                     </el-tree> | ||||||
|  |                   </div> | ||||||
|  |                 </div> | ||||||
|  |               </div> | ||||||
|  |               <div class="border border-gray-300 rounded overflow-hidden flex-1 w-1/3"> | ||||||
|  |                 <div class="flex flex-col border border-gray-300 rounded overflow-hidden h-full"> | ||||||
|  |                   <div class="flex justify-between items-center px-4 py-3 bg-gray-50 border-b border-gray-300"> | ||||||
|  |                     <h3 class="m-0 text-gray-800 text-base font-medium">API ({{ importPreviewData.apis?.length || 0 }}项)</h3> | ||||||
|  |                   </div> | ||||||
|  |                   <div class="flex-1 p-2 min-h-[300px] max-h-[400px] overflow-y-auto"> | ||||||
|  |                     <el-tree | ||||||
|  |                       :data="previewApiTreeData" | ||||||
|  |                       :props="apiTreeProps" | ||||||
|  |                       node-key="ID" | ||||||
|  |                       :expand-on-click-node="false" | ||||||
|  |                       :check-on-click-node="false" | ||||||
|  |                       :show-checkbox="false" | ||||||
|  |                       default-expand-all | ||||||
|  |                     > | ||||||
|  |                       <template #default="{ data }"> | ||||||
|  |                         <div class="flex-1 flex items-center justify-between text-sm pr-2"> | ||||||
|  |                           <span>{{ data.description }}</span> | ||||||
|  |                           <span class="text-gray-500 text-xs ml-2">{{ data.path }} [{{ data.method }}]</span> | ||||||
|  |                         </div> | ||||||
|  |                       </template> | ||||||
|  |                     </el-tree> | ||||||
|  |                   </div> | ||||||
|  |                 </div> | ||||||
|  |               </div> | ||||||
|  |               <div class="border border-gray-300 rounded overflow-hidden flex-1 w-1/3"> | ||||||
|  |                 <div class="flex flex-col border border-gray-300 rounded overflow-hidden h-full"> | ||||||
|  |                   <div class="flex justify-between items-center px-4 py-3 bg-gray-50 border-b border-gray-300"> | ||||||
|  |                     <h3 class="m-0 text-gray-800 text-base font-medium">字典 ({{ importPreviewData.dictionaries?.length || 0 }}项)</h3> | ||||||
|  |                   </div> | ||||||
|  |                   <div class="flex-1 p-2 min-h-[300px] max-h-[400px] overflow-y-auto"> | ||||||
|  |                     <el-tree | ||||||
|  |                       :data="previewDictTreeData" | ||||||
|  |                       :props="dictTreeProps" | ||||||
|  |                       node-key="ID" | ||||||
|  |                       :expand-on-click-node="false" | ||||||
|  |                       :check-on-click-node="false" | ||||||
|  |                       :show-checkbox="false" | ||||||
|  |                       default-expand-all | ||||||
|  |                     > | ||||||
|  |                       <template #default="{ data }"> | ||||||
|  |                         <div class="flex-1 flex items-center justify-between text-sm pr-2"> | ||||||
|  |                           <span>{{ data.name || data.label }}</span> | ||||||
|  |                           <span class="text-gray-500 text-xs ml-2"> | ||||||
|  |                             {{ data.type || (data.value ? `值: ${data.value}` : '') }} | ||||||
|  |                           </span> | ||||||
|  |                         </div> | ||||||
|  |                       </template> | ||||||
|  |                     </el-tree> | ||||||
|  |                   </div> | ||||||
|  |                 </div> | ||||||
|  |               </div> | ||||||
|  |             </div> | ||||||
|  |           </div> | ||||||
|  |         </el-form-item> | ||||||
|  |       </el-form> | ||||||
|  |     </el-drawer> | ||||||
|  |  | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup> | ||||||
|  | import { | ||||||
|  |   deleteSysVersion, | ||||||
|  |   deleteSysVersionByIds, | ||||||
|  |   findSysVersion, | ||||||
|  |   getSysVersionList, | ||||||
|  |   exportVersion, | ||||||
|  |   importVersion, | ||||||
|  |   downloadVersionJson | ||||||
|  | } from '@/api/version' | ||||||
|  |  | ||||||
|  | // 导入菜单和API相关接口 | ||||||
|  | import { getMenuList } from '@/api/menu' | ||||||
|  | import { getApiList } from '@/api/api' | ||||||
|  | import { getSysDictionaryList } from '@/api/sysDictionary' | ||||||
|  |  | ||||||
|  | // 全量引入格式化工具 请按需保留 | ||||||
|  | import { formatDate } from '@/utils/format' | ||||||
|  | import { ElMessage, ElMessageBox } from 'element-plus' | ||||||
|  | import { UploadFilled } from '@element-plus/icons-vue' | ||||||
|  | import { ref, watch } from 'vue' | ||||||
|  | import { useAppStore } from "@/pinia" | ||||||
|  |  | ||||||
|  | defineOptions({ | ||||||
|  |   name: 'SysVersion' | ||||||
|  | }) | ||||||
|  |  | ||||||
|  | const appStore = useAppStore() | ||||||
|  |  | ||||||
|  | // 控制更多查询条件显示/隐藏状态 | ||||||
|  | const showAllQuery = ref(false) | ||||||
|  |  | ||||||
|  | // 导出相关数据 | ||||||
|  | const exportDialogVisible = ref(false) | ||||||
|  | const exportLoading = ref(false) | ||||||
|  | const exportForm = ref({ | ||||||
|  |   versionName: '', | ||||||
|  |   versionCode: '', | ||||||
|  |   description: '', | ||||||
|  |   menuIds: [], | ||||||
|  |   apiIds: [], | ||||||
|  |   dictIds: [] | ||||||
|  | }) | ||||||
|  |  | ||||||
|  | // 树形结构相关数据 | ||||||
|  | const menuTreeData = ref([]) | ||||||
|  | const apiTreeData = ref([]) | ||||||
|  | const dictTreeData = ref([]) | ||||||
|  | const selectedMenuIds = ref([]) | ||||||
|  | const selectedApiIds = ref([]) | ||||||
|  | const selectedDictIds = ref([]) | ||||||
|  | const menuFilterText = ref('') | ||||||
|  | const apiFilterTextName = ref('') | ||||||
|  | const apiFilterTextPath = ref('') | ||||||
|  | const dictFilterText = ref('') | ||||||
|  |  | ||||||
|  | // 树形组件引用 | ||||||
|  | const menuTreeRef = ref(null) | ||||||
|  | const apiTreeRef = ref(null) | ||||||
|  | const dictTreeRef = ref(null) | ||||||
|  |  | ||||||
|  | // 树形属性配置 | ||||||
|  | const menuTreeProps = ref({ | ||||||
|  |   children: 'children', | ||||||
|  |   label: function (data) { | ||||||
|  |     return data.meta?.title || data.title | ||||||
|  |   } | ||||||
|  | }) | ||||||
|  |  | ||||||
|  | const apiTreeProps = ref({ | ||||||
|  |   children: 'children', | ||||||
|  |   label: 'description' | ||||||
|  | }) | ||||||
|  |  | ||||||
|  | const dictTreeProps = ref({ | ||||||
|  |   children: 'sysDictionaryDetails', | ||||||
|  |   label: function (data) { | ||||||
|  |     // 如果是字典主项,显示字典名称 | ||||||
|  |     if (data.name) { | ||||||
|  |       return data.name | ||||||
|  |     } | ||||||
|  |     // 如果是字典详情项,显示标签 | ||||||
|  |     if (data.label) { | ||||||
|  |       return data.label | ||||||
|  |     } | ||||||
|  |     return '未知项' | ||||||
|  |   } | ||||||
|  | }) | ||||||
|  |  | ||||||
|  | // 导入相关数据 | ||||||
|  | const importDialogVisible = ref(false) | ||||||
|  | const importLoading = ref(false) | ||||||
|  | const importJsonContent = ref('') | ||||||
|  | const importPreviewData = ref(null) | ||||||
|  | const uploadRef = ref(null) | ||||||
|  | const previewMenuTreeData = ref([]) | ||||||
|  | const previewApiTreeData = ref([]) | ||||||
|  | const previewDictTreeData = ref([]) | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | const elSearchFormRef = ref() | ||||||
|  |  | ||||||
|  | // =========== 表格控制部分 =========== | ||||||
|  | const page = ref(1) | ||||||
|  | const total = ref(0) | ||||||
|  | const pageSize = ref(10) | ||||||
|  | const tableData = ref([]) | ||||||
|  | const searchInfo = ref({}) | ||||||
|  | // 重置 | ||||||
|  | const onReset = () => { | ||||||
|  |   searchInfo.value = {} | ||||||
|  |   getTableData() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // 搜索 | ||||||
|  | const onSubmit = () => { | ||||||
|  |   elSearchFormRef.value?.validate(async (valid) => { | ||||||
|  |     if (!valid) return | ||||||
|  |     page.value = 1 | ||||||
|  |     getTableData() | ||||||
|  |   }) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // 分页 | ||||||
|  | const handleSizeChange = (val) => { | ||||||
|  |   pageSize.value = val | ||||||
|  |   getTableData() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // 修改页面容量 | ||||||
|  | const handleCurrentChange = (val) => { | ||||||
|  |   page.value = val | ||||||
|  |   getTableData() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // 查询 | ||||||
|  | const getTableData = async () => { | ||||||
|  |   const table = await getSysVersionList({ page: page.value, pageSize: pageSize.value, ...searchInfo.value }) | ||||||
|  |   if (table.code === 0) { | ||||||
|  |     tableData.value = table.data.list | ||||||
|  |     total.value = table.data.total | ||||||
|  |     page.value = table.data.page | ||||||
|  |     pageSize.value = table.data.pageSize | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | getTableData() | ||||||
|  |  | ||||||
|  | // ============== 表格控制部分结束 =============== | ||||||
|  |  | ||||||
|  | // 多选数据 | ||||||
|  | const multipleSelection = ref([]) | ||||||
|  | // 多选 | ||||||
|  | const handleSelectionChange = (val) => { | ||||||
|  |   multipleSelection.value = val | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // 删除行 | ||||||
|  | const deleteRow = (row) => { | ||||||
|  |   ElMessageBox.confirm('确定要删除吗?', '提示', { | ||||||
|  |     confirmButtonText: '确定', | ||||||
|  |     cancelButtonText: '取消', | ||||||
|  |     type: 'warning' | ||||||
|  |   }).then(() => { | ||||||
|  |     deleteSysVersionFunc(row) | ||||||
|  |   }) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // 多选删除 | ||||||
|  | const onDelete = async () => { | ||||||
|  |   ElMessageBox.confirm('确定要删除吗?', '提示', { | ||||||
|  |     confirmButtonText: '确定', | ||||||
|  |     cancelButtonText: '取消', | ||||||
|  |     type: 'warning' | ||||||
|  |   }).then(async () => { | ||||||
|  |     const IDs = [] | ||||||
|  |     if (multipleSelection.value.length === 0) { | ||||||
|  |       ElMessage({ | ||||||
|  |         type: 'warning', | ||||||
|  |         message: '请选择要删除的数据' | ||||||
|  |       }) | ||||||
|  |       return | ||||||
|  |     } | ||||||
|  |     multipleSelection.value && | ||||||
|  |       multipleSelection.value.map(item => { | ||||||
|  |         IDs.push(item.ID) | ||||||
|  |       }) | ||||||
|  |     const res = await deleteSysVersionByIds({ IDs }) | ||||||
|  |     if (res.code === 0) { | ||||||
|  |       ElMessage({ | ||||||
|  |         type: 'success', | ||||||
|  |         message: '删除成功' | ||||||
|  |       }) | ||||||
|  |       if (tableData.value.length === IDs.length && page.value > 1) { | ||||||
|  |         page.value-- | ||||||
|  |       } | ||||||
|  |       getTableData() | ||||||
|  |     } | ||||||
|  |   }) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // 删除行 | ||||||
|  | const deleteSysVersionFunc = async (row) => { | ||||||
|  |   const res = await deleteSysVersion({ ID: row.ID }) | ||||||
|  |   if (res.code === 0) { | ||||||
|  |     ElMessage({ | ||||||
|  |       type: 'success', | ||||||
|  |       message: '删除成功' | ||||||
|  |     }) | ||||||
|  |     if (tableData.value.length === 1 && page.value > 1) { | ||||||
|  |       page.value-- | ||||||
|  |     } | ||||||
|  |     getTableData() | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const detailForm = ref({}) | ||||||
|  |  | ||||||
|  | // 查看详情控制标记 | ||||||
|  | const detailShow = ref(false) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | // 打开详情弹窗 | ||||||
|  | const openDetailShow = () => { | ||||||
|  |   detailShow.value = true | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | // 打开详情 | ||||||
|  | const getDetails = async (row) => { | ||||||
|  |   // 打开弹窗 | ||||||
|  |   const res = await findSysVersion({ ID: row.ID }) | ||||||
|  |   if (res.code === 0) { | ||||||
|  |     detailForm.value = res.data | ||||||
|  |     openDetailShow() | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | // 关闭详情弹窗 | ||||||
|  | const closeDetailShow = () => { | ||||||
|  |   detailShow.value = false | ||||||
|  |   detailForm.value = {} | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | // 获取菜单和API列表 | ||||||
|  | const getMenuAndApiList = async () => { | ||||||
|  |   try { | ||||||
|  |     // 获取菜单列表 | ||||||
|  |     const menuRes = await getMenuList() | ||||||
|  |     if (menuRes.code === 0) { | ||||||
|  |       menuTreeData.value = menuRes.data || [] | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // 获取API列表 | ||||||
|  |     const apiRes = await getApiList({ page: 1, pageSize: 9999 }) | ||||||
|  |     if (apiRes.code === 0) { | ||||||
|  |       console.log('原始API数据:', apiRes.data) | ||||||
|  |       const apis = apiRes.data.list || [] | ||||||
|  |       apiTreeData.value = buildApiTree(apis) | ||||||
|  |     } | ||||||
|  |   } catch (error) { | ||||||
|  |     console.error('获取数据失败:', error) | ||||||
|  |     ElMessage.error('获取菜单或API数据失败') | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // 获取字典列表 | ||||||
|  | const getDictList = async () => { | ||||||
|  |   try { | ||||||
|  |     const dictRes = await getSysDictionaryList({ page: 1, pageSize: 9999 }) | ||||||
|  |     if (dictRes.code === 0) { | ||||||
|  |       dictTreeData.value = dictRes.data || [] | ||||||
|  |     } | ||||||
|  |   } catch (error) { | ||||||
|  |     console.error('获取字典数据失败:', error) | ||||||
|  |     ElMessage.error('获取字典数据失败') | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // 构建API树形结构 | ||||||
|  | const buildApiTree = (apis) => { | ||||||
|  |   const apiObj = {} | ||||||
|  |   apis.forEach((item) => { | ||||||
|  |     item.onlyId = 'p:' + item.path + 'm:' + item.method | ||||||
|  |     if (Object.prototype.hasOwnProperty.call(apiObj, item.apiGroup)) { | ||||||
|  |       apiObj[item.apiGroup].push(item) | ||||||
|  |     } else { | ||||||
|  |       Object.assign(apiObj, { [item.apiGroup]: [item] }) | ||||||
|  |     } | ||||||
|  |   }) | ||||||
|  |   const apiTree = [] | ||||||
|  |   for (const key in apiObj) { | ||||||
|  |     const treeNode = { | ||||||
|  |       ID: key, | ||||||
|  |       description: key + '组', | ||||||
|  |       children: apiObj[key] | ||||||
|  |     } | ||||||
|  |     apiTree.push(treeNode) | ||||||
|  |   } | ||||||
|  |   return apiTree | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // 树形组件事件处理方法 | ||||||
|  | const filterMenuNode = (value, data) => { | ||||||
|  |   if (!value) return true | ||||||
|  |   const title = data.meta?.title || data.title || '' | ||||||
|  |   return title.indexOf(value) !== -1 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const filterApiNode = (value, data) => { | ||||||
|  |   if (!apiFilterTextName.value && !apiFilterTextPath.value) return true | ||||||
|  |   let matchesName, matchesPath | ||||||
|  |   if (!apiFilterTextName.value) { | ||||||
|  |     matchesName = true | ||||||
|  |   } else { | ||||||
|  |     matchesName = data.description && data.description.includes(apiFilterTextName.value) | ||||||
|  |   } | ||||||
|  |   if (!apiFilterTextPath.value) { | ||||||
|  |     matchesPath = true | ||||||
|  |   } else { | ||||||
|  |     matchesPath = data.path && data.path.includes(apiFilterTextPath.value) | ||||||
|  |   } | ||||||
|  |   return matchesName && matchesPath | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const filterDictNode = (value, data) => { | ||||||
|  |   if (!value) return true | ||||||
|  |   const name = data.name || '' | ||||||
|  |   const type = data.type || '' | ||||||
|  |   const desc = data.desc || '' | ||||||
|  |   const label = data.label || '' | ||||||
|  |   const dataValue = data.value || '' | ||||||
|  |   return name.indexOf(value) !== -1 ||  | ||||||
|  |          type.indexOf(value) !== -1 ||  | ||||||
|  |          desc.indexOf(value) !== -1 ||  | ||||||
|  |          label.indexOf(value) !== -1 ||  | ||||||
|  |          dataValue.indexOf(value) !== -1 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const onMenuCheck = (data, checked) => { | ||||||
|  |   if (checked.checkedKeys) { | ||||||
|  |     selectedMenuIds.value = checked.checkedKeys | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const onApiCheck = (data, checked) => { | ||||||
|  |   if (checked.checkedKeys) { | ||||||
|  |     selectedApiIds.value = checked.checkedKeys | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const onDictCheck = (data, checked) => { | ||||||
|  |   if (checked.checkedKeys) { | ||||||
|  |     selectedDictIds.value = checked.checkedKeys | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // 监听过滤文本变化 | ||||||
|  | watch(menuFilterText, (val) => { | ||||||
|  |   if (menuTreeRef.value) { | ||||||
|  |     menuTreeRef.value.filter(val) | ||||||
|  |   } | ||||||
|  | }) | ||||||
|  |  | ||||||
|  | watch([apiFilterTextName, apiFilterTextPath], () => { | ||||||
|  |   if (apiTreeRef.value) { | ||||||
|  |     apiTreeRef.value.filter('') | ||||||
|  |   } | ||||||
|  | }) | ||||||
|  |  | ||||||
|  | watch(dictFilterText, (val) => { | ||||||
|  |   if (dictTreeRef.value) { | ||||||
|  |     dictTreeRef.value.filter(val) | ||||||
|  |   } | ||||||
|  | }) | ||||||
|  |  | ||||||
|  | // 导出相关方法 | ||||||
|  | const openExportDialog = async () => { | ||||||
|  |   exportDialogVisible.value = true | ||||||
|  |   await getMenuAndApiList() | ||||||
|  |   await getDictList() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const closeExportDialog = () => { | ||||||
|  |   exportDialogVisible.value = false | ||||||
|  |   exportForm.value = { | ||||||
|  |     versionName: '', | ||||||
|  |     versionCode: '', | ||||||
|  |     description: '', | ||||||
|  |     menuIds: [], | ||||||
|  |     apiIds: [], | ||||||
|  |     dictIds: [] | ||||||
|  |   } | ||||||
|  |   selectedMenuIds.value = [] | ||||||
|  |   selectedApiIds.value = [] | ||||||
|  |   selectedDictIds.value = [] | ||||||
|  |   menuFilterText.value = '' | ||||||
|  |   apiFilterTextName.value = '' | ||||||
|  |   apiFilterTextPath.value = '' | ||||||
|  |   dictFilterText.value = '' | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const handleExport = async () => { | ||||||
|  |   if (!exportForm.value.versionName || !exportForm.value.versionCode) { | ||||||
|  |     ElMessage.warning('请填写版本名称和版本号') | ||||||
|  |     return | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   exportLoading.value = true | ||||||
|  |   try { | ||||||
|  |     // 获取选中的菜单、API和字典 | ||||||
|  |     const checkedMenus = menuTreeRef.value ? menuTreeRef.value.getCheckedNodes(false, true) : [] | ||||||
|  |     const checkedApis = apiTreeRef.value ? apiTreeRef.value.getCheckedNodes(true) : [] | ||||||
|  |     const checkedDicts = dictTreeRef.value ? dictTreeRef.value.getCheckedNodes(true) : [] | ||||||
|  |  | ||||||
|  |     const menuIds = checkedMenus.map(menu => menu.ID) | ||||||
|  |     const apiIds = checkedApis.map(api => api.ID) | ||||||
|  |     const dictIds = checkedDicts.map(dict => dict.ID) | ||||||
|  |  | ||||||
|  |     exportForm.value.menuIds = menuIds | ||||||
|  |     exportForm.value.apiIds = apiIds | ||||||
|  |     exportForm.value.dictIds = dictIds | ||||||
|  |  | ||||||
|  |     const res = await exportVersion(exportForm.value) | ||||||
|  |     if (res.code !== 0) { | ||||||
|  |       ElMessage.error(res.msg || '创建发版失败') | ||||||
|  |       return | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     ElMessage.success('创建发版成功') | ||||||
|  |     closeExportDialog() | ||||||
|  |     getTableData() // 刷新表格数据 | ||||||
|  |   } catch (error) { | ||||||
|  |     console.error('创建发版失败:', error) | ||||||
|  |     ElMessage.error('创建发版失败') | ||||||
|  |   } finally { | ||||||
|  |     exportLoading.value = false | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // 导入相关方法 | ||||||
|  | const openImportDialog = () => { | ||||||
|  |   importDialogVisible.value = true | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const closeImportDialog = () => { | ||||||
|  |   importDialogVisible.value = false | ||||||
|  |   importJsonContent.value = '' | ||||||
|  |   importPreviewData.value = null | ||||||
|  |   previewMenuTreeData.value = [] | ||||||
|  |   previewApiTreeData.value = [] | ||||||
|  |   // 清理上传文件 | ||||||
|  |   if (uploadRef.value) { | ||||||
|  |     uploadRef.value.clearFiles() | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // 文件上传处理函数 | ||||||
|  | const handleFileChange = (file) => { | ||||||
|  |   if (!file.raw) return | ||||||
|  |    | ||||||
|  |   // 验证文件类型 | ||||||
|  |   if (!file.name.toLowerCase().endsWith('.json')) { | ||||||
|  |     ElMessage.error('只能上传JSON文件') | ||||||
|  |     uploadRef.value.clearFiles() | ||||||
|  |     return | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   // 读取文件内容 | ||||||
|  |   const reader = new FileReader() | ||||||
|  |   reader.onload = (e) => { | ||||||
|  |     try { | ||||||
|  |       const content = e.target.result | ||||||
|  |       // 验证JSON格式 | ||||||
|  |       JSON.parse(content) | ||||||
|  |       importJsonContent.value = content | ||||||
|  |       handleJsonContentChange() | ||||||
|  |       ElMessage.success('文件上传成功') | ||||||
|  |     } catch (error) { | ||||||
|  |       ElMessage.error('JSON文件格式错误') | ||||||
|  |       uploadRef.value.clearFiles() | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   reader.readAsText(file.raw) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const handleFileRemove = () => { | ||||||
|  |   importJsonContent.value = '' | ||||||
|  |   importPreviewData.value = null | ||||||
|  |   previewMenuTreeData.value = [] | ||||||
|  |   previewApiTreeData.value = [] | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // 计算菜单总数(递归计算所有菜单项) | ||||||
|  | const getTotalMenuCount = () => { | ||||||
|  |   if (!importPreviewData.value?.menus) return 0 | ||||||
|  |    | ||||||
|  |   const countMenus = (menus) => { | ||||||
|  |     let count = 0 | ||||||
|  |     menus.forEach(menu => { | ||||||
|  |       count += 1 // 当前菜单 | ||||||
|  |       if (menu.children && menu.children.length > 0) { | ||||||
|  |         count += countMenus(menu.children) // 递归计算子菜单 | ||||||
|  |       } | ||||||
|  |     }) | ||||||
|  |     return count | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   return countMenus(importPreviewData.value.menus) | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | const handleJsonContentChange = () => { | ||||||
|  |   if (!importJsonContent.value.trim()) { | ||||||
|  |     importPreviewData.value = null | ||||||
|  |     previewMenuTreeData.value = [] | ||||||
|  |     previewApiTreeData.value = [] | ||||||
|  |     previewDictTreeData.value = [] | ||||||
|  |     return | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   try { | ||||||
|  |     const data = JSON.parse(importJsonContent.value) | ||||||
|  |  | ||||||
|  |     // 构建预览数据 | ||||||
|  |     importPreviewData.value = { | ||||||
|  |       menus: data.menus || [], | ||||||
|  |       apis: data.apis || [], | ||||||
|  |       dictionaries: data.dictionaries || [] | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // 直接使用菜单数据,因为它已经是树形结构(包含children字段) | ||||||
|  |     if (data.menus && data.menus.length > 0) { | ||||||
|  |       previewMenuTreeData.value = data.menus | ||||||
|  |     } else { | ||||||
|  |       previewMenuTreeData.value = [] | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // 构建API树形数据(按分组组织) | ||||||
|  |     if (data.apis && data.apis.length > 0) { | ||||||
|  |       const apiGroups = {} | ||||||
|  |       data.apis.forEach(api => { | ||||||
|  |         const group = api.apiGroup || '未分组' | ||||||
|  |         if (!apiGroups[group]) { | ||||||
|  |           apiGroups[group] = { | ||||||
|  |             ID: `group_${group}`, | ||||||
|  |             description: group, | ||||||
|  |             path: '', | ||||||
|  |             method: '', | ||||||
|  |             children: [] | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |         apiGroups[group].children.push(api) | ||||||
|  |       }) | ||||||
|  |       previewApiTreeData.value = Object.values(apiGroups) | ||||||
|  |     } else { | ||||||
|  |       previewApiTreeData.value = [] | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // 处理字典数据 | ||||||
|  |     if (data.dictionaries && data.dictionaries.length > 0) { | ||||||
|  |       previewDictTreeData.value = data.dictionaries | ||||||
|  |     } else { | ||||||
|  |       previewDictTreeData.value = [] | ||||||
|  |     } | ||||||
|  |   } catch (error) { | ||||||
|  |     console.error('JSON解析失败:', error) | ||||||
|  |     importPreviewData.value = null | ||||||
|  |     previewMenuTreeData.value = [] | ||||||
|  |     previewApiTreeData.value = [] | ||||||
|  |     previewDictTreeData.value = [] | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const handleImport = async () => { | ||||||
|  |   if (!importJsonContent.value.trim()) { | ||||||
|  |     ElMessage.warning('请输入版本JSON') | ||||||
|  |     return | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   try { | ||||||
|  |     JSON.parse(importJsonContent.value) | ||||||
|  |   } catch (error) { | ||||||
|  |     ElMessage.error('JSON格式错误,请检查输入内容') | ||||||
|  |     return | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   importLoading.value = true | ||||||
|  |   try { | ||||||
|  |     const data = JSON.parse(importJsonContent.value) | ||||||
|  |     const res = await importVersion(data) | ||||||
|  |     if (res.code === 0) { | ||||||
|  |       ElMessage.success('导入成功') | ||||||
|  |       closeImportDialog() | ||||||
|  |       getTableData() // 刷新表格数据 | ||||||
|  |     } else { | ||||||
|  |       ElMessage.error(res.msg || '导入失败') | ||||||
|  |     } | ||||||
|  |   } catch (error) { | ||||||
|  |     console.error('导入失败:', error) | ||||||
|  |     ElMessage.error('导入失败') | ||||||
|  |   } finally { | ||||||
|  |     importLoading.value = false | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // 下载版本JSON | ||||||
|  | const downloadJson = async (row) => { | ||||||
|  |   try { | ||||||
|  |     const res = await downloadVersionJson({ ID: row.ID }) | ||||||
|  |     // 处理axios响应,获取实际的blob数据 | ||||||
|  |     // 当responseType为blob时,axios拦截器会返回完整的response对象 | ||||||
|  |     let blob | ||||||
|  |     if (res instanceof Blob) { | ||||||
|  |       blob = res | ||||||
|  |     } else if (res.data instanceof Blob) { | ||||||
|  |       blob = res.data | ||||||
|  |     } else { | ||||||
|  |       // 如果不是blob,可能是错误响应,尝试从response中获取 | ||||||
|  |       blob = res | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const url = window.URL.createObjectURL(blob) | ||||||
|  |     const link = document.createElement('a') | ||||||
|  |     link.href = url | ||||||
|  |     link.download = `${row.versionName}_${row.versionCode}.json` | ||||||
|  |     document.body.appendChild(link) | ||||||
|  |     link.click() | ||||||
|  |     document.body.removeChild(link) | ||||||
|  |     window.URL.revokeObjectURL(url) | ||||||
|  |  | ||||||
|  |     ElMessage.success('下载成功') | ||||||
|  |   } catch (error) { | ||||||
|  |     console.error('下载失败:', error) | ||||||
|  |     ElMessage.error('下载失败') | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style scoped> | ||||||
|  | /* Element Plus 树形组件样式优化 */ | ||||||
|  | :deep(.el-tree) { | ||||||
|  |   background-color: transparent; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | :deep(.el-tree-node__content) { | ||||||
|  |   height: 32px; | ||||||
|  |   line-height: 32px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | :deep(.el-tree-node__label) { | ||||||
|  |   font-size: 14px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | :deep(.el-scrollbar__view) { | ||||||
|  |   padding: 0; | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										26
									
								
								uno.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								uno.config.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | |||||||
|  | import { defineConfig } from '@unocss/vite'; | ||||||
|  | import presetWind3 from '@unocss/preset-wind3'; | ||||||
|  | import transformerDirectives from '@unocss/transformer-directives' | ||||||
|  |  | ||||||
|  | export default defineConfig({ | ||||||
|  |   theme: { | ||||||
|  |     backgroundColor: { | ||||||
|  |       main: '#F5F5F5' | ||||||
|  |     }, | ||||||
|  |     textColor: { | ||||||
|  |       active: 'var(--el-color-primary)' | ||||||
|  |     }, | ||||||
|  |     boxShadowColor: { | ||||||
|  |       active: 'var(--el-color-primary)' | ||||||
|  |     }, | ||||||
|  |     borderColor: { | ||||||
|  |       'table-border': 'var(--el-border-color-lighter)' | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   presets: [ | ||||||
|  |     presetWind3({ dark: 'class' }) | ||||||
|  |   ], | ||||||
|  |   transformers: [ | ||||||
|  |     transformerDirectives(), | ||||||
|  |   ], | ||||||
|  | }) | ||||||
| @@ -1,4 +1,5 @@ | |||||||
| import legacyPlugin from '@vitejs/plugin-legacy' | import legacyPlugin from '@vitejs/plugin-legacy' | ||||||
|  | import { viteLogo } from './src/core/config' | ||||||
| import Banner from 'vite-plugin-banner' | import Banner from 'vite-plugin-banner' | ||||||
| import * as path from 'path' | import * as path from 'path' | ||||||
| import * as dotenv from 'dotenv' | import * as dotenv from 'dotenv' | ||||||
| @@ -8,6 +9,8 @@ import vueDevTools from 'vite-plugin-vue-devtools' | |||||||
| import VueFilePathPlugin from './vitePlugin/componentName/index.js' | import VueFilePathPlugin from './vitePlugin/componentName/index.js' | ||||||
| import { svgBuilder } from 'vite-auto-import-svg' | import { svgBuilder } from 'vite-auto-import-svg' | ||||||
| import { AddSecret } from './vitePlugin/secret' | import { AddSecret } from './vitePlugin/secret' | ||||||
|  | import UnoCSS from '@unocss/vite' | ||||||
|  |  | ||||||
| // @see https://cn.vitejs.dev/config/ | // @see https://cn.vitejs.dev/config/ | ||||||
| export default ({ mode }) => { | export default ({ mode }) => { | ||||||
|   AddSecret('') |   AddSecret('') | ||||||
| @@ -20,6 +23,8 @@ export default ({ mode }) => { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   viteLogo(process.env) | ||||||
|  |  | ||||||
|   const timestamp = Date.parse(new Date()) |   const timestamp = Date.parse(new Date()) | ||||||
|  |  | ||||||
|   const optimizeDeps = {} |   const optimizeDeps = {} | ||||||
| @@ -39,9 +44,13 @@ export default ({ mode }) => { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   const base = "/" | ||||||
|  |   const root = "./" | ||||||
|  |   const outDir = "dist" | ||||||
|  |  | ||||||
|   const config = { |   const config = { | ||||||
|     base: '/', // 编译后js导入的资源路径 |     base: base, // 编译后js导入的资源路径 | ||||||
|     root: './', // index.html文件所在位置 |     root: root, // index.html文件所在位置 | ||||||
|     publicDir: 'public', // 静态资源文件夹 |     publicDir: 'public', // 静态资源文件夹 | ||||||
|     resolve: { |     resolve: { | ||||||
|       alias |       alias | ||||||
| @@ -70,14 +79,13 @@ export default ({ mode }) => { | |||||||
|           rewrite: (path) => |           rewrite: (path) => | ||||||
|             path.replace(new RegExp('^' + process.env.VITE_BASE_API), '') |             path.replace(new RegExp('^' + process.env.VITE_BASE_API), '') | ||||||
|         } |         } | ||||||
|       }, |       } | ||||||
|       allowedHosts: ['all','lckt.echol.top','lckt.hnlc5588.cn'], |  | ||||||
|     }, |     }, | ||||||
|     build: { |     build: { | ||||||
|       minify: 'terser', // 是否进行压缩,boolean | 'terser' | 'esbuild',默认使用terser |       minify: 'terser', // 是否进行压缩,boolean | 'terser' | 'esbuild',默认使用terser | ||||||
|       manifest: false, // 是否产出manifest.json |       manifest: false, // 是否产出manifest.json | ||||||
|       sourcemap: false, // 是否产出sourcemap.json |       sourcemap: false, // 是否产出sourcemap.json | ||||||
|       outDir: 'dist', // 产出目录 |       outDir: outDir, // 产出目录 | ||||||
|       terserOptions: { |       terserOptions: { | ||||||
|         compress: { |         compress: { | ||||||
|           //生产环境时移除console |           //生产环境时移除console | ||||||
| @@ -103,10 +111,10 @@ export default ({ mode }) => { | |||||||
|         ] |         ] | ||||||
|       }), |       }), | ||||||
|       vuePlugin(), |       vuePlugin(), | ||||||
|       svgBuilder('./src/assets/icons/'), |       svgBuilder(['./src/plugin/','./src/assets/icons/'],base, outDir,'assets', NODE_ENV), | ||||||
|       svgBuilder('./src/plugin/'), |  | ||||||
|       [Banner(`\n Build based on gin-vue-admin \n Time : ${timestamp}`)], |       [Banner(`\n Build based on gin-vue-admin \n Time : ${timestamp}`)], | ||||||
|       VueFilePathPlugin('./src/pathInfo.json') |       VueFilePathPlugin('./src/pathInfo.json'), | ||||||
|  |       UnoCSS() | ||||||
|     ] |     ] | ||||||
|   } |   } | ||||||
|   return config |   return config | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user