first commit

This commit is contained in:
2026-06-03 10:09:03 +08:00
commit 81f5039458
6266 changed files with 188236 additions and 0 deletions
+680
View File
@@ -0,0 +1,680 @@
<template>
<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="modules-section" v-if="moduleList.length > 0">
<!-- 功能模块 (type=1) -->
<div v-if="basicModules.length > 0" class="category-section">
<div class="category-title">功能模块</div>
<div class="module-grid">
<div
v-for="module in basicModules"
: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>
<!-- 系统设置 (type=2) -->
<div v-if="systemModules.length > 0" class="category-section">
<div class="category-title">系统设置</div>
<div class="module-grid">
<div
v-for="module in systemModules"
: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>
<!-- 未分类 (type=0) -->
<div v-if="uncategorizedModules.length > 0" class="category-section">
<div class="category-title">未分类</div>
<div class="module-grid">
<div
v-for="module in uncategorizedModules"
: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>
<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>
<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 { getTenantList } 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;
type: number;
title?: string;
}
const router = useRouter();
const authStore = useAuthStore();
const menuStore = useMenuStore();
// 根据type分组模块
const basicModules = computed(() =>
moduleList.value.filter((item) => item.type === 1)
);
const systemModules = computed(() =>
moduleList.value.filter((item) => item.type === 2)
);
const uncategorizedModules = computed(() =>
moduleList.value.filter((item) => item.type === 0)
);
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 getTenantList();
if (res.code === 200) {
const list = res.data?.list || [];
const filteredList = list
.filter((item: ModuleItem) => item.status === 1 && item.is_show === 1)
.sort((a, b) => Number(a.sort) - Number(b.sort));
moduleList.value = filteredList
.map((item: ModuleItem) => {
const mapped = {
...item,
type: item.type ?? 0, // 默认为0(未分类)
title: item.name // 添加title字段用于显示分类标题
};
return mapped;
});
}
} catch (error) {
console.error("加载模块列表失败:", error);
}
}
// 组件挂载时加载模块列表
onMounted(() => {
loadModules();
initTheme();
});
</script>
<style scoped lang="less">
// 定义一些可复用的变量(可选,建议根据项目规范调整)
@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;
.category-section {
margin-bottom: 32px;
.category-title {
font-size: 16px;
font-weight: 600;
color: @text-main;
margin: 0 0 20px 0;
padding-left: 4px;
border-left: 4px solid #667eea;
}
}
.module-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 20px;
}
.module-card {
height: 180px;
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: 3;
-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>