V2.8.4Beta 极致融合AI编辑器 让开发速度更进一步 (#2060)
* fix(style): 修复 border 额外的 reset 导致 tailwind border 属性生效异常的问题 * feat: 添加错误预览组件并优化请求错误处理逻辑 * optimize: select and update necessary fields in `ChangePassword` method - Simplify `ChangePassword` method signature by removing unnecessary return type. - Use `Select()` to fetch only the necessary fields (`id` and `password`) from the database. - Replace `Save()` with `Update()` for more efficient password update operation. Note: use `Save(&user)` to update the whole user record, which will cover other unchanged fields as well, causing data inconsistency when data race conditions. * feat(menu): 版本更新为2.8.4,给菜单增加按钮和参数的预制打包 * feat(menu): 恢复空白的配置文件 * Remove unused `SideMode` field from `ChangeUserInfo` struct Remove unused and deprecated `SideMode` field from user request model. * feat(automation): 增加可以自动生成CURD和续写方法的MCP * fix(mcp): 确保始终返回目录结构信息 * fix(mcp): 当不需要创建模块时提前返回目录结构信息 * feat(automation): 增加可以自动生成CURD和续写方法的MCP * feat(mcp): 添加GAG工具用户确认流程和自动字典创建功能 实现三步工作流程:分析、确认、执行 新增自动字典创建功能,当字段使用字典类型时自动检查并创建字典 添加用户确认机制,确保创建操作前获得用户明确确认 * feat(version): 新增版本管理功能,支持创建、导入、导出和下载版本数据 新增版本管理模块,包含以下功能: 1. 版本数据的增删改查 2. 版本创建功能,可选择关联菜单和API 3. 版本导入导出功能 4. 版本JSON数据下载 5. 相关前端页面和接口实现 * refactor(version): 简化版本管理删除逻辑并移除无用字段 移除版本管理中的状态、创建者、更新者和删除者字段 简化删除和批量删除方法的实现,去除事务和用户ID参数 更新自动生成配置的默认值说明 * feat(版本管理): 新增版本管理功能模块 * fix(menu): 修复递归创建菜单时关联数据未正确处理的问题 * feat(mcp): 添加预设计模块扫描功能以支持代码自动生成 在自动化模块分析器中添加对预设计模块的扫描功能,包括: - 新增PredesignedModuleInfo结构体存储模块信息 - 实现scanPredesignedModules方法扫描plugin和model目录 - 在分析响应中添加predesignedModules字段 - 更新帮助文档说明预设计模块的使用方式 这些修改使系统能够识别并利用现有的预设计模块,提高代码生成效率并减少重复工作。 * feat(mcp): 新增API、菜单和字典生成工具并优化自动生成模块 * docs(mcp): 更新菜单和API创建工具的描述信息 * feat(mcp): 添加字典查询工具用于AI生成逻辑时了解可用字典选项 * feat: 在创建菜单/API/模块结果中添加权限分配提醒 为菜单创建、API创建和模块创建的结果消息添加权限分配提醒,帮助用户了解后续需要进行的权限配置步骤 * refactor(mcp): 统一使用WithBoolean替换WithBool并优化错误处理 * docs(mcp): 更新API创建工具的说明和错误处理日志 * feat(mcp): 添加插件意图检测功能并增强验证逻辑 --------- Co-authored-by: Azir <2075125282@qq.com> Co-authored-by: Feng.YJ <jxfengyijie@gmail.com> Co-authored-by: piexlMax(奇淼 <qimiaojiangjizhao@gmail.com>
This commit is contained in:
co-authored by
Azir
Feng.YJ
piexlMax(奇淼
parent
39abc7b632
commit
273d7bd50e
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gin-vue-admin",
|
||||
"version": "2.8.2",
|
||||
"version": "2.8.4",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "node openDocument.js && vite --host --mode development",
|
||||
|
||||
+30
-28
@@ -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>
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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>
|
||||
@@ -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>
|
||||
<button class="text-gray-400 hover:text-gray-600 transition-colors" @click="closeModal">
|
||||
<close />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 弹窗内容 -->
|
||||
<div class="p-6">
|
||||
<!-- 错误类型 -->
|
||||
<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">
|
||||
<button class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors font-medium text-sm shadow-sm" @click="handleConfirm">
|
||||
确定
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineProps, defineEmits, ref, computed, onMounted } 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>
|
||||
@@ -17,7 +17,7 @@ export const viteLogo = (env) => {
|
||||
`> 欢迎使用Gin-Vue-Admin,开源地址:https://github.com/flipped-aurora/gin-vue-admin`
|
||||
)
|
||||
)
|
||||
console.log(greenText(`> 当前版本:v2.8.3`))
|
||||
console.log(greenText(`> 当前版本:v2.8.4`))
|
||||
console.log(greenText(`> 加群方式:微信:shouzi_1994 QQ群:470239250`))
|
||||
console.log(
|
||||
greenText(`> 项目地址:https://github.com/flipped-aurora/gin-vue-admin`)
|
||||
|
||||
@@ -10,7 +10,7 @@ export default {
|
||||
register(app)
|
||||
console.log(`
|
||||
欢迎使用 Gin-Vue-Admin
|
||||
当前版本:v2.8.3
|
||||
当前版本:v2.8.4
|
||||
加群方式:微信:shouzi_1994 QQ群:622360840
|
||||
项目地址:https://github.com/flipped-aurora/gin-vue-admin
|
||||
插件市场:https://plugin.gin-vue-admin.com
|
||||
|
||||
@@ -74,6 +74,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/plugin/announcement/form/info.vue": "InfoForm",
|
||||
"/src/plugin/announcement/view/info.vue": "Info",
|
||||
"/src/plugin/email/view/index.vue": "Email"
|
||||
|
||||
@@ -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;
|
||||
|
||||
+39
-105
@@ -1,11 +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'
|
||||
|
||||
// 添加一个状态变量,用于跟踪是否已有错误弹窗显示
|
||||
let errorBoxVisible = false
|
||||
|
||||
const service = axios.create({
|
||||
baseURL: import.meta.env.VITE_BASE_API,
|
||||
@@ -24,24 +21,24 @@ const showLoading = (
|
||||
) => {
|
||||
const loadDom = document.getElementById('gva-base-load-dom')
|
||||
activeAxios++
|
||||
|
||||
|
||||
// 清除之前的定时器
|
||||
if (timer) {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
|
||||
|
||||
// 清除强制关闭定时器
|
||||
if (forceCloseTimer) {
|
||||
clearTimeout(forceCloseTimer)
|
||||
}
|
||||
|
||||
|
||||
timer = setTimeout(() => {
|
||||
// 再次检查activeAxios状态,防止竞态条件
|
||||
if (activeAxios > 0 && !isLoadingVisible) {
|
||||
if (!option.target) option.target = loadDom
|
||||
loadingInstance = ElLoading.service(option)
|
||||
isLoadingVisible = true
|
||||
|
||||
|
||||
// 设置强制关闭定时器,防止loading永远不关闭(30秒超时)
|
||||
forceCloseTimer = setTimeout(() => {
|
||||
if (isLoadingVisible && loadingInstance) {
|
||||
@@ -60,12 +57,12 @@ const closeLoading = () => {
|
||||
if (activeAxios <= 0) {
|
||||
activeAxios = 0 // 确保不会变成负数
|
||||
clearTimeout(timer)
|
||||
|
||||
|
||||
if (forceCloseTimer) {
|
||||
clearTimeout(forceCloseTimer)
|
||||
forceCloseTimer = null
|
||||
}
|
||||
|
||||
|
||||
if (isLoadingVisible && loadingInstance) {
|
||||
loadingInstance.close()
|
||||
isLoadingVisible = false
|
||||
@@ -78,17 +75,17 @@ const closeLoading = () => {
|
||||
const resetLoading = () => {
|
||||
activeAxios = 0
|
||||
isLoadingVisible = false
|
||||
|
||||
|
||||
if (timer) {
|
||||
clearTimeout(timer)
|
||||
timer = null
|
||||
}
|
||||
|
||||
|
||||
if (forceCloseTimer) {
|
||||
clearTimeout(forceCloseTimer)
|
||||
forceCloseTimer = null
|
||||
}
|
||||
|
||||
|
||||
if (loadingInstance) {
|
||||
try {
|
||||
loadingInstance.close()
|
||||
@@ -98,6 +95,7 @@ const resetLoading = () => {
|
||||
loadingInstance = null
|
||||
}
|
||||
}
|
||||
|
||||
// http request 拦截器
|
||||
service.interceptors.request.use(
|
||||
(config) => {
|
||||
@@ -117,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) => {
|
||||
@@ -158,105 +159,38 @@ service.interceptors.response.use(
|
||||
closeLoading()
|
||||
}
|
||||
|
||||
// 如果已经有错误弹窗显示,则不再显示新的弹窗
|
||||
if (errorBoxVisible) {
|
||||
return error
|
||||
}
|
||||
|
||||
if (!error.response) {
|
||||
// 网络错误时重置loading状态
|
||||
// 网络错误
|
||||
resetLoading()
|
||||
errorBoxVisible = true
|
||||
ElMessageBox.confirm(
|
||||
`
|
||||
<p>检测到请求错误</p>
|
||||
<p>${error}</p>
|
||||
`,
|
||||
'请求报错',
|
||||
{
|
||||
dangerouslyUseHTMLString: true,
|
||||
distinguishCancelAndClose: true,
|
||||
confirmButtonText: '稍后重试',
|
||||
cancelButtonText: '取消'
|
||||
}
|
||||
).finally(() => {
|
||||
// 弹窗关闭后重置状态
|
||||
errorBoxVisible = false
|
||||
emitter.emit('show-error', {
|
||||
code: 'network',
|
||||
message: getErrorMessage(error)
|
||||
})
|
||||
return
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
switch (error.response.status) {
|
||||
case 500:
|
||||
errorBoxVisible = true
|
||||
ElMessageBox.confirm(
|
||||
`
|
||||
<p>检测到接口错误${error}</p>
|
||||
<p>错误码<span style="color:red"> 500 </span>:此类错误内容常见于后台panic,请先查看后台日志,如果影响您正常使用可强制登出清理缓存</p>
|
||||
`,
|
||||
'接口报错',
|
||||
{
|
||||
dangerouslyUseHTMLString: true,
|
||||
distinguishCancelAndClose: true,
|
||||
confirmButtonText: '清理缓存',
|
||||
cancelButtonText: '取消'
|
||||
}
|
||||
).then(() => {
|
||||
// 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 })
|
||||
}).finally(() => {
|
||||
// 弹窗关闭后重置状态
|
||||
errorBoxVisible = false
|
||||
})
|
||||
break
|
||||
case 404:
|
||||
errorBoxVisible = true
|
||||
ElMessageBox.confirm(
|
||||
`
|
||||
<p>检测到接口错误${error}</p>
|
||||
<p>错误码<span style="color:red"> 404 </span>:此类错误多为接口未注册(或未重启)或者请求路径(方法)与api路径(方法)不符--如果为自动化代码请检查是否存在空格</p>
|
||||
`,
|
||||
'接口报错',
|
||||
{
|
||||
dangerouslyUseHTMLString: true,
|
||||
distinguishCancelAndClose: true,
|
||||
confirmButtonText: '我知道了',
|
||||
cancelButtonText: '取消'
|
||||
}
|
||||
).finally(() => {
|
||||
// 弹窗关闭后重置状态
|
||||
errorBoxVisible = false
|
||||
})
|
||||
break
|
||||
case 401:
|
||||
errorBoxVisible = true
|
||||
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 })
|
||||
}).finally(() => {
|
||||
// 弹窗关闭后重置状态
|
||||
errorBoxVisible = false
|
||||
})
|
||||
break
|
||||
}
|
||||
})
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
@@ -0,0 +1,905 @@
|
||||
<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-5 w-full">
|
||||
<!-- 菜单选择 -->
|
||||
<div class="flex flex-col border border-gray-300 rounded overflow-hidden h-full flex-1 w-1/2">
|
||||
<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/2">
|
||||
<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>
|
||||
</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-5 w-full">
|
||||
<div class="border border-gray-300 rounded overflow-hidden flex-1 w-1/2">
|
||||
<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/2">
|
||||
<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>
|
||||
</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 { getDictFunc, formatDate, filterDict } from '@/utils/format'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { UploadFilled } from '@element-plus/icons-vue'
|
||||
import { ref, reactive, watch } from 'vue'
|
||||
// 引入按钮权限标识
|
||||
import { useBtnAuth } from '@/utils/btnAuth'
|
||||
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: []
|
||||
})
|
||||
|
||||
// 树形结构相关数据
|
||||
const menuTreeData = ref([])
|
||||
const apiTreeData = ref([])
|
||||
const selectedMenuIds = ref([])
|
||||
const selectedApiIds = ref([])
|
||||
const menuFilterText = ref('')
|
||||
const apiFilterTextName = ref('')
|
||||
const apiFilterTextPath = ref('')
|
||||
|
||||
// 树形组件引用
|
||||
const menuTreeRef = ref(null)
|
||||
const apiTreeRef = ref(null)
|
||||
|
||||
// 树形属性配置
|
||||
const menuTreeProps = ref({
|
||||
children: 'children',
|
||||
label: function (data) {
|
||||
return data.meta?.title || data.title
|
||||
}
|
||||
})
|
||||
|
||||
const apiTreeProps = ref({
|
||||
children: 'children',
|
||||
label: 'description'
|
||||
})
|
||||
|
||||
// 导入相关数据
|
||||
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 rule = reactive({
|
||||
versionName: [{
|
||||
required: true,
|
||||
message: '请输入版本名称',
|
||||
trigger: ['input', 'blur'],
|
||||
},
|
||||
{
|
||||
whitespace: true,
|
||||
message: '不能只输入空格',
|
||||
trigger: ['input', 'blur'],
|
||||
}
|
||||
],
|
||||
versionCode: [{
|
||||
required: true,
|
||||
message: '请输入版本号',
|
||||
trigger: ['input', 'blur'],
|
||||
},
|
||||
{
|
||||
whitespace: true,
|
||||
message: '不能只输入空格',
|
||||
trigger: ['input', 'blur'],
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
const elFormRef = 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数据失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 构建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 onMenuCheck = (data, checked) => {
|
||||
if (checked.checkedKeys) {
|
||||
selectedMenuIds.value = checked.checkedKeys
|
||||
}
|
||||
}
|
||||
|
||||
const onApiCheck = (data, checked) => {
|
||||
if (checked.checkedKeys) {
|
||||
selectedApiIds.value = checked.checkedKeys
|
||||
}
|
||||
}
|
||||
|
||||
// 监听过滤文本变化
|
||||
watch(menuFilterText, (val) => {
|
||||
if (menuTreeRef.value) {
|
||||
menuTreeRef.value.filter(val)
|
||||
}
|
||||
})
|
||||
|
||||
watch([apiFilterTextName, apiFilterTextPath], () => {
|
||||
if (apiTreeRef.value) {
|
||||
apiTreeRef.value.filter('')
|
||||
}
|
||||
})
|
||||
|
||||
// 导出相关方法
|
||||
const openExportDialog = async () => {
|
||||
exportDialogVisible.value = true
|
||||
await getMenuAndApiList()
|
||||
}
|
||||
|
||||
const closeExportDialog = () => {
|
||||
exportDialogVisible.value = false
|
||||
exportForm.value = {
|
||||
versionName: '',
|
||||
versionCode: '',
|
||||
description: '',
|
||||
menuIds: [],
|
||||
apiIds: []
|
||||
}
|
||||
selectedMenuIds.value = []
|
||||
selectedApiIds.value = []
|
||||
menuFilterText.value = ''
|
||||
apiFilterTextName.value = ''
|
||||
apiFilterTextPath.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 menuIds = checkedMenus.map(menu => menu.ID)
|
||||
const apiIds = checkedApis.map(api => api.ID)
|
||||
|
||||
exportForm.value.menuIds = menuIds
|
||||
exportForm.value.apiIds = apiIds
|
||||
|
||||
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 buildTreeData = (data, parentId = 0) => {
|
||||
const tree = []
|
||||
// 处理parentId可能为字符串"0"或数字0的情况
|
||||
const targetParentId = parentId === 0 ? [0, "0"] : [parentId]
|
||||
const items = data.filter(item => targetParentId.includes(item.parentId))
|
||||
|
||||
items.forEach(item => {
|
||||
const children = buildTreeData(data, item.ID)
|
||||
if (children.length > 0) {
|
||||
item.children = children
|
||||
}
|
||||
tree.push(item)
|
||||
})
|
||||
|
||||
return tree
|
||||
}
|
||||
|
||||
const handleJsonContentChange = () => {
|
||||
if (!importJsonContent.value.trim()) {
|
||||
importPreviewData.value = null
|
||||
previewMenuTreeData.value = []
|
||||
previewApiTreeData.value = []
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const data = JSON.parse(importJsonContent.value)
|
||||
|
||||
// 构建预览数据
|
||||
importPreviewData.value = {
|
||||
menus: data.menus || [],
|
||||
apis: data.apis || []
|
||||
}
|
||||
|
||||
// 直接使用菜单数据,因为它已经是树形结构(包含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 = []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('JSON解析失败:', error)
|
||||
importPreviewData.value = null
|
||||
previewMenuTreeData.value = []
|
||||
previewApiTreeData.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>
|
||||
Reference in New Issue
Block a user