更新架构

This commit is contained in:
2026-01-27 18:01:54 +08:00
parent 2420ce7660
commit da378a01be
33 changed files with 1490 additions and 239 deletions
+14 -3
View File
@@ -1,4 +1,4 @@
<script setup>
<script setup lang="ts">
import CommonAside from '@/components/CommonAside.vue';
import CommonHeader from '@/components/CommonHeader.vue';
import { useTabsStore } from '@/stores';
@@ -10,7 +10,7 @@ import { ElMessage } from 'element-plus';
const tabsStore = useTabsStore();
const router = useRouter();
const route = useRoute();
const defaultDashboardPath = '/dashboard';
const defaultDashboardPath = '/home';
// 根据当前路由恢复 tab(刷新时使用)
function restoreTabFromRoute() {
@@ -100,6 +100,12 @@ watch(
const handleAsideMenuClick = async (menuItem) => {
const targetPath = menuItem.path;
if (targetPath === '/home') {
tabsStore.closeAll();
router.push('/home');
return;
}
// 先添加tab
tabsStore.addTab({
title: menuItem.title,
@@ -163,6 +169,11 @@ watch(
// 如果新值和当前路由路径不同,且不是初始化(oldVal 不为 undefined),才进行跳转
// 注意:这个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;
+2 -2
View File
@@ -57,7 +57,7 @@ const lineChartInstance = shallowRef<echarts.ECharts | null>(null);
const pieChartInstance = shallowRef<echarts.ECharts | null>(null);
const summaryData = ref<SummaryItem[]>([
{ title: '总用户数', value: 12840, icon: User, color: '#409EFF', percentage: 12, isUp: true },
{ title: '总用户数', value: 12840, icon: User, color: '#3973FF', percentage: 12, isUp: true },
{ title: '今日新增', value: 156, icon: Pointer, color: '#67C23A', percentage: 5, isUp: true },
{ title: '活跃用户', value: 3420, icon: Connection, color: '#E6A23C', percentage: 2, isUp: false },
{ title: '留存率', value: 85, icon: Histogram, color: '#F56C6C', percentage: 1, isUp: true },
@@ -84,7 +84,7 @@ const initCharts = () => {
smooth: true,
data: [120, 132, 101, 134, 90, 230, 210],
areaStyle: { opacity: 0.3 },
itemStyle: { color: '#409EFF' }
itemStyle: { color: '#3973FF' }
}
]
});
@@ -170,7 +170,7 @@ onMounted(() => {
transition: all 0.3s;
&:hover {
border-color: #409eff;
border-color: #3973ff;
}
}
@@ -329,7 +329,7 @@ onMounted(async () => {
}
.name-link {
color: #409eff;
color: #3973ff;
cursor: pointer;
text-decoration: none;
transition: color 0.3s;
+3 -3
View File
@@ -379,7 +379,7 @@ onMounted(async () => {
.welcome-section {
margin-bottom: 16px;
padding: 32px;
background: linear-gradient(135deg, #062da3 0%, #4f84ff 100%);
background: linear-gradient(135deg, #3973ff 0%, #4f84ff 100%);
border-radius: 12px;
box-shadow: 0 4px 12px rgba(6, 45, 163, 0.2);
@@ -440,7 +440,7 @@ onMounted(async () => {
}
&.income .stat-icon-wrapper {
background: linear-gradient(135deg, #062da3 0%, #4f84ff 100%);
background: linear-gradient(135deg, #3973ff 0%, #4f84ff 100%);
}
&.users .stat-icon-wrapper {
@@ -456,7 +456,7 @@ onMounted(async () => {
}
&.knowledge .stat-icon-wrapper {
background: linear-gradient(135deg, #062da3 0%, #4f84ff 100%);
background: linear-gradient(135deg, #3973ff 0%, #4f84ff 100%);
}
&.employees .stat-icon-wrapper {
+608 -6
View File
@@ -1,11 +1,613 @@
<script setup>
</script>
<template>
this is home
<div class="home-container">
<div class="home-top">
<div class="toolbar">
<div class="toolbar-left">
<span class="system-name">系统管理平台</span>
</div>
<div class="toolbar-right">
<el-tooltip content="刷新菜单" placement="bottom">
<el-button
circle
:loading="refreshLoading"
class="toolbar-btn"
@click="handleRefreshMenus"
>
<el-icon><RefreshRight /></el-icon>
</el-button>
</el-tooltip>
<el-tooltip
:content="
currentTheme === 'dark' ? '切换到亮色模式' : '切换到暗色模式'
"
placement="bottom"
>
<el-button
circle
:icon="themeIcon"
class="toolbar-btn"
@click="toggleTheme"
/>
</el-tooltip>
<el-dropdown trigger="click" @command="handleCommand">
<span class="user-dropdown-link">
<el-avatar :size="32" :src="userAvatar">
{{ userName?.charAt(0)?.toUpperCase() }}
</el-avatar>
<span class="user-name">{{ userName }}</span>
<el-icon><ArrowDown /></el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="profile">
<el-icon><User /></el-icon>
<span>个人中心</span>
</el-dropdown-item>
<el-dropdown-item divided command="logout">
<el-icon><SwitchButton /></el-icon>
<span>退出登录</span>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</div>
<div class="home-wrapper">
<!-- <div class="welcome-section">
<div class="welcome-content">
<h1 class="welcome-title">
<span class="greeting">您好</span>
<span class="username">{{ userName }}</span>
</h1>
<p class="welcome-subtitle">欢迎进入系统管理平台</p>
<p class="welcome-desc">选择下方功能模块开始管理您的系统</p>
</div>
<div class="welcome-decoration">
<div class="decoration-circle circle-1"></div>
<div class="decoration-circle circle-2"></div>
<div class="decoration-circle circle-3"></div>
</div>
</div> -->
<div class="modules-section" v-if="moduleList.length > 0">
<div class="module-grid">
<div
v-for="module in moduleList"
:key="module.path"
class="module-card"
@click="handleNavigate(module.path)"
>
<div class="card-header">
<span class="card-title">{{ module.name }}</span>
<div class="card-icon-wrapper" v-html="module.icon"></div>
</div>
<div class="card-divider"></div>
<div class="card-content">
<p class="card-desc">{{ module.description || "暂无描述" }}</p>
</div>
</div>
</div>
</div>
<div v-else class="empty-state">
<el-icon :size="64" class="empty-icon"><Grid /></el-icon>
<p class="empty-text">暂无功能模块</p>
<p class="empty-tip">请联系管理员配置系统菜单</p>
</div>
</div>
</div>
</template>
<style scoped>
<script setup lang="ts">
import { ref, computed, onMounted } from "vue";
import { useRouter } from "vue-router";
import { ElMessage } from "element-plus";
import {
ArrowDown,
User,
SwitchButton,
Grid,
RefreshRight,
Sunny,
Moon,
} from "@element-plus/icons-vue";
import { getModulesList } from "@/api/modules";
import { useAuthStore } from "@/stores/auth";
import { useMenuStore } from "@/stores/menu";
interface ModuleItem {
id: number;
name: string;
code: string;
path: string;
icon: string;
description: string;
sort: number;
status: number;
}
const router = useRouter();
const authStore = useAuthStore();
const menuStore = useMenuStore();
const moduleList = ref<ModuleItem[]>([]);
const refreshLoading = ref(false);
const THEME_STORAGE_KEY = "element-plus-theme";
const isDark = ref(false);
const initTheme = () => {
const savedTheme = localStorage.getItem(THEME_STORAGE_KEY);
if (savedTheme) {
isDark.value = savedTheme === "dark";
} else {
const prefersDark = window.matchMedia(
"(prefers-color-scheme: dark)",
).matches;
isDark.value = prefersDark;
}
applyTheme();
};
const applyTheme = () => {
const htmlElement = document.documentElement;
if (isDark.value) {
htmlElement.classList.add("dark");
localStorage.setItem(THEME_STORAGE_KEY, "dark");
} else {
htmlElement.classList.remove("dark");
localStorage.setItem(THEME_STORAGE_KEY, "light");
}
};
const toggleTheme = () => {
isDark.value = !isDark.value;
applyTheme();
};
const currentTheme = computed(() => (isDark.value ? "dark" : "light"));
const themeIcon = computed(() => (isDark.value ? Sunny : Moon));
const userName = computed(
() => authStore.user?.name || authStore.user?.account || "用户",
);
const userAvatar = computed(() => authStore.user?.avatar || "");
// 处理导航跳转
function handleNavigate(path: string) {
if (path) router.push(path);
}
// 刷新菜单
async function handleRefreshMenus() {
refreshLoading.value = true;
try {
await menuStore.refreshMenus();
ElMessage.success("菜单已刷新");
} catch {
ElMessage.error("刷新菜单失败");
} finally {
refreshLoading.value = false;
}
}
// 处理用户操作命令(个人中心、退出登录)
async function handleCommand(command: string) {
switch (command) {
case "profile":
router.push("/user/userProfile");
break;
case "logout":
await handleLogout();
break;
}
}
// 处理退出登录逻辑
async function handleLogout() {
try {
const user = authStore.user;
if (user?.id) await authStore.logout(user.id);
} catch (error) {
console.error("退出登录接口调用失败:", error);
}
authStore.clearToken();
localStorage.removeItem("user");
sessionStorage.removeItem("user");
localStorage.removeItem("tenant");
sessionStorage.removeItem("tenant");
menuStore.resetMenus();
router.push("/login");
}
// 加载模块列表
async function loadModules() {
try {
const res = await getModulesList();
if (res.code === 200) {
const list = res.data?.list || [];
moduleList.value = list
.filter((item: ModuleItem) => item.status === 1 && item.is_show === 1)
.sort((a, b) => Number(a.sort) - Number(b.sort));
}
} catch (error) {
console.error("加载模块列表失败:", error);
}
}
// 组件挂载时加载模块列表
onMounted(() => {
loadModules();
initTheme();
});
</script>
<style scoped lang="scss">
// 定义一些可复用的变量(可选,建议根据项目规范调整)
$primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
$bg-gradient: linear-gradient(135deg, #f5f7fa 0%, #e4e8eb 100%);
$text-main: #1a1a2e;
$text-regular: #606266;
.home-container {
min-height: 100vh;
padding: 0;
.home-top {
background: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
position: sticky;
top: 0;
z-index: 100;
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
max-width: 1400px;
margin: 0 auto;
padding: 12px 24px;
.toolbar-left {
display: flex;
align-items: center;
.system-name {
font-size: 18px;
font-weight: 600;
color: $text-main;
}
}
.toolbar-right {
display: flex;
align-items: center;
gap: 12px;
.toolbar-btn {
background: #f5f7fa;
border-color: #e4e7ed;
color: $text-regular;
&:hover {
background: #f0f2f5;
border-color: #c0c4cc;
color: #3973ff;
}
}
.user-dropdown-link {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
padding: 4px 8px;
border-radius: 8px;
transition: background-color 0.3s;
&:hover {
background: #f5f7fa;
}
.user-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
}
.user-name {
font-size: 14px;
color: $text-regular;
max-width: 80px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
}
}
.home-wrapper {
max-width: 1400px;
margin: 0 auto;
padding: 40px 24px;
.welcome-section {
position: relative;
background: $primary-gradient;
border-radius: 20px;
padding: 48px 56px;
margin-bottom: 40px;
overflow: hidden;
box-shadow: 0 10px 40px rgba(102, 126, 234, 0.3);
.welcome-content {
position: relative;
z-index: 1;
.welcome-title {
font-size: 36px;
font-weight: 700;
color: #fff;
margin: 0 0 12px 0;
.greeting {
font-weight: 400;
opacity: 0.9;
}
.username {
font-weight: 700;
}
}
.welcome-subtitle {
font-size: 18px;
color: rgba(255, 255, 255, 0.9);
margin: 0 0 8px 0;
}
.welcome-desc {
font-size: 14px;
color: rgba(255, 255, 255, 0.7);
margin: 0;
}
}
.welcome-decoration {
position: absolute;
right: 60px;
top: 50%;
transform: translateY(-50%);
.decoration-circle {
position: absolute;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
&.circle-1 {
width: 200px;
height: 200px;
top: -80px;
right: -60px;
}
&.circle-2 {
width: 120px;
height: 120px;
bottom: -40px;
right: 120px;
}
&.circle-3 {
width: 60px;
height: 60px;
top: 20px;
right: -20px;
}
}
}
}
.modules-section {
margin-top: 20px;
.module-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
}
.module-card {
height: 220px;
background: #fff;
border-radius: 12px;
padding: 30px;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
border: 1px solid #e8e8e8;
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
.card-icon-wrapper {
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
color: #3973ff;
font-size: 30px;
}
.card-title {
font-size: 18px;
font-weight: 600;
color: $text-main;
}
}
.card-divider {
border-bottom: 1px solid #dfdfdf;
margin: 15px 0;
transition: opacity 0.3s ease;
}
.card-content {
.card-desc {
font-size: 14px;
color: #909399;
margin: 0;
line-height: 1.5;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
}
&:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(102, 126, 234, 0.15);
background-color: #3973ff;
.card-title,
.card-desc {
color: #fff;
}
.card-icon-wrapper {
color: #fff;
}
.card-divider {
opacity: 0;
}
}
}
}
.empty-state {
text-align: center;
padding: 80px 20px;
.empty-icon {
color: #c0c4cc;
margin-bottom: 16px;
}
.empty-text {
font-size: 18px;
color: #606266;
margin: 0 0 8px 0;
}
.empty-tip {
font-size: 14px;
color: #909399;
margin: 0;
}
}
}
@media (max-width: 1200px) {
.modules-section {
.module-grid {
grid-template-columns: repeat(3, 1fr);
}
}
}
@media (max-width: 992px) {
.modules-section {
.module-grid {
grid-template-columns: repeat(2, 1fr);
}
}
}
@media (max-width: 768px) {
.welcome-section {
padding: 32px 24px;
}
.welcome-title {
font-size: 28px;
}
.welcome-decoration {
display: none;
}
.modules-section {
.module-grid {
grid-template-columns: 1fr;
}
}
}
html.dark & {
.home-top {
background: #1a1a1a;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
.toolbar {
.toolbar-left .system-name {
color: #e0e0e0;
}
.toolbar-right {
.toolbar-btn {
background: #2d2d2d;
border-color: #3d3d3d;
color: #b0b0b0;
&:hover {
background: #3d3d3d;
border-color: #4d4d4d;
color: #3973ff;
}
}
.user-dropdown-link {
&:hover {
background: #2d2d2d;
}
.user-name {
color: #b0b0b0;
}
}
}
}
}
.modules-section {
.module-card {
background: #1a1a1a;
border-color: #3d3d3d;
.card-title {
color: #e0e0e0 !important;
}
.card-desc {
color: #8c8c8c !important;
}
.card-divider {
border-color: #3d3d3d;
}
}
}
}
}
</style>
+87 -84
View File
@@ -1,84 +1,3 @@
<script setup>
import { ref, onMounted } from "vue";
import { useRouter } from "vue-router";
import { useAuthStore } from "@/stores/auth";
import { login } from "@/api/login";
const router = useRouter();
const authStore = useAuthStore();
const account = ref("");
const password = ref("");
const passwordVisible = ref(false);
const rememberMe = ref(false);
const loading = ref(false);
const errorMsg = ref("");
onMounted(() => {
const savedUser = localStorage.getItem("loginAccount");
const savedRemember = localStorage.getItem("loginRememberMe");
if (savedRemember === "true") {
account.value = savedUser;
rememberMe.value = true;
}
});
const handleLogin = async () => {
errorMsg.value = "";
if (!account.value || !password.value) {
errorMsg.value = "请输入用户名和密码";
return;
}
// 记住我本地存储
if (rememberMe.value) {
localStorage.setItem("loginAccount", account.value);
localStorage.setItem("loginRememberMe", "true");
} else {
localStorage.removeItem("loginAccount");
localStorage.setItem("loginRememberMe", "false");
}
loading.value = true;
try {
const res = await login(account.value, password.value);
if (res && res.code === 200) {
authStore.setLoginInfo(res.data);
// 登录成功后重置 tabs store 为初始状态
const { useTabsStore } = await import("@/stores");
const tabsStore = useTabsStore();
tabsStore.resetTabs();
// 登录成功后缓存菜单
try {
} catch (menuError) {
console.error("Failed to process login", menuError);
// 菜单加载失败不影响登录流程
}
router.push({ path: "/dashboard" });
} else {
errorMsg.value = res.msg || "登录失败";
}
} catch (err) {
errorMsg.value =
err?.response?.data?.msg || err?.message || "登录失败,请重试";
} finally {
loading.value = false;
}
};
// 跳转注册、忘记密码页面
const goRegister = () => {
router.push({ path: "/register" });
};
const goForget = () => {
router.push({ path: "/forget" });
};
</script>
<template>
<div class="login-bg">
<div class="login-card">
@@ -266,7 +185,86 @@ const goForget = () => {
<div class="login-light light2"></div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { useRouter } from "vue-router";
import { useAuthStore } from "@/stores/auth";
import { login } from "@/api/login";
const router = useRouter();
const authStore = useAuthStore();
const account = ref("");
const password = ref("");
const passwordVisible = ref(false);
const rememberMe = ref(false);
const loading = ref(false);
const errorMsg = ref("");
onMounted(() => {
const savedUser = localStorage.getItem("loginAccount");
const savedRemember = localStorage.getItem("loginRememberMe");
if (savedRemember === "true") {
account.value = savedUser;
rememberMe.value = true;
}
});
const handleLogin = async () => {
errorMsg.value = "";
if (!account.value || !password.value) {
errorMsg.value = "请输入用户名和密码";
return;
}
// 记住我本地存储
if (rememberMe.value) {
localStorage.setItem("loginAccount", account.value);
localStorage.setItem("loginRememberMe", "true");
} else {
localStorage.removeItem("loginAccount");
localStorage.setItem("loginRememberMe", "false");
}
loading.value = true;
try {
const res = await login(account.value, password.value);
if (res && res.code === 200) {
authStore.setLoginInfo(res.data);
// 登录成功后重置 tabs store 为初始状态
const { useTabsStore } = await import("@/stores");
const tabsStore = useTabsStore();
tabsStore.resetTabs();
// 登录成功后缓存菜单
try {
} catch (menuError) {
console.error("登录处理失败", menuError);
// 菜单加载失败不影响登录流程
}
router.push({ path: "/home" });
} else {
errorMsg.value = res.msg || "登录失败";
}
} catch (err) {
errorMsg.value =
err?.response?.data?.msg || err?.message || "登录失败,请重试";
} finally {
loading.value = false;
}
};
// 跳转注册、忘记密码页面
const goRegister = () => {
router.push({ path: "/register" });
};
const goForget = () => {
router.push({ path: "/forget" });
};
</script>
<style scoped>
.login-bg {
min-height: 100vh;
@@ -283,7 +281,8 @@ const goForget = () => {
min-width: 770px;
background: rgba(255, 255, 255, 0.95);
border-radius: 22px;
box-shadow: 0 8px 36px 0 rgba(73, 150, 255, 0.14),
box-shadow:
0 8px 36px 0 rgba(73, 150, 255, 0.14),
0 1.5px 4px 0 rgba(30, 42, 79, 0.05);
overflow: hidden;
z-index: 10;
@@ -408,7 +407,9 @@ const goForget = () => {
border: 1.3px solid #d6e6fa;
border-radius: 7px;
box-sizing: border-box;
transition: border 0.2s, box-shadow 0.2s;
transition:
border 0.2s,
box-shadow 0.2s;
background: #f7fbfe;
margin-bottom: 3px;
}
@@ -457,7 +458,9 @@ const goForget = () => {
letter-spacing: 1px;
margin-top: 15px;
cursor: pointer;
transition: background 0.2s, transform 0.13s;
transition:
background 0.2s,
transform 0.13s;
}
.login-btn:active {
transform: scale(0.98);
+2 -2
View File
@@ -200,7 +200,7 @@ const clientInfo = computed(() => {
// 获取内存使用颜色
const getMemoryColor = (usage: number | undefined) => {
if (!usage) return '#409eff';
if (!usage) return '#3973ff';
if (usage >= 90) return '#f56c6c';
if (usage >= 70) return '#e6a23c';
return '#67c23a';
@@ -208,7 +208,7 @@ const getMemoryColor = (usage: number | undefined) => {
// 获取磁盘使用颜色
const getDiskColor = (usage: number | undefined) => {
if (!usage) return '#409eff';
if (!usage) return '#3973ff';
if (usage >= 90) return '#f56c6c';
if (usage >= 70) return '#e6a23c';
return '#67c23a';
@@ -291,7 +291,7 @@ const handleClose = () => {
text-align: center;
em {
color: #409eff;
color: #3973ff;
font-style: normal;
}
}
+13 -13
View File
@@ -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 {
+472
View File
@@ -0,0 +1,472 @@
<template>
<div class="container-box">
<div class="header-bar">
<h2>模块管理</h2>
<div class="header-actions">
<el-button type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>
添加模块
</el-button>
<el-button @click="refresh">
<el-icon><Refresh /></el-icon>
刷新
</el-button>
</div>
</div>
<el-divider></el-divider>
<el-table
:data="modules"
style="width: 100%"
v-loading="loading"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" align="center" />
<el-table-column prop="id" label="ID" width="80" align="center" />
<el-table-column
prop="name"
label="模块名称"
min-width="150"
align="center"
>
<template #default="{ row }">
<div class="module-name">
<span class="module-icon" v-html="row.icon"></span>
<span>{{ row.name }}</span>
</div>
</template>
</el-table-column>
<el-table-column
prop="code"
label="模块编码"
min-width="120"
align="center"
/>
<el-table-column
prop="path"
label="路由路径"
min-width="150"
align="center"
>
<template #default="{ row }">
<el-tag size="small" type="info">{{ row.path || "-" }}</el-tag>
</template>
</el-table-column>
<el-table-column
prop="description"
label="描述"
min-width="200"
align="center"
show-overflow-tooltip
/>
<el-table-column prop="sort" label="排序" width="100" align="center" />
<el-table-column prop="is_show" label="显示" width="80" align="center">
<template #default="{ row }">
<el-switch
v-model="row.is_show"
:active-value="1"
:inactive-value="0"
@change="handleShowChange(row)"
/>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="80" align="center">
<template #default="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'danger'">
{{ row.status === 1 ? "启用" : "禁用" }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="200" align="center" fixed="right">
<template #default="{ row }">
<el-button size="small" @click="handleEdit(row)">编辑</el-button>
<el-button size="small" type="danger" @click="handleDelete(row)"
>删除</el-button
>
</template>
</el-table-column>
</el-table>
<div class="table-footer" v-if="selectedModules.length > 0">
<span>已选择 {{ selectedModules.length }} </span>
<el-button type="danger" size="small" @click="handleBatchDelete"
>批量删除</el-button
>
</div>
<el-divider></el-divider>
<div class="tips-section">
<el-alert title="模块管理说明" type="info" :closable="false" show-icon>
<template #default>
<p>1. 模块用于管理系统功能单元每个模块包含独立的路由图标和描述</p>
<p>2. 模块编码用于程序识别请确保唯一性</p>
<p>3. 禁用状态的模块将不会在菜单中显示</p>
</template>
</el-alert>
</div>
<el-dialog
v-model="dialogVisible"
:title="dialogType === 'add' ? '添加模块' : '编辑模块'"
width="600px"
:close-on-click-modal="false"
>
<el-form
ref="formRef"
:model="formData"
:rules="rules"
label-width="100px"
>
<el-form-item label="模块名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入模块名称" />
</el-form-item>
<el-form-item label="模块编码" prop="code">
<el-input
v-model="formData.code"
placeholder="请输入模块编码(英文)"
:disabled="dialogType === 'edit'"
/>
</el-form-item>
<el-form-item label="路由路径" prop="path">
<el-input
v-model="formData.path"
placeholder="请输入路由路径,如 /system/modules"
/>
</el-form-item>
<el-form-item label="图标" prop="icon">
<el-input
v-model="formData.icon"
placeholder="请输入图标类名,如 Grid"
>
<template #append>
<el-popover placement="bottom-end" :width="400" trigger="click">
<template #reference>
<el-button link>选择图标</el-button>
</template>
<div class="icon-grid">
<div
v-for="icon in iconList"
:key="icon"
class="icon-item"
:class="{ active: formData.icon === icon }"
@click="formData.icon = icon"
>
<el-icon :size="20"><component :is="icon" /></el-icon>
</div>
</div>
</el-popover>
</template>
</el-input>
</el-form-item>
<el-form-item label="描述" prop="description">
<el-input
v-model="formData.description"
type="textarea"
:rows="3"
placeholder="请输入模块描述"
/>
</el-form-item>
<el-form-item label="排序" prop="sort">
<el-input-number
v-model="formData.sort"
:min="0"
:max="999"
controls-position="right"
/>
</el-form-item>
<el-form-item label="是否显示" prop="is_show">
<el-switch
v-model="formData.is_show"
:active-value="1"
:inactive-value="0"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="formData.status">
<el-radio :label="1">启用</el-radio>
<el-radio :label="0">禁用</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit" :loading="submitLoading"
>确定</el-button
>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted, shallowRef } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import {
getModulesList,
getModuleDetail,
addModule,
editModule,
deleteModule,
batchDeleteModules,
changeModuleStatus,
} from "@/api/modules";
const loading = ref(false);
const modules = ref([]);
const selectedModules = ref([]);
const dialogVisible = ref(false);
const dialogType = ref("add");
const formRef = ref(null);
const submitLoading = ref(false);
const formData = ref({
name: "",
code: "",
path: "",
icon: "",
description: "",
sort: 0,
is_show: 1,
status: 1,
});
const rules = {
name: [{ required: true, message: "请输入模块名称", trigger: "blur" }],
code: [{ required: true, message: "请输入模块编码", trigger: "blur" }],
};
function getIconComponent(iconName) {
return iconComponents.value[iconName] || Grid;
}
async function fetchModules() {
loading.value = true;
try {
const res = await getModulesList();
if (res.code === 200 && res.data) {
modules.value = res.data.list || [];
}
} catch (error) {
console.error("获取模块列表失败:", error);
ElMessage.error("获取模块列表失败");
} finally {
loading.value = false;
}
}
function refresh() {
fetchModules();
}
function handleSelectionChange(selection) {
selectedModules.value = selection;
}
function handleAdd() {
dialogType.value = "add";
formData.value = {
name: "",
code: "",
path: "",
icon: "",
description: "",
sort: 0,
is_show: 1,
status: 1,
};
dialogVisible.value = true;
}
async function handleEdit(row) {
dialogType.value = "edit";
try {
const res = await getModuleDetail(row.id);
if (res.code === 200 && res.data) {
formData.value = { ...res.data };
dialogVisible.value = true;
} else {
ElMessage.error(res.msg || "获取模块详情失败");
}
} catch (error) {
console.error("获取模块详情失败:", error);
ElMessage.error("获取模块详情失败");
}
}
async function handleSubmit() {
try {
await formRef.value.validate();
submitLoading.value = true;
if (dialogType.value === "add") {
const res = await addModule(formData.value);
if (res.code === 200) {
ElMessage.success("添加成功");
dialogVisible.value = false;
fetchModules();
} else {
ElMessage.error(res.msg || "添加失败");
}
} else {
const res = await editModule(formData.value.id, formData.value);
if (res.code === 200) {
ElMessage.success("编辑成功");
dialogVisible.value = false;
fetchModules();
} else {
ElMessage.error(res.msg || "编辑失败");
}
}
} catch (error) {
console.error("提交失败:", error);
} finally {
submitLoading.value = false;
}
}
async function handleDelete(row) {
try {
await ElMessageBox.confirm("确定要删除该模块吗?", "提示", {
type: "warning",
});
const res = await deleteModule(row.id);
if (res.code === 200) {
ElMessage.success("删除成功");
fetchModules();
} else {
ElMessage.error(res.msg || "删除失败");
}
} catch (error) {
if (error !== "cancel") {
console.error("删除失败:", error);
ElMessage.error("删除失败");
}
}
}
async function handleBatchDelete() {
try {
await ElMessageBox.confirm(
`确定要删除选中的 ${selectedModules.value.length} 个模块吗?`,
"提示",
{
type: "warning",
},
);
const ids = selectedModules.value.map((item) => item.id);
const res = await batchDeleteModules(ids);
if (res.code === 200) {
ElMessage.success("批量删除成功");
fetchModules();
} else {
ElMessage.error(res.msg || "批量删除失败");
}
} catch (error) {
if (error !== "cancel") {
console.error("批量删除失败:", error);
ElMessage.error("批量删除失败");
}
}
}
async function handleShowChange(row) {
try {
const res = await changeModuleStatus(row.id, row.is_show);
if (res.code !== 200) {
ElMessage.error(res.msg || "状态修改失败");
row.is_show = row.is_show ? 0 : 1;
}
} catch (error) {
console.error("状态修改失败:", error);
row.is_show = row.is_show ? 0 : 1;
}
}
onMounted(() => {
fetchModules();
});
</script>
<style scoped lang="scss">
.container-box {
padding: 20px;
background: var(--el-bg-color);
border-radius: 8px;
}
.header-bar {
display: flex;
justify-content: space-between;
align-items: center;
h2 {
margin: 0;
font-size: 18px;
font-weight: 600;
color: var(--el-text-color-primary);
}
}
.header-actions {
display: flex;
gap: 12px;
}
.module-name {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
.module-icon {
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
color: #667eea;
}
}
.table-footer {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 0;
}
.tips-section {
margin-top: 20px;
p {
margin: 4px 0;
font-size: 13px;
color: var(--el-text-color-secondary);
}
}
.icon-grid {
display: grid;
grid-template-columns: repeat(8, 1fr);
gap: 8px;
max-height: 200px;
overflow-y: auto;
.icon-item {
display: flex;
align-items: center;
justify-content: center;
padding: 8px;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s;
&:hover,
&.active {
background: var(--el-color-primary-light-9);
color: var(--el-color-primary);
}
}
}
</style>