更新架构

This commit is contained in:
李志强 2026-01-27 18:01:54 +08:00
parent 2420ce7660
commit da378a01be
33 changed files with 1490 additions and 239 deletions

2
components.d.ts vendored
View File

@ -47,6 +47,7 @@ declare module 'vue' {
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
ElOption: typeof import('element-plus/es')['ElOption']
ElPagination: typeof import('element-plus/es')['ElPagination']
ElPopover: typeof import('element-plus/es')['ElPopover']
ElProgress: typeof import('element-plus/es')['ElProgress']
ElRadio: typeof import('element-plus/es')['ElRadio']
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
@ -61,6 +62,7 @@ declare module 'vue' {
ElTabPane: typeof import('element-plus/es')['ElTabPane']
ElTabs: typeof import('element-plus/es')['ElTabs']
ElTag: typeof import('element-plus/es')['ElTag']
ElTooltip: typeof import('element-plus/es')['ElTooltip']
ElTree: typeof import('element-plus/es')['ElTree']
ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect']
ElUpload: typeof import('element-plus/es')['ElUpload']

61
src/api/modules.js Normal file
View File

@ -0,0 +1,61 @@
import request from '@/utils/request';
export function getModulesList() {
return request({
url: '/admin/modules/list',
method: 'get',
});
}
export function getModuleDetail(id) {
return request({
url: `/admin/modules/${id}`,
method: 'get',
});
}
export function addModule(data) {
return request({
url: '/admin/modules',
method: 'post',
data,
});
}
export function editModule(id, data) {
return request({
url: `/admin/modules/${id}`,
method: 'put',
data,
});
}
export function deleteModule(id) {
return request({
url: `/admin/modules/${id}`,
method: 'delete',
});
}
export function batchDeleteModules(ids) {
return request({
url: '/admin/modules/batchDelete',
method: 'post',
data: { ids },
});
}
export function changeModuleStatus(id, status) {
return request({
url: '/admin/modules/status',
method: 'post',
data: { id, status },
});
}
export function getModulesSelectList() {
return request({
url: '/admin/modules/select/list',
method: 'get',
});
}

View File

@ -27,16 +27,16 @@
:default-active="route.path"
>
<!-- 菜单标题 -->
<h3>{{ isCollapse ? '管理' : '云泽管理后台' }}</h3>
<h3>{{ isCollapse ? "管理" : currentModule?.title || "子菜单" }}</h3>
<!-- 固定仪表盘 -->
<el-menu-item index="/dashboard">
<i class="fa-solid fa-gauge"></i>
<template #title>仪表盘</template>
<!-- 无模块时显示提示 -->
<el-menu-item v-if="!currentModule" index="/home">
<i class="fa-solid fa-house"></i>
<template #title>返回首页</template>
</el-menu-item>
<!-- 动态菜单项 -->
<template v-for="item in list" :key="item.id">
<template v-for="item in displayMenus" :key="item.id">
<!-- 如果没有子菜单渲染为菜单项 -->
<el-menu-item
v-if="!item.children || item.children.length === 0"
@ -82,12 +82,21 @@
</template>
<!-- 继续递归渲染子菜单 -->
<template v-for="grandchild in child.children" :key="grandchild.id">
<template
v-for="grandchild in child.children"
:key="grandchild.id"
>
<el-menu-item
v-if="!grandchild.children || grandchild.children.length === 0"
v-if="
!grandchild.children || grandchild.children.length === 0
"
:index="grandchild.path || grandchild.id.toString()"
>
<i v-if="grandchild.icon" :class="grandchild.icon" class="menu-icon"></i>
<i
v-if="grandchild.icon"
:class="grandchild.icon"
class="menu-icon"
></i>
<template #title>
<span>{{ grandchild.title }}</span>
</template>
@ -99,16 +108,29 @@
:unique-opened="true"
>
<template #title>
<i v-if="grandchild.icon" :class="grandchild.icon" class="menu-icon"></i>
<i
v-if="grandchild.icon"
:class="grandchild.icon"
class="menu-icon"
></i>
<span>{{ grandchild.title }}</span>
</template>
<!-- 继续递归渲染... -->
<template v-for="greatGrandchild in grandchild.children" :key="greatGrandchild.id">
<el-menu-item
:index="greatGrandchild.path || greatGrandchild.id.toString()"
<template
v-for="greatGrandchild in grandchild.children"
:key="greatGrandchild.id"
>
<i v-if="greatGrandchild.icon" :class="greatGrandchild.icon" class="menu-icon"></i>
<el-menu-item
:index="
greatGrandchild.path || greatGrandchild.id.toString()
"
>
<i
v-if="greatGrandchild.icon"
:class="greatGrandchild.icon"
class="menu-icon"
></i>
<template #title>
<span>{{ greatGrandchild.title }}</span>
</template>
@ -125,9 +147,9 @@
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from "vue";
import { ref, computed, onMounted, onUnmounted, watch } from "vue";
import { useRouter, useRoute } from "vue-router";
import { Document, Warning } from '@element-plus/icons-vue';
import { Document, Warning } from "@element-plus/icons-vue";
import { useAllDataStore, useMenuStore } from "@/stores";
const emit = defineEmits(["menu-click"]);
@ -137,29 +159,102 @@ const route = useRoute();
const menuStore = useMenuStore();
const loading = computed(() => menuStore.loading);
const hasError = computed(() => menuStore.error);
const errorMsg = computed(() => menuStore.error || '加载菜单失败');
const errorMsg = computed(() => menuStore.error || "加载菜单失败");
const store = useAllDataStore();
const isCollapse = computed(() => store.state.isCollapse);
const width = computed(() => (store.state.isCollapse ? "64px" : "200px"));
//
const asideBgColor = ref('#304156');
const asideTextColor = ref('#bfcbd9');
const activeColor = ref('#409EFF');
const activeBgColor = ref('#409EFF');
const asideBgColor = ref("#304156");
const asideTextColor = ref("#bfcbd9");
const activeColor = ref("#3973FF");
const activeBgColor = ref("#3973FF");
const list = computed(() => {
const menuData = menuStore.menus;
if (!menuData || menuData.length === 0) {
return [];
const currentModuleId = ref(null);
const findMenuItem = (menus, targetIndex) => {
for (const menu of menus) {
if (menu.path === targetIndex) {
return menu;
}
if (menu.children && menu.children.length > 0) {
const found = findMenuItem(menu.children, targetIndex);
if (found) return found;
}
}
return null;
};
const findParentModule = (menus, currentPath) => {
for (const menu of menus) {
if (!menu.path || menu.path === "/home") {
if (menu.children && menu.children.length > 0) {
const found = findParentModule(menu.children, currentPath);
if (found) return found;
}
continue;
}
if (currentPath === menu.path || currentPath.startsWith(menu.path + "/")) {
return menu;
}
const processMenus = (menus) => {
if (menu.children && menu.children.length > 0) {
for (const child of menu.children) {
if (!child.path) continue;
if (
currentPath === child.path ||
currentPath.startsWith(child.path + "/")
) {
return menu;
}
if (child.children && child.children.length > 0) {
for (const grandchild of child.children) {
if (!grandchild.path) continue;
if (
currentPath === grandchild.path ||
currentPath.startsWith(grandchild.path + "/")
) {
return menu;
}
}
}
}
}
}
return null;
};
const findCurrentMenu = findParentModule;
const currentModule = computed(() => {
const path = route.path;
if (path === "/home") {
currentModuleId.value = null;
return null;
}
const menu = findCurrentMenu(list.value, path);
if (menu) {
currentModuleId.value = menu.id;
}
return menu;
});
const displayMenus = computed(() => {
if (!currentModule.value) {
return [];
}
return currentModule.value.children || [];
});
const processMenus = (menus) => {
return menus
.filter((menu) => {
if (menu.path && menu.path.trim() !== '') return true;
if (menu.path && menu.path.trim() !== "") return true;
if (menu.children && menu.children.length > 0) return true;
return false;
})
@ -172,14 +267,20 @@ const list = computed(() => {
component_path: menu.component_path,
parentId: menu.pid || 0,
order: menu.sort || 0,
children: menu.children ? processMenus(menu.children) : []
children: menu.children ? processMenus(menu.children) : [],
}));
};
};
const list = computed(() => {
const menuData = menuStore.menus;
if (!menuData || menuData.length === 0) {
return [];
}
const allMenus = processMenus(menuData);
const sortMenusRecursively = (menus) => {
menus.forEach(menu => {
menus.forEach((menu) => {
if (menu.children && menu.children.length > 0) {
menu.children.sort((a, b) => {
const orderA = Number(a.order) ?? 999999;
@ -199,28 +300,13 @@ const list = computed(() => {
return allMenus;
});
//
const findMenuItem = (menus, targetIndex) => {
for (const menu of menus) {
if (menu.path === targetIndex) {
return menu;
}
if (menu.children && menu.children.length > 0) {
const found = findMenuItem(menu.children, targetIndex);
if (found) return found;
}
}
return null;
};
//
const handleMenuSelect = (index) => {
if (index === "/dashboard") {
if (index === "/home") {
emit("menu-click", {
path: "/dashboard",
title: "仪表盘",
icon: "fa-solid fa-gauge",
component_path: "/dashboard"
path: "/home",
title: "首页",
icon: "fa-solid fa-house",
component_path: "/home",
});
return;
}
@ -234,17 +320,22 @@ const handleMenuSelect = (index) => {
const fetchMenus = async () => {
try {
await menuStore.fetchMenus();
} catch (error) {
// menuStore
}
} catch (error) {}
};
const handleMenuRefresh = () => {
fetchMenus();
};
onMounted(() => {
watch(
() => route.path,
() => {
findCurrentMenu(list.value, route.path);
},
{ immediate: true },
);
onMounted(() => {
if (!menuStore.menus || menuStore.menus.length === 0) {
setTimeout(() => {
fetchMenus();
@ -255,7 +346,6 @@ onMounted(() => {
});
onUnmounted(() => {
window.removeEventListener("menu-cache-refreshed", handleMenuRefresh);
});
</script>
@ -263,14 +353,16 @@ onUnmounted(() => {
<style scoped lang="less">
.common-aside {
height: 100%;
transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1), background-color 0.3s ease;
transition:
width 0.3s cubic-bezier(0.4, 0, 0.2, 1),
background-color 0.3s ease;
overflow: hidden;
position: relative;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.1);
html:not(.dark) & {
background: linear-gradient(135deg, #062da3 0%, #0a4a8a 100%);
background: #3973ff;
box-shadow: 2px 0 12px rgba(6, 45, 163, 0.3);
}
@ -293,12 +385,15 @@ onUnmounted(() => {
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
}
h3 {
line-height: 60px;
text-align: center;
@ -317,7 +412,8 @@ h3 {
padding: 16px 8px;
background: transparent;
.el-menu-item, .el-sub-menu__title {
.el-menu-item,
.el-sub-menu__title {
color: rgba(255, 255, 255, 0.85);
transition: all 0.3s ease;
border-radius: 8px;
@ -342,20 +438,30 @@ h3 {
//
.el-menu-item.is-active {
background-color: rgba(79, 132, 255, 0.2) !important;
html:not(.dark) & {
background-color: rgba(57, 115, 255, 0.3) !important;
}
html.dark & {
background-color: rgba(60, 60, 60, 0.8) !important;
}
color: #ffffff !important;
border-left: 3px solid #4f84ff;
margin-left: -3px;
.menu-icon {
color: #4f84ff;
color: #fff;
}
}
//
.el-menu-item:hover:not(.is-active),
.el-sub-menu__title:hover {
html:not(.dark) & {
background-color: rgba(255, 255, 255, 0.1) !important;
}
html.dark & {
background-color: rgba(60, 60, 60, 0.8) !important;
}
color: #ffffff !important;
}
@ -378,7 +484,7 @@ h3 {
//
html.dark & {
.el-menu-item.is-active {
background: rgba(64, 158, 255, 0.1) !important;
background: rgba(219, 148, 148, 0.8) !important;
color: var(--el-color-primary-light-3) !important;
border-left-color: var(--el-color-primary);

View File

@ -15,6 +15,15 @@
</el-breadcrumb> -->
</div>
<div class="r-content">
<!-- 返回首页按钮 -->
<el-tooltip content="返回首页" placement="top">
<el-button circle @click="goHome" class="home-btn" title="返回首页">
<el-icon>
<HomeFilled />
</el-icon>
</el-button>
</el-tooltip>
<!-- 更新缓存按钮 -->
<el-button circle :icon="Refresh" @click="refreshCache" class="refresh-cache-btn" :loading="cacheLoading"
title="更新菜单缓存" />
@ -71,10 +80,10 @@
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from "vue";
import { useRouter, useRoute } from "vue-router";
import { useAllDataStore, useMenuStore } from "@/stores";
import { useAllDataStore, useMenuStore, useTabsStore } from "@/stores";
import { useAuthStore } from "@/stores/auth";
import { logout } from "@/api/login";
import { User, SwitchButton, Sunny, Moon, Refresh, Bell } from '@element-plus/icons-vue';
import { User, SwitchButton, Sunny, Moon, Refresh, Bell, HomeFilled } from '@element-plus/icons-vue';
import { ElMessage } from 'element-plus';
const router = useRouter();
@ -93,6 +102,7 @@ interface Breadcrumb {
}
const menuStore = useMenuStore();
const tabsStore = useTabsStore();
const cacheLoading = ref(false);
// 使 store
const menuList = computed(() => menuStore.menus);
@ -182,6 +192,11 @@ const handleCollapse = () => {
store.state.isCollapse = !store.state.isCollapse;
};
const goHome = () => {
tabsStore.closeAll();
router.push('/home');
};
const handleCommand = async (command) => {
if (command === 'profile') {
router.push('/user/userProfile');
@ -329,9 +344,9 @@ onUnmounted(() => {
border-bottom: 1px solid var(--el-border-color-lighter);
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
// 使 #062da3
// 使 #3973ff
html:not(.dark) & {
background-color: #062da3;
background-color: #3973ff;
border-bottom-color: rgba(79, 132, 255, 0.3);
}
}
@ -368,6 +383,7 @@ onUnmounted(() => {
gap: 16px;
.refresh-cache-btn,
.home-btn,
.theme-toggle-btn {
// 使 Element Plus
background-color: var(--el-fill-color-light);

View File

@ -1,7 +1,7 @@
import { createRouter, createWebHashHistory } from "vue-router";
import { convertMenusToRoutes } from "./dynamicRoutes";
// 静态路由登录页独立404 页面独立
// 静态路由登录页独立404 页面独立home 导航门户独立
const staticRoutes = [
{
path: "/login",
@ -14,22 +14,14 @@ const staticRoutes = [
name: "Main",
component: () => import("@/views/Main.vue"),
redirect: "/dashboard",
children: [
{
path: "dashboard",
name: "Dashboard",
component: () => import("@/views/dashboard/index.vue"),
meta: {
requiresAuth: true,
title: "仪表盘",
icon: "fa-solid fa-gauge",
id: 1,
isStatic: true
}
}
],
meta: { requiresAuth: true }
},
{
path: "/home",
name: "Home",
component: () => import("@/views/home/index.vue"),
meta: { requiresAuth: true, title: "系统导航", isStandalone: true }
},
{
path: "/:pathMatch(.*)*",
name: "NotFound",
@ -108,21 +100,7 @@ function addDynamicRoutes(menus) {
component: () => import("@/views/Main.vue"),
redirect: "/dashboard",
meta: { requiresAuth: true },
children: [
{
path: "dashboard",
name: "Dashboard",
component: () => import("@/views/dashboard/index.vue"),
meta: {
requiresAuth: true,
title: "仪表盘",
icon: "fa-solid fa-gauge",
id: 1,
isStatic: true
}
},
...dynamicRoutes // 嵌套路由直接加入 children
]
children: dynamicRoutes // 嵌套路由直接加入 children
});
dynamicRoutesAdded = true;
@ -167,7 +145,7 @@ router.beforeEach(async (to, from, next) => {
if (!dynamicRoutesAdded) {
await loadAndAddDynamicRoutes();
}
next({ path: "/" });
next({ path: "/home" });
} else {
next();
}
@ -186,9 +164,9 @@ router.beforeEach(async (to, from, next) => {
return;
}
// 如果匹配不到路由,跳转到404
// 如果匹配不到路由,跳转到首页导航门户
if (to.matched.length === 0) {
next({ name: "NotFound" });
next({ path: "/home" });
return;
}

View File

@ -38,7 +38,7 @@ import { ref as vueRef } from 'vue';
*/
export const useTabsStore = defineTabsStore('tabs', () => {
// 固定首页tab
const defaultDashboardPath = '/dashboard';
const defaultDashboardPath = '/home';
// 从 localStorage 恢复 tabs 状态
function loadTabsFromStorage() {
@ -48,16 +48,16 @@ export const useTabsStore = defineTabsStore('tabs', () => {
if (savedTabs) {
const tabs = JSON.parse(savedTabs);
// 确保至少包含首页
const hasDashboard = tabs.some(t => t.fullPath === defaultDashboardPath);
if (!hasDashboard) {
tabs.unshift({ title: '首页', fullPath: defaultDashboardPath, name: 'Dashboard' });
const hasHome = tabs.some(t => t.fullPath === defaultDashboardPath);
if (!hasHome) {
tabs.unshift({ title: '首页', fullPath: defaultDashboardPath, name: 'Home' });
}
return tabs;
}
} catch (e) {
console.warn('恢复 tabs 失败:', e);
}
return [{ title: '首页', fullPath: defaultDashboardPath, name: 'Dashboard' }];
return [{ title: '首页', fullPath: defaultDashboardPath, name: 'Home' }];
}
// 保存 tabs 到 localStorage

View File

@ -1,4 +1,4 @@
<script setup>
<script setup lang="ts">
import CommonAside from '@/components/CommonAside.vue';
import CommonHeader from '@/components/CommonHeader.vue';
import { useTabsStore } from '@/stores';
@ -10,7 +10,7 @@ import { ElMessage } from 'element-plus';
const tabsStore = useTabsStore();
const router = useRouter();
const route = useRoute();
const defaultDashboardPath = '/dashboard';
const defaultDashboardPath = '/home';
// tab使
function restoreTabFromRoute() {
@ -100,6 +100,12 @@ watch(
const handleAsideMenuClick = async (menuItem) => {
const targetPath = menuItem.path;
if (targetPath === '/home') {
tabsStore.closeAll();
router.push('/home');
return;
}
// tab
tabsStore.addTab({
title: menuItem.title,
@ -163,6 +169,11 @@ watch(
// oldVal undefined
// watchtabhandleAsideMenuClick
if (newVal && oldVal !== undefined && router.currentRoute.value.fullPath !== newVal) {
// tab
if (newVal === '/home') {
tabsStore.closeAll();
}
//
const routeExists = router.resolve(newVal).matched.length > 0;
@ -528,7 +539,7 @@ const canCloseRight = computed(() => {
height: 100vh;
}
.main-header {
background-color: var(--header-bg-color, #0081ff);
background-color: var(--header-bg-color, #3973ff);
transition: background-color 0.3s ease;
height: 80px;
padding: 0;

View File

@ -57,7 +57,7 @@ const lineChartInstance = shallowRef<echarts.ECharts | null>(null);
const pieChartInstance = shallowRef<echarts.ECharts | null>(null);
const summaryData = ref<SummaryItem[]>([
{ title: '总用户数', value: 12840, icon: User, color: '#409EFF', percentage: 12, isUp: true },
{ title: '总用户数', value: 12840, icon: User, color: '#3973FF', percentage: 12, isUp: true },
{ title: '今日新增', value: 156, icon: Pointer, color: '#67C23A', percentage: 5, isUp: true },
{ title: '活跃用户', value: 3420, icon: Connection, color: '#E6A23C', percentage: 2, isUp: false },
{ title: '留存率', value: 85, icon: Histogram, color: '#F56C6C', percentage: 1, isUp: true },
@ -84,7 +84,7 @@ const initCharts = () => {
smooth: true,
data: [120, 132, 101, 134, 90, 230, 210],
areaStyle: { opacity: 0.3 },
itemStyle: { color: '#409EFF' }
itemStyle: { color: '#3973FF' }
}
]
});

View File

@ -170,7 +170,7 @@ onMounted(() => {
transition: all 0.3s;
&:hover {
border-color: #409eff;
border-color: #3973ff;
}
}

View File

@ -329,7 +329,7 @@ onMounted(async () => {
}
.name-link {
color: #409eff;
color: #3973ff;
cursor: pointer;
text-decoration: none;
transition: color 0.3s;

View File

@ -379,7 +379,7 @@ onMounted(async () => {
.welcome-section {
margin-bottom: 16px;
padding: 32px;
background: linear-gradient(135deg, #062da3 0%, #4f84ff 100%);
background: linear-gradient(135deg, #3973ff 0%, #4f84ff 100%);
border-radius: 12px;
box-shadow: 0 4px 12px rgba(6, 45, 163, 0.2);
@ -440,7 +440,7 @@ onMounted(async () => {
}
&.income .stat-icon-wrapper {
background: linear-gradient(135deg, #062da3 0%, #4f84ff 100%);
background: linear-gradient(135deg, #3973ff 0%, #4f84ff 100%);
}
&.users .stat-icon-wrapper {
@ -456,7 +456,7 @@ onMounted(async () => {
}
&.knowledge .stat-icon-wrapper {
background: linear-gradient(135deg, #062da3 0%, #4f84ff 100%);
background: linear-gradient(135deg, #3973ff 0%, #4f84ff 100%);
}
&.employees .stat-icon-wrapper {

View File

@ -1,11 +1,613 @@
<script setup>
</script>
<template>
this is home
<div class="home-container">
<div class="home-top">
<div class="toolbar">
<div class="toolbar-left">
<span class="system-name">系统管理平台</span>
</div>
<div class="toolbar-right">
<el-tooltip content="刷新菜单" placement="bottom">
<el-button
circle
:loading="refreshLoading"
class="toolbar-btn"
@click="handleRefreshMenus"
>
<el-icon><RefreshRight /></el-icon>
</el-button>
</el-tooltip>
<el-tooltip
:content="
currentTheme === 'dark' ? '切换到亮色模式' : '切换到暗色模式'
"
placement="bottom"
>
<el-button
circle
:icon="themeIcon"
class="toolbar-btn"
@click="toggleTheme"
/>
</el-tooltip>
<el-dropdown trigger="click" @command="handleCommand">
<span class="user-dropdown-link">
<el-avatar :size="32" :src="userAvatar">
{{ userName?.charAt(0)?.toUpperCase() }}
</el-avatar>
<span class="user-name">{{ userName }}</span>
<el-icon><ArrowDown /></el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="profile">
<el-icon><User /></el-icon>
<span>个人中心</span>
</el-dropdown-item>
<el-dropdown-item divided command="logout">
<el-icon><SwitchButton /></el-icon>
<span>退出登录</span>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</div>
<div class="home-wrapper">
<!-- <div class="welcome-section">
<div class="welcome-content">
<h1 class="welcome-title">
<span class="greeting">您好</span>
<span class="username">{{ userName }}</span>
</h1>
<p class="welcome-subtitle">欢迎进入系统管理平台</p>
<p class="welcome-desc">选择下方功能模块开始管理您的系统</p>
</div>
<div class="welcome-decoration">
<div class="decoration-circle circle-1"></div>
<div class="decoration-circle circle-2"></div>
<div class="decoration-circle circle-3"></div>
</div>
</div> -->
<div class="modules-section" v-if="moduleList.length > 0">
<div class="module-grid">
<div
v-for="module in moduleList"
:key="module.path"
class="module-card"
@click="handleNavigate(module.path)"
>
<div class="card-header">
<span class="card-title">{{ module.name }}</span>
<div class="card-icon-wrapper" v-html="module.icon"></div>
</div>
<div class="card-divider"></div>
<div class="card-content">
<p class="card-desc">{{ module.description || "暂无描述" }}</p>
</div>
</div>
</div>
</div>
<div v-else class="empty-state">
<el-icon :size="64" class="empty-icon"><Grid /></el-icon>
<p class="empty-text">暂无功能模块</p>
<p class="empty-tip">请联系管理员配置系统菜单</p>
</div>
</div>
</div>
</template>
<style scoped>
<script setup lang="ts">
import { ref, computed, onMounted } from "vue";
import { useRouter } from "vue-router";
import { ElMessage } from "element-plus";
import {
ArrowDown,
User,
SwitchButton,
Grid,
RefreshRight,
Sunny,
Moon,
} from "@element-plus/icons-vue";
import { getModulesList } from "@/api/modules";
import { useAuthStore } from "@/stores/auth";
import { useMenuStore } from "@/stores/menu";
interface ModuleItem {
id: number;
name: string;
code: string;
path: string;
icon: string;
description: string;
sort: number;
status: number;
}
const router = useRouter();
const authStore = useAuthStore();
const menuStore = useMenuStore();
const moduleList = ref<ModuleItem[]>([]);
const refreshLoading = ref(false);
const THEME_STORAGE_KEY = "element-plus-theme";
const isDark = ref(false);
const initTheme = () => {
const savedTheme = localStorage.getItem(THEME_STORAGE_KEY);
if (savedTheme) {
isDark.value = savedTheme === "dark";
} else {
const prefersDark = window.matchMedia(
"(prefers-color-scheme: dark)",
).matches;
isDark.value = prefersDark;
}
applyTheme();
};
const applyTheme = () => {
const htmlElement = document.documentElement;
if (isDark.value) {
htmlElement.classList.add("dark");
localStorage.setItem(THEME_STORAGE_KEY, "dark");
} else {
htmlElement.classList.remove("dark");
localStorage.setItem(THEME_STORAGE_KEY, "light");
}
};
const toggleTheme = () => {
isDark.value = !isDark.value;
applyTheme();
};
const currentTheme = computed(() => (isDark.value ? "dark" : "light"));
const themeIcon = computed(() => (isDark.value ? Sunny : Moon));
const userName = computed(
() => authStore.user?.name || authStore.user?.account || "用户",
);
const userAvatar = computed(() => authStore.user?.avatar || "");
//
function handleNavigate(path: string) {
if (path) router.push(path);
}
//
async function handleRefreshMenus() {
refreshLoading.value = true;
try {
await menuStore.refreshMenus();
ElMessage.success("菜单已刷新");
} catch {
ElMessage.error("刷新菜单失败");
} finally {
refreshLoading.value = false;
}
}
// 退
async function handleCommand(command: string) {
switch (command) {
case "profile":
router.push("/user/userProfile");
break;
case "logout":
await handleLogout();
break;
}
}
// 退
async function handleLogout() {
try {
const user = authStore.user;
if (user?.id) await authStore.logout(user.id);
} catch (error) {
console.error("退出登录接口调用失败:", error);
}
authStore.clearToken();
localStorage.removeItem("user");
sessionStorage.removeItem("user");
localStorage.removeItem("tenant");
sessionStorage.removeItem("tenant");
menuStore.resetMenus();
router.push("/login");
}
//
async function loadModules() {
try {
const res = await getModulesList();
if (res.code === 200) {
const list = res.data?.list || [];
moduleList.value = list
.filter((item: ModuleItem) => item.status === 1 && item.is_show === 1)
.sort((a, b) => Number(a.sort) - Number(b.sort));
}
} catch (error) {
console.error("加载模块列表失败:", error);
}
}
//
onMounted(() => {
loadModules();
initTheme();
});
</script>
<style scoped lang="scss">
//
$primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
$bg-gradient: linear-gradient(135deg, #f5f7fa 0%, #e4e8eb 100%);
$text-main: #1a1a2e;
$text-regular: #606266;
.home-container {
min-height: 100vh;
padding: 0;
.home-top {
background: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
position: sticky;
top: 0;
z-index: 100;
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
max-width: 1400px;
margin: 0 auto;
padding: 12px 24px;
.toolbar-left {
display: flex;
align-items: center;
.system-name {
font-size: 18px;
font-weight: 600;
color: $text-main;
}
}
.toolbar-right {
display: flex;
align-items: center;
gap: 12px;
.toolbar-btn {
background: #f5f7fa;
border-color: #e4e7ed;
color: $text-regular;
&:hover {
background: #f0f2f5;
border-color: #c0c4cc;
color: #3973ff;
}
}
.user-dropdown-link {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
padding: 4px 8px;
border-radius: 8px;
transition: background-color 0.3s;
&:hover {
background: #f5f7fa;
}
.user-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
}
.user-name {
font-size: 14px;
color: $text-regular;
max-width: 80px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
}
}
.home-wrapper {
max-width: 1400px;
margin: 0 auto;
padding: 40px 24px;
.welcome-section {
position: relative;
background: $primary-gradient;
border-radius: 20px;
padding: 48px 56px;
margin-bottom: 40px;
overflow: hidden;
box-shadow: 0 10px 40px rgba(102, 126, 234, 0.3);
.welcome-content {
position: relative;
z-index: 1;
.welcome-title {
font-size: 36px;
font-weight: 700;
color: #fff;
margin: 0 0 12px 0;
.greeting {
font-weight: 400;
opacity: 0.9;
}
.username {
font-weight: 700;
}
}
.welcome-subtitle {
font-size: 18px;
color: rgba(255, 255, 255, 0.9);
margin: 0 0 8px 0;
}
.welcome-desc {
font-size: 14px;
color: rgba(255, 255, 255, 0.7);
margin: 0;
}
}
.welcome-decoration {
position: absolute;
right: 60px;
top: 50%;
transform: translateY(-50%);
.decoration-circle {
position: absolute;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
&.circle-1 {
width: 200px;
height: 200px;
top: -80px;
right: -60px;
}
&.circle-2 {
width: 120px;
height: 120px;
bottom: -40px;
right: 120px;
}
&.circle-3 {
width: 60px;
height: 60px;
top: 20px;
right: -20px;
}
}
}
}
.modules-section {
margin-top: 20px;
.module-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
}
.module-card {
height: 220px;
background: #fff;
border-radius: 12px;
padding: 30px;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
border: 1px solid #e8e8e8;
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
.card-icon-wrapper {
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
color: #3973ff;
font-size: 30px;
}
.card-title {
font-size: 18px;
font-weight: 600;
color: $text-main;
}
}
.card-divider {
border-bottom: 1px solid #dfdfdf;
margin: 15px 0;
transition: opacity 0.3s ease;
}
.card-content {
.card-desc {
font-size: 14px;
color: #909399;
margin: 0;
line-height: 1.5;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
}
&:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(102, 126, 234, 0.15);
background-color: #3973ff;
.card-title,
.card-desc {
color: #fff;
}
.card-icon-wrapper {
color: #fff;
}
.card-divider {
opacity: 0;
}
}
}
}
.empty-state {
text-align: center;
padding: 80px 20px;
.empty-icon {
color: #c0c4cc;
margin-bottom: 16px;
}
.empty-text {
font-size: 18px;
color: #606266;
margin: 0 0 8px 0;
}
.empty-tip {
font-size: 14px;
color: #909399;
margin: 0;
}
}
}
@media (max-width: 1200px) {
.modules-section {
.module-grid {
grid-template-columns: repeat(3, 1fr);
}
}
}
@media (max-width: 992px) {
.modules-section {
.module-grid {
grid-template-columns: repeat(2, 1fr);
}
}
}
@media (max-width: 768px) {
.welcome-section {
padding: 32px 24px;
}
.welcome-title {
font-size: 28px;
}
.welcome-decoration {
display: none;
}
.modules-section {
.module-grid {
grid-template-columns: 1fr;
}
}
}
html.dark & {
.home-top {
background: #1a1a1a;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
.toolbar {
.toolbar-left .system-name {
color: #e0e0e0;
}
.toolbar-right {
.toolbar-btn {
background: #2d2d2d;
border-color: #3d3d3d;
color: #b0b0b0;
&:hover {
background: #3d3d3d;
border-color: #4d4d4d;
color: #3973ff;
}
}
.user-dropdown-link {
&:hover {
background: #2d2d2d;
}
.user-name {
color: #b0b0b0;
}
}
}
}
}
.modules-section {
.module-card {
background: #1a1a1a;
border-color: #3d3d3d;
.card-title {
color: #e0e0e0 !important;
}
.card-desc {
color: #8c8c8c !important;
}
.card-divider {
border-color: #3d3d3d;
}
}
}
}
}
</style>

View File

@ -1,84 +1,3 @@
<script setup>
import { ref, onMounted } from "vue";
import { useRouter } from "vue-router";
import { useAuthStore } from "@/stores/auth";
import { login } from "@/api/login";
const router = useRouter();
const authStore = useAuthStore();
const account = ref("");
const password = ref("");
const passwordVisible = ref(false);
const rememberMe = ref(false);
const loading = ref(false);
const errorMsg = ref("");
onMounted(() => {
const savedUser = localStorage.getItem("loginAccount");
const savedRemember = localStorage.getItem("loginRememberMe");
if (savedRemember === "true") {
account.value = savedUser;
rememberMe.value = true;
}
});
const handleLogin = async () => {
errorMsg.value = "";
if (!account.value || !password.value) {
errorMsg.value = "请输入用户名和密码";
return;
}
//
if (rememberMe.value) {
localStorage.setItem("loginAccount", account.value);
localStorage.setItem("loginRememberMe", "true");
} else {
localStorage.removeItem("loginAccount");
localStorage.setItem("loginRememberMe", "false");
}
loading.value = true;
try {
const res = await login(account.value, password.value);
if (res && res.code === 200) {
authStore.setLoginInfo(res.data);
// tabs store
const { useTabsStore } = await import("@/stores");
const tabsStore = useTabsStore();
tabsStore.resetTabs();
//
try {
} catch (menuError) {
console.error("Failed to process login", menuError);
//
}
router.push({ path: "/dashboard" });
} else {
errorMsg.value = res.msg || "登录失败";
}
} catch (err) {
errorMsg.value =
err?.response?.data?.msg || err?.message || "登录失败,请重试";
} finally {
loading.value = false;
}
};
//
const goRegister = () => {
router.push({ path: "/register" });
};
const goForget = () => {
router.push({ path: "/forget" });
};
</script>
<template>
<div class="login-bg">
<div class="login-card">
@ -266,7 +185,86 @@ const goForget = () => {
<div class="login-light light2"></div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { useRouter } from "vue-router";
import { useAuthStore } from "@/stores/auth";
import { login } from "@/api/login";
const router = useRouter();
const authStore = useAuthStore();
const account = ref("");
const password = ref("");
const passwordVisible = ref(false);
const rememberMe = ref(false);
const loading = ref(false);
const errorMsg = ref("");
onMounted(() => {
const savedUser = localStorage.getItem("loginAccount");
const savedRemember = localStorage.getItem("loginRememberMe");
if (savedRemember === "true") {
account.value = savedUser;
rememberMe.value = true;
}
});
const handleLogin = async () => {
errorMsg.value = "";
if (!account.value || !password.value) {
errorMsg.value = "请输入用户名和密码";
return;
}
//
if (rememberMe.value) {
localStorage.setItem("loginAccount", account.value);
localStorage.setItem("loginRememberMe", "true");
} else {
localStorage.removeItem("loginAccount");
localStorage.setItem("loginRememberMe", "false");
}
loading.value = true;
try {
const res = await login(account.value, password.value);
if (res && res.code === 200) {
authStore.setLoginInfo(res.data);
// tabs store
const { useTabsStore } = await import("@/stores");
const tabsStore = useTabsStore();
tabsStore.resetTabs();
//
try {
} catch (menuError) {
console.error("登录处理失败", menuError);
//
}
router.push({ path: "/home" });
} else {
errorMsg.value = res.msg || "登录失败";
}
} catch (err) {
errorMsg.value =
err?.response?.data?.msg || err?.message || "登录失败,请重试";
} finally {
loading.value = false;
}
};
//
const goRegister = () => {
router.push({ path: "/register" });
};
const goForget = () => {
router.push({ path: "/forget" });
};
</script>
<style scoped>
.login-bg {
min-height: 100vh;
@ -283,7 +281,8 @@ const goForget = () => {
min-width: 770px;
background: rgba(255, 255, 255, 0.95);
border-radius: 22px;
box-shadow: 0 8px 36px 0 rgba(73, 150, 255, 0.14),
box-shadow:
0 8px 36px 0 rgba(73, 150, 255, 0.14),
0 1.5px 4px 0 rgba(30, 42, 79, 0.05);
overflow: hidden;
z-index: 10;
@ -408,7 +407,9 @@ const goForget = () => {
border: 1.3px solid #d6e6fa;
border-radius: 7px;
box-sizing: border-box;
transition: border 0.2s, box-shadow 0.2s;
transition:
border 0.2s,
box-shadow 0.2s;
background: #f7fbfe;
margin-bottom: 3px;
}
@ -457,7 +458,9 @@ const goForget = () => {
letter-spacing: 1px;
margin-top: 15px;
cursor: pointer;
transition: background 0.2s, transform 0.13s;
transition:
background 0.2s,
transform 0.13s;
}
.login-btn:active {
transform: scale(0.98);

View File

@ -200,7 +200,7 @@ const clientInfo = computed(() => {
// 使
const getMemoryColor = (usage: number | undefined) => {
if (!usage) return '#409eff';
if (!usage) return '#3973ff';
if (usage >= 90) return '#f56c6c';
if (usage >= 70) return '#e6a23c';
return '#67c23a';
@ -208,7 +208,7 @@ const getMemoryColor = (usage: number | undefined) => {
// 使
const getDiskColor = (usage: number | undefined) => {
if (!usage) return '#409eff';
if (!usage) return '#3973ff';
if (usage >= 90) return '#f56c6c';
if (usage >= 70) return '#e6a23c';
return '#67c23a';

View File

@ -291,7 +291,7 @@ const handleClose = () => {
text-align: center;
em {
color: #409eff;
color: #3973ff;
font-style: normal;
}
}

View File

@ -160,7 +160,7 @@
<video :src="getFileUrl(file.url)" alt="file" />
</div>
<div v-else-if="isDocument(file)">
<el-icon :size="48" color="#409eff">
<el-icon :size="48" color="#3973ff">
<Document />
</el-icon>
</div>
@ -584,7 +584,7 @@ const handleRenameCategorySuccess = () => {
selectedGroup.value.id === currentRenameCategoryId.value
) {
const updatedGroup = groups.value.find(
(g: any) => g.id === currentRenameCategoryId.value
(g: any) => g.id === currentRenameCategoryId.value,
);
if (updatedGroup) {
selectGroup(updatedGroup);
@ -633,7 +633,7 @@ const loadFiles = async () => {
cateId,
currentPage.value,
pageSize.value,
fileSearchQuery.value
fileSearchQuery.value,
);
if (res.code === 200 && res.data) {
//
@ -646,7 +646,7 @@ const loadFiles = async () => {
} else {
//
const group = groups.value.find(
(g) => g.id === selectedGroup.value!.id
(g) => g.id === selectedGroup.value!.id,
);
if (group) {
group.total = res.data.total || 0;
@ -987,12 +987,12 @@ onMounted(() => {
gap: 8px;
font-size: 18px;
font-weight: 600;
color: #303133;
color: var(--el-text-color-primary);
}
.subtitle {
font-size: 13px;
color: #909399;
color: var(--el-text-color-placeholder);
}
}
@ -1030,13 +1030,13 @@ onMounted(() => {
justify-content: space-between;
padding: 12px 16px;
margin-bottom: 8px;
background: #f5f7fa;
// background: #f5f7fa;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
&:hover {
background: #e6f0ff;
// background: #e6f0ff;
transform: translateX(4px);
}
@ -1059,7 +1059,7 @@ onMounted(() => {
gap: 8px;
font-size: 15px;
font-weight: 500;
color: #303133;
color: var(--el-text-color-primary);
margin-bottom: 4px;
.group-icon {
@ -1254,7 +1254,7 @@ onMounted(() => {
max-height: 80vh;
overflow: hidden;
position: relative;
background: #f5f7fa;
// background: #f5f7fa;
border-radius: 8px;
.preview-image-wrapper {

View File

@ -0,0 +1,472 @@
<template>
<div class="container-box">
<div class="header-bar">
<h2>模块管理</h2>
<div class="header-actions">
<el-button type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>
添加模块
</el-button>
<el-button @click="refresh">
<el-icon><Refresh /></el-icon>
刷新
</el-button>
</div>
</div>
<el-divider></el-divider>
<el-table
:data="modules"
style="width: 100%"
v-loading="loading"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" align="center" />
<el-table-column prop="id" label="ID" width="80" align="center" />
<el-table-column
prop="name"
label="模块名称"
min-width="150"
align="center"
>
<template #default="{ row }">
<div class="module-name">
<span class="module-icon" v-html="row.icon"></span>
<span>{{ row.name }}</span>
</div>
</template>
</el-table-column>
<el-table-column
prop="code"
label="模块编码"
min-width="120"
align="center"
/>
<el-table-column
prop="path"
label="路由路径"
min-width="150"
align="center"
>
<template #default="{ row }">
<el-tag size="small" type="info">{{ row.path || "-" }}</el-tag>
</template>
</el-table-column>
<el-table-column
prop="description"
label="描述"
min-width="200"
align="center"
show-overflow-tooltip
/>
<el-table-column prop="sort" label="排序" width="100" align="center" />
<el-table-column prop="is_show" label="显示" width="80" align="center">
<template #default="{ row }">
<el-switch
v-model="row.is_show"
:active-value="1"
:inactive-value="0"
@change="handleShowChange(row)"
/>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="80" align="center">
<template #default="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'danger'">
{{ row.status === 1 ? "启用" : "禁用" }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="200" align="center" fixed="right">
<template #default="{ row }">
<el-button size="small" @click="handleEdit(row)">编辑</el-button>
<el-button size="small" type="danger" @click="handleDelete(row)"
>删除</el-button
>
</template>
</el-table-column>
</el-table>
<div class="table-footer" v-if="selectedModules.length > 0">
<span>已选择 {{ selectedModules.length }} </span>
<el-button type="danger" size="small" @click="handleBatchDelete"
>批量删除</el-button
>
</div>
<el-divider></el-divider>
<div class="tips-section">
<el-alert title="模块管理说明" type="info" :closable="false" show-icon>
<template #default>
<p>1. 模块用于管理系统功能单元每个模块包含独立的路由图标和描述</p>
<p>2. 模块编码用于程序识别请确保唯一性</p>
<p>3. 禁用状态的模块将不会在菜单中显示</p>
</template>
</el-alert>
</div>
<el-dialog
v-model="dialogVisible"
:title="dialogType === 'add' ? '添加模块' : '编辑模块'"
width="600px"
:close-on-click-modal="false"
>
<el-form
ref="formRef"
:model="formData"
:rules="rules"
label-width="100px"
>
<el-form-item label="模块名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入模块名称" />
</el-form-item>
<el-form-item label="模块编码" prop="code">
<el-input
v-model="formData.code"
placeholder="请输入模块编码(英文)"
:disabled="dialogType === 'edit'"
/>
</el-form-item>
<el-form-item label="路由路径" prop="path">
<el-input
v-model="formData.path"
placeholder="请输入路由路径,如 /system/modules"
/>
</el-form-item>
<el-form-item label="图标" prop="icon">
<el-input
v-model="formData.icon"
placeholder="请输入图标类名,如 Grid"
>
<template #append>
<el-popover placement="bottom-end" :width="400" trigger="click">
<template #reference>
<el-button link>选择图标</el-button>
</template>
<div class="icon-grid">
<div
v-for="icon in iconList"
:key="icon"
class="icon-item"
:class="{ active: formData.icon === icon }"
@click="formData.icon = icon"
>
<el-icon :size="20"><component :is="icon" /></el-icon>
</div>
</div>
</el-popover>
</template>
</el-input>
</el-form-item>
<el-form-item label="描述" prop="description">
<el-input
v-model="formData.description"
type="textarea"
:rows="3"
placeholder="请输入模块描述"
/>
</el-form-item>
<el-form-item label="排序" prop="sort">
<el-input-number
v-model="formData.sort"
:min="0"
:max="999"
controls-position="right"
/>
</el-form-item>
<el-form-item label="是否显示" prop="is_show">
<el-switch
v-model="formData.is_show"
:active-value="1"
:inactive-value="0"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="formData.status">
<el-radio :label="1">启用</el-radio>
<el-radio :label="0">禁用</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit" :loading="submitLoading"
>确定</el-button
>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted, shallowRef } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import {
getModulesList,
getModuleDetail,
addModule,
editModule,
deleteModule,
batchDeleteModules,
changeModuleStatus,
} from "@/api/modules";
const loading = ref(false);
const modules = ref([]);
const selectedModules = ref([]);
const dialogVisible = ref(false);
const dialogType = ref("add");
const formRef = ref(null);
const submitLoading = ref(false);
const formData = ref({
name: "",
code: "",
path: "",
icon: "",
description: "",
sort: 0,
is_show: 1,
status: 1,
});
const rules = {
name: [{ required: true, message: "请输入模块名称", trigger: "blur" }],
code: [{ required: true, message: "请输入模块编码", trigger: "blur" }],
};
function getIconComponent(iconName) {
return iconComponents.value[iconName] || Grid;
}
async function fetchModules() {
loading.value = true;
try {
const res = await getModulesList();
if (res.code === 200 && res.data) {
modules.value = res.data.list || [];
}
} catch (error) {
console.error("获取模块列表失败:", error);
ElMessage.error("获取模块列表失败");
} finally {
loading.value = false;
}
}
function refresh() {
fetchModules();
}
function handleSelectionChange(selection) {
selectedModules.value = selection;
}
function handleAdd() {
dialogType.value = "add";
formData.value = {
name: "",
code: "",
path: "",
icon: "",
description: "",
sort: 0,
is_show: 1,
status: 1,
};
dialogVisible.value = true;
}
async function handleEdit(row) {
dialogType.value = "edit";
try {
const res = await getModuleDetail(row.id);
if (res.code === 200 && res.data) {
formData.value = { ...res.data };
dialogVisible.value = true;
} else {
ElMessage.error(res.msg || "获取模块详情失败");
}
} catch (error) {
console.error("获取模块详情失败:", error);
ElMessage.error("获取模块详情失败");
}
}
async function handleSubmit() {
try {
await formRef.value.validate();
submitLoading.value = true;
if (dialogType.value === "add") {
const res = await addModule(formData.value);
if (res.code === 200) {
ElMessage.success("添加成功");
dialogVisible.value = false;
fetchModules();
} else {
ElMessage.error(res.msg || "添加失败");
}
} else {
const res = await editModule(formData.value.id, formData.value);
if (res.code === 200) {
ElMessage.success("编辑成功");
dialogVisible.value = false;
fetchModules();
} else {
ElMessage.error(res.msg || "编辑失败");
}
}
} catch (error) {
console.error("提交失败:", error);
} finally {
submitLoading.value = false;
}
}
async function handleDelete(row) {
try {
await ElMessageBox.confirm("确定要删除该模块吗?", "提示", {
type: "warning",
});
const res = await deleteModule(row.id);
if (res.code === 200) {
ElMessage.success("删除成功");
fetchModules();
} else {
ElMessage.error(res.msg || "删除失败");
}
} catch (error) {
if (error !== "cancel") {
console.error("删除失败:", error);
ElMessage.error("删除失败");
}
}
}
async function handleBatchDelete() {
try {
await ElMessageBox.confirm(
`确定要删除选中的 ${selectedModules.value.length} 个模块吗?`,
"提示",
{
type: "warning",
},
);
const ids = selectedModules.value.map((item) => item.id);
const res = await batchDeleteModules(ids);
if (res.code === 200) {
ElMessage.success("批量删除成功");
fetchModules();
} else {
ElMessage.error(res.msg || "批量删除失败");
}
} catch (error) {
if (error !== "cancel") {
console.error("批量删除失败:", error);
ElMessage.error("批量删除失败");
}
}
}
async function handleShowChange(row) {
try {
const res = await changeModuleStatus(row.id, row.is_show);
if (res.code !== 200) {
ElMessage.error(res.msg || "状态修改失败");
row.is_show = row.is_show ? 0 : 1;
}
} catch (error) {
console.error("状态修改失败:", error);
row.is_show = row.is_show ? 0 : 1;
}
}
onMounted(() => {
fetchModules();
});
</script>
<style scoped lang="scss">
.container-box {
padding: 20px;
background: var(--el-bg-color);
border-radius: 8px;
}
.header-bar {
display: flex;
justify-content: space-between;
align-items: center;
h2 {
margin: 0;
font-size: 18px;
font-weight: 600;
color: var(--el-text-color-primary);
}
}
.header-actions {
display: flex;
gap: 12px;
}
.module-name {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
.module-icon {
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
color: #667eea;
}
}
.table-footer {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 0;
}
.tips-section {
margin-top: 20px;
p {
margin: 4px 0;
font-size: 13px;
color: var(--el-text-color-secondary);
}
}
.icon-grid {
display: grid;
grid-template-columns: repeat(8, 1fr);
gap: 8px;
max-height: 200px;
overflow-y: auto;
.icon-item {
display: flex;
align-items: center;
justify-content: center;
padding: 8px;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s;
&:hover,
&.active {
background: var(--el-color-primary-light-9);
color: var(--el-color-primary);
}
}
}
</style>