🎨 更新用户版本
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"
|
||||
name="keywords"
|
||||
/>
|
||||
<link rel="icon" href="favicon.ico" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<title></title>
|
||||
<style>
|
||||
.transition-colors {
|
||||
|
4
limit.js
4
limit.js
@@ -1,6 +1,6 @@
|
||||
// 运行项目前通过node执行此脚本 (此脚本与 node_modules 目录同级)
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
const wfPath = path.resolve(__dirname, './node_modules/.bin')
|
||||
|
||||
fs.readdir(wfPath, (err, files) => {
|
||||
|
33
package.json
33
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gin-vue-admin",
|
||||
"version": "2.8.0",
|
||||
"version": "2.8.5",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vite --host --mode development",
|
||||
@@ -13,6 +13,7 @@
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
"@form-create/designer": "^3.2.6",
|
||||
"@form-create/element-ui": "^3.2.10",
|
||||
"@unocss/transformer-directives": "^66.4.2",
|
||||
"@vue-office/docx": "^1.6.2",
|
||||
"@vue-office/excel": "^1.7.11",
|
||||
"@vue-office/pdf": "^2.0.2",
|
||||
@@ -21,15 +22,17 @@
|
||||
"@wangeditor/editor": "^5.1.23",
|
||||
"@wangeditor/editor-for-vue": "^5.1.12",
|
||||
"ace-builds": "^1.36.4",
|
||||
"axios": "^1.7.7",
|
||||
"axios": "1.8.2",
|
||||
"chokidar": "^4.0.0",
|
||||
"core-js": "^3.38.1",
|
||||
"echarts": "5.5.1",
|
||||
"element-plus": "^2.8.5",
|
||||
"element-plus": "^2.10.2",
|
||||
"highlight.js": "^11.10.0",
|
||||
"install": "^0.13.0",
|
||||
"marked": "14.1.1",
|
||||
"marked-highlight": "^2.1.4",
|
||||
"mitt": "^3.0.1",
|
||||
"npm": "^11.3.0",
|
||||
"nprogress": "^0.2.0",
|
||||
"path": "^0.12.7",
|
||||
"pinia": "^2.2.2",
|
||||
@@ -37,39 +40,45 @@
|
||||
"screenfull": "^6.0.2",
|
||||
"sortablejs": "^1.15.3",
|
||||
"spark-md5": "^3.0.2",
|
||||
"tailwindcss": "^3.4.10",
|
||||
"universal-cookie": "^7",
|
||||
"vform3-builds": "^3.0.10",
|
||||
"vite-auto-import-svg": "^1.1.0",
|
||||
"vite-auto-import-svg": "^1.9.0",
|
||||
"vue": "^3.5.7",
|
||||
"vue-cropper": "^1.1.4",
|
||||
"vue-echarts": "^7.0.3",
|
||||
"vue-qr": "^4.0.9",
|
||||
"vue-router": "^4.4.3",
|
||||
"vue3-ace-editor": "^2.2.4",
|
||||
"vue3-sfc-loader": "^0.9.5",
|
||||
"vuedraggable": "^4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/eslint-parser": "^7.25.1",
|
||||
"@eslint/js": "^9.14.0",
|
||||
"@vitejs/plugin-legacy": "^5.4.2",
|
||||
"@vitejs/plugin-vue": "^5.1.4",
|
||||
"@eslint/js": "^8.56.0",
|
||||
"@unocss/extractor-svelte": "^66.4.2",
|
||||
"@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-eslint": "~5.0.8",
|
||||
"@vue/cli-plugin-router": "~5.0.8",
|
||||
"@vue/cli-plugin-vuex": "~5.0.8",
|
||||
"@vue/cli-service": "~5.0.8",
|
||||
"@vue/compiler-sfc": "^3.5.1",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"babel-plugin-import": "^1.13.8",
|
||||
"chalk": "^5.3.0",
|
||||
"dotenv": "^16.4.5",
|
||||
"eslint": "^9.14.0",
|
||||
"eslint-plugin-vue": "^9.30.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-vue": "^9.19.2",
|
||||
"globals": "^16.3.0",
|
||||
"sass": "^1.78.0",
|
||||
"terser": "^5.31.6",
|
||||
"vite": "^5.4.3",
|
||||
"unocss": "^66.4.2",
|
||||
"vite": "^6.2.3",
|
||||
"vite-plugin-banner": "^0.8.0",
|
||||
"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>
|
||||
<div
|
||||
id="app"
|
||||
class="bg-gray-50 text-slate-700 dark:text-slate-500 dark:bg-slate-800"
|
||||
>
|
||||
<div id="app" class="bg-gray-50 text-slate-700 dark:text-slate-500 dark:bg-slate-800">
|
||||
<el-config-provider :locale="zhCn">
|
||||
<router-view />
|
||||
<Application />
|
||||
</el-config-provider>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
|
||||
import { useAppStore } from '@/pinia'
|
||||
useAppStore()
|
||||
defineOptions({
|
||||
name: 'App'
|
||||
})
|
||||
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
|
||||
import Application from '@/components/application/index.vue'
|
||||
import { useAppStore } from '@/pinia'
|
||||
|
||||
useAppStore()
|
||||
defineOptions({
|
||||
name: 'App'
|
||||
})
|
||||
</script>
|
||||
<style lang="scss">
|
||||
// 引入初始化样式
|
||||
#app {
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
font-weight: 400 !important;
|
||||
}
|
||||
.el-button {
|
||||
font-weight: 400 !important;
|
||||
}
|
||||
// 引入初始化样式
|
||||
#app {
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
font-weight: 400 !important;
|
||||
}
|
||||
|
||||
.gva-body-h {
|
||||
min-height: calc(100% - 3rem);
|
||||
}
|
||||
.el-button {
|
||||
font-weight: 400 !important;
|
||||
}
|
||||
|
||||
.gva-container {
|
||||
height: calc(100% - 2.5rem);
|
||||
}
|
||||
.gva-container2 {
|
||||
height: calc(100% - 4.5rem);
|
||||
}
|
||||
.gva-body-h {
|
||||
min-height: calc(100% - 3rem);
|
||||
}
|
||||
|
||||
.gva-container {
|
||||
height: calc(100% - 2.5rem);
|
||||
}
|
||||
|
||||
.gva-container2 {
|
||||
height: calc(100% - 4.5rem);
|
||||
}
|
||||
</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) => {
|
||||
return service({
|
||||
url: '/autoCode/addFunc',
|
||||
@@ -196,3 +207,29 @@ export const initAPI = (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
|
||||
* @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>
|
||||
<a
|
||||
class="font-bold text-active"
|
||||
href="https://echol.cn"
|
||||
>Echo</a
|
||||
href="https://github.com/flipped-aurora/gin-vue-admin"
|
||||
>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>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -25,4 +35,10 @@
|
||||
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>
|
||||
|
@@ -55,7 +55,6 @@
|
||||
const deepMenus = (menus) => {
|
||||
const arr = []
|
||||
menus?.forEach((menu) => {
|
||||
if (!menu?.children) return
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
arr.push(...deepMenus(menu.children))
|
||||
} 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'
|
||||
|
||||
const props = defineProps({
|
||||
filterDeleted: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
templateId: {
|
||||
type: String,
|
||||
required: true
|
||||
@@ -43,6 +47,11 @@ import { exportExcel } from '@/api/exportTemplate'
|
||||
baseUrl = ""
|
||||
}
|
||||
const paramsCopy = JSON.parse(JSON.stringify(props.condition))
|
||||
|
||||
if (props.filterDeleted) {
|
||||
paramsCopy.filterDeleted = 'true'
|
||||
}
|
||||
|
||||
if (props.limit) {
|
||||
paramsCopy.limit = props.limit
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="border border-solid border-gray-100 h-full">
|
||||
<div class="border border-solid border-gray-100 h-full z-10">
|
||||
<Toolbar
|
||||
:editor="editorRef"
|
||||
:default-config="toolbarConfig"
|
||||
@@ -8,7 +8,7 @@
|
||||
<Editor
|
||||
v-model="valueHtml"
|
||||
class="overflow-y-hidden mt-0.5"
|
||||
style="min-height: 18rem"
|
||||
style="height: 18rem"
|
||||
:default-config="editorConfig"
|
||||
mode="default"
|
||||
@onCreated="handleCreated"
|
||||
@@ -27,7 +27,7 @@
|
||||
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getUrl } from '@/utils/image'
|
||||
import botLogo from '@/assets/bot_logo.png'
|
||||
import { useUserStore } from '@/pinia/modules/user'
|
||||
|
||||
const emits = defineEmits(['change', 'update:modelValue'])
|
||||
|
||||
@@ -36,14 +36,11 @@
|
||||
emits('update:modelValue', valueHtml.value)
|
||||
}
|
||||
|
||||
const userStore = useUserStore()
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
useWatermark: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
@@ -53,94 +50,14 @@
|
||||
const toolbarConfig = {}
|
||||
const editorConfig = {
|
||||
placeholder: '请输入内容...',
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
MENU_CONF: {}
|
||||
}
|
||||
editorConfig.MENU_CONF['uploadImage','uploadVideo'] = {
|
||||
editorConfig.MENU_CONF['uploadImage'] = {
|
||||
fieldName: 'file',
|
||||
server: basePath + '/fileUploadAndDownload/upload?noSave=1',
|
||||
headers: {
|
||||
'x-token': userStore.token,
|
||||
},
|
||||
customInsert(res, insertFn) {
|
||||
if (res.code === 0) {
|
||||
const urlPath = getUrl(res.data.file.url)
|
||||
@@ -169,94 +86,8 @@
|
||||
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>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
|
@@ -47,7 +47,7 @@
|
||||
|
||||
const model = defineModel({ type: Array })
|
||||
|
||||
const fileList = ref(model.value)
|
||||
const fileList = ref(model.value || [])
|
||||
|
||||
const emits = defineEmits(['on-success', 'on-error'])
|
||||
|
||||
|
@@ -2,9 +2,27 @@
|
||||
<div>
|
||||
<selectComponent :rounded="rounded" v-if="!props.multiple" :model="model" @chooseItem="openChooseImg" @deleteItem="openChooseImg" />
|
||||
<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"
|
||||
@deleteItem="deleteImg(index)"
|
||||
/>
|
||||
<draggable
|
||||
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"
|
||||
@chooseItem="openChooseImg" @deleteItem="openChooseImg"
|
||||
/>
|
||||
@@ -140,6 +158,7 @@ import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
ArrowLeftBold,
|
||||
CloseBold,
|
||||
Menu,
|
||||
MoreFilled,
|
||||
Picture as IconPicture,
|
||||
Plus,
|
||||
@@ -149,6 +168,7 @@ import selectComponent from '@/components/selectImage/selectComponent.vue'
|
||||
import { addCategory, deleteCategory, getCategoryList } from '@/api/attachmentCategory'
|
||||
import CropperImage from "@/components/upload/cropper.vue";
|
||||
import QRCodeUpload from "@/components/upload/QR-code.vue";
|
||||
import draggable from 'vuedraggable'
|
||||
|
||||
const imageUrl = ref('')
|
||||
const imageCommon = ref('')
|
||||
@@ -424,6 +444,20 @@ const useSelectedImages = () => {
|
||||
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>
|
||||
<style scoped>
|
||||
.selected {
|
||||
@@ -450,4 +484,20 @@ const useSelectedImages = () => {
|
||||
border-left-color: transparent;
|
||||
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>
|
||||
|
@@ -1,10 +1,12 @@
|
||||
/**
|
||||
* 网站配置文件
|
||||
*/
|
||||
import packageInfo from '../../package.json'
|
||||
|
||||
const greenText = (text) => `\x1b[32m${text}\x1b[0m`
|
||||
|
||||
const config = {
|
||||
appName: 'Lckt-Admin',
|
||||
appName: 'Gin-Vue-Admin',
|
||||
appLogo: 'logo.png',
|
||||
showViteLogo: true,
|
||||
logs: []
|
||||
@@ -12,20 +14,7 @@ const config = {
|
||||
|
||||
export const viteLogo = (env) => {
|
||||
if (config.showViteLogo) {
|
||||
console.log(
|
||||
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(greenText(`> 当前版本:v${packageInfo.version}`))
|
||||
console.log(
|
||||
greenText(
|
||||
`> 默认自动化文档地址:http://127.0.0.1:${env.VITE_SERVER_PORT}/swagger/index.html`
|
||||
@@ -34,18 +23,6 @@ export const viteLogo = (env) => {
|
||||
console.log(
|
||||
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')
|
||||
}
|
||||
}
|
||||
|
@@ -4,24 +4,10 @@
|
||||
* */
|
||||
// 加载网站配置文件夹
|
||||
import { register } from './global'
|
||||
import packageInfo from '../../package.json'
|
||||
|
||||
export default {
|
||||
install: (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 中时……
|
||||
mounted: function (el, binding) {
|
||||
const userInfo = userStore.userInfo
|
||||
let type = ''
|
||||
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 === '') {
|
||||
if (!binding.value){
|
||||
el.parentNode.removeChild(el)
|
||||
return
|
||||
}
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import './style/element_visiable.scss'
|
||||
import 'element-plus/theme-chalk/dark/css-vars.css'
|
||||
import 'uno.css';
|
||||
import { createApp } from 'vue'
|
||||
import ElementPlus from 'element-plus'
|
||||
|
||||
|
@@ -35,13 +35,21 @@
|
||||
"/src/view/layout/aside/headMode.vue": "GvaAside",
|
||||
"/src/view/layout/aside/index.vue": "Index",
|
||||
"/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/tools.vue": "Tools",
|
||||
"/src/view/layout/iframe.vue": "GvaLayoutIframe",
|
||||
"/src/view/layout/index.vue": "GvaLayout",
|
||||
"/src/view/layout/screenfull/index.vue": "Screenfull",
|
||||
"/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/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/tabs/index.vue": "HistoryComponent",
|
||||
"/src/view/login/index.vue": "Login",
|
||||
@@ -69,6 +77,9 @@
|
||||
"/src/view/systemTools/autoCode/component/fieldDialog.vue": "FieldDialog",
|
||||
"/src/view/systemTools/autoCode/component/previewCodeDialog.vue": "PreviewCodeDialog",
|
||||
"/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/autoPkg/autoPkg.vue": "AutoPkg",
|
||||
"/src/view/systemTools/exportTemplate/exportTemplate.vue": "ExportTemplate",
|
||||
@@ -77,6 +88,7 @@
|
||||
"/src/view/systemTools/installPlugin/index.vue": "Index",
|
||||
"/src/view/systemTools/pubPlug/pubPlug.vue": "PubPlug",
|
||||
"/src/view/systemTools/system/system.vue": "Config",
|
||||
"/src/view/systemTools/version/version.vue": "SysVersion",
|
||||
"/src/view/user/index.vue": "UserManage",
|
||||
"/src/view/user/user/index.vue": "Index",
|
||||
"/src/view/user/user/teacherApply.vue": "TeacherApply",
|
||||
|
@@ -115,7 +115,7 @@ router.beforeEach(async (to, from) => {
|
||||
|
||||
return {
|
||||
name: 'Login',
|
||||
query: { redirect: to.href }
|
||||
query: { redirect: to.fullPath }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,7 +126,7 @@ router.beforeEach(async (to, from) => {
|
||||
return {
|
||||
name: 'Login',
|
||||
query: {
|
||||
redirect: document.location.hash
|
||||
redirect: to.fullPath
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@@ -99,6 +99,27 @@ export const useAppStore = defineStore('app', () => {
|
||||
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(() => {
|
||||
document.documentElement.classList.toggle('html-weakenss', config.weakness)
|
||||
@@ -128,6 +149,7 @@ export const useAppStore = defineStore('app', () => {
|
||||
toggleConfigSideItemHeight,
|
||||
toggleConfigWatermark,
|
||||
toggleSideMode,
|
||||
toggleTransition
|
||||
toggleTransition,
|
||||
resetConfig
|
||||
}
|
||||
})
|
||||
|
@@ -1,9 +1,10 @@
|
||||
import { asyncRouterHandle } from '@/utils/asyncRouter'
|
||||
import { emitter } from '@/utils/bus.js'
|
||||
import { asyncMenu, getMenuList } from '@/api/menu'
|
||||
import { asyncMenu } from '@/api/menu'
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, watchEffect } from 'vue'
|
||||
import pathInfo from '@/pathInfo.json'
|
||||
import {useRoute} from "vue-router";
|
||||
|
||||
const notLayoutRouterArr = []
|
||||
const keepAliveRoutersArr = []
|
||||
@@ -51,13 +52,33 @@ export const useRouterStore = defineStore('router', () => {
|
||||
const asyncRouterFlag = ref(0)
|
||||
const setKeepAliveRouters = (history) => {
|
||||
const keepArrTemp = []
|
||||
|
||||
// 1. 首先添加原有的keepAlive配置
|
||||
keepArrTemp.push(...keepAliveRoutersArr)
|
||||
|
||||
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]) {
|
||||
keepArrTemp.push(nameMap[item.name])
|
||||
}
|
||||
})
|
||||
|
||||
keepAliveRouters.value = Array.from(new Set(keepArrTemp))
|
||||
}
|
||||
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
emitter.on('setKeepAlive', setKeepAliveRouters)
|
||||
|
||||
const asyncRouters = ref([])
|
||||
@@ -80,14 +101,32 @@ export const useRouterStore = defineStore('router', () => {
|
||||
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(() => {
|
||||
let topActive = sessionStorage.getItem('topActive')
|
||||
// 初始化菜单内容,防止重复添加
|
||||
topMenu.value = [];
|
||||
asyncRouters.value[0]?.children.forEach((item) => {
|
||||
if (item.hidden) return
|
||||
menuMap[item.name] = item
|
||||
topMenu.value.push({ ...item, children: [] })
|
||||
})
|
||||
|
||||
if (!topActive || topActive === 'undefined' || topActive === 'null') {
|
||||
topActive = findTopActive(menuMap, route.name);
|
||||
}
|
||||
setLeftMenu(topActive)
|
||||
})
|
||||
|
||||
@@ -106,10 +145,8 @@ export const useRouterStore = defineStore('router', () => {
|
||||
children: []
|
||||
}
|
||||
]
|
||||
// const asyncRouterRes = await asyncMenu()
|
||||
const asyncRouterRes = await getMenuList()
|
||||
// const asyncRouter = asyncRouterRes.data.menus
|
||||
const asyncRouter = asyncRouterRes.data
|
||||
const asyncRouterRes = await asyncMenu()
|
||||
const asyncRouter = asyncRouterRes.data.menus
|
||||
asyncRouter &&
|
||||
asyncRouter.push({
|
||||
path: 'reload',
|
||||
|
@@ -71,7 +71,6 @@ export const useUserStore = defineStore('user', () => {
|
||||
const res = await login(loginInfo)
|
||||
|
||||
if (res.code !== 0) {
|
||||
ElMessage.error(res.message || '登录失败')
|
||||
return false
|
||||
}
|
||||
// 登陆成功,设置用户信息和权限相关信息
|
||||
@@ -88,8 +87,13 @@ export const useUserStore = defineStore('user', () => {
|
||||
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)) {
|
||||
ElMessage.error('请联系管理员进行授权')
|
||||
ElMessage.error('不存在可以登陆的首页,请联系管理员进行配置')
|
||||
} else {
|
||||
await router.replace({ name: userInfo.value.authority.defaultRouter })
|
||||
}
|
||||
@@ -124,9 +128,12 @@ export const useUserStore = defineStore('user', () => {
|
||||
/* 清理数据 */
|
||||
const ClearStorage = async () => {
|
||||
token.value = ''
|
||||
xToken.value = ''
|
||||
// 使用remove方法正确删除cookie
|
||||
xToken.remove()
|
||||
sessionStorage.clear()
|
||||
// 清理所有相关的localStorage项
|
||||
localStorage.removeItem('originSetting')
|
||||
localStorage.removeItem('token')
|
||||
}
|
||||
|
||||
return {
|
||||
|
@@ -99,7 +99,7 @@
|
||||
>
|
||||
<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">
|
||||
{{ formatDate(scope.row.CreatedAt) }}
|
||||
</template>
|
||||
|
@@ -31,30 +31,6 @@ const routes = [
|
||||
},
|
||||
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({
|
||||
|
@@ -1,9 +1,6 @@
|
||||
@use '@/style/main.scss';
|
||||
@use '@/style/reset';
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
.el-button {
|
||||
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-menu-item .el-menu-tooltip__trigger {
|
||||
justify-content: center;
|
||||
|
@@ -17,7 +17,15 @@
|
||||
}
|
||||
|
||||
.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 {
|
||||
@@ -34,9 +42,7 @@
|
||||
.gva-search-box {
|
||||
@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 {
|
||||
@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;
|
||||
}
|
||||
}
|
||||
.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,
|
||||
th,
|
||||
td {
|
||||
border: none;
|
||||
// border: none;
|
||||
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB',
|
||||
'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import axios from 'axios' // 引入axios
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { useUserStore } from '@/pinia/modules/user'
|
||||
import { ElLoading, ElMessage } from 'element-plus'
|
||||
import { emitter } from '@/utils/bus'
|
||||
import router from '@/router/index'
|
||||
import { ElLoading } from 'element-plus'
|
||||
|
||||
const service = axios.create({
|
||||
baseURL: import.meta.env.VITE_BASE_API,
|
||||
@@ -11,6 +11,9 @@ const service = axios.create({
|
||||
let activeAxios = 0
|
||||
let timer
|
||||
let loadingInstance
|
||||
let isLoadingVisible = false
|
||||
let forceCloseTimer
|
||||
|
||||
const showLoading = (
|
||||
option = {
|
||||
target: null
|
||||
@@ -18,13 +21,33 @@ const showLoading = (
|
||||
) => {
|
||||
const loadDom = document.getElementById('gva-base-load-dom')
|
||||
activeAxios++
|
||||
|
||||
// 清除之前的定时器
|
||||
if (timer) {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
|
||||
// 清除强制关闭定时器
|
||||
if (forceCloseTimer) {
|
||||
clearTimeout(forceCloseTimer)
|
||||
}
|
||||
|
||||
timer = setTimeout(() => {
|
||||
if (activeAxios > 0) {
|
||||
// 再次检查activeAxios状态,防止竞态条件
|
||||
if (activeAxios > 0 && !isLoadingVisible) {
|
||||
if (!option.target) option.target = loadDom
|
||||
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)
|
||||
}
|
||||
@@ -32,10 +55,47 @@ const showLoading = (
|
||||
const closeLoading = () => {
|
||||
activeAxios--
|
||||
if (activeAxios <= 0) {
|
||||
activeAxios = 0 // 确保不会变成负数
|
||||
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 拦截器
|
||||
service.interceptors.request.use(
|
||||
(config) => {
|
||||
@@ -55,15 +115,18 @@ service.interceptors.request.use(
|
||||
if (!error.config.donNotShowLoading) {
|
||||
closeLoading()
|
||||
}
|
||||
ElMessage({
|
||||
showClose: true,
|
||||
message: error,
|
||||
type: 'error'
|
||||
emitter.emit('show-error', {
|
||||
code: 'request',
|
||||
message: error.message || '请求发送失败'
|
||||
})
|
||||
return error
|
||||
}
|
||||
)
|
||||
|
||||
function getErrorMessage(error) {
|
||||
return error.response?.data?.msg || '请求失败'
|
||||
}
|
||||
|
||||
// http response 拦截器
|
||||
service.interceptors.response.use(
|
||||
(response) => {
|
||||
@@ -74,6 +137,9 @@ service.interceptors.response.use(
|
||||
if (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.headers.msg) {
|
||||
response.data.msg = decodeURI(response.headers.msg)
|
||||
@@ -94,79 +160,43 @@ service.interceptors.response.use(
|
||||
}
|
||||
|
||||
if (!error.response) {
|
||||
ElMessageBox.confirm(
|
||||
`
|
||||
<p>检测到请求错误</p>
|
||||
<p>${error}</p>
|
||||
`,
|
||||
'请求报错',
|
||||
{
|
||||
dangerouslyUseHTMLString: true,
|
||||
distinguishCancelAndClose: true,
|
||||
confirmButtonText: '稍后重试',
|
||||
cancelButtonText: '取消'
|
||||
// 网络错误
|
||||
resetLoading()
|
||||
emitter.emit('show-error', {
|
||||
code: 'network',
|
||||
message: getErrorMessage(error)
|
||||
})
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
// HTTP 状态码错误
|
||||
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) {
|
||||
case 500:
|
||||
ElMessageBox.confirm(
|
||||
`
|
||||
<p>检测到接口错误${error}</p>
|
||||
<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
|
||||
emitter.emit('show-error', {
|
||||
code: error.response.status,
|
||||
message: getErrorMessage(error)
|
||||
})
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// 监听页面卸载事件,确保loading被正确清理
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('beforeunload', resetLoading)
|
||||
window.addEventListener('unload', resetLoading)
|
||||
}
|
||||
|
||||
// 导出service和resetLoading函数
|
||||
export { resetLoading }
|
||||
export default service
|
||||
|
@@ -24,6 +24,7 @@
|
||||
<script setup>
|
||||
import { useUserStore } from '@/pinia/modules/user'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { emitter } from '@/utils/bus'
|
||||
|
||||
defineOptions({
|
||||
name: 'Error'
|
||||
@@ -32,6 +33,17 @@
|
||||
const userStore = useUserStore()
|
||||
const router = useRouter()
|
||||
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>
|
||||
|
@@ -136,7 +136,7 @@
|
||||
// @ts-ignore
|
||||
import { initDB } from '@/api/initdb'
|
||||
import { reactive, ref } from 'vue'
|
||||
import { ElLoading, ElMessage } from 'element-plus'
|
||||
import { ElLoading, ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
defineOptions({
|
||||
@@ -274,7 +274,25 @@
|
||||
type: 'success',
|
||||
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()
|
||||
} catch (_) {
|
||||
|
@@ -37,7 +37,7 @@
|
||||
const menuComponent = computed(() => {
|
||||
if (
|
||||
props.routerInfo.children &&
|
||||
props.routerInfo.children.filter((item) => !item.hidden).length
|
||||
props.routerInfo.children?.filter((item) => !item.hidden).length
|
||||
) {
|
||||
return AsyncSubmenu
|
||||
} else {
|
||||
|
@@ -1,14 +1,16 @@
|
||||
<template>
|
||||
<el-menu-item
|
||||
:index="routerInfo.name"
|
||||
class="dark:text-slate-300 overflow-hidden"
|
||||
:style="{
|
||||
height: sideHeight
|
||||
}"
|
||||
height: sideHeight
|
||||
}"
|
||||
>
|
||||
<el-icon v-if="routerInfo.meta.icon">
|
||||
<component :is="routerInfo.meta.icon" />
|
||||
</el-icon>
|
||||
<template v-else>
|
||||
{{ isCollapse ? routerInfo.meta.title[0] : "" }}
|
||||
</template>
|
||||
<template #title>
|
||||
{{ routerInfo.meta.title }}
|
||||
</template>
|
||||
@@ -16,7 +18,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import {computed, inject} from 'vue'
|
||||
import { useAppStore } from '@/pinia'
|
||||
import { storeToRefs } from 'pinia'
|
||||
const appStore = useAppStore()
|
||||
@@ -35,6 +37,10 @@
|
||||
}
|
||||
})
|
||||
|
||||
const isCollapse = inject('isCollapse', {
|
||||
default: false
|
||||
})
|
||||
|
||||
const sideHeight = computed(() => {
|
||||
return config.value.layout_side_item_height + 'px'
|
||||
})
|
||||
|
@@ -7,7 +7,7 @@
|
||||
<el-menu
|
||||
:default-active="routerStore.topActive"
|
||||
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
|
||||
@select="(index, _, ele) => selectMenuItem(index, _, ele, true)"
|
||||
>
|
||||
@@ -34,7 +34,7 @@
|
||||
:collapse="isCollapse"
|
||||
:collapse-transition="false"
|
||||
:default-active="active"
|
||||
class="border-r-0 w-full"
|
||||
class="!border-r-0 w-full"
|
||||
unique-opened
|
||||
@select="(index, _, ele) => selectMenuItem(index, _, ele, false)"
|
||||
>
|
||||
|
@@ -1,13 +1,16 @@
|
||||
<template>
|
||||
<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"
|
||||
ref="menuContainer"
|
||||
>
|
||||
<el-menu
|
||||
:default-active="active"
|
||||
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
|
||||
:ellipsis="shouldEllipsis"
|
||||
@select="selectMenuItem"
|
||||
ref="menuRef"
|
||||
>
|
||||
<template v-for="item in routerStore.asyncRouters[0].children">
|
||||
<aside-component
|
||||
@@ -23,7 +26,7 @@
|
||||
|
||||
<script setup>
|
||||
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 { useRouterStore } from '@/pinia/modules/router'
|
||||
import { useAppStore } from '@/pinia'
|
||||
@@ -39,6 +42,26 @@
|
||||
const routerStore = useRouterStore()
|
||||
const isCollapse = ref(false)
|
||||
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(() => {
|
||||
if (route.name === 'Iframe') {
|
||||
active.value = decodeURIComponent(route.query.url)
|
||||
@@ -53,10 +76,24 @@
|
||||
} else {
|
||||
isCollapse.value = false
|
||||
}
|
||||
// 设备变化时重新计算
|
||||
calculateEllipsis()
|
||||
})
|
||||
|
||||
// 当路由变化时重新计算
|
||||
watchEffect(() => {
|
||||
if (route.name) {
|
||||
nextTick(calculateEllipsis)
|
||||
}
|
||||
})
|
||||
|
||||
provide('isCollapse', isCollapse)
|
||||
|
||||
onMounted(() => {
|
||||
calculateEllipsis()
|
||||
window.addEventListener('resize', calculateEllipsis)
|
||||
})
|
||||
|
||||
const selectMenuItem = (index) => {
|
||||
const query = {}
|
||||
const params = {}
|
||||
@@ -70,30 +107,26 @@
|
||||
})
|
||||
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 })
|
||||
}
|
||||
if (index === 'Iframe') {
|
||||
query.url = decodeURIComponent(index)
|
||||
}
|
||||
router.push({ name: index, query, params })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
<style lang="scss">
|
||||
.el-menu--horizontal.el-menu,
|
||||
.el-menu--horizontal > .el-menu-item.is-active {
|
||||
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 {
|
||||
background-color: var(--el-color-primary-light-8) !important;
|
||||
}
|
||||
|
@@ -4,7 +4,8 @@
|
||||
v-if="
|
||||
config.side_mode === 'normal' ||
|
||||
(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'" />
|
||||
@@ -12,6 +13,9 @@
|
||||
v-if="config.side_mode === 'combination' && device !== 'mobile'"
|
||||
:mode="mode"
|
||||
/>
|
||||
<sidebar-mode
|
||||
v-if="config.side_mode === 'sidebar' && device !== 'mobile'"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -19,6 +23,7 @@
|
||||
import NormalMode from './normalMode.vue'
|
||||
import HeadMode from './headMode.vue'
|
||||
import CombinationMode from './combinationMode.vue'
|
||||
import SidebarMode from './sidebarMode.vue'
|
||||
|
||||
defineProps({
|
||||
mode: {
|
||||
|
@@ -11,7 +11,7 @@
|
||||
:collapse="isCollapse"
|
||||
:collapse-transition="false"
|
||||
:default-active="active"
|
||||
class="border-r-0 w-full"
|
||||
class="!border-r-0 w-full"
|
||||
unique-opened
|
||||
@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-dropdown @command="toDoc">
|
||||
<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 />
|
||||
</el-icon>
|
||||
@@ -29,7 +29,7 @@
|
||||
<el-tooltip class="" effect="dark" content="搜索" placement="bottom">
|
||||
<el-icon
|
||||
@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 />
|
||||
</el-icon>
|
||||
@@ -37,7 +37,7 @@
|
||||
|
||||
<el-tooltip class="" effect="dark" content="系统设置" placement="bottom">
|
||||
<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"
|
||||
>
|
||||
<Setting />
|
||||
@@ -46,7 +46,7 @@
|
||||
|
||||
<el-tooltip class="" effect="dark" content="刷新" placement="bottom">
|
||||
<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' : ''"
|
||||
@click="toggleRefresh"
|
||||
>
|
||||
@@ -61,14 +61,14 @@
|
||||
>
|
||||
<el-icon
|
||||
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)"
|
||||
>
|
||||
<Sunny />
|
||||
</el-icon>
|
||||
<el-icon
|
||||
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)"
|
||||
>
|
||||
<Moon />
|
||||
|
@@ -11,10 +11,10 @@
|
||||
:content="userStore.userInfo.nickName"
|
||||
/>
|
||||
<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
|
||||
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 == 'combination')
|
||||
"
|
||||
@@ -34,7 +34,7 @@
|
||||
id="gva-base-load-dom"
|
||||
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">
|
||||
<component :is="Component" :key="route.fullPath" />
|
||||
</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"
|
||||
:size="width"
|
||||
:show-close="false"
|
||||
class="theme-config-drawer"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-lg">系统配置</span>
|
||||
<el-button type="primary" @click="saveConfig">保存配置</el-button>
|
||||
<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">
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white font-inter">系统配置</h2>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
class="reset-btn"
|
||||
:style="{ backgroundColor: config.primaryColor, borderColor: config.primaryColor }"
|
||||
@click="resetConfig"
|
||||
>
|
||||
重置配置
|
||||
</el-button>
|
||||
</div>
|
||||
</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>显示标签页</div>
|
||||
<el-switch
|
||||
v-model="config.showTabs"
|
||||
@change="appStore.toggleTabs"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div class="flex-shrink-0">页面切换动画</div>
|
||||
<el-select
|
||||
v-model="config.transition_type"
|
||||
@change="appStore.toggleTransition"
|
||||
class="w-40"
|
||||
<div class="bg-white dark:bg-gray-900">
|
||||
<div class="px-8 pt-4 pb-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<div class="flex justify-center">
|
||||
<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">
|
||||
<div
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
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]"
|
||||
:class="[
|
||||
activeTab === tab.key
|
||||
? 'text-white shadow-md 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="activeTab === tab.key ? { backgroundColor: config.primaryColor } : {}"
|
||||
@click="activeTab = tab.key"
|
||||
>
|
||||
<el-option value="fade" label="淡入淡出" />
|
||||
<el-option value="slide" label="滑动" />
|
||||
<el-option value="zoom" label="缩放" />
|
||||
<el-option value="none" label="无动画" />
|
||||
</el-select>
|
||||
{{ tab.label }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-8">
|
||||
<Title title="layout 大小配置"></Title>
|
||||
<div class="mt-2 text-md p-2 flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div>侧边栏展开宽度</div>
|
||||
<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 class="pb-8 h-full overflow-y-auto">
|
||||
<div class="transition-all duration-300 ease-in-out">
|
||||
<AppearanceSettings v-if="activeTab === 'appearance'" />
|
||||
<LayoutSettings v-else-if="activeTab === 'layout'" />
|
||||
<GeneralSettings v-else-if="activeTab === 'general'" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <el-alert type="warning" :closable="false">
|
||||
请注意,所有配置请保存到本地文件的
|
||||
<el-tag>config.json</el-tag> 文件中,否则刷新页面后会丢失配置
|
||||
</el-alert>-->
|
||||
</div>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useAppStore } from '@/pinia'
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useAppStore } from '@/pinia'
|
||||
import { setSelfSetting } from '@/api/user'
|
||||
import Title from './title.vue'
|
||||
const appStore = useAppStore()
|
||||
const { config, device } = storeToRefs(appStore)
|
||||
import AppearanceSettings from './modules/appearance/index.vue'
|
||||
import LayoutSettings from './modules/layout/index.vue'
|
||||
import GeneralSettings from './modules/general/index.vue'
|
||||
|
||||
defineOptions({
|
||||
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(() => {
|
||||
return device.value === 'mobile' ? '100%' : '500px'
|
||||
})
|
||||
|
||||
const colors = [
|
||||
'#EB2F96',
|
||||
'#3b82f6',
|
||||
'#2FEB54',
|
||||
'#EBEB2F',
|
||||
'#EB2F2F',
|
||||
'#2FEBEB'
|
||||
]
|
||||
|
||||
const drawer = defineModel('drawer', {
|
||||
default: true,
|
||||
type: Boolean
|
||||
})
|
||||
|
||||
const options = ['dark', 'light', 'auto']
|
||||
const sideModes = [
|
||||
{
|
||||
label: '正常模式',
|
||||
value: 'normal'
|
||||
},
|
||||
{
|
||||
label: '顶部菜单栏模式',
|
||||
value: 'head'
|
||||
},
|
||||
{
|
||||
label: '组合模式',
|
||||
value: 'combination'
|
||||
}
|
||||
]
|
||||
|
||||
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)
|
||||
if (res.code === 0) {
|
||||
localStorage.setItem('originSetting', JSON.stringify(config.value))
|
||||
ElMessage.success('保存成功')
|
||||
drawer.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const customColor = ref('')
|
||||
const resetConfig = () => {
|
||||
appStore.resetConfig()
|
||||
}
|
||||
|
||||
watch(config, async () => {
|
||||
await saveConfig();
|
||||
}, { deep: true });
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
::v-deep(.el-drawer__header) {
|
||||
@apply border-gray-400 dark:border-gray-600;
|
||||
.theme-config-drawer {
|
||||
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>
|
||||
|
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"
|
||||
:closable="!(historys.length === 1 && $route.name === defaultRouter)"
|
||||
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)"
|
||||
@tab-click="changeTab"
|
||||
@tab-remove="removeTab"
|
||||
|
@@ -531,7 +531,7 @@
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '添加成功',
|
||||
message: '添加成功,请到角色管理页面分配权限',
|
||||
showClose: true
|
||||
})
|
||||
syncApiData.value.newApis = syncApiData.value.newApis.filter(
|
||||
|
@@ -23,7 +23,7 @@
|
||||
<template #default="{ node, data }">
|
||||
<span class="custom-tree-node">
|
||||
<span>{{ node.label }}</span>
|
||||
<span v-if="node.checked">
|
||||
<span v-if="node.checked && !data.name?.startsWith('http://') && !data.name?.startsWith('https://')">
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
@@ -31,13 +31,13 @@
|
||||
color:
|
||||
row.defaultRouter === data.name ? '#E6A23C' : '#85ce61'
|
||||
}"
|
||||
@click="() => setDefault(data)"
|
||||
@click.stop="() => setDefault(data)"
|
||||
>
|
||||
{{ row.defaultRouter === data.name ? '首页' : '设为首页' }}
|
||||
</el-button>
|
||||
</span>
|
||||
<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>
|
||||
</span>
|
||||
@@ -132,7 +132,7 @@
|
||||
defaultRouter: data.name
|
||||
})
|
||||
if (res.code === 0) {
|
||||
ElMessage({ type: 'success', message: '设置成功' })
|
||||
relation()
|
||||
emit('changeRow', 'defaultRouter', res.data.authority.defaultRouter)
|
||||
}
|
||||
}
|
||||
|
@@ -119,287 +119,394 @@
|
||||
</template>
|
||||
|
||||
<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
|
||||
|
||||
<!-- 基础信息区域 -->
|
||||
<div class="border-b border-gray-200">
|
||||
<h3 class="font-semibold text-gray-700 mb-4">基础信息</h3>
|
||||
<el-form
|
||||
v-if="dialogFormVisible"
|
||||
ref="menuForm"
|
||||
:inline="true"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-position="top"
|
||||
>
|
||||
<el-row class="w-full">
|
||||
<el-col :span="24">
|
||||
<el-form-item label="文件路径" prop="component">
|
||||
<components-cascader
|
||||
:component="form.component"
|
||||
@change="fmtComponent"
|
||||
/>
|
||||
<div class="form-tip">
|
||||
<el-icon><InfoFilled /></el-icon>
|
||||
<span>如果菜单包含子菜单,请创建router-view二级路由页面或者</span>
|
||||
<el-button
|
||||
size="small"
|
||||
type="text"
|
||||
@click="form.component = 'view/routerHolder.vue'"
|
||||
>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<el-input
|
||||
v-model="form.path"
|
||||
:disabled="!checkFlag"
|
||||
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-row class="w-full">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="父节点ID">
|
||||
<el-cascader
|
||||
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>
|
||||
</template>
|
||||
<el-input
|
||||
v-model="form.meta.activeName"
|
||||
:placeholder="form.name"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<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-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-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-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row class="w-full">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="展示名称" prop="meta.title">
|
||||
<el-input
|
||||
v-model="form.meta.title"
|
||||
autocomplete="off"
|
||||
placeholder="请输入菜单展示名称"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="路由Name" prop="path">
|
||||
<el-input
|
||||
v-model="form.name"
|
||||
autocomplete="off"
|
||||
placeholder="唯一英文字符串"
|
||||
@change="changeName"
|
||||
/>
|
||||
</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 label="父节点ID">
|
||||
<el-cascader
|
||||
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
|
||||
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-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-row>
|
||||
</el-form>
|
||||
<div>
|
||||
<div class="flex items-center gap-2">
|
||||
<el-button type="primary" icon="edit" @click="addParameter(form)">
|
||||
<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>
|
||||
</div>
|
||||
<el-table :data="form.parameters" style="width: 100%; margin-top: 12px">
|
||||
<el-table-column
|
||||
align="left"
|
||||
prop="type"
|
||||
label="参数类型"
|
||||
width="180"
|
||||
>
|
||||
<template #default="scope">
|
||||
<el-select v-model="scope.row.type" placeholder="请选择">
|
||||
<el-option key="query" value="query" label="query" />
|
||||
<el-option key="params" value="params" label="params" />
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="left" prop="key" label="参数key" width="180">
|
||||
<template #default="scope">
|
||||
<div>
|
||||
<el-input v-model="scope.row.key" />
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="left" prop="value" label="参数值">
|
||||
<template #default="scope">
|
||||
<div>
|
||||
<el-input v-model="scope.row.value" />
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="left">
|
||||
<template #default="scope">
|
||||
<div>
|
||||
<el-button
|
||||
type="danger"
|
||||
icon="delete"
|
||||
@click="deleteParameter(form.parameters, scope.$index)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="flex items-center gap-2 mt-3">
|
||||
<el-button type="primary" icon="edit" @click="addBtn(form)">
|
||||
新增可控按钮
|
||||
</el-button>
|
||||
<el-icon
|
||||
class="cursor-pointer"
|
||||
@click="
|
||||
toDoc('https://www.gin-vue-admin.com/guide/web/button-auth.html')
|
||||
"
|
||||
>
|
||||
<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>
|
||||
<el-table
|
||||
:data="form.parameters"
|
||||
style="width: 100%"
|
||||
class="parameter-table"
|
||||
>
|
||||
<el-table-column
|
||||
align="center"
|
||||
prop="type"
|
||||
label="参数类型"
|
||||
width="150"
|
||||
>
|
||||
<template #default="scope">
|
||||
<el-select
|
||||
v-model="scope.row.type"
|
||||
placeholder="请选择"
|
||||
size="small"
|
||||
>
|
||||
<el-option key="query" value="query" label="query" />
|
||||
<el-option key="params" value="params" label="params" />
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" prop="key" label="参数key" width="150">
|
||||
<template #default="scope">
|
||||
<el-input
|
||||
v-model="scope.row.key"
|
||||
size="small"
|
||||
placeholder="请输入参数key"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" prop="value" label="参数值">
|
||||
<template #default="scope">
|
||||
<el-input
|
||||
v-model="scope.row.value"
|
||||
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="deleteParameter(form.parameters, scope.$index)"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</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>
|
||||
</div>
|
||||
</template>
|
||||
@@ -417,7 +524,7 @@
|
||||
import { canRemoveAuthorityBtnApi } from '@/api/authorityBtn'
|
||||
import { reactive, ref } from 'vue'
|
||||
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 { toLowerCase } from '@/utils/stringFun'
|
||||
import ComponentsCascader from '@/view/superAdmin/menu/components/components-cascader.vue'
|
||||
@@ -593,7 +700,7 @@
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: isEdit.value ? '编辑成功' : '添加成功!'
|
||||
message: isEdit.value ? '编辑成功' : '添加成功,请到角色管理页面分配权限'
|
||||
})
|
||||
getTableData()
|
||||
}
|
||||
@@ -677,4 +784,52 @@
|
||||
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>
|
||||
|
@@ -268,16 +268,16 @@
|
||||
>
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label="参数名称">
|
||||
{{ detailFrom.name }}
|
||||
{{ detailForm.name }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="参数键">
|
||||
{{ detailFrom.key }}
|
||||
{{ detailForm.key }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="参数值">
|
||||
{{ detailFrom.value }}
|
||||
{{ detailForm.value }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="参数说明">
|
||||
{{ detailFrom.desc }}
|
||||
{{ detailForm.desc }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-drawer>
|
||||
@@ -574,7 +574,7 @@
|
||||
})
|
||||
}
|
||||
|
||||
const detailFrom = ref({})
|
||||
const detailForm = ref({})
|
||||
|
||||
// 查看详情控制标记
|
||||
const detailShow = ref(false)
|
||||
@@ -589,7 +589,7 @@
|
||||
// 打开弹窗
|
||||
const res = await findSysParams({ ID: row.ID })
|
||||
if (res.code === 0) {
|
||||
detailFrom.value = res.data
|
||||
detailForm.value = res.data
|
||||
openDetailShow()
|
||||
}
|
||||
}
|
||||
@@ -597,7 +597,7 @@
|
||||
// 关闭详情弹窗
|
||||
const closeDetailShow = () => {
|
||||
detailShow.value = false
|
||||
detailFrom.value = {}
|
||||
detailForm.value = {}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@@ -143,6 +143,38 @@
|
||||
/>
|
||||
</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
|
||||
v-model="addUserDialog"
|
||||
:size="appStore.drawerSize"
|
||||
@@ -332,28 +364,81 @@
|
||||
|
||||
initPage()
|
||||
|
||||
const resetPasswordFunc = (row) => {
|
||||
ElMessageBox.confirm('是否将此用户密码重置为123456?', '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
const res = await resetPassword({
|
||||
ID: row.ID
|
||||
// 重置密码对话框相关
|
||||
const resetPwdDialog = ref(false)
|
||||
const resetPwdForm = ref(null)
|
||||
const resetPwdInfo = ref({
|
||||
ID: '',
|
||||
userName: '',
|
||||
nickName: '',
|
||||
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 = () => {
|
||||
tableData.value &&
|
||||
tableData.value.forEach((user) => {
|
||||
|
@@ -206,7 +206,7 @@
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="TableName" class="w-full">
|
||||
<el-form-item label="abbreviation" prop="abbreviation" class="w-full">
|
||||
<template #label>
|
||||
<el-tooltip
|
||||
content="简称会作为入参对象名和路由group"
|
||||
@@ -268,7 +268,7 @@
|
||||
prop="package"
|
||||
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
|
||||
v-for="item in pkgs"
|
||||
:key="item.ID"
|
||||
@@ -307,6 +307,7 @@
|
||||
</template>
|
||||
<el-select
|
||||
v-model="form.businessDB"
|
||||
clearable
|
||||
placeholder="选择业务库"
|
||||
class="w-full"
|
||||
>
|
||||
@@ -534,7 +535,7 @@
|
||||
width="160"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-input :disabled="row.disabled" v-model="row.fieldName" />
|
||||
<el-input disabled v-model="row.fieldName" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
@@ -696,7 +697,7 @@
|
||||
style="width: 100%"
|
||||
placeholder="请选择字段查询条件"
|
||||
clearable
|
||||
:disabled="row.fieldType !== 'json' || row.disabled"
|
||||
:disabled="row.fieldType === 'json' || row.disabled"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in typeSearchOptions"
|
||||
@@ -1541,15 +1542,6 @@
|
||||
}
|
||||
init()
|
||||
|
||||
watch(
|
||||
() => route.params.id,
|
||||
() => {
|
||||
if (route.name === 'autoCodeEdit') {
|
||||
init()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(()=>form.value.generateServer,()=>{
|
||||
if(!form.value.generateServer){
|
||||
form.value.autoCreateApiToSql = false
|
||||
@@ -1566,6 +1558,7 @@
|
||||
|
||||
const catchData = () => {
|
||||
window.sessionStorage.setItem('autoCode', JSON.stringify(form.value))
|
||||
ElMessage.success('暂存成功')
|
||||
}
|
||||
|
||||
const getCatch = () => {
|
||||
@@ -1619,6 +1612,8 @@
|
||||
reader.onload = (e) => {
|
||||
try {
|
||||
form.value = JSON.parse(e.target.result)
|
||||
form.value.generateServer = true
|
||||
form.value.generateWeb = true
|
||||
ElMessage.success('JSON 文件导入成功')
|
||||
} catch (_) {
|
||||
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('不能为中文'))
|
||||
} else if (/^\d+$/.test(value[0])) {
|
||||
callback(new Error('不能够以数字开头'))
|
||||
} else if (!/^[a-zA-Z0-9_]+$/.test(value)) {
|
||||
callback(new Error('只能包含英文字母、数字和下划线'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
|
@@ -509,7 +509,15 @@ JOINS模式下不支持导入
|
||||
{
|
||||
label: 'NOT BETWEEN',
|
||||
value: 'NOT BETWEEN'
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'IN',
|
||||
value: 'IN'
|
||||
},
|
||||
{
|
||||
label: 'NOT IN',
|
||||
value: 'NOT IN'
|
||||
},
|
||||
])
|
||||
|
||||
const addCondition = () => {
|
||||
|
@@ -202,6 +202,9 @@
|
||||
<el-form-item label="是否为ssl">
|
||||
<el-switch v-model="config.email['is-ssl']" />
|
||||
</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-input
|
||||
v-model.trim="config.email.secret"
|
||||
@@ -938,7 +941,7 @@
|
||||
</el-form>
|
||||
<div class="mt-4">
|
||||
<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>
|
||||
</template>
|
||||
@@ -1010,7 +1013,7 @@
|
||||
}
|
||||
initForm()
|
||||
const reload = () => {
|
||||
ElMessageBox.confirm('确定要重启服务?', '警告', {
|
||||
ElMessageBox.confirm('确定要重载服务?', '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
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 { viteLogo } from './src/core/config'
|
||||
import Banner from 'vite-plugin-banner'
|
||||
import * as path from 'path'
|
||||
import * as dotenv from 'dotenv'
|
||||
@@ -8,6 +9,8 @@ import vueDevTools from 'vite-plugin-vue-devtools'
|
||||
import VueFilePathPlugin from './vitePlugin/componentName/index.js'
|
||||
import { svgBuilder } from 'vite-auto-import-svg'
|
||||
import { AddSecret } from './vitePlugin/secret'
|
||||
import UnoCSS from '@unocss/vite'
|
||||
|
||||
// @see https://cn.vitejs.dev/config/
|
||||
export default ({ mode }) => {
|
||||
AddSecret('')
|
||||
@@ -20,6 +23,8 @@ export default ({ mode }) => {
|
||||
}
|
||||
}
|
||||
|
||||
viteLogo(process.env)
|
||||
|
||||
const timestamp = Date.parse(new Date())
|
||||
|
||||
const optimizeDeps = {}
|
||||
@@ -39,9 +44,13 @@ export default ({ mode }) => {
|
||||
}
|
||||
}
|
||||
|
||||
const base = "/"
|
||||
const root = "./"
|
||||
const outDir = "dist"
|
||||
|
||||
const config = {
|
||||
base: '/', // 编译后js导入的资源路径
|
||||
root: './', // index.html文件所在位置
|
||||
base: base, // 编译后js导入的资源路径
|
||||
root: root, // index.html文件所在位置
|
||||
publicDir: 'public', // 静态资源文件夹
|
||||
resolve: {
|
||||
alias
|
||||
@@ -70,14 +79,13 @@ export default ({ mode }) => {
|
||||
rewrite: (path) =>
|
||||
path.replace(new RegExp('^' + process.env.VITE_BASE_API), '')
|
||||
}
|
||||
},
|
||||
allowedHosts: ['all','lckt.echol.top','lckt.hnlc5588.cn'],
|
||||
}
|
||||
},
|
||||
build: {
|
||||
minify: 'terser', // 是否进行压缩,boolean | 'terser' | 'esbuild',默认使用terser
|
||||
manifest: false, // 是否产出manifest.json
|
||||
sourcemap: false, // 是否产出sourcemap.json
|
||||
outDir: 'dist', // 产出目录
|
||||
outDir: outDir, // 产出目录
|
||||
terserOptions: {
|
||||
compress: {
|
||||
//生产环境时移除console
|
||||
@@ -103,10 +111,10 @@ export default ({ mode }) => {
|
||||
]
|
||||
}),
|
||||
vuePlugin(),
|
||||
svgBuilder('./src/assets/icons/'),
|
||||
svgBuilder('./src/plugin/'),
|
||||
svgBuilder(['./src/plugin/','./src/assets/icons/'],base, outDir,'assets', NODE_ENV),
|
||||
[Banner(`\n Build based on gin-vue-admin \n Time : ${timestamp}`)],
|
||||
VueFilePathPlugin('./src/pathInfo.json')
|
||||
VueFilePathPlugin('./src/pathInfo.json'),
|
||||
UnoCSS()
|
||||
]
|
||||
}
|
||||
return config
|
||||
|
Reference in New Issue
Block a user