增加字典

This commit is contained in:
2025-11-07 17:38:59 +08:00
parent b159d71a77
commit 8b8b0c54a1
10 changed files with 1892 additions and 97 deletions
+108
View 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
})
}
+117 -89
View File
@@ -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
View 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>