更新架构
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']
|
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
||||||
ElOption: typeof import('element-plus/es')['ElOption']
|
ElOption: typeof import('element-plus/es')['ElOption']
|
||||||
ElPagination: typeof import('element-plus/es')['ElPagination']
|
ElPagination: typeof import('element-plus/es')['ElPagination']
|
||||||
|
ElPopover: typeof import('element-plus/es')['ElPopover']
|
||||||
ElProgress: typeof import('element-plus/es')['ElProgress']
|
ElProgress: typeof import('element-plus/es')['ElProgress']
|
||||||
ElRadio: typeof import('element-plus/es')['ElRadio']
|
ElRadio: typeof import('element-plus/es')['ElRadio']
|
||||||
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
|
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
|
||||||
@ -61,6 +62,7 @@ declare module 'vue' {
|
|||||||
ElTabPane: typeof import('element-plus/es')['ElTabPane']
|
ElTabPane: typeof import('element-plus/es')['ElTabPane']
|
||||||
ElTabs: typeof import('element-plus/es')['ElTabs']
|
ElTabs: typeof import('element-plus/es')['ElTabs']
|
||||||
ElTag: typeof import('element-plus/es')['ElTag']
|
ElTag: typeof import('element-plus/es')['ElTag']
|
||||||
|
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
||||||
ElTree: typeof import('element-plus/es')['ElTree']
|
ElTree: typeof import('element-plus/es')['ElTree']
|
||||||
ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect']
|
ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect']
|
||||||
ElUpload: typeof import('element-plus/es')['ElUpload']
|
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"
|
:default-active="route.path"
|
||||||
>
|
>
|
||||||
<!-- 菜单标题 -->
|
<!-- 菜单标题 -->
|
||||||
<h3>{{ isCollapse ? '管理' : '云泽管理后台' }}</h3>
|
<h3>{{ isCollapse ? "管理" : currentModule?.title || "子菜单" }}</h3>
|
||||||
|
|
||||||
<!-- 固定仪表盘 -->
|
<!-- 无模块时显示提示 -->
|
||||||
<el-menu-item index="/dashboard">
|
<el-menu-item v-if="!currentModule" index="/home">
|
||||||
<i class="fa-solid fa-gauge"></i>
|
<i class="fa-solid fa-house"></i>
|
||||||
<template #title>仪表盘</template>
|
<template #title>返回首页</template>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
|
|
||||||
<!-- 动态菜单项 -->
|
<!-- 动态菜单项 -->
|
||||||
<template v-for="item in list" :key="item.id">
|
<template v-for="item in displayMenus" :key="item.id">
|
||||||
<!-- 如果没有子菜单,渲染为菜单项 -->
|
<!-- 如果没有子菜单,渲染为菜单项 -->
|
||||||
<el-menu-item
|
<el-menu-item
|
||||||
v-if="!item.children || item.children.length === 0"
|
v-if="!item.children || item.children.length === 0"
|
||||||
@ -65,7 +65,7 @@
|
|||||||
v-if="!child.children || child.children.length === 0"
|
v-if="!child.children || child.children.length === 0"
|
||||||
:index="child.path || child.id.toString()"
|
: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>
|
<template #title>
|
||||||
<span>{{ child.title }}</span>
|
<span>{{ child.title }}</span>
|
||||||
</template>
|
</template>
|
||||||
@ -82,12 +82,21 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 继续递归渲染子菜单 -->
|
<!-- 继续递归渲染子菜单 -->
|
||||||
<template v-for="grandchild in child.children" :key="grandchild.id">
|
<template
|
||||||
|
v-for="grandchild in child.children"
|
||||||
|
:key="grandchild.id"
|
||||||
|
>
|
||||||
<el-menu-item
|
<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()"
|
: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>
|
<template #title>
|
||||||
<span>{{ grandchild.title }}</span>
|
<span>{{ grandchild.title }}</span>
|
||||||
</template>
|
</template>
|
||||||
@ -99,16 +108,29 @@
|
|||||||
:unique-opened="true"
|
:unique-opened="true"
|
||||||
>
|
>
|
||||||
<template #title>
|
<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>
|
<span>{{ grandchild.title }}</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 继续递归渲染... -->
|
<!-- 继续递归渲染... -->
|
||||||
<template v-for="greatGrandchild in grandchild.children" :key="greatGrandchild.id">
|
<template
|
||||||
|
v-for="greatGrandchild in grandchild.children"
|
||||||
|
:key="greatGrandchild.id"
|
||||||
|
>
|
||||||
<el-menu-item
|
<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>
|
<template #title>
|
||||||
<span>{{ greatGrandchild.title }}</span>
|
<span>{{ greatGrandchild.title }}</span>
|
||||||
</template>
|
</template>
|
||||||
@ -125,9 +147,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted, onUnmounted } from "vue";
|
import { ref, computed, onMounted, onUnmounted, watch } from "vue";
|
||||||
import { useRouter, useRoute } from "vue-router";
|
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";
|
import { useAllDataStore, useMenuStore } from "@/stores";
|
||||||
|
|
||||||
const emit = defineEmits(["menu-click"]);
|
const emit = defineEmits(["menu-click"]);
|
||||||
@ -137,17 +159,117 @@ const route = useRoute();
|
|||||||
const menuStore = useMenuStore();
|
const menuStore = useMenuStore();
|
||||||
const loading = computed(() => menuStore.loading);
|
const loading = computed(() => menuStore.loading);
|
||||||
const hasError = computed(() => menuStore.error);
|
const hasError = computed(() => menuStore.error);
|
||||||
const errorMsg = computed(() => menuStore.error || '加载菜单失败');
|
const errorMsg = computed(() => menuStore.error || "加载菜单失败");
|
||||||
|
|
||||||
const store = useAllDataStore();
|
const store = useAllDataStore();
|
||||||
const isCollapse = computed(() => store.state.isCollapse);
|
const isCollapse = computed(() => store.state.isCollapse);
|
||||||
const width = computed(() => (store.state.isCollapse ? "64px" : "200px"));
|
const width = computed(() => (store.state.isCollapse ? "64px" : "200px"));
|
||||||
|
|
||||||
// 菜单颜色配置
|
const asideBgColor = ref("#304156");
|
||||||
const asideBgColor = ref('#304156');
|
const asideTextColor = ref("#bfcbd9");
|
||||||
const asideTextColor = ref('#bfcbd9');
|
const activeColor = ref("#3973FF");
|
||||||
const activeColor = ref('#409EFF');
|
const activeBgColor = ref("#3973FF");
|
||||||
const activeBgColor = ref('#409EFF');
|
|
||||||
|
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 list = computed(() => {
|
||||||
const menuData = menuStore.menus;
|
const menuData = menuStore.menus;
|
||||||
@ -155,31 +277,10 @@ const list = computed(() => {
|
|||||||
return [];
|
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 allMenus = processMenus(menuData);
|
||||||
|
|
||||||
const sortMenusRecursively = (menus) => {
|
const sortMenusRecursively = (menus) => {
|
||||||
menus.forEach(menu => {
|
menus.forEach((menu) => {
|
||||||
if (menu.children && menu.children.length > 0) {
|
if (menu.children && menu.children.length > 0) {
|
||||||
menu.children.sort((a, b) => {
|
menu.children.sort((a, b) => {
|
||||||
const orderA = Number(a.order) ?? 999999;
|
const orderA = Number(a.order) ?? 999999;
|
||||||
@ -199,28 +300,13 @@ const list = computed(() => {
|
|||||||
return allMenus;
|
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) => {
|
const handleMenuSelect = (index) => {
|
||||||
if (index === "/dashboard") {
|
if (index === "/home") {
|
||||||
emit("menu-click", {
|
emit("menu-click", {
|
||||||
path: "/dashboard",
|
path: "/home",
|
||||||
title: "仪表盘",
|
title: "首页",
|
||||||
icon: "fa-solid fa-gauge",
|
icon: "fa-solid fa-house",
|
||||||
component_path: "/dashboard"
|
component_path: "/home",
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -234,17 +320,22 @@ const handleMenuSelect = (index) => {
|
|||||||
const fetchMenus = async () => {
|
const fetchMenus = async () => {
|
||||||
try {
|
try {
|
||||||
await menuStore.fetchMenus();
|
await menuStore.fetchMenus();
|
||||||
} catch (error) {
|
} catch (error) {}
|
||||||
// 错误已在 menuStore 中处理
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMenuRefresh = () => {
|
const handleMenuRefresh = () => {
|
||||||
fetchMenus();
|
fetchMenus();
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
watch(
|
||||||
|
() => route.path,
|
||||||
|
() => {
|
||||||
|
findCurrentMenu(list.value, route.path);
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
if (!menuStore.menus || menuStore.menus.length === 0) {
|
if (!menuStore.menus || menuStore.menus.length === 0) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
fetchMenus();
|
fetchMenus();
|
||||||
@ -255,7 +346,6 @@ onMounted(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
|
|
||||||
window.removeEventListener("menu-cache-refreshed", handleMenuRefresh);
|
window.removeEventListener("menu-cache-refreshed", handleMenuRefresh);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@ -263,14 +353,16 @@ onUnmounted(() => {
|
|||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
.common-aside {
|
.common-aside {
|
||||||
height: 100%;
|
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;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.1);
|
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
html:not(.dark) & {
|
html:not(.dark) & {
|
||||||
background: linear-gradient(135deg, #062da3 0%, #0a4a8a 100%);
|
background: #3973ff;
|
||||||
box-shadow: 2px 0 12px rgba(6, 45, 163, 0.3);
|
box-shadow: 2px 0 12px rgba(6, 45, 163, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -293,12 +385,15 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
0% { transform: rotate(0deg); }
|
0% {
|
||||||
100% { transform: rotate(360deg); }
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
line-height: 60px;
|
line-height: 60px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@ -317,7 +412,8 @@ h3 {
|
|||||||
padding: 16px 8px;
|
padding: 16px 8px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
|
|
||||||
.el-menu-item, .el-sub-menu__title {
|
.el-menu-item,
|
||||||
|
.el-sub-menu__title {
|
||||||
color: rgba(255, 255, 255, 0.85);
|
color: rgba(255, 255, 255, 0.85);
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
@ -342,20 +438,30 @@ h3 {
|
|||||||
|
|
||||||
// 高亮样式
|
// 高亮样式
|
||||||
.el-menu-item.is-active {
|
.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;
|
color: #ffffff !important;
|
||||||
border-left: 3px solid #4f84ff;
|
border-left: 3px solid #4f84ff;
|
||||||
margin-left: -3px;
|
margin-left: -3px;
|
||||||
|
|
||||||
.menu-icon {
|
.menu-icon {
|
||||||
color: #4f84ff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 悬浮样式
|
// 悬浮样式
|
||||||
.el-menu-item:hover:not(.is-active),
|
.el-menu-item:hover:not(.is-active),
|
||||||
.el-sub-menu__title:hover {
|
.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;
|
color: #ffffff !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -378,7 +484,7 @@ h3 {
|
|||||||
// 暗色主题适配
|
// 暗色主题适配
|
||||||
html.dark & {
|
html.dark & {
|
||||||
.el-menu-item.is-active {
|
.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;
|
color: var(--el-color-primary-light-3) !important;
|
||||||
border-left-color: var(--el-color-primary);
|
border-left-color: var(--el-color-primary);
|
||||||
|
|
||||||
|
|||||||
@ -15,6 +15,15 @@
|
|||||||
</el-breadcrumb> -->
|
</el-breadcrumb> -->
|
||||||
</div>
|
</div>
|
||||||
<div class="r-content">
|
<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"
|
<el-button circle :icon="Refresh" @click="refreshCache" class="refresh-cache-btn" :loading="cacheLoading"
|
||||||
title="更新菜单缓存" />
|
title="更新菜单缓存" />
|
||||||
@ -71,10 +80,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, onUnmounted } from "vue";
|
import { ref, computed, onMounted, onUnmounted } from "vue";
|
||||||
import { useRouter, useRoute } from "vue-router";
|
import { useRouter, useRoute } from "vue-router";
|
||||||
import { useAllDataStore, useMenuStore } from "@/stores";
|
import { useAllDataStore, useMenuStore, useTabsStore } from "@/stores";
|
||||||
import { useAuthStore } from "@/stores/auth";
|
import { useAuthStore } from "@/stores/auth";
|
||||||
import { logout } from "@/api/login";
|
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';
|
import { ElMessage } from 'element-plus';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -93,6 +102,7 @@ interface Breadcrumb {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const menuStore = useMenuStore();
|
const menuStore = useMenuStore();
|
||||||
|
const tabsStore = useTabsStore();
|
||||||
const cacheLoading = ref(false);
|
const cacheLoading = ref(false);
|
||||||
// 使用 store 中的菜单数据
|
// 使用 store 中的菜单数据
|
||||||
const menuList = computed(() => menuStore.menus);
|
const menuList = computed(() => menuStore.menus);
|
||||||
@ -182,6 +192,11 @@ const handleCollapse = () => {
|
|||||||
store.state.isCollapse = !store.state.isCollapse;
|
store.state.isCollapse = !store.state.isCollapse;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const goHome = () => {
|
||||||
|
tabsStore.closeAll();
|
||||||
|
router.push('/home');
|
||||||
|
};
|
||||||
|
|
||||||
const handleCommand = async (command) => {
|
const handleCommand = async (command) => {
|
||||||
if (command === 'profile') {
|
if (command === 'profile') {
|
||||||
router.push('/user/userProfile');
|
router.push('/user/userProfile');
|
||||||
@ -329,9 +344,9 @@ onUnmounted(() => {
|
|||||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||||
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
|
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
|
||||||
|
|
||||||
// 亮色主题下使用 #062da3 背景
|
// 亮色主题下使用 #3973ff 背景
|
||||||
html:not(.dark) & {
|
html:not(.dark) & {
|
||||||
background-color: #062da3;
|
background-color: #3973ff;
|
||||||
border-bottom-color: rgba(79, 132, 255, 0.3);
|
border-bottom-color: rgba(79, 132, 255, 0.3);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -368,6 +383,7 @@ onUnmounted(() => {
|
|||||||
gap: 16px;
|
gap: 16px;
|
||||||
|
|
||||||
.refresh-cache-btn,
|
.refresh-cache-btn,
|
||||||
|
.home-btn,
|
||||||
.theme-toggle-btn {
|
.theme-toggle-btn {
|
||||||
// 使用 Element Plus 的填充色变量
|
// 使用 Element Plus 的填充色变量
|
||||||
background-color: var(--el-fill-color-light);
|
background-color: var(--el-fill-color-light);
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { createRouter, createWebHashHistory } from "vue-router";
|
import { createRouter, createWebHashHistory } from "vue-router";
|
||||||
import { convertMenusToRoutes } from "./dynamicRoutes";
|
import { convertMenusToRoutes } from "./dynamicRoutes";
|
||||||
|
|
||||||
// 静态路由:登录页独立,404 页面独立
|
// 静态路由:登录页独立,404 页面独立,home 导航门户独立
|
||||||
const staticRoutes = [
|
const staticRoutes = [
|
||||||
{
|
{
|
||||||
path: "/login",
|
path: "/login",
|
||||||
@ -14,22 +14,14 @@ const staticRoutes = [
|
|||||||
name: "Main",
|
name: "Main",
|
||||||
component: () => import("@/views/Main.vue"),
|
component: () => import("@/views/Main.vue"),
|
||||||
redirect: "/dashboard",
|
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 }
|
meta: { requiresAuth: true }
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/home",
|
||||||
|
name: "Home",
|
||||||
|
component: () => import("@/views/home/index.vue"),
|
||||||
|
meta: { requiresAuth: true, title: "系统导航", isStandalone: true }
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/:pathMatch(.*)*",
|
path: "/:pathMatch(.*)*",
|
||||||
name: "NotFound",
|
name: "NotFound",
|
||||||
@ -108,21 +100,7 @@ function addDynamicRoutes(menus) {
|
|||||||
component: () => import("@/views/Main.vue"),
|
component: () => import("@/views/Main.vue"),
|
||||||
redirect: "/dashboard",
|
redirect: "/dashboard",
|
||||||
meta: { requiresAuth: true },
|
meta: { requiresAuth: true },
|
||||||
children: [
|
children: dynamicRoutes // 嵌套路由直接加入 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
|
|
||||||
]
|
|
||||||
});
|
});
|
||||||
|
|
||||||
dynamicRoutesAdded = true;
|
dynamicRoutesAdded = true;
|
||||||
@ -167,7 +145,7 @@ router.beforeEach(async (to, from, next) => {
|
|||||||
if (!dynamicRoutesAdded) {
|
if (!dynamicRoutesAdded) {
|
||||||
await loadAndAddDynamicRoutes();
|
await loadAndAddDynamicRoutes();
|
||||||
}
|
}
|
||||||
next({ path: "/" });
|
next({ path: "/home" });
|
||||||
} else {
|
} else {
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
@ -186,9 +164,9 @@ router.beforeEach(async (to, from, next) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果匹配不到路由,跳转到404
|
// 如果匹配不到路由,跳转到首页导航门户
|
||||||
if (to.matched.length === 0) {
|
if (to.matched.length === 0) {
|
||||||
next({ name: "NotFound" });
|
next({ path: "/home" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -38,7 +38,7 @@ import { ref as vueRef } from 'vue';
|
|||||||
*/
|
*/
|
||||||
export const useTabsStore = defineTabsStore('tabs', () => {
|
export const useTabsStore = defineTabsStore('tabs', () => {
|
||||||
// 固定首页tab
|
// 固定首页tab
|
||||||
const defaultDashboardPath = '/dashboard';
|
const defaultDashboardPath = '/home';
|
||||||
|
|
||||||
// 从 localStorage 恢复 tabs 状态
|
// 从 localStorage 恢复 tabs 状态
|
||||||
function loadTabsFromStorage() {
|
function loadTabsFromStorage() {
|
||||||
@ -48,16 +48,16 @@ export const useTabsStore = defineTabsStore('tabs', () => {
|
|||||||
if (savedTabs) {
|
if (savedTabs) {
|
||||||
const tabs = JSON.parse(savedTabs);
|
const tabs = JSON.parse(savedTabs);
|
||||||
// 确保至少包含首页
|
// 确保至少包含首页
|
||||||
const hasDashboard = tabs.some(t => t.fullPath === defaultDashboardPath);
|
const hasHome = tabs.some(t => t.fullPath === defaultDashboardPath);
|
||||||
if (!hasDashboard) {
|
if (!hasHome) {
|
||||||
tabs.unshift({ title: '首页', fullPath: defaultDashboardPath, name: 'Dashboard' });
|
tabs.unshift({ title: '首页', fullPath: defaultDashboardPath, name: 'Home' });
|
||||||
}
|
}
|
||||||
return tabs;
|
return tabs;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('恢复 tabs 失败:', e);
|
console.warn('恢复 tabs 失败:', e);
|
||||||
}
|
}
|
||||||
return [{ title: '首页', fullPath: defaultDashboardPath, name: 'Dashboard' }];
|
return [{ title: '首页', fullPath: defaultDashboardPath, name: 'Home' }];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存 tabs 到 localStorage
|
// 保存 tabs 到 localStorage
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
<script setup>
|
<script setup lang="ts">
|
||||||
import CommonAside from '@/components/CommonAside.vue';
|
import CommonAside from '@/components/CommonAside.vue';
|
||||||
import CommonHeader from '@/components/CommonHeader.vue';
|
import CommonHeader from '@/components/CommonHeader.vue';
|
||||||
import { useTabsStore } from '@/stores';
|
import { useTabsStore } from '@/stores';
|
||||||
@ -10,7 +10,7 @@ import { ElMessage } from 'element-plus';
|
|||||||
const tabsStore = useTabsStore();
|
const tabsStore = useTabsStore();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const defaultDashboardPath = '/dashboard';
|
const defaultDashboardPath = '/home';
|
||||||
|
|
||||||
// 根据当前路由恢复 tab(刷新时使用)
|
// 根据当前路由恢复 tab(刷新时使用)
|
||||||
function restoreTabFromRoute() {
|
function restoreTabFromRoute() {
|
||||||
@ -100,6 +100,12 @@ watch(
|
|||||||
const handleAsideMenuClick = async (menuItem) => {
|
const handleAsideMenuClick = async (menuItem) => {
|
||||||
const targetPath = menuItem.path;
|
const targetPath = menuItem.path;
|
||||||
|
|
||||||
|
if (targetPath === '/home') {
|
||||||
|
tabsStore.closeAll();
|
||||||
|
router.push('/home');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 先添加tab
|
// 先添加tab
|
||||||
tabsStore.addTab({
|
tabsStore.addTab({
|
||||||
title: menuItem.title,
|
title: menuItem.title,
|
||||||
@ -163,6 +169,11 @@ watch(
|
|||||||
// 如果新值和当前路由路径不同,且不是初始化(oldVal 不为 undefined),才进行跳转
|
// 如果新值和当前路由路径不同,且不是初始化(oldVal 不为 undefined),才进行跳转
|
||||||
// 注意:这个watch主要用于处理tab点击切换,菜单点击由handleAsideMenuClick直接处理
|
// 注意:这个watch主要用于处理tab点击切换,菜单点击由handleAsideMenuClick直接处理
|
||||||
if (newVal && oldVal !== undefined && router.currentRoute.value.fullPath !== newVal) {
|
if (newVal && oldVal !== undefined && router.currentRoute.value.fullPath !== newVal) {
|
||||||
|
// 如果切换到首页,关闭其他所有tab
|
||||||
|
if (newVal === '/home') {
|
||||||
|
tabsStore.closeAll();
|
||||||
|
}
|
||||||
|
|
||||||
// 检查路由是否存在
|
// 检查路由是否存在
|
||||||
const routeExists = router.resolve(newVal).matched.length > 0;
|
const routeExists = router.resolve(newVal).matched.length > 0;
|
||||||
|
|
||||||
@ -528,7 +539,7 @@ const canCloseRight = computed(() => {
|
|||||||
height: 100vh;
|
height: 100vh;
|
||||||
}
|
}
|
||||||
.main-header {
|
.main-header {
|
||||||
background-color: var(--header-bg-color, #0081ff);
|
background-color: var(--header-bg-color, #3973ff);
|
||||||
transition: background-color 0.3s ease;
|
transition: background-color 0.3s ease;
|
||||||
height: 80px;
|
height: 80px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|||||||
@ -57,7 +57,7 @@ const lineChartInstance = shallowRef<echarts.ECharts | null>(null);
|
|||||||
const pieChartInstance = shallowRef<echarts.ECharts | null>(null);
|
const pieChartInstance = shallowRef<echarts.ECharts | null>(null);
|
||||||
|
|
||||||
const summaryData = ref<SummaryItem[]>([
|
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: 156, icon: Pointer, color: '#67C23A', percentage: 5, isUp: true },
|
||||||
{ title: '活跃用户', value: 3420, icon: Connection, color: '#E6A23C', percentage: 2, isUp: false },
|
{ title: '活跃用户', value: 3420, icon: Connection, color: '#E6A23C', percentage: 2, isUp: false },
|
||||||
{ title: '留存率', value: 85, icon: Histogram, color: '#F56C6C', percentage: 1, isUp: true },
|
{ title: '留存率', value: 85, icon: Histogram, color: '#F56C6C', percentage: 1, isUp: true },
|
||||||
@ -84,7 +84,7 @@ const initCharts = () => {
|
|||||||
smooth: true,
|
smooth: true,
|
||||||
data: [120, 132, 101, 134, 90, 230, 210],
|
data: [120, 132, 101, 134, 90, 230, 210],
|
||||||
areaStyle: { opacity: 0.3 },
|
areaStyle: { opacity: 0.3 },
|
||||||
itemStyle: { color: '#409EFF' }
|
itemStyle: { color: '#3973FF' }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|||||||
@ -170,7 +170,7 @@ onMounted(() => {
|
|||||||
transition: all 0.3s;
|
transition: all 0.3s;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
border-color: #409eff;
|
border-color: #3973ff;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -329,7 +329,7 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.name-link {
|
.name-link {
|
||||||
color: #409eff;
|
color: #3973ff;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
transition: color 0.3s;
|
transition: color 0.3s;
|
||||||
@ -379,7 +379,7 @@ onMounted(async () => {
|
|||||||
.welcome-section {
|
.welcome-section {
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
padding: 32px;
|
padding: 32px;
|
||||||
background: linear-gradient(135deg, #062da3 0%, #4f84ff 100%);
|
background: linear-gradient(135deg, #3973ff 0%, #4f84ff 100%);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
box-shadow: 0 4px 12px rgba(6, 45, 163, 0.2);
|
box-shadow: 0 4px 12px rgba(6, 45, 163, 0.2);
|
||||||
|
|
||||||
@ -440,7 +440,7 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.income .stat-icon-wrapper {
|
&.income .stat-icon-wrapper {
|
||||||
background: linear-gradient(135deg, #062da3 0%, #4f84ff 100%);
|
background: linear-gradient(135deg, #3973ff 0%, #4f84ff 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.users .stat-icon-wrapper {
|
&.users .stat-icon-wrapper {
|
||||||
@ -456,7 +456,7 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.knowledge .stat-icon-wrapper {
|
&.knowledge .stat-icon-wrapper {
|
||||||
background: linear-gradient(135deg, #062da3 0%, #4f84ff 100%);
|
background: linear-gradient(135deg, #3973ff 0%, #4f84ff 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.employees .stat-icon-wrapper {
|
&.employees .stat-icon-wrapper {
|
||||||
|
|||||||
@ -1,11 +1,613 @@
|
|||||||
<script setup>
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<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>
|
</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>
|
</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>
|
<template>
|
||||||
<div class="login-bg">
|
<div class="login-bg">
|
||||||
<div class="login-card">
|
<div class="login-card">
|
||||||
@ -266,7 +185,86 @@ const goForget = () => {
|
|||||||
<div class="login-light light2"></div>
|
<div class="login-light light2"></div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</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>
|
<style scoped>
|
||||||
.login-bg {
|
.login-bg {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
@ -283,7 +281,8 @@ const goForget = () => {
|
|||||||
min-width: 770px;
|
min-width: 770px;
|
||||||
background: rgba(255, 255, 255, 0.95);
|
background: rgba(255, 255, 255, 0.95);
|
||||||
border-radius: 22px;
|
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);
|
0 1.5px 4px 0 rgba(30, 42, 79, 0.05);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
@ -408,7 +407,9 @@ const goForget = () => {
|
|||||||
border: 1.3px solid #d6e6fa;
|
border: 1.3px solid #d6e6fa;
|
||||||
border-radius: 7px;
|
border-radius: 7px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
transition: border 0.2s, box-shadow 0.2s;
|
transition:
|
||||||
|
border 0.2s,
|
||||||
|
box-shadow 0.2s;
|
||||||
background: #f7fbfe;
|
background: #f7fbfe;
|
||||||
margin-bottom: 3px;
|
margin-bottom: 3px;
|
||||||
}
|
}
|
||||||
@ -457,7 +458,9 @@ const goForget = () => {
|
|||||||
letter-spacing: 1px;
|
letter-spacing: 1px;
|
||||||
margin-top: 15px;
|
margin-top: 15px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background 0.2s, transform 0.13s;
|
transition:
|
||||||
|
background 0.2s,
|
||||||
|
transform 0.13s;
|
||||||
}
|
}
|
||||||
.login-btn:active {
|
.login-btn:active {
|
||||||
transform: scale(0.98);
|
transform: scale(0.98);
|
||||||
|
|||||||
@ -200,7 +200,7 @@ const clientInfo = computed(() => {
|
|||||||
|
|
||||||
// 获取内存使用颜色
|
// 获取内存使用颜色
|
||||||
const getMemoryColor = (usage: number | undefined) => {
|
const getMemoryColor = (usage: number | undefined) => {
|
||||||
if (!usage) return '#409eff';
|
if (!usage) return '#3973ff';
|
||||||
if (usage >= 90) return '#f56c6c';
|
if (usage >= 90) return '#f56c6c';
|
||||||
if (usage >= 70) return '#e6a23c';
|
if (usage >= 70) return '#e6a23c';
|
||||||
return '#67c23a';
|
return '#67c23a';
|
||||||
@ -208,7 +208,7 @@ const getMemoryColor = (usage: number | undefined) => {
|
|||||||
|
|
||||||
// 获取磁盘使用颜色
|
// 获取磁盘使用颜色
|
||||||
const getDiskColor = (usage: number | undefined) => {
|
const getDiskColor = (usage: number | undefined) => {
|
||||||
if (!usage) return '#409eff';
|
if (!usage) return '#3973ff';
|
||||||
if (usage >= 90) return '#f56c6c';
|
if (usage >= 90) return '#f56c6c';
|
||||||
if (usage >= 70) return '#e6a23c';
|
if (usage >= 70) return '#e6a23c';
|
||||||
return '#67c23a';
|
return '#67c23a';
|
||||||
|
|||||||
@ -291,7 +291,7 @@ const handleClose = () => {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
em {
|
em {
|
||||||
color: #409eff;
|
color: #3973ff;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,7 +21,7 @@
|
|||||||
<el-icon><Grid /></el-icon>
|
<el-icon><Grid /></el-icon>
|
||||||
<span>文件分组</span>
|
<span>文件分组</span>
|
||||||
</div>
|
</div>
|
||||||
<el-button
|
<el-button
|
||||||
type="primary"
|
type="primary"
|
||||||
size="small"
|
size="small"
|
||||||
@click="handleCreateCategory"
|
@click="handleCreateCategory"
|
||||||
@ -160,7 +160,7 @@
|
|||||||
<video :src="getFileUrl(file.url)" alt="file" />
|
<video :src="getFileUrl(file.url)" alt="file" />
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="isDocument(file)">
|
<div v-else-if="isDocument(file)">
|
||||||
<el-icon :size="48" color="#409eff">
|
<el-icon :size="48" color="#3973ff">
|
||||||
<Document />
|
<Document />
|
||||||
</el-icon>
|
</el-icon>
|
||||||
</div>
|
</div>
|
||||||
@ -584,7 +584,7 @@ const handleRenameCategorySuccess = () => {
|
|||||||
selectedGroup.value.id === currentRenameCategoryId.value
|
selectedGroup.value.id === currentRenameCategoryId.value
|
||||||
) {
|
) {
|
||||||
const updatedGroup = groups.value.find(
|
const updatedGroup = groups.value.find(
|
||||||
(g: any) => g.id === currentRenameCategoryId.value
|
(g: any) => g.id === currentRenameCategoryId.value,
|
||||||
);
|
);
|
||||||
if (updatedGroup) {
|
if (updatedGroup) {
|
||||||
selectGroup(updatedGroup);
|
selectGroup(updatedGroup);
|
||||||
@ -633,7 +633,7 @@ const loadFiles = async () => {
|
|||||||
cateId,
|
cateId,
|
||||||
currentPage.value,
|
currentPage.value,
|
||||||
pageSize.value,
|
pageSize.value,
|
||||||
fileSearchQuery.value
|
fileSearchQuery.value,
|
||||||
);
|
);
|
||||||
if (res.code === 200 && res.data) {
|
if (res.code === 200 && res.data) {
|
||||||
// 更新总数
|
// 更新总数
|
||||||
@ -646,7 +646,7 @@ const loadFiles = async () => {
|
|||||||
} else {
|
} else {
|
||||||
// 更新普通分组的文件数量
|
// 更新普通分组的文件数量
|
||||||
const group = groups.value.find(
|
const group = groups.value.find(
|
||||||
(g) => g.id === selectedGroup.value!.id
|
(g) => g.id === selectedGroup.value!.id,
|
||||||
);
|
);
|
||||||
if (group) {
|
if (group) {
|
||||||
group.total = res.data.total || 0;
|
group.total = res.data.total || 0;
|
||||||
@ -942,8 +942,8 @@ function handleDelete(row) {
|
|||||||
res && typeof res.code !== "undefined"
|
res && typeof res.code !== "undefined"
|
||||||
? res
|
? res
|
||||||
: res && res.data
|
: res && res.data
|
||||||
? res.data
|
? res.data
|
||||||
: res;
|
: res;
|
||||||
if (resp.code === 200) {
|
if (resp.code === 200) {
|
||||||
ElMessage.success("删除成功");
|
ElMessage.success("删除成功");
|
||||||
loadFiles();
|
loadFiles();
|
||||||
@ -987,12 +987,12 @@ onMounted(() => {
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #303133;
|
color: var(--el-text-color-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.subtitle {
|
.subtitle {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #909399;
|
color: var(--el-text-color-placeholder);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1030,13 +1030,13 @@ onMounted(() => {
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
background: #f5f7fa;
|
// background: #f5f7fa;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.3s;
|
transition: all 0.3s;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: #e6f0ff;
|
// background: #e6f0ff;
|
||||||
transform: translateX(4px);
|
transform: translateX(4px);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1059,7 +1059,7 @@ onMounted(() => {
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #303133;
|
color: var(--el-text-color-primary);
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
|
|
||||||
.group-icon {
|
.group-icon {
|
||||||
@ -1254,7 +1254,7 @@ onMounted(() => {
|
|||||||
max-height: 80vh;
|
max-height: 80vh;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
background: #f5f7fa;
|
// background: #f5f7fa;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
|
||||||
.preview-image-wrapper {
|
.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