增加字典
This commit is contained in:
parent
b159d71a77
commit
8b8b0c54a1
108
pc/src/api/dict.js
Normal file
108
pc/src/api/dict.js
Normal file
@ -0,0 +1,108 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 获取字典类型列表
|
||||
export function getDictTypes(params) {
|
||||
return request({
|
||||
url: '/api/dict/types',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// 根据ID获取字典类型
|
||||
export function getDictTypeById(id) {
|
||||
return request({
|
||||
url: `/api/dict/types/${id}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 添加字典类型
|
||||
export function addDictType(data) {
|
||||
return request({
|
||||
url: '/api/dict/types',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 更新字典类型
|
||||
export function updateDictType(id, data) {
|
||||
return request({
|
||||
url: `/api/dict/types/${id}`,
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 删除字典类型
|
||||
export function deleteDictType(id) {
|
||||
return request({
|
||||
url: `/api/dict/types/${id}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
// 获取字典项列表
|
||||
export function getDictItems(params) {
|
||||
return request({
|
||||
url: '/api/dict/items',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// 根据ID获取字典项
|
||||
export function getDictItemById(id) {
|
||||
return request({
|
||||
url: `/api/dict/items/${id}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 添加字典项
|
||||
export function addDictItem(data) {
|
||||
return request({
|
||||
url: '/api/dict/items',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 更新字典项
|
||||
export function updateDictItem(id, data) {
|
||||
return request({
|
||||
url: `/api/dict/items/${id}`,
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 删除字典项
|
||||
export function deleteDictItem(id) {
|
||||
return request({
|
||||
url: `/api/dict/items/${id}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
// 根据字典编码获取字典项(用于业务查询)
|
||||
export function getDictItemsByCode(code, includeDisabled = false) {
|
||||
return request({
|
||||
url: `/api/dict/items/code/${code}`,
|
||||
method: 'get',
|
||||
params: {
|
||||
include_disabled: includeDisabled ? '1' : '0'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 批量更新字典项排序
|
||||
export function batchUpdateDictItemSort(data) {
|
||||
return request({
|
||||
url: '/api/dict/items/sort',
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
<template>
|
||||
<el-aside :width="width" class="common-aside">
|
||||
<div v-if="loading" class="loading-spinner"> <!-- Show spinner if loading -->
|
||||
<i class="el-icon-loading" style="font-size: 24px; color: #fff;"></i>
|
||||
<div v-if="loading" class="loading-spinner">
|
||||
<!-- Show spinner if loading -->
|
||||
<i class="el-icon-loading" style="font-size: 24px; color: #fff"></i>
|
||||
</div>
|
||||
<el-menu v-else
|
||||
<el-menu
|
||||
v-else
|
||||
:collapse="isCollapse"
|
||||
:collapse-transition="false"
|
||||
:background-color="asideBgColor"
|
||||
@ -31,37 +33,37 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import { useAllDataStore, useMenuStore } from '@/stores';
|
||||
import MenuTreeItem from './MenuTreeItem.vue';
|
||||
import { ref, computed, onMounted, onUnmounted } from "vue";
|
||||
import { useRouter, useRoute } from "vue-router";
|
||||
import { useAllDataStore, useMenuStore } from "@/stores";
|
||||
import MenuTreeItem from "./MenuTreeItem.vue";
|
||||
|
||||
const emit = defineEmits(['menu-click']);
|
||||
const emit = defineEmits(["menu-click"]);
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const menuStore = useMenuStore();
|
||||
const loading = computed(() => menuStore.loading);
|
||||
const loading = computed(() => menuStore.loading);
|
||||
|
||||
const store = useAllDataStore();
|
||||
const isCollapse = computed(() => store.state.isCollapse);
|
||||
const width = computed(() => store.state.isCollapse ? '64px' : '180px');
|
||||
const width = computed(() => (store.state.isCollapse ? "64px" : "180px"));
|
||||
// 使用 Element Plus 的颜色变量,主题切换时会自动适配
|
||||
// 这些值会被 el-menu 组件使用,el-menu 会自动适配主题
|
||||
// 计算是否为暗色主题(响应式)
|
||||
const isDark = ref(document.documentElement.classList.contains('dark'));
|
||||
const isDark = ref(document.documentElement.classList.contains("dark"));
|
||||
|
||||
// 监听主题变化
|
||||
const updateTheme = () => {
|
||||
isDark.value = document.documentElement.classList.contains('dark');
|
||||
isDark.value = document.documentElement.classList.contains("dark");
|
||||
};
|
||||
|
||||
// 使用 MutationObserver 监听 html 元素的 class 变化(主题切换时更新)
|
||||
let themeObserver = null;
|
||||
|
||||
// 自定义颜色配置
|
||||
const BG_COLOR = '#062da3'; // 背景和 active 颜色
|
||||
const HOVER_COLOR = '#4f84ff'; // hover 颜色
|
||||
const BG_COLOR = "#062da3"; // 背景和 active 颜色
|
||||
const HOVER_COLOR = "#4f84ff"; // hover 颜色
|
||||
|
||||
// 将 hex 颜色转换为 rgba
|
||||
function hexToRgba(hex, alpha = 1) {
|
||||
@ -74,7 +76,7 @@ function hexToRgba(hex, alpha = 1) {
|
||||
// 亮色主题使用自定义背景色,暗色主题使用默认背景
|
||||
const asideBgColor = computed(() => {
|
||||
if (isDark.value) {
|
||||
return 'var(--el-bg-color)';
|
||||
return "var(--el-bg-color)";
|
||||
}
|
||||
// 亮色主题使用 #062da3 背景
|
||||
return BG_COLOR;
|
||||
@ -82,18 +84,18 @@ const asideBgColor = computed(() => {
|
||||
|
||||
const asideTextColor = computed(() => {
|
||||
if (isDark.value) {
|
||||
return 'var(--el-text-color-primary)';
|
||||
return "var(--el-text-color-primary)";
|
||||
}
|
||||
// 亮色主题使用白色文字
|
||||
return '#ffffff';
|
||||
return "#ffffff";
|
||||
});
|
||||
|
||||
const activeColor = ref('#ffffff'); // active 文字颜色为白色
|
||||
const activeColor = ref("#ffffff"); // active 文字颜色为白色
|
||||
|
||||
// hover 和 active 背景色
|
||||
const activeBgColor = computed(() => {
|
||||
if (isDark.value) {
|
||||
return 'rgba(255, 255, 255, 0.1)';
|
||||
return "rgba(255, 255, 255, 0.1)";
|
||||
}
|
||||
// 亮色主题 active 使用 #4f84ff
|
||||
return HOVER_COLOR;
|
||||
@ -104,19 +106,29 @@ const activeBgColor = computed(() => {
|
||||
// 将后端数据转换为前端需要的格式
|
||||
const transformMenuData = (menus) => {
|
||||
if (!menus || menus.length === 0) {
|
||||
console.warn('菜单数据为空');
|
||||
console.warn("菜单数据为空");
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
// console.log('原始菜单数据:', menus);
|
||||
|
||||
|
||||
// 功能页面路径关键词,这些菜单不应该显示在侧边栏
|
||||
// 注意:只过滤真正的功能页面,不过滤包含这些关键词的父菜单
|
||||
const functionPageKeywords = ['/detail', '/add', '/edit', '/delete', '/create', '/update'];
|
||||
|
||||
const functionPageKeywords = [
|
||||
"/detail",
|
||||
"/add",
|
||||
"/edit",
|
||||
"/delete",
|
||||
"/create",
|
||||
"/update",
|
||||
];
|
||||
|
||||
// 需要隐藏的子菜单路径(这些子菜单不应该显示在侧边栏)
|
||||
const hiddenSubMenuPaths = ['/apps/knowledge/category', '/apps/knowledge/tag'];
|
||||
|
||||
const hiddenSubMenuPaths = [
|
||||
"/apps/knowledge/category",
|
||||
"/apps/knowledge/tag",
|
||||
];
|
||||
|
||||
// 判断是否是功能页面(更精确的判断)
|
||||
const isFunctionPage = (path) => {
|
||||
if (!path) return false;
|
||||
@ -124,26 +136,28 @@ const transformMenuData = (menus) => {
|
||||
// 检查路径是否以这些关键词结尾,或者是这些关键词的组合
|
||||
// 例如:/apps/knowledge/detail 是功能页面
|
||||
// 但 /apps/knowledge 不是功能页面
|
||||
return functionPageKeywords.some(keyword => {
|
||||
return functionPageKeywords.some((keyword) => {
|
||||
// 检查路径是否以关键词结尾,或者路径中包含 /keyword/ 或 /keyword
|
||||
return lowerPath.endsWith(keyword) ||
|
||||
lowerPath.includes(`/${keyword}/`) ||
|
||||
lowerPath.endsWith(`/${keyword}`);
|
||||
return (
|
||||
lowerPath.endsWith(keyword) ||
|
||||
lowerPath.includes(`/${keyword}/`) ||
|
||||
lowerPath.endsWith(`/${keyword}`)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// 判断是否是需要隐藏的子菜单
|
||||
const isHiddenSubMenu = (path) => {
|
||||
if (!path) return false;
|
||||
const lowerPath = path.toLowerCase();
|
||||
return hiddenSubMenuPaths.some(hiddenPath => {
|
||||
return lowerPath === hiddenPath || lowerPath.startsWith(hiddenPath + '/');
|
||||
return hiddenSubMenuPaths.some((hiddenPath) => {
|
||||
return lowerPath === hiddenPath || lowerPath.startsWith(hiddenPath + "/");
|
||||
});
|
||||
};
|
||||
|
||||
// 首先映射字段格式,只保留页面菜单(menu_type=1),并过滤掉功能页面和需要隐藏的子菜单
|
||||
const allMenus = menus
|
||||
.filter(menu => {
|
||||
.filter((menu) => {
|
||||
// 只显示页面菜单,不显示API权限菜单
|
||||
if (menu.menuType !== 1 && menu.menuType !== undefined) {
|
||||
// console.log('过滤掉非页面菜单:', menu);
|
||||
@ -161,29 +175,29 @@ const transformMenuData = (menus) => {
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.map(menu => ({
|
||||
.map((menu) => ({
|
||||
id: menu.id,
|
||||
path: menu.path,
|
||||
icon: menu.icon || 'fa-circle',
|
||||
icon: menu.icon || "fa-circle",
|
||||
label: menu.name,
|
||||
route: menu.path,
|
||||
parentId: menu.parentId || 0,
|
||||
order: menu.order || 0,
|
||||
children: []
|
||||
children: [],
|
||||
}));
|
||||
|
||||
// console.log('过滤后的菜单数据:', allMenus);
|
||||
|
||||
// 构建菜单映射表(只包含有效的页面菜单)
|
||||
const menuMap = new Map();
|
||||
allMenus.forEach(menu => {
|
||||
allMenus.forEach((menu) => {
|
||||
menuMap.set(menu.id, menu);
|
||||
});
|
||||
|
||||
// 构建树形结构
|
||||
// 如果父菜单不在当前数据中,将菜单项作为根菜单显示
|
||||
const rootMenus = [];
|
||||
allMenus.forEach(menu => {
|
||||
allMenus.forEach((menu) => {
|
||||
if (menu.parentId === 0 || !menu.parentId) {
|
||||
// 顶级菜单直接加入
|
||||
rootMenus.push(menu);
|
||||
@ -199,34 +213,41 @@ const transformMenuData = (menus) => {
|
||||
} else {
|
||||
// 如果父菜单不存在,可能是父菜单被过滤掉了,或者父菜单不在当前返回的数据中
|
||||
// 这种情况下,将该菜单作为根菜单显示(避免菜单丢失)
|
||||
console.warn('菜单的父菜单不存在,将作为根菜单显示。菜单ID:', menu.id, '父菜单ID:', menu.parentId, '菜单路径:', menu.path);
|
||||
console.warn(
|
||||
"菜单的父菜单不存在,将作为根菜单显示。菜单ID:",
|
||||
menu.id,
|
||||
"父菜单ID:",
|
||||
menu.parentId,
|
||||
"菜单路径:",
|
||||
menu.path
|
||||
);
|
||||
rootMenus.push(menu);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// console.log('构建后的菜单树:', rootMenus);
|
||||
|
||||
// 按 order 排序(确保排序正确)
|
||||
const sortMenus = (menus) => {
|
||||
if (!menus || menus.length === 0) return;
|
||||
|
||||
|
||||
// 对当前层级的菜单进行排序
|
||||
menus.sort((a, b) => {
|
||||
const orderA = Number(a.order) ?? 999999; // 没有order的排在最后
|
||||
const orderB = Number(b.order) ?? 999999;
|
||||
const diff = orderA - orderB;
|
||||
|
||||
|
||||
// 如果 order 相同,按 id 排序(保证稳定性)
|
||||
if (diff === 0) {
|
||||
return (a.id || 0) - (b.id || 0);
|
||||
}
|
||||
|
||||
|
||||
return diff;
|
||||
});
|
||||
|
||||
|
||||
// 递归排序子菜单
|
||||
menus.forEach(menu => {
|
||||
menus.forEach((menu) => {
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
sortMenus(menu.children);
|
||||
}
|
||||
@ -245,7 +266,7 @@ const fetchMenus = async () => {
|
||||
await menuStore.fetchMenus();
|
||||
// 菜单数据会自动通过 computed 更新
|
||||
} catch (error) {
|
||||
console.error('获取菜单异常:', error);
|
||||
console.error("获取菜单异常:", error);
|
||||
}
|
||||
};
|
||||
|
||||
@ -270,22 +291,22 @@ onMounted(() => {
|
||||
themeObserver = new MutationObserver(() => {
|
||||
updateTheme();
|
||||
});
|
||||
|
||||
|
||||
themeObserver.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ['class']
|
||||
attributeFilter: ["class"],
|
||||
});
|
||||
|
||||
|
||||
// 初始化时检查一次主题
|
||||
updateTheme();
|
||||
|
||||
|
||||
// 延迟一点获取菜单,确保主题已初始化
|
||||
setTimeout(() => {
|
||||
fetchMenus();
|
||||
}, 100);
|
||||
|
||||
|
||||
// 监听菜单缓存刷新事件
|
||||
window.addEventListener('menu-cache-refreshed', handleMenuRefresh);
|
||||
window.addEventListener("menu-cache-refreshed", handleMenuRefresh);
|
||||
});
|
||||
|
||||
// 组件卸载时清理事件监听
|
||||
@ -293,7 +314,7 @@ onUnmounted(() => {
|
||||
if (themeObserver) {
|
||||
themeObserver.disconnect();
|
||||
}
|
||||
window.removeEventListener('menu-cache-refreshed', handleMenuRefresh);
|
||||
window.removeEventListener("menu-cache-refreshed", handleMenuRefresh);
|
||||
});
|
||||
|
||||
// 计算属性:统一排序所有菜单项(不再区分有无子菜单)
|
||||
@ -307,9 +328,9 @@ const sortedMenuList = computed(() => {
|
||||
}
|
||||
return orderA - orderB;
|
||||
});
|
||||
|
||||
|
||||
// 确保子菜单也排序
|
||||
sorted.forEach(menu => {
|
||||
sorted.forEach((menu) => {
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
menu.children.sort((a, b) => {
|
||||
const orderA = Number(a.order) ?? 999999;
|
||||
@ -321,36 +342,36 @@ const sortedMenuList = computed(() => {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
return sorted;
|
||||
});
|
||||
|
||||
// 固定的仪表盘菜单项
|
||||
const dashboardMenuItem = {
|
||||
path: '/dashboard',
|
||||
label: '仪表盘',
|
||||
icon: 'fa-solid fa-gauge',
|
||||
route: '/dashboard',
|
||||
order: 0
|
||||
path: "/dashboard",
|
||||
label: "仪表盘",
|
||||
icon: "fa-solid fa-gauge",
|
||||
route: "/dashboard",
|
||||
order: 0,
|
||||
};
|
||||
|
||||
// 菜单点击事件:emit 通知父组件
|
||||
const handleMenuSelect = (index) => {
|
||||
// 如果是仪表盘路径,直接使用固定的仪表盘菜单项
|
||||
if (index === '/dashboard') {
|
||||
emit('menu-click', dashboardMenuItem);
|
||||
if (index === "/dashboard") {
|
||||
emit("menu-click", dashboardMenuItem);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const menuItem = findMenuItemByPath(list.value, index);
|
||||
if (menuItem && menuItem.path) {
|
||||
// 使用 path 而不是 route,确保菜单项有路径
|
||||
const menuToEmit = {
|
||||
...menuItem,
|
||||
route: menuItem.path, // 兼容性:添加 route 字段
|
||||
label: menuItem.name || menuItem.label
|
||||
label: menuItem.name || menuItem.label,
|
||||
};
|
||||
emit('menu-click', menuToEmit);
|
||||
emit("menu-click", menuToEmit);
|
||||
}
|
||||
};
|
||||
|
||||
@ -379,7 +400,7 @@ const findMenuItemByPath = (menus, path) => {
|
||||
position: relative;
|
||||
display: block !important;
|
||||
visibility: visible !important;
|
||||
|
||||
|
||||
// 亮色主题下的特殊处理
|
||||
html:not(.dark) & {
|
||||
background-color: #062da3;
|
||||
@ -392,14 +413,14 @@ const findMenuItemByPath = (menus, path) => {
|
||||
margin-right: 8px;
|
||||
font-size: 16px;
|
||||
transition: var(--transition-fast);
|
||||
|
||||
|
||||
// 亮色主题下默认使用白色
|
||||
html:not(.dark) & {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
}
|
||||
|
||||
h3{
|
||||
h3 {
|
||||
line-height: 80px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -408,7 +429,7 @@ h3{
|
||||
font-weight: bold;
|
||||
// 亮色主题使用白色,暗色主题使用默认文字颜色
|
||||
color: var(--el-text-color-primary);
|
||||
|
||||
|
||||
html:not(.dark) & {
|
||||
color: #ffffff;
|
||||
}
|
||||
@ -419,102 +440,102 @@ h3{
|
||||
.el-menu-item {
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
|
||||
|
||||
// hover 状态:亮色主题使用 #4f84ff
|
||||
&:hover:not(.is-active) {
|
||||
background-color: #4f84ff !important;
|
||||
color: #ffffff !important;
|
||||
|
||||
|
||||
// 确保图标在 hover 状态下也有正确的颜色
|
||||
.icons {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// active 状态:使用 #4f84ff 背景色,白色文字
|
||||
&.is-active {
|
||||
background-color: #4f84ff !important;
|
||||
color: #ffffff !important;
|
||||
font-weight: 600;
|
||||
|
||||
|
||||
// 添加左侧边框作为指示器(使用更亮的颜色)
|
||||
border-left: 3px solid #ffffff;
|
||||
margin-left: -3px; // 使用负边距补偿,保持文字位置不变
|
||||
|
||||
|
||||
// 确保图标在 active 状态下也有正确的颜色
|
||||
.icons {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 子菜单项也需要相同的效果
|
||||
.el-sub-menu {
|
||||
.el-menu-item {
|
||||
&:hover:not(.is-active) {
|
||||
background-color: #4f84ff !important;
|
||||
color: #ffffff !important;
|
||||
|
||||
|
||||
.icons {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&.is-active {
|
||||
background-color: #4f84ff !important;
|
||||
color: #ffffff !important;
|
||||
font-weight: 600;
|
||||
|
||||
|
||||
// 添加左侧边框作为指示器
|
||||
border-left: 3px solid #ffffff;
|
||||
margin-left: -3px; // 使用负边距补偿,保持文字位置不变
|
||||
|
||||
|
||||
.icons {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 暗色主题下的特殊处理
|
||||
html.dark & {
|
||||
.el-menu-item {
|
||||
&:hover:not(.is-active) {
|
||||
background-color: rgba(255, 255, 255, 0.08) !important;
|
||||
color: var(--el-color-primary-light-3) !important;
|
||||
|
||||
|
||||
.icons {
|
||||
color: var(--el-color-primary-light-3) !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&.is-active {
|
||||
background-color: rgba(255, 255, 255, 0.12) !important;
|
||||
color: var(--el-color-primary-light-3) !important;
|
||||
border-left-color: var(--el-color-primary-light-3);
|
||||
|
||||
|
||||
.icons {
|
||||
color: var(--el-color-primary-light-3) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.el-sub-menu {
|
||||
.el-menu-item {
|
||||
&:hover:not(.is-active) {
|
||||
background-color: rgba(255, 255, 255, 0.08) !important;
|
||||
color: var(--el-color-primary-light-3) !important;
|
||||
|
||||
|
||||
.icons {
|
||||
color: var(--el-color-primary-light-3) !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&.is-active {
|
||||
background-color: rgba(255, 255, 255, 0.12) !important;
|
||||
color: var(--el-color-primary-light-3) !important;
|
||||
border-left-color: var(--el-color-primary-light-3);
|
||||
|
||||
|
||||
.icons {
|
||||
color: var(--el-color-primary-light-3) !important;
|
||||
}
|
||||
@ -523,4 +544,11 @@ h3{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-sub-menu__title),
|
||||
:deep(.el-menu-item) {
|
||||
span {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
616
pc/src/views/system/dict/index.vue
Normal file
616
pc/src/views/system/dict/index.vue
Normal file
@ -0,0 +1,616 @@
|
||||
<template>
|
||||
<div class="dict-container">
|
||||
<div class="header-bar">
|
||||
<h2>字典管理</h2>
|
||||
<div class="header-actions">
|
||||
<el-button type="primary" @click="handleAddType">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加字典类型
|
||||
</el-button>
|
||||
<el-button @click="refresh">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
刷新
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-divider></el-divider>
|
||||
|
||||
<div class="dict-content">
|
||||
<!-- 左侧:字典类型列表 -->
|
||||
<div class="dict-type-panel">
|
||||
<div class="panel-header">
|
||||
<h3>字典类型</h3>
|
||||
<el-input
|
||||
v-model="typeSearchKeyword"
|
||||
placeholder="搜索字典类型"
|
||||
clearable
|
||||
@input="handleTypeSearch"
|
||||
style="width: 200px"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
<el-table
|
||||
:data="filteredDictTypes"
|
||||
stripe
|
||||
highlight-current-row
|
||||
@current-change="handleTypeSelect"
|
||||
v-loading="typeLoading"
|
||||
style="width: 100%"
|
||||
height="calc(100vh - 280px)"
|
||||
>
|
||||
<el-table-column prop="dict_code" label="编码" width="150" />
|
||||
<el-table-column prop="dict_name" label="名称" min-width="120" />
|
||||
<el-table-column prop="status" label="状态" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'info'" size="small">
|
||||
{{ row.status === 1 ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="120" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" link @click="handleEditType(row)">
|
||||
<el-icon><Edit /></el-icon>
|
||||
</el-button>
|
||||
<el-button size="small" type="danger" link @click="handleDeleteType(row)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:字典项列表 -->
|
||||
<div class="dict-item-panel">
|
||||
<div class="panel-header">
|
||||
<h3>字典项</h3>
|
||||
<div class="panel-actions">
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="handleAddItem"
|
||||
:disabled="!currentDictType"
|
||||
>
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加字典项
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!currentDictType" class="empty-state">
|
||||
<el-empty description="请先选择字典类型" />
|
||||
</div>
|
||||
<el-table
|
||||
v-else
|
||||
:data="dictItems"
|
||||
stripe
|
||||
v-loading="itemLoading"
|
||||
style="width: 100%"
|
||||
height="calc(100vh - 280px)"
|
||||
>
|
||||
<el-table-column prop="dict_label" label="标签" min-width="120" />
|
||||
<el-table-column prop="dict_value" label="值" min-width="120" />
|
||||
<el-table-column prop="sort" label="排序" width="80" align="center" />
|
||||
<el-table-column prop="status" label="状态" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'info'" size="small">
|
||||
{{ row.status === 1 ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="color" label="颜色" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.color" :color="row.color" size="small">{{ row.color }}</el-tag>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="120" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" link @click="handleEditItem(row)">
|
||||
<el-icon><Edit /></el-icon>
|
||||
</el-button>
|
||||
<el-button size="small" type="danger" link @click="handleDeleteItem(row)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 字典类型编辑对话框 -->
|
||||
<el-dialog
|
||||
v-model="typeDialogVisible"
|
||||
:title="typeDialogTitle"
|
||||
width="600px"
|
||||
@close="handleTypeDialogClose"
|
||||
>
|
||||
<el-form :model="typeForm" :rules="typeRules" ref="typeFormRef" label-width="100px">
|
||||
<el-form-item label="字典编码" prop="dict_code">
|
||||
<el-input
|
||||
v-model="typeForm.dict_code"
|
||||
placeholder="请输入字典编码(字母、数字、下划线)"
|
||||
:disabled="typeForm.id > 0"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="字典名称" prop="dict_name">
|
||||
<el-input v-model="typeForm.dict_name" placeholder="请输入字典名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="父级字典" prop="parent_id">
|
||||
<el-select v-model="typeForm.parent_id" placeholder="请选择父级字典" clearable>
|
||||
<el-option
|
||||
v-for="type in dictTypes"
|
||||
:key="type.id"
|
||||
:label="type.dict_name"
|
||||
:value="type.id"
|
||||
:disabled="type.id === typeForm.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="typeForm.status">
|
||||
<el-radio :label="1">启用</el-radio>
|
||||
<el-radio :label="0">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序" prop="sort">
|
||||
<el-input-number v-model="typeForm.sort" :min="0" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input
|
||||
v-model="typeForm.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入备注"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="typeDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleTypeSubmit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 字典项编辑对话框 -->
|
||||
<el-dialog
|
||||
v-model="itemDialogVisible"
|
||||
:title="itemDialogTitle"
|
||||
width="600px"
|
||||
@close="handleItemDialogClose"
|
||||
>
|
||||
<el-form :model="itemForm" :rules="itemRules" ref="itemFormRef" label-width="100px">
|
||||
<el-form-item label="字典标签" prop="dict_label">
|
||||
<el-input v-model="itemForm.dict_label" placeholder="请输入字典标签(显示值)" />
|
||||
</el-form-item>
|
||||
<el-form-item label="字典值" prop="dict_value">
|
||||
<el-input
|
||||
v-model="itemForm.dict_value"
|
||||
placeholder="请输入字典值(存储值)"
|
||||
:disabled="itemForm.id > 0"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="父级项" prop="parent_id">
|
||||
<el-select v-model="itemForm.parent_id" placeholder="请选择父级项" clearable>
|
||||
<el-option
|
||||
v-for="item in dictItems"
|
||||
:key="item.id"
|
||||
:label="item.dict_label"
|
||||
:value="item.id"
|
||||
:disabled="item.id === itemForm.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="itemForm.status">
|
||||
<el-radio :label="1">启用</el-radio>
|
||||
<el-radio :label="0">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序" prop="sort">
|
||||
<el-input-number v-model="itemForm.sort" :min="0" />
|
||||
</el-form-item>
|
||||
<el-form-item label="颜色" prop="color">
|
||||
<el-input v-model="itemForm.color" placeholder="如:#1890ff" />
|
||||
</el-form-item>
|
||||
<el-form-item label="图标" prop="icon">
|
||||
<el-input v-model="itemForm.icon" placeholder="如:el-icon-success" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input
|
||||
v-model="itemForm.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入备注"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="itemDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleItemSubmit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Edit, Delete, Refresh, Search } from '@element-plus/icons-vue'
|
||||
import {
|
||||
getDictTypes,
|
||||
addDictType,
|
||||
updateDictType,
|
||||
deleteDictType,
|
||||
getDictItems,
|
||||
addDictItem,
|
||||
updateDictItem,
|
||||
deleteDictItem
|
||||
} from '@/api/dict'
|
||||
|
||||
// 数据
|
||||
const dictTypes = ref([])
|
||||
const filteredDictTypes = ref([])
|
||||
const dictItems = ref([])
|
||||
const currentDictType = ref(null)
|
||||
const typeSearchKeyword = ref('')
|
||||
const typeLoading = ref(false)
|
||||
const itemLoading = ref(false)
|
||||
|
||||
// 对话框
|
||||
const typeDialogVisible = ref(false)
|
||||
const itemDialogVisible = ref(false)
|
||||
const typeFormRef = ref(null)
|
||||
const itemFormRef = ref(null)
|
||||
|
||||
// 表单数据
|
||||
const typeForm = reactive({
|
||||
id: 0,
|
||||
dict_code: '',
|
||||
dict_name: '',
|
||||
parent_id: 0,
|
||||
status: 1,
|
||||
sort: 0,
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const itemForm = reactive({
|
||||
id: 0,
|
||||
dict_type_id: 0,
|
||||
dict_label: '',
|
||||
dict_value: '',
|
||||
parent_id: 0,
|
||||
status: 1,
|
||||
sort: 0,
|
||||
color: '',
|
||||
icon: '',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const typeRules = {
|
||||
dict_code: [
|
||||
{ required: true, message: '请输入字典编码', trigger: 'blur' },
|
||||
{ pattern: /^[A-Za-z0-9_]+$/, message: '字典编码只能包含字母、数字、下划线', trigger: 'blur' }
|
||||
],
|
||||
dict_name: [{ required: true, message: '请输入字典名称', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const itemRules = {
|
||||
dict_label: [{ required: true, message: '请输入字典标签', trigger: 'blur' }],
|
||||
dict_value: [{ required: true, message: '请输入字典值', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
// 计算属性
|
||||
const typeDialogTitle = computed(() => {
|
||||
return typeForm.id > 0 ? '编辑字典类型' : '添加字典类型'
|
||||
})
|
||||
|
||||
const itemDialogTitle = computed(() => {
|
||||
return itemForm.id > 0 ? '编辑字典项' : '添加字典项'
|
||||
})
|
||||
|
||||
// 方法
|
||||
const fetchDictTypes = async () => {
|
||||
typeLoading.value = true
|
||||
try {
|
||||
const res = await getDictTypes()
|
||||
if (res.success) {
|
||||
dictTypes.value = res.data || []
|
||||
filteredDictTypes.value = dictTypes.value
|
||||
} else {
|
||||
ElMessage.error(res.message || '获取字典类型失败')
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('获取字典类型失败: ' + error.message)
|
||||
} finally {
|
||||
typeLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const fetchDictItems = async (dictTypeId) => {
|
||||
if (!dictTypeId) {
|
||||
dictItems.value = []
|
||||
return
|
||||
}
|
||||
itemLoading.value = true
|
||||
try {
|
||||
const res = await getDictItems({ dict_type_id: dictTypeId })
|
||||
if (res.success) {
|
||||
dictItems.value = res.data || []
|
||||
} else {
|
||||
ElMessage.error(res.message || '获取字典项失败')
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('获取字典项失败: ' + error.message)
|
||||
} finally {
|
||||
itemLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleTypeSearch = () => {
|
||||
if (!typeSearchKeyword.value) {
|
||||
filteredDictTypes.value = dictTypes.value
|
||||
return
|
||||
}
|
||||
const keyword = typeSearchKeyword.value.toLowerCase()
|
||||
filteredDictTypes.value = dictTypes.value.filter(
|
||||
(type) =>
|
||||
type.dict_code.toLowerCase().includes(keyword) ||
|
||||
type.dict_name.toLowerCase().includes(keyword)
|
||||
)
|
||||
}
|
||||
|
||||
const handleTypeSelect = (row) => {
|
||||
currentDictType.value = row
|
||||
if (row) {
|
||||
fetchDictItems(row.id)
|
||||
} else {
|
||||
dictItems.value = []
|
||||
}
|
||||
}
|
||||
|
||||
const handleAddType = () => {
|
||||
Object.assign(typeForm, {
|
||||
id: 0,
|
||||
dict_code: '',
|
||||
dict_name: '',
|
||||
parent_id: 0,
|
||||
status: 1,
|
||||
sort: 0,
|
||||
remark: ''
|
||||
})
|
||||
typeDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleEditType = (row) => {
|
||||
Object.assign(typeForm, {
|
||||
id: row.id,
|
||||
dict_code: row.dict_code,
|
||||
dict_name: row.dict_name,
|
||||
parent_id: row.parent_id || 0,
|
||||
status: row.status,
|
||||
sort: row.sort || 0,
|
||||
remark: row.remark || ''
|
||||
})
|
||||
typeDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleDeleteType = async (row) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该字典类型吗?删除后该类型下的所有字典项也将无法使用。', '提示', {
|
||||
type: 'warning'
|
||||
})
|
||||
const res = await deleteDictType(row.id)
|
||||
if (res.success) {
|
||||
ElMessage.success('删除成功')
|
||||
if (currentDictType.value && currentDictType.value.id === row.id) {
|
||||
currentDictType.value = null
|
||||
dictItems.value = []
|
||||
}
|
||||
fetchDictTypes()
|
||||
} else {
|
||||
ElMessage.error(res.message || '删除失败')
|
||||
}
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('删除失败: ' + error.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleTypeSubmit = async () => {
|
||||
if (!typeFormRef.value) return
|
||||
await typeFormRef.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
try {
|
||||
let res
|
||||
if (typeForm.id > 0) {
|
||||
res = await updateDictType(typeForm.id, typeForm)
|
||||
} else {
|
||||
res = await addDictType(typeForm)
|
||||
}
|
||||
if (res.success) {
|
||||
ElMessage.success(typeForm.id > 0 ? '更新成功' : '添加成功')
|
||||
typeDialogVisible.value = false
|
||||
fetchDictTypes()
|
||||
if (currentDictType.value && currentDictType.value.id === typeForm.id) {
|
||||
fetchDictItems(typeForm.id)
|
||||
}
|
||||
} else {
|
||||
ElMessage.error(res.message || '操作失败')
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('操作失败: ' + error.message)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleTypeDialogClose = () => {
|
||||
typeFormRef.value?.resetFields()
|
||||
}
|
||||
|
||||
const handleAddItem = () => {
|
||||
if (!currentDictType.value) {
|
||||
ElMessage.warning('请先选择字典类型')
|
||||
return
|
||||
}
|
||||
Object.assign(itemForm, {
|
||||
id: 0,
|
||||
dict_type_id: currentDictType.value.id,
|
||||
dict_label: '',
|
||||
dict_value: '',
|
||||
parent_id: 0,
|
||||
status: 1,
|
||||
sort: 0,
|
||||
color: '',
|
||||
icon: '',
|
||||
remark: ''
|
||||
})
|
||||
itemDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleEditItem = (row) => {
|
||||
Object.assign(itemForm, {
|
||||
id: row.id,
|
||||
dict_type_id: row.dict_type_id,
|
||||
dict_label: row.dict_label,
|
||||
dict_value: row.dict_value,
|
||||
parent_id: row.parent_id || 0,
|
||||
status: row.status,
|
||||
sort: row.sort || 0,
|
||||
color: row.color || '',
|
||||
icon: row.icon || '',
|
||||
remark: row.remark || ''
|
||||
})
|
||||
itemDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleDeleteItem = async (row) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该字典项吗?', '提示', {
|
||||
type: 'warning'
|
||||
})
|
||||
const res = await deleteDictItem(row.id)
|
||||
if (res.success) {
|
||||
ElMessage.success('删除成功')
|
||||
fetchDictItems(currentDictType.value.id)
|
||||
} else {
|
||||
ElMessage.error(res.message || '删除失败')
|
||||
}
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('删除失败: ' + error.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleItemSubmit = async () => {
|
||||
if (!itemFormRef.value) return
|
||||
await itemFormRef.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
try {
|
||||
let res
|
||||
if (itemForm.id > 0) {
|
||||
res = await updateDictItem(itemForm.id, itemForm)
|
||||
} else {
|
||||
res = await addDictItem(itemForm)
|
||||
}
|
||||
if (res.success) {
|
||||
ElMessage.success(itemForm.id > 0 ? '更新成功' : '添加成功')
|
||||
itemDialogVisible.value = false
|
||||
fetchDictItems(currentDictType.value.id)
|
||||
} else {
|
||||
ElMessage.error(res.message || '操作失败')
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('操作失败: ' + error.message)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleItemDialogClose = () => {
|
||||
itemFormRef.value?.resetFields()
|
||||
}
|
||||
|
||||
const refresh = () => {
|
||||
fetchDictTypes()
|
||||
if (currentDictType.value) {
|
||||
fetchDictItems(currentDictType.value.id)
|
||||
}
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
fetchDictTypes()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.dict-container {
|
||||
padding: 20px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.header-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.dict-content {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
|
||||
.dict-type-panel,
|
||||
.dict-item-panel {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
padding: 15px;
|
||||
background: #fff;
|
||||
|
||||
.panel-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.panel-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
511
server/controllers/dict.go
Normal file
511
server/controllers/dict.go
Normal file
@ -0,0 +1,511 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"server/models"
|
||||
"server/services"
|
||||
"strconv"
|
||||
|
||||
"github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
// DictController 字典管理控制器
|
||||
type DictController struct {
|
||||
web.Controller
|
||||
}
|
||||
|
||||
// GetDictTypes 获取字典类型列表
|
||||
func (c *DictController) GetDictTypes() {
|
||||
// 获取租户ID
|
||||
tenantIdData := c.Ctx.Input.GetData("tenantId")
|
||||
tenantId := 0
|
||||
if tenantIdData != nil {
|
||||
if tid, ok := tenantIdData.(int); ok {
|
||||
tenantId = tid
|
||||
}
|
||||
}
|
||||
|
||||
parentId, _ := c.GetInt("parent_id", -1)
|
||||
statusStr := c.GetString("status")
|
||||
|
||||
var status *int8
|
||||
if statusStr != "" {
|
||||
s, _ := strconv.ParseInt(statusStr, 10, 8)
|
||||
statusVal := int8(s)
|
||||
status = &statusVal
|
||||
}
|
||||
|
||||
dictTypes, err := services.GetDictTypes(tenantId, parentId, status)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "查询字典类型失败: " + err.Error(),
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": true,
|
||||
"data": dictTypes,
|
||||
}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// GetDictTypeById 根据ID获取字典类型
|
||||
func (c *DictController) GetDictTypeById() {
|
||||
idStr := c.Ctx.Input.Param(":id")
|
||||
id, err := strconv.Atoi(idStr)
|
||||
if err != nil || id <= 0 {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "参数错误",
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
dictType, err := services.GetDictTypeById(id)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "字典类型不存在",
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": true,
|
||||
"data": dictType,
|
||||
}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// AddDictType 添加字典类型
|
||||
func (c *DictController) AddDictType() {
|
||||
var dictType models.DictType
|
||||
if err := json.Unmarshal(c.Ctx.Input.RequestBody, &dictType); err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "参数解析失败: " + err.Error(),
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
// 获取租户ID和用户名
|
||||
tenantIdData := c.Ctx.Input.GetData("tenantId")
|
||||
tenantId := 0
|
||||
if tenantIdData != nil {
|
||||
if tid, ok := tenantIdData.(int); ok {
|
||||
tenantId = tid
|
||||
}
|
||||
}
|
||||
usernameData := c.Ctx.Input.GetData("username")
|
||||
username := ""
|
||||
if usernameData != nil {
|
||||
if u, ok := usernameData.(string); ok {
|
||||
username = u
|
||||
}
|
||||
}
|
||||
|
||||
// 设置租户ID和创建人
|
||||
dictType.TenantId = tenantId
|
||||
dictType.CreateBy = username
|
||||
if dictType.Status == 0 {
|
||||
dictType.Status = 1 // 默认启用
|
||||
}
|
||||
|
||||
id, err := services.AddDictType(&dictType)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": true,
|
||||
"message": "添加成功",
|
||||
"data": map[string]interface{}{"id": id},
|
||||
}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// UpdateDictType 更新字典类型
|
||||
func (c *DictController) UpdateDictType() {
|
||||
idStr := c.Ctx.Input.Param(":id")
|
||||
id, err := strconv.Atoi(idStr)
|
||||
if err != nil || id <= 0 {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "参数错误",
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
var dictType models.DictType
|
||||
if err := json.Unmarshal(c.Ctx.Input.RequestBody, &dictType); err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "参数解析失败: " + err.Error(),
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
// 获取用户名
|
||||
usernameData := c.Ctx.Input.GetData("username")
|
||||
username := ""
|
||||
if usernameData != nil {
|
||||
if u, ok := usernameData.(string); ok {
|
||||
username = u
|
||||
}
|
||||
}
|
||||
|
||||
dictType.Id = id
|
||||
dictType.UpdateBy = username
|
||||
|
||||
err = services.UpdateDictType(&dictType)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": true,
|
||||
"message": "更新成功",
|
||||
}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// DeleteDictType 删除字典类型
|
||||
func (c *DictController) DeleteDictType() {
|
||||
idStr := c.Ctx.Input.Param(":id")
|
||||
id, err := strconv.Atoi(idStr)
|
||||
if err != nil || id <= 0 {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "参数错误",
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
// 获取租户ID
|
||||
tenantIdData := c.Ctx.Input.GetData("tenantId")
|
||||
tenantId := 0
|
||||
if tenantIdData != nil {
|
||||
if tid, ok := tenantIdData.(int); ok {
|
||||
tenantId = tid
|
||||
}
|
||||
}
|
||||
|
||||
err = services.DeleteDictType(id, tenantId)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": true,
|
||||
"message": "删除成功",
|
||||
}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// GetDictItems 获取字典项列表
|
||||
func (c *DictController) GetDictItems() {
|
||||
dictTypeId, _ := c.GetInt("dict_type_id", 0)
|
||||
if dictTypeId <= 0 {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "参数错误:dict_type_id 必填",
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
parentIdStr := c.GetString("parent_id")
|
||||
statusStr := c.GetString("status")
|
||||
|
||||
var parentId *int
|
||||
if parentIdStr != "" {
|
||||
pid, _ := strconv.Atoi(parentIdStr)
|
||||
parentId = &pid
|
||||
}
|
||||
|
||||
var status *int8
|
||||
if statusStr != "" {
|
||||
s, _ := strconv.ParseInt(statusStr, 10, 8)
|
||||
statusVal := int8(s)
|
||||
status = &statusVal
|
||||
}
|
||||
|
||||
dictItems, err := services.GetDictItems(dictTypeId, parentId, status)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "查询字典项失败: " + err.Error(),
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": true,
|
||||
"data": dictItems,
|
||||
}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// GetDictItemById 根据ID获取字典项
|
||||
func (c *DictController) GetDictItemById() {
|
||||
idStr := c.Ctx.Input.Param(":id")
|
||||
id, err := strconv.Atoi(idStr)
|
||||
if err != nil || id <= 0 {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "参数错误",
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
dictItem, err := services.GetDictItemById(id)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "字典项不存在",
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": true,
|
||||
"data": dictItem,
|
||||
}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// AddDictItem 添加字典项
|
||||
func (c *DictController) AddDictItem() {
|
||||
var dictItem models.DictItem
|
||||
if err := json.Unmarshal(c.Ctx.Input.RequestBody, &dictItem); err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "参数解析失败: " + err.Error(),
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
if dictItem.DictTypeId <= 0 {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "参数错误:dict_type_id 必填",
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
// 获取用户名
|
||||
usernameData := c.Ctx.Input.GetData("username")
|
||||
username := ""
|
||||
if usernameData != nil {
|
||||
if u, ok := usernameData.(string); ok {
|
||||
username = u
|
||||
}
|
||||
}
|
||||
|
||||
// 设置创建人
|
||||
dictItem.CreateBy = username
|
||||
if dictItem.Status == 0 {
|
||||
dictItem.Status = 1 // 默认启用
|
||||
}
|
||||
|
||||
id, err := services.AddDictItem(&dictItem)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": true,
|
||||
"message": "添加成功",
|
||||
"data": map[string]interface{}{"id": id},
|
||||
}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// UpdateDictItem 更新字典项
|
||||
func (c *DictController) UpdateDictItem() {
|
||||
idStr := c.Ctx.Input.Param(":id")
|
||||
id, err := strconv.Atoi(idStr)
|
||||
if err != nil || id <= 0 {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "参数错误",
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
var dictItem models.DictItem
|
||||
if err := json.Unmarshal(c.Ctx.Input.RequestBody, &dictItem); err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "参数解析失败: " + err.Error(),
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
// 获取用户名
|
||||
usernameData := c.Ctx.Input.GetData("username")
|
||||
username := ""
|
||||
if usernameData != nil {
|
||||
if u, ok := usernameData.(string); ok {
|
||||
username = u
|
||||
}
|
||||
}
|
||||
|
||||
dictItem.Id = id
|
||||
dictItem.UpdateBy = username
|
||||
|
||||
err = services.UpdateDictItem(&dictItem)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": true,
|
||||
"message": "更新成功",
|
||||
}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// DeleteDictItem 删除字典项
|
||||
func (c *DictController) DeleteDictItem() {
|
||||
idStr := c.Ctx.Input.Param(":id")
|
||||
id, err := strconv.Atoi(idStr)
|
||||
if err != nil || id <= 0 {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "参数错误",
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
err = services.DeleteDictItem(id)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": true,
|
||||
"message": "删除成功",
|
||||
}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// GetDictItemsByCode 根据字典编码获取字典项(用于业务查询)
|
||||
func (c *DictController) GetDictItemsByCode() {
|
||||
dictCode := c.Ctx.Input.Param(":code")
|
||||
if dictCode == "" {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "参数错误:code 必填",
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
// 获取租户ID
|
||||
tenantIdData := c.Ctx.Input.GetData("tenantId")
|
||||
tenantId := 0
|
||||
if tenantIdData != nil {
|
||||
if tid, ok := tenantIdData.(int); ok {
|
||||
tenantId = tid
|
||||
}
|
||||
}
|
||||
|
||||
includeDisabled := c.GetString("include_disabled") == "1"
|
||||
|
||||
dictItems, err := services.GetDictItemsByCode(dictCode, tenantId, includeDisabled)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "查询字典项失败: " + err.Error(),
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": true,
|
||||
"data": dictItems,
|
||||
}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// BatchUpdateDictItemSort 批量更新字典项排序
|
||||
func (c *DictController) BatchUpdateDictItemSort() {
|
||||
var items []struct {
|
||||
Id int `json:"id"`
|
||||
Sort int `json:"sort"`
|
||||
}
|
||||
if err := json.Unmarshal(c.Ctx.Input.RequestBody, &items); err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "参数解析失败: " + err.Error(),
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
err := services.BatchUpdateDictItemSort(items)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": true,
|
||||
"message": "更新成功",
|
||||
}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
58
server/models/dict.go
Normal file
58
server/models/dict.go
Normal file
@ -0,0 +1,58 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/beego/beego/v2/client/orm"
|
||||
)
|
||||
|
||||
// DictType 字典类型模型
|
||||
type DictType struct {
|
||||
Id int `orm:"auto" json:"id"`
|
||||
TenantId int `orm:"column(tenant_id);default(0)" json:"tenant_id"` // 0表示平台字典,>0表示租户字典
|
||||
DictCode string `orm:"column(dict_code);size(50)" json:"dict_code"` // 字典编码(唯一)
|
||||
DictName string `orm:"column(dict_name);size(100)" json:"dict_name"` // 字典名称
|
||||
ParentId int `orm:"column(parent_id);default(0)" json:"parent_id"` // 父级字典ID(支持多级)
|
||||
Status int8 `orm:"column(status);default(1)" json:"status"` // 0-禁用,1-启用
|
||||
Sort int `orm:"column(sort);default(0)" json:"sort"` // 排序号
|
||||
Remark string `orm:"column(remark);size(500);null" json:"remark"` // 备注
|
||||
CreateBy string `orm:"column(create_by);size(50);null" json:"create_by"`
|
||||
CreateTime time.Time `orm:"column(create_time);type(datetime);auto_now_add" json:"create_time"`
|
||||
UpdateBy string `orm:"column(update_by);size(50);null" json:"update_by"`
|
||||
UpdateTime time.Time `orm:"column(update_time);type(datetime);auto_now" json:"update_time"`
|
||||
IsDeleted int8 `orm:"column(is_deleted);default(0)" json:"is_deleted"` // 0-未删,1-已删
|
||||
}
|
||||
|
||||
// TableName 设置表名
|
||||
func (d *DictType) TableName() string {
|
||||
return "sys_dict_type"
|
||||
}
|
||||
|
||||
// DictItem 字典项模型
|
||||
type DictItem struct {
|
||||
Id int `orm:"auto" json:"id"`
|
||||
DictTypeId int `orm:"column(dict_type_id)" json:"dict_type_id"` // 关联字典类型ID
|
||||
DictLabel string `orm:"column(dict_label);size(100)" json:"dict_label"` // 字典标签(显示值)
|
||||
DictValue string `orm:"column(dict_value);size(100)" json:"dict_value"` // 字典值(存储值)
|
||||
ParentId int `orm:"column(parent_id);default(0)" json:"parent_id"` // 父级字典项ID(支持多级)
|
||||
Status int8 `orm:"column(status);default(1)" json:"status"` // 0-禁用,1-启用
|
||||
Sort int `orm:"column(sort);default(0)" json:"sort"` // 排序号
|
||||
Color string `orm:"column(color);size(20);null" json:"color"` // 颜色标记
|
||||
Icon string `orm:"column(icon);size(50);null" json:"icon"` // 图标
|
||||
Remark string `orm:"column(remark);size(500);null" json:"remark"` // 备注
|
||||
CreateBy string `orm:"column(create_by);size(50);null" json:"create_by"`
|
||||
CreateTime time.Time `orm:"column(create_time);type(datetime);auto_now_add" json:"create_time"`
|
||||
UpdateBy string `orm:"column(update_by);size(50);null" json:"update_by"`
|
||||
UpdateTime time.Time `orm:"column(update_time);type(datetime);auto_now" json:"update_time"`
|
||||
IsDeleted int8 `orm:"column(is_deleted);default(0)" json:"is_deleted"` // 0-未删,1-已删
|
||||
}
|
||||
|
||||
// TableName 设置表名
|
||||
func (d *DictItem) TableName() string {
|
||||
return "sys_dict_item"
|
||||
}
|
||||
|
||||
func init() {
|
||||
orm.RegisterModel(new(DictType))
|
||||
orm.RegisterModel(new(DictItem))
|
||||
}
|
||||
@ -324,7 +324,6 @@ func GetAllMenuPermissionsForUser(userId int, userType string, roleId int) ([]*M
|
||||
|
||||
// 分配角色权限,更新role菜单ID
|
||||
func AssignRolePermissions(roleId int, menuIds []int, createBy string) error {
|
||||
o := orm.NewOrm()
|
||||
var jsonData []byte
|
||||
var err error
|
||||
if len(menuIds) == 0 {
|
||||
@ -335,11 +334,54 @@ func AssignRolePermissions(roleId int, menuIds []int, createBy string) error {
|
||||
return fmt.Errorf("序列化菜单ID失败: %v", err)
|
||||
}
|
||||
}
|
||||
_, err = o.Raw("UPDATE yz_roles SET menu_ids = ?, update_by = ?, update_time = NOW() WHERE role_id = ?", string(jsonData), createBy, roleId).Exec()
|
||||
if err != nil {
|
||||
return fmt.Errorf("更新角色权限失败: %v", err)
|
||||
|
||||
// 使用重试机制处理连接失效问题
|
||||
maxRetries := 3
|
||||
var lastErr error
|
||||
|
||||
for i := 0; i < maxRetries; i++ {
|
||||
o := orm.NewOrm()
|
||||
|
||||
// 在执行前检查连接有效性
|
||||
db, dbErr := orm.GetDB("default")
|
||||
if dbErr == nil {
|
||||
// 尝试 Ping 检查连接
|
||||
if pingErr := db.Ping(); pingErr != nil {
|
||||
// 连接失效,等待后重试
|
||||
if i < maxRetries-1 {
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
lastErr = fmt.Errorf("数据库连接失效: %v", pingErr)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试执行更新
|
||||
_, err = o.Raw("UPDATE yz_roles SET menu_ids = ?, update_by = ?, update_time = NOW() WHERE role_id = ?", string(jsonData), createBy, roleId).Exec()
|
||||
if err == nil {
|
||||
// 成功,返回
|
||||
return nil
|
||||
}
|
||||
|
||||
// 检查是否是连接错误
|
||||
errStr := err.Error()
|
||||
if strings.Contains(errStr, "invalid connection") ||
|
||||
strings.Contains(errStr, "driver: bad connection") ||
|
||||
strings.Contains(errStr, "connection reset") {
|
||||
lastErr = err
|
||||
// 如果是连接错误且不是最后一次重试,等待后继续
|
||||
if i < maxRetries-1 {
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
// 非连接错误,直接返回
|
||||
return fmt.Errorf("更新角色权限失败: %v", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
// 所有重试都失败了
|
||||
return fmt.Errorf("更新角色权限失败(已重试%d次): %v", maxRetries, lastErr)
|
||||
}
|
||||
|
||||
// 获取用户所有权限(从所属角色)
|
||||
|
||||
@ -44,6 +44,8 @@ func Init(version string) {
|
||||
orm.RegisterModel(new(Knowledge))
|
||||
orm.RegisterModel(new(KnowledgeCategory))
|
||||
orm.RegisterModel(new(KnowledgeTag))
|
||||
orm.RegisterModel(new(DictType))
|
||||
orm.RegisterModel(new(DictItem))
|
||||
|
||||
ormConfig, err := beego.AppConfig.String("orm")
|
||||
if err != nil {
|
||||
@ -60,6 +62,7 @@ func Init(version string) {
|
||||
}
|
||||
|
||||
// 构建连接字符串,添加连接池和性能优化参数
|
||||
// Go MySQL驱动会自动通过连接池管理连接,不需要额外的reconnect参数
|
||||
dsn := user + ":" + pass + "@tcp(" + urls + ")/" + dbName + "?charset=utf8mb4&parseTime=True&loc=Local&timeout=10s&readTimeout=30s&writeTimeout=30s"
|
||||
fmt.Println("数据库连接字符串:", dsn)
|
||||
|
||||
@ -76,9 +79,12 @@ func Init(version string) {
|
||||
}
|
||||
|
||||
// 设置连接池参数
|
||||
dbConn.SetMaxIdleConns(10) // 设置空闲连接池中连接的最大数量
|
||||
dbConn.SetMaxOpenConns(100) // 设置打开数据库连接的最大数量
|
||||
dbConn.SetConnMaxLifetime(time.Hour) // 设置连接可复用的最大时间
|
||||
dbConn.SetMaxIdleConns(10) // 设置空闲连接池中连接的最大数量
|
||||
dbConn.SetMaxOpenConns(100) // 设置打开数据库连接的最大数量
|
||||
dbConn.SetConnMaxLifetime(30 * time.Minute) // 设置连接可复用的最大时间(30分钟,避免连接过期)
|
||||
// 注意:SetConnMaxIdleTime 在 Go 1.15+ 和 database/sql 1.4+ 中可用
|
||||
// 如果版本不支持,可以移除这一行
|
||||
// dbConn.SetConnMaxIdleTime(10 * time.Minute) // 设置空闲连接的最大空闲时间
|
||||
|
||||
fmt.Println("数据库连接成功!")
|
||||
fmt.Printf("当前项目版本: %s\n", version)
|
||||
|
||||
@ -315,6 +315,14 @@ func init() {
|
||||
beego.Router("/api/dashboard/platform-stats", &controllers.DashboardController{}, "get:GetPlatformStats")
|
||||
beego.Router("/api/dashboard/tenant-stats", &controllers.DashboardController{}, "get:GetTenantStats")
|
||||
|
||||
// 字典管理路由
|
||||
beego.Router("/api/dict/types", &controllers.DictController{}, "get:GetDictTypes;post:AddDictType")
|
||||
beego.Router("/api/dict/types/:id", &controllers.DictController{}, "get:GetDictTypeById;put:UpdateDictType;delete:DeleteDictType")
|
||||
beego.Router("/api/dict/items", &controllers.DictController{}, "get:GetDictItems;post:AddDictItem")
|
||||
beego.Router("/api/dict/items/:id", &controllers.DictController{}, "get:GetDictItemById;put:UpdateDictItem;delete:DeleteDictItem")
|
||||
beego.Router("/api/dict/items/code/:code", &controllers.DictController{}, "get:GetDictItemsByCode")
|
||||
beego.Router("/api/dict/items/sort", &controllers.DictController{}, "put:BatchUpdateDictItemSort")
|
||||
|
||||
// 手动配置特殊路由(无法通过自动路由处理的)
|
||||
beego.Router("/api/allmenu", &controllers.MenuController{}, "get:GetAllMenus")
|
||||
beego.Router("/api/program-categories/public", &controllers.ProgramCategoryController{}, "get:GetProgramCategoriesPublic")
|
||||
|
||||
373
server/services/dict.go
Normal file
373
server/services/dict.go
Normal file
@ -0,0 +1,373 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"server/models"
|
||||
"time"
|
||||
|
||||
"github.com/beego/beego/v2/client/orm"
|
||||
)
|
||||
|
||||
// ========== 字典类型相关服务 ==========
|
||||
|
||||
// AddDictType 添加字典类型
|
||||
func AddDictType(dictType *models.DictType) (int64, error) {
|
||||
// 校验字典编码格式(只能包含字母、数字、下划线)
|
||||
matched, err := regexp.MatchString(`^[A-Za-z0-9_]+$`, dictType.DictCode)
|
||||
if err != nil || !matched {
|
||||
return 0, fmt.Errorf("字典编码格式错误,只能包含字母、数字、下划线")
|
||||
}
|
||||
|
||||
o := orm.NewOrm()
|
||||
|
||||
// 检查字典编码是否已存在(同一租户下唯一)
|
||||
existType := &models.DictType{}
|
||||
err = o.Raw("SELECT id FROM sys_dict_type WHERE dict_code = ? AND tenant_id = ? AND is_deleted = 0",
|
||||
dictType.DictCode, dictType.TenantId).QueryRow(existType)
|
||||
if err == nil {
|
||||
return 0, fmt.Errorf("字典编码 %s 已存在", dictType.DictCode)
|
||||
}
|
||||
|
||||
// 插入新字典类型
|
||||
id, err := o.Insert(dictType)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("添加字典类型失败: %v", err)
|
||||
}
|
||||
|
||||
// 清除相关缓存
|
||||
clearDictCache(dictType.TenantId, dictType.DictCode)
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// UpdateDictType 更新字典类型
|
||||
func UpdateDictType(dictType *models.DictType) error {
|
||||
o := orm.NewOrm()
|
||||
|
||||
// 获取原字典类型信息
|
||||
oldType := &models.DictType{Id: dictType.Id}
|
||||
err := o.Read(oldType)
|
||||
if err != nil {
|
||||
return fmt.Errorf("字典类型不存在: %v", err)
|
||||
}
|
||||
|
||||
// 不允许修改 dict_code(避免业务引用失效)
|
||||
if oldType.DictCode != dictType.DictCode {
|
||||
return fmt.Errorf("不允许修改字典编码")
|
||||
}
|
||||
|
||||
// 更新字段(排除 dict_code)
|
||||
_, err = o.Update(dictType, "dict_name", "parent_id", "status", "sort", "remark", "update_by", "update_time")
|
||||
if err != nil {
|
||||
return fmt.Errorf("更新字典类型失败: %v", err)
|
||||
}
|
||||
|
||||
// 清除相关缓存
|
||||
clearDictCache(dictType.TenantId, dictType.DictCode)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteDictType 删除字典类型(逻辑删除)
|
||||
func DeleteDictType(id int, tenantId int) error {
|
||||
o := orm.NewOrm()
|
||||
|
||||
// 检查是否存在
|
||||
dictType := &models.DictType{Id: id}
|
||||
err := o.Read(dictType)
|
||||
if err != nil {
|
||||
return fmt.Errorf("字典类型不存在: %v", err)
|
||||
}
|
||||
|
||||
// 检查租户权限
|
||||
if dictType.TenantId != tenantId {
|
||||
return fmt.Errorf("无权操作该字典类型")
|
||||
}
|
||||
|
||||
// 检查是否关联字典项
|
||||
var count int
|
||||
err = o.Raw("SELECT COUNT(*) FROM sys_dict_item WHERE dict_type_id = ? AND is_deleted = 0", id).QueryRow(&count)
|
||||
if err == nil && count > 0 {
|
||||
return fmt.Errorf("该字典类型下存在字典项,请先删除字典项")
|
||||
}
|
||||
|
||||
// 逻辑删除
|
||||
dictType.IsDeleted = 1
|
||||
dictType.UpdateTime = time.Now()
|
||||
_, err = o.Update(dictType, "is_deleted", "update_time")
|
||||
if err != nil {
|
||||
return fmt.Errorf("删除字典类型失败: %v", err)
|
||||
}
|
||||
|
||||
// 清除相关缓存
|
||||
clearDictCache(dictType.TenantId, dictType.DictCode)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetDictTypeById 根据ID获取字典类型
|
||||
func GetDictTypeById(id int) (*models.DictType, error) {
|
||||
o := orm.NewOrm()
|
||||
dictType := &models.DictType{Id: id}
|
||||
err := o.Read(dictType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if dictType.IsDeleted == 1 {
|
||||
return nil, fmt.Errorf("字典类型已删除")
|
||||
}
|
||||
return dictType, nil
|
||||
}
|
||||
|
||||
// GetDictTypes 获取字典类型列表
|
||||
func GetDictTypes(tenantId int, parentId int, status *int8) ([]*models.DictType, error) {
|
||||
o := orm.NewOrm()
|
||||
qs := o.QueryTable("sys_dict_type").Filter("is_deleted", 0)
|
||||
|
||||
// 租户过滤:0表示平台字典,>0表示租户字典
|
||||
if tenantId > 0 {
|
||||
// 租户用户只能看到自己的字典和平台字典
|
||||
qs = qs.Filter("tenant_id__in", 0, tenantId)
|
||||
} else {
|
||||
// 平台用户只能看到平台字典
|
||||
qs = qs.Filter("tenant_id", 0)
|
||||
}
|
||||
|
||||
// 父级过滤
|
||||
if parentId >= 0 {
|
||||
qs = qs.Filter("parent_id", parentId)
|
||||
}
|
||||
|
||||
// 状态过滤
|
||||
if status != nil {
|
||||
qs = qs.Filter("status", *status)
|
||||
}
|
||||
|
||||
var dictTypes []*models.DictType
|
||||
_, err := qs.OrderBy("sort", "create_time").All(&dictTypes)
|
||||
return dictTypes, err
|
||||
}
|
||||
|
||||
// ========== 字典项相关服务 ==========
|
||||
|
||||
// AddDictItem 添加字典项
|
||||
func AddDictItem(dictItem *models.DictItem) (int64, error) {
|
||||
o := orm.NewOrm()
|
||||
|
||||
// 检查字典类型是否存在
|
||||
dictType := &models.DictType{Id: dictItem.DictTypeId}
|
||||
err := o.Read(dictType)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("字典类型不存在: %v", err)
|
||||
}
|
||||
if dictType.IsDeleted == 1 || dictType.Status == 0 {
|
||||
return 0, fmt.Errorf("字典类型已删除或已禁用")
|
||||
}
|
||||
|
||||
// 检查同一字典类型下 dict_value 是否已存在
|
||||
existItem := &models.DictItem{}
|
||||
err = o.Raw("SELECT id FROM sys_dict_item WHERE dict_type_id = ? AND dict_value = ? AND is_deleted = 0",
|
||||
dictItem.DictTypeId, dictItem.DictValue).QueryRow(existItem)
|
||||
if err == nil {
|
||||
return 0, fmt.Errorf("字典值 %s 已存在", dictItem.DictValue)
|
||||
}
|
||||
|
||||
// 插入新字典项
|
||||
id, err := o.Insert(dictItem)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("添加字典项失败: %v", err)
|
||||
}
|
||||
|
||||
// 清除相关缓存
|
||||
clearDictCacheByTypeId(dictItem.DictTypeId)
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// UpdateDictItem 更新字典项
|
||||
func UpdateDictItem(dictItem *models.DictItem) error {
|
||||
o := orm.NewOrm()
|
||||
|
||||
// 获取原字典项信息
|
||||
oldItem := &models.DictItem{Id: dictItem.Id}
|
||||
err := o.Read(oldItem)
|
||||
if err != nil {
|
||||
return fmt.Errorf("字典项不存在: %v", err)
|
||||
}
|
||||
|
||||
// 不允许修改 dict_value(避免业务存储值与字典不匹配)
|
||||
if oldItem.DictValue != dictItem.DictValue {
|
||||
return fmt.Errorf("不允许修改字典值")
|
||||
}
|
||||
|
||||
// 如果修改了 dict_type_id,需要检查新类型是否存在
|
||||
if oldItem.DictTypeId != dictItem.DictTypeId {
|
||||
dictType := &models.DictType{Id: dictItem.DictTypeId}
|
||||
err = o.Read(dictType)
|
||||
if err != nil {
|
||||
return fmt.Errorf("字典类型不存在: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 更新字段(排除 dict_value)
|
||||
_, err = o.Update(dictItem, "dict_label", "dict_type_id", "parent_id", "status", "sort", "color", "icon", "remark", "update_by", "update_time")
|
||||
if err != nil {
|
||||
return fmt.Errorf("更新字典项失败: %v", err)
|
||||
}
|
||||
|
||||
// 清除相关缓存
|
||||
clearDictCacheByTypeId(oldItem.DictTypeId)
|
||||
if oldItem.DictTypeId != dictItem.DictTypeId {
|
||||
clearDictCacheByTypeId(dictItem.DictTypeId)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteDictItem 删除字典项(逻辑删除)
|
||||
func DeleteDictItem(id int) error {
|
||||
o := orm.NewOrm()
|
||||
|
||||
// 检查是否存在
|
||||
dictItem := &models.DictItem{Id: id}
|
||||
err := o.Read(dictItem)
|
||||
if err != nil {
|
||||
return fmt.Errorf("字典项不存在: %v", err)
|
||||
}
|
||||
|
||||
// 检查是否有子项
|
||||
var count int
|
||||
err = o.Raw("SELECT COUNT(*) FROM sys_dict_item WHERE parent_id = ? AND is_deleted = 0", id).QueryRow(&count)
|
||||
if err == nil && count > 0 {
|
||||
return fmt.Errorf("该字典项下存在子项,请先删除子项")
|
||||
}
|
||||
|
||||
// 逻辑删除
|
||||
dictItem.IsDeleted = 1
|
||||
dictItem.UpdateTime = time.Now()
|
||||
_, err = o.Update(dictItem, "is_deleted", "update_time")
|
||||
if err != nil {
|
||||
return fmt.Errorf("删除字典项失败: %v", err)
|
||||
}
|
||||
|
||||
// 清除相关缓存
|
||||
clearDictCacheByTypeId(dictItem.DictTypeId)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetDictItemById 根据ID获取字典项
|
||||
func GetDictItemById(id int) (*models.DictItem, error) {
|
||||
o := orm.NewOrm()
|
||||
dictItem := &models.DictItem{Id: id}
|
||||
err := o.Read(dictItem)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if dictItem.IsDeleted == 1 {
|
||||
return nil, fmt.Errorf("字典项已删除")
|
||||
}
|
||||
return dictItem, nil
|
||||
}
|
||||
|
||||
// GetDictItems 获取字典项列表
|
||||
func GetDictItems(dictTypeId int, parentId *int, status *int8) ([]*models.DictItem, error) {
|
||||
o := orm.NewOrm()
|
||||
qs := o.QueryTable("sys_dict_item").Filter("is_deleted", 0).Filter("dict_type_id", dictTypeId)
|
||||
|
||||
// 父级过滤
|
||||
if parentId != nil {
|
||||
qs = qs.Filter("parent_id", *parentId)
|
||||
}
|
||||
|
||||
// 状态过滤
|
||||
if status != nil {
|
||||
qs = qs.Filter("status", *status)
|
||||
}
|
||||
|
||||
var dictItems []*models.DictItem
|
||||
_, err := qs.OrderBy("sort", "create_time").All(&dictItems)
|
||||
return dictItems, err
|
||||
}
|
||||
|
||||
// GetDictItemsByCode 根据字典编码获取字典项列表(支持缓存)
|
||||
func GetDictItemsByCode(dictCode string, tenantId int, includeDisabled bool) ([]*models.DictItem, error) {
|
||||
o := orm.NewOrm()
|
||||
|
||||
// 先查询字典类型
|
||||
dictType := &models.DictType{}
|
||||
err := o.Raw("SELECT * FROM sys_dict_type WHERE dict_code = ? AND tenant_id = ? AND is_deleted = 0 AND status = 1",
|
||||
dictCode, tenantId).QueryRow(dictType)
|
||||
if err != nil {
|
||||
// 如果租户字典不存在,尝试查询平台字典
|
||||
if tenantId > 0 {
|
||||
err = o.Raw("SELECT * FROM sys_dict_type WHERE dict_code = ? AND tenant_id = 0 AND is_deleted = 0 AND status = 1",
|
||||
dictCode).QueryRow(dictType)
|
||||
if err != nil {
|
||||
return []*models.DictItem{}, nil // 返回空列表而不是错误
|
||||
}
|
||||
} else {
|
||||
return []*models.DictItem{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// 查询字典项
|
||||
qs := o.QueryTable("sys_dict_item").Filter("dict_type_id", dictType.Id).Filter("is_deleted", 0)
|
||||
if !includeDisabled {
|
||||
qs = qs.Filter("status", 1)
|
||||
}
|
||||
|
||||
var dictItems []*models.DictItem
|
||||
_, err = qs.OrderBy("sort", "create_time").All(&dictItems)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dictItems, nil
|
||||
}
|
||||
|
||||
// ========== 缓存相关(简化版,实际应使用Redis) ==========
|
||||
|
||||
// 清除字典缓存(简化实现,实际应使用Redis)
|
||||
func clearDictCache(tenantId int, dictCode string) {
|
||||
// TODO: 实现Redis缓存清除
|
||||
// 格式:SYS_DICT:ITEM:{tenantId}:{dictCode}
|
||||
}
|
||||
|
||||
// 根据字典类型ID清除缓存
|
||||
func clearDictCacheByTypeId(dictTypeId int) {
|
||||
o := orm.NewOrm()
|
||||
dictType := &models.DictType{Id: dictTypeId}
|
||||
err := o.Read(dictType)
|
||||
if err == nil {
|
||||
clearDictCache(dictType.TenantId, dictType.DictCode)
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 批量操作 ==========
|
||||
|
||||
// BatchUpdateDictItemSort 批量更新字典项排序
|
||||
func BatchUpdateDictItemSort(items []struct {
|
||||
Id int `json:"id"`
|
||||
Sort int `json:"sort"`
|
||||
}) error {
|
||||
o := orm.NewOrm()
|
||||
|
||||
for _, item := range items {
|
||||
dictItem := &models.DictItem{Id: item.Id}
|
||||
err := o.Read(dictItem)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
dictItem.Sort = item.Sort
|
||||
dictItem.UpdateTime = time.Now()
|
||||
_, err = o.Update(dictItem, "sort", "update_time")
|
||||
if err != nil {
|
||||
return fmt.Errorf("更新字典项排序失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
45
server/sql/dict_tables.sql
Normal file
45
server/sql/dict_tables.sql
Normal file
@ -0,0 +1,45 @@
|
||||
-- 字典类型表
|
||||
CREATE TABLE IF NOT EXISTS `sys_dict_type` (
|
||||
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`tenant_id` int(11) NOT NULL DEFAULT '0' COMMENT '租户ID(0表示平台字典,>0表示租户字典)',
|
||||
`dict_code` varchar(50) NOT NULL COMMENT '字典编码(唯一,如 USER_STATUS)',
|
||||
`dict_name` varchar(100) NOT NULL COMMENT '字典名称(如 用户状态)',
|
||||
`parent_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '父级字典ID(0表示一级字典)',
|
||||
`status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '状态(0-禁用,1-启用)',
|
||||
`sort` int(11) NOT NULL DEFAULT '0' COMMENT '排序号',
|
||||
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
|
||||
`create_by` varchar(50) DEFAULT NULL COMMENT '创建人',
|
||||
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`update_by` varchar(50) DEFAULT NULL COMMENT '更新人',
|
||||
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '逻辑删除(0-未删,1-已删)',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_dict_code_tenant` (`dict_code`, `is_deleted`),
|
||||
KEY `idx_parent_id` (`parent_id`, `is_deleted`),
|
||||
KEY `idx_status` (`status`, `is_deleted`),
|
||||
KEY `idx_tenant_id` (`tenant_id`, `is_deleted`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='字典类型表';
|
||||
|
||||
-- 字典项表
|
||||
CREATE TABLE IF NOT EXISTS `sys_dict_item` (
|
||||
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`dict_type_id` bigint(20) NOT NULL COMMENT '字典类型ID',
|
||||
`dict_label` varchar(100) NOT NULL COMMENT '字典标签(显示值,如 正常)',
|
||||
`dict_value` varchar(100) NOT NULL COMMENT '字典值(存储值,如 1)',
|
||||
`parent_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '父级字典项ID(0表示一级项)',
|
||||
`status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '状态(0-禁用,1-启用)',
|
||||
`sort` int(11) NOT NULL DEFAULT '0' COMMENT '排序号',
|
||||
`color` varchar(20) DEFAULT NULL COMMENT '颜色标记(如 #1890ff)',
|
||||
`icon` varchar(50) DEFAULT NULL COMMENT '图标(如 el-icon-success)',
|
||||
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
|
||||
`create_by` varchar(50) DEFAULT NULL COMMENT '创建人',
|
||||
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`update_by` varchar(50) DEFAULT NULL COMMENT '更新人',
|
||||
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '逻辑删除(0-未删,1-已删)',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_dict_type_parent_status` (`dict_type_id`, `parent_id`, `status`, `is_deleted`),
|
||||
UNIQUE KEY `uk_dict_type_value` (`dict_type_id`, `dict_value`, `is_deleted`),
|
||||
KEY `idx_parent_id` (`parent_id`, `status`, `is_deleted`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='字典项表';
|
||||
|
||||
Loading…
Reference in New Issue
Block a user