更新架构
This commit is contained in:
1 parent
2420ce7660
commit
da378a01be
33 files changed
+1490
-239
No files matched your search
@@ -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',
|
||||
});
|
||||
}
|
||||
+187
-81
@@ -27,16 +27,16 @@
|
||||
:default-active="route.path"
|
||||
>
|
||||
<!-- 菜单标题 -->
|
||||
<h3>{{ isCollapse ? '管理' : '云泽管理后台' }}</h3>
|
||||
|
||||
<!-- 固定仪表盘 -->
|
||||
<el-menu-item index="/dashboard">
|
||||
<i class="fa-solid fa-gauge"></i>
|
||||
<template #title>仪表盘</template>
|
||||
<h3>{{ isCollapse ? "管理" : currentModule?.title || "子菜单" }}</h3>
|
||||
|
||||
<!-- 无模块时显示提示 -->
|
||||
<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"
|
||||
@@ -65,7 +65,7 @@
|
||||
v-if="!child.children || child.children.length === 0"
|
||||
:index="child.path || child.id.toString()"
|
||||
>
|
||||
<i v-if="child.icon" :class="child.icon" class="menu-icon"></i>
|
||||
<i v-if="child.icon" :class="child.icon" class="menu-icon"></i>
|
||||
<template #title>
|
||||
<span>{{ child.title }}</span>
|
||||
</template>
|
||||
@@ -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">
|
||||
<template
|
||||
v-for="greatGrandchild in grandchild.children"
|
||||
:key="greatGrandchild.id"
|
||||
>
|
||||
<el-menu-item
|
||||
:index="greatGrandchild.path || greatGrandchild.id.toString()"
|
||||
:index="
|
||||
greatGrandchild.path || greatGrandchild.id.toString()
|
||||
"
|
||||
>
|
||||
<i v-if="greatGrandchild.icon" :class="greatGrandchild.icon" class="menu-icon"></i>
|
||||
<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,17 +159,117 @@ 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 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.children && menu.children.length > 0) return true;
|
||||
return false;
|
||||
})
|
||||
.map((menu) => ({
|
||||
id: menu.id,
|
||||
path: menu.path,
|
||||
icon: menu.icon || "Document",
|
||||
title: menu.title,
|
||||
route: menu.path,
|
||||
component_path: menu.component_path,
|
||||
parentId: menu.pid || 0,
|
||||
order: menu.sort || 0,
|
||||
children: menu.children ? processMenus(menu.children) : [],
|
||||
}));
|
||||
};
|
||||
|
||||
const list = computed(() => {
|
||||
const menuData = menuStore.menus;
|
||||
@@ -155,31 +277,10 @@ const list = computed(() => {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
const processMenus = (menus) => {
|
||||
return menus
|
||||
.filter((menu) => {
|
||||
if (menu.path && menu.path.trim() !== '') return true;
|
||||
if (menu.children && menu.children.length > 0) return true;
|
||||
return false;
|
||||
})
|
||||
.map((menu) => ({
|
||||
id: menu.id,
|
||||
path: menu.path,
|
||||
icon: menu.icon || "Document",
|
||||
title: menu.title,
|
||||
route: menu.path,
|
||||
component_path: menu.component_path,
|
||||
parentId: menu.pid || 0,
|
||||
order: menu.sort || 0,
|
||||
children: menu.children ? processMenus(menu.children) : []
|
||||
}));
|
||||
};
|
||||
|
||||
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 {
|
||||
background-color: rgba(255, 255, 255, 0.1) !important;
|
||||
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);
|
||||
|
||||
+11
-33
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+5
-5
@@ -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
|
||||
|
||||
+14
-3
@@ -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' }
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+1
-1
@@ -170,7 +170,7 @@ onMounted(() => {
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
border-color: #409eff;
|
||||
border-color: #3973ff;
|
||||
}
|
||||
}
|
||||
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -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 {
|
||||
|
||||
+608
-6
@@ -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>
|
||||
+87
-84
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<el-icon><Grid /></el-icon>
|
||||
<span>文件分组</span>
|
||||
</div>
|
||||
<el-button
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="handleCreateCategory"
|
||||
@@ -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;
|
||||
@@ -942,8 +942,8 @@ function handleDelete(row) {
|
||||
res && typeof res.code !== "undefined"
|
||||
? res
|
||||
: res && res.data
|
||||
? res.data
|
||||
: res;
|
||||
? res.data
|
||||
: res;
|
||||
if (resp.code === 200) {
|
||||
ElMessage.success("删除成功");
|
||||
loadFiles();
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
Reference in new issue
Block a user