Files
yunzerwebsiteallinone/backend/src/views/home/index.vue
T
2026-09-09 23:51:00 +08:00

978 lines
24 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="home-container" :class="{ dark: isDark }">
<div class="home-top">
<div class="toolbar">
<div class="toolbar-left">
<template v-if="tenantName">
<span class="tenant-name" :title="tenantName">
<el-icon class="tenant-icon"><OfficeBuilding /></el-icon>
{{ tenantName }}
</span>
</template>
</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-meta">
<span class="user-name">{{ userName }}</span>
<span v-if="userSubtitle" class="user-subtitle">{{ userSubtitle }}</span>
</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)"
>
<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)"
>
<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)"
>
<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 class="home-footer">
<span class="copyright">云泽管理平台</span>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from "vue";
import { useRouter } from "vue-router";
import { ElMessage } from "element-plus";
import {
ArrowDown,
User,
SwitchButton,
Grid,
RefreshRight,
Sunny,
Moon,
OfficeBuilding,
} from "@element-plus/icons-vue";
import { getTenantList } from "@/api/modules";
import { getCurrentUserProfile, logout } from "@/api/login";
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 || "");
// 当前登录用户的角色 / 部门 / 职位(来自后端 /backend/getCurrentUserProfile)
const userProfile = reactive({ department: "", position: "", role_name: "" });
// 优先展示「部门 · 职位」,二者皆无时回退到角色名
const userSubtitle = computed(() => {
const dept = (userProfile.department || "").trim();
const pos = (userProfile.position || "").trim();
if (dept || pos) {
return [dept, pos].filter(Boolean).join(" · ");
}
return (userProfile.role_name || "").trim();
});
const fetchUserProfile = async () => {
if (!authStore.token) return;
try {
const res = await getCurrentUserProfile();
if (res && res.code === 200 && res.data) {
userProfile.department = res.data.department || "";
userProfile.position = res.data.position || "";
userProfile.role_name = res.data.role_name || "";
}
} catch (e) {
console.error("Failed to fetch current user profile:", e);
}
};
// 当前租户(企业)名称,来自后端 /backend/getCurrentUser 或登录接口返回的 tenant_name
const tenantName = computed(() => authStore.user?.tenant_name || "");
// 处理导航跳转
function handleNavigate(module: ModuleItem) {
const isWebsiteManagement = module.name === "网站管理" || module.path.startsWith("/apps/cms");
const targetPath = isWebsiteManagement
? "/apps/cms/analytics/content"
: module.path;
if (targetPath) router.push(targetPath);
}
// 刷新菜单
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 logout(user);
} 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();
// 拉取当前登录用户的角色 / 部门 / 职位,用于右上角展示
if (authStore.token) {
fetchUserProfile();
}
// 兜底:若登录时未带租户名称(老会话/缓存),主动拉取一次以显示企业名称
if (!tenantName.value) {
authStore.fetchCurrentUser();
}
});
</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;
display: flex;
flex-direction: column;
.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;
.tenant-name {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 15px;
font-weight: 500;
color: @text-regular;
max-width: 220px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
.tenant-icon {
font-size: 16px;
color: #3973ff;
}
}
}
.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-meta {
display: flex;
flex-direction: column;
justify-content: center;
line-height: 1.2;
min-width: 0;
}
.user-name {
font-size: 14px;
color: @text-regular;
max-width: 120px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.user-subtitle {
font-size: 12px;
color: #909399;
max-width: 160px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
}
}
.home-wrapper {
flex: 1 0 auto;
width: 100%;
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;
}
}
}
.home-footer {
flex: 0 0 auto;
margin-top: auto;
width: 100%;
border-top: 1px solid #ebeef5;
padding: 18px 24px 28px;
text-align: center;
color: #909399;
background: transparent;
.copyright {
font-size: 13px;
letter-spacing: 0.5px;
}
}
@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: repeat(2, 1fr);
}
}
}
// ===== 移动端优先适配(手机端为主要使用场景)=====
@media (max-width: 768px) {
.home-top {
.toolbar {
padding: 8px 12px;
.toolbar-left {
min-width: 0;
flex-shrink: 1;
}
.toolbar-right {
gap: 6px;
flex-shrink: 0;
// 扩大移动端点击区域(≥44px)
.toolbar-btn {
width: 40px;
height: 40px;
}
.user-dropdown-link {
padding: 6px 8px;
min-height: 40px;
.user-name {
max-width: 72px;
}
}
}
}
}
.home-wrapper {
padding: 20px 12px;
.modules-section {
margin-top: 8px;
.category-section {
margin-bottom: 24px;
.category-title {
margin-bottom: 14px;
}
}
.module-grid {
gap: 12px;
}
.module-card {
height: auto;
min-height: 132px;
padding: 16px;
border-radius: 10px;
.card-header {
gap: 8px;
.card-title {
font-size: 16px;
word-break: break-all;
}
.card-icon-wrapper {
width: 24px;
height: 24px;
font-size: 24px;
flex-shrink: 0;
}
}
.card-divider {
margin: 12px 0;
}
.card-content .card-desc {
font-size: 13px;
-webkit-line-clamp: 2;
}
}
}
.empty-state {
padding: 48px 16px;
}
}
}
// 超窄屏手机:保持每行 2 个卡片,收紧间距避免溢出,用户名隐藏只保留头像
@media (max-width: 480px) {
.home-top {
.toolbar {
padding: 8px 10px;
.toolbar-right {
.user-dropdown-link {
padding: 6px 4px;
.user-meta {
display: none;
}
}
}
.toolbar-left {
.tenant-name {
display: none;
}
}
}
}
.home-wrapper {
padding: 16px 10px;
.modules-section {
.module-grid {
grid-template-columns: repeat(2, 1fr);
gap: 8px;
}
.module-card {
min-height: 0;
padding: 12px;
border-radius: 8px;
.card-header {
gap: 6px;
.card-title {
font-size: 14px;
}
.card-icon-wrapper {
width: 20px;
height: 20px;
font-size: 20px;
}
}
.card-divider {
margin: 10px 0;
}
.card-content .card-desc {
font-size: 12px;
}
}
}
}
}
html.dark & {
.home-top {
background: #1a1a1a;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
.toolbar {
.toolbar-left .tenant-name {
color: #cfd3dc;
.tenant-icon {
color: #5a8dff;
}
}
.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: #cfd3dc;
}
.user-subtitle {
color: #8c8c8c;
}
}
}
}
}
.modules-section {
.category-title {
color: #f5f7fa;
border-left-color: #667eea;
}
.module-card {
background: #1a1a1a;
border-color: #3d3d3d;
.card-title {
color: #ffffff !important;
font-weight: 700;
}
.card-desc {
color: #8c8c8c !important;
}
.card-divider {
border-color: #3d3d3d;
}
}
}
.home-footer {
border-top-color: #2d2d2d;
color: #8c8c8c;
}
}
// 在容器自身挂上 .dark 时同样应用暗色样式,避免仅靠 <html class="dark"> 时未命中
&.dark {
.modules-section .category-title {
color: #ffffff !important;
border-left-color: #667eea;
font-weight: 700;
}
.modules-section .module-card .card-title {
color: #ffffff !important;
font-weight: 700;
}
.modules-section .module-card .card-desc {
color: #8c8c8c !important;
}
}
}
</style>