更新架构
This commit is contained in:
parent
2420ce7660
commit
da378a01be
2
components.d.ts
vendored
2
components.d.ts
vendored
@ -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
61
src/api/modules.js
Normal 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',
|
||||
});
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
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,11 +385,14 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
h3 {
|
||||
line-height: 60px;
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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),才进行跳转
|
||||
// 注意:这个watch主要用于处理tab点击切换,菜单点击由handleAsideMenuClick直接处理
|
||||
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;
|
||||
|
||||
@ -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' }
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
@ -170,7 +170,7 @@ onMounted(() => {
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
border-color: #409eff;
|
||||
border-color: #3973ff;
|
||||
}
|
||||
}
|
||||
|
||||
@ -329,7 +329,7 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
.name-link {
|
||||
color: #409eff;
|
||||
color: #3973ff;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition: color 0.3s;
|
||||
@ -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 {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -291,7 +291,7 @@ const handleClose = () => {
|
||||
text-align: center;
|
||||
|
||||
em {
|
||||
color: #409eff;
|
||||
color: #3973ff;
|
||||
font-style: normal;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
472
src/views/system/modules/index.vue
Normal file
472
src/views/system/modules/index.vue
Normal 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>
|
||||
Loading…
Reference in New Issue
Block a user