1276 lines
32 KiB
Vue
1276 lines
32 KiB
Vue
<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"
|
||
:class="{ disabled: !module.enabled }"
|
||
@click="handleNavigate(module)"
|
||
>
|
||
<div class="card-header">
|
||
<span class="card-title">
|
||
{{ module.name }}
|
||
<el-icon v-if="module.locked" class="lock-badge"><Lock /></el-icon>
|
||
</span>
|
||
<div class="card-icon-wrapper" v-html="module.icon"></div>
|
||
</div>
|
||
<div class="card-divider"></div>
|
||
<div class="card-content">
|
||
<template v-if="module.enabled">
|
||
<p class="card-desc">{{ module.description || "暂无描述" }}</p>
|
||
</template>
|
||
<template v-else>
|
||
<div class="card-locked">
|
||
<el-icon class="lock-icon"><Lock /></el-icon>
|
||
<p class="lock-tip">请购买套餐后使用</p>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
<template v-if="!module.enabled">
|
||
<el-button
|
||
size="small"
|
||
type="primary"
|
||
link
|
||
class="buy-btn"
|
||
@click.stop="handleBuyPackage(module)"
|
||
>
|
||
购买套餐
|
||
</el-button>
|
||
</template>
|
||
</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"
|
||
:class="{ disabled: !module.enabled }"
|
||
@click="handleNavigate(module)"
|
||
>
|
||
<div class="card-header">
|
||
<span class="card-title">
|
||
{{ module.name }}
|
||
<el-icon v-if="module.locked" class="lock-badge"><Lock /></el-icon>
|
||
</span>
|
||
<div class="card-icon-wrapper" v-html="module.icon"></div>
|
||
</div>
|
||
<div class="card-divider"></div>
|
||
<div class="card-content">
|
||
<template v-if="module.enabled">
|
||
<p class="card-desc">{{ module.description || "暂无描述" }}</p>
|
||
</template>
|
||
<template v-else>
|
||
<div class="card-locked">
|
||
<el-icon class="lock-icon"><Lock /></el-icon>
|
||
<p class="lock-tip">请购买套餐后使用</p>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
<template v-if="!module.enabled">
|
||
<el-button
|
||
size="small"
|
||
type="primary"
|
||
link
|
||
class="buy-btn"
|
||
@click.stop="handleBuyPackage(module)"
|
||
>
|
||
购买套餐
|
||
</el-button>
|
||
</template>
|
||
</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"
|
||
:class="{ disabled: !module.enabled }"
|
||
@click="handleNavigate(module)"
|
||
>
|
||
<div class="card-header">
|
||
<span class="card-title">
|
||
{{ module.name }}
|
||
<el-icon v-if="module.locked" class="lock-badge"><Lock /></el-icon>
|
||
</span>
|
||
<div class="card-icon-wrapper" v-html="module.icon"></div>
|
||
</div>
|
||
<div class="card-divider"></div>
|
||
<div class="card-content">
|
||
<template v-if="module.enabled">
|
||
<p class="card-desc">{{ module.description || "暂无描述" }}</p>
|
||
</template>
|
||
<template v-else>
|
||
<div class="card-locked">
|
||
<el-icon class="lock-icon"><Lock /></el-icon>
|
||
<p class="lock-tip">请购买套餐后使用</p>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
<template v-if="!module.enabled">
|
||
<el-button
|
||
size="small"
|
||
type="primary"
|
||
link
|
||
class="buy-btn"
|
||
@click.stop="handleBuyPackage(module)"
|
||
>
|
||
购买套餐
|
||
</el-button>
|
||
</template>
|
||
</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, ElMessageBox } from "element-plus";
|
||
|
||
import {
|
||
ArrowDown,
|
||
User,
|
||
SwitchButton,
|
||
Grid,
|
||
RefreshRight,
|
||
Sunny,
|
||
Moon,
|
||
OfficeBuilding,
|
||
Lock,
|
||
Lock,
|
||
} 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;
|
||
enabled?: boolean;
|
||
}
|
||
|
||
const router = useRouter();
|
||
const authStore = useAuthStore();
|
||
const menuStore = useMenuStore();
|
||
|
||
// 已开通模块排在前面,套餐未开通(锁定)的模块沉到后方
|
||
function sortUnlockedFirst(list: ModuleItem[]): ModuleItem[] {
|
||
return [...list].sort(
|
||
(a, b) => Number(!!a.locked) - Number(!!b.locked)
|
||
);
|
||
}
|
||
|
||
// 根据type分组模块
|
||
const basicModules = computed(() =>
|
||
sortUnlockedFirst(moduleList.value.filter((item) => item.type === 1))
|
||
);
|
||
const systemModules = computed(() =>
|
||
sortUnlockedFirst(moduleList.value.filter((item) => item.type === 2))
|
||
);
|
||
const uncategorizedModules = computed(() =>
|
||
sortUnlockedFirst(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 || "");
|
||
|
||
// 当前租户生效套餐名称(用于未开通提示)
|
||
const packageName = ref("");
|
||
// 套餐购买链接(平台端配置,为空时点击「购买」仅提示联系管理员)
|
||
const purchaseUrl = ref("");
|
||
|
||
// 套餐未开通模块:不跳转,提示购买套餐后使用
|
||
function showLockedTip(module: ModuleItem) {
|
||
const pkg = packageName.value ? `「${packageName.value}」` : "当前";
|
||
ElMessageBox.alert(
|
||
`${pkg}套餐未包含「${module.name}」功能模块,请购买或升级套餐后使用。如有疑问请联系平台管理员。`,
|
||
"功能未开通",
|
||
{
|
||
confirmButtonText: "我知道了",
|
||
type: "warning",
|
||
customClass: "module-locked-tip"
|
||
}
|
||
).catch(() => {});
|
||
}
|
||
|
||
// 处理导航跳转
|
||
function handleNavigate(module: ModuleItem) {
|
||
if (module.enabled === false) {
|
||
ElMessage.warning(`「${module.name}」暂未开通,请先购买对应套餐`);
|
||
return;
|
||
}
|
||
|
||
const isWebsiteManagement = module.name === "网站管理" || module.path.startsWith("/apps/cms");
|
||
const targetPath = isWebsiteManagement
|
||
? "/apps/cms/analytics/content"
|
||
: module.path;
|
||
|
||
if (targetPath) router.push(targetPath);
|
||
}
|
||
|
||
// 未开通功能 → 引导购买套餐
|
||
function handleBuyPackage(module: ModuleItem) {
|
||
ElMessage.info(`购买「${module.name}」套餐功能 —— 请联系管理员或前往平台套餐中心`);
|
||
// TODO: 未来接入购买页面后 router.push(...) 到对应套餐购买界面
|
||
}
|
||
|
||
// 刷新菜单
|
||
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)
|
||
.map((item: ModuleItem) => ({
|
||
...item,
|
||
enabled: item.enabled !== false, // 后端返回 enabled,缺失时默认 true
|
||
title: item.name,
|
||
}))
|
||
.sort((a: ModuleItem, b: ModuleItem) => {
|
||
// 已开通在前
|
||
if (a.enabled !== b.enabled) return a.enabled ? -1 : 1;
|
||
// 同状态内按 sort 升序、id 升序
|
||
const sortDiff = Number(a.sort) - Number(b.sort);
|
||
if (sortDiff !== 0) return sortDiff;
|
||
return Number(a.id) - Number(b.id);
|
||
});
|
||
moduleList.value = filteredList;
|
||
}
|
||
} 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 {
|
||
min-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 {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
color: @text-main;
|
||
|
||
.lock-badge {
|
||
font-size: 14px;
|
||
color: #c0c4cc;
|
||
}
|
||
}
|
||
}
|
||
|
||
.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;
|
||
}
|
||
}
|
||
|
||
.card-lock-tip {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 8px;
|
||
margin-top: 12px;
|
||
font-size: 12px;
|
||
color: #c0c4cc;
|
||
|
||
.buy-btn {
|
||
flex-shrink: 0;
|
||
padding: 2px 10px;
|
||
border: 1px solid #3973ff;
|
||
border-radius: 12px;
|
||
color: #3973ff;
|
||
font-size: 12px;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
|
||
&:hover {
|
||
background: #3973ff;
|
||
border-color: #3973ff;
|
||
color: #fff;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 套餐未开通模块:置灰 + 禁止点击效果
|
||
&.is-locked {
|
||
background: #fafafa;
|
||
border-style: dashed;
|
||
border-color: #dcdfe6;
|
||
box-shadow: none;
|
||
cursor: not-allowed;
|
||
filter: grayscale(1);
|
||
opacity: 0.8;
|
||
|
||
.card-title {
|
||
color: #909399;
|
||
}
|
||
|
||
.card-desc {
|
||
color: #a8abb2;
|
||
}
|
||
|
||
.card-icon-wrapper {
|
||
color: #c0c4cc;
|
||
}
|
||
|
||
.card-divider {
|
||
border-color: #e4e7ed;
|
||
}
|
||
|
||
&:hover {
|
||
transform: none;
|
||
background: #fafafa;
|
||
box-shadow: none;
|
||
|
||
.card-title,
|
||
.card-desc {
|
||
color: #909399;
|
||
}
|
||
|
||
.card-icon-wrapper {
|
||
color: #c0c4cc;
|
||
}
|
||
|
||
.card-divider {
|
||
opacity: 1;
|
||
}
|
||
}
|
||
}
|
||
|
||
&: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;
|
||
}
|
||
}
|
||
|
||
// 未开通的套餐模块(disabled 状态)
|
||
&.disabled {
|
||
cursor: not-allowed;
|
||
background: #f7f8fa;
|
||
border-color: #e4e7ed;
|
||
box-shadow: none;
|
||
|
||
.card-title {
|
||
color: #909399;
|
||
}
|
||
|
||
.card-icon-wrapper {
|
||
color: #c0c4cc;
|
||
}
|
||
|
||
.card-divider {
|
||
border-bottom-color: #ebeef5;
|
||
}
|
||
|
||
// hover 禁用所有悬浮效果
|
||
&:hover {
|
||
transform: none;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||
background: #f7f8fa;
|
||
|
||
.card-title,
|
||
.card-desc {
|
||
color: #909399;
|
||
}
|
||
|
||
.card-icon-wrapper {
|
||
color: #c0c4cc;
|
||
}
|
||
|
||
.card-divider {
|
||
opacity: 1;
|
||
}
|
||
}
|
||
|
||
// 锁图标 + 提示文字
|
||
.card-locked {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 6px 0 0;
|
||
|
||
.lock-icon {
|
||
font-size: 22px;
|
||
color: #c0c4cc;
|
||
}
|
||
|
||
.lock-tip {
|
||
font-size: 12px;
|
||
color: #909399;
|
||
margin: 0;
|
||
}
|
||
}
|
||
|
||
.buy-btn {
|
||
margin-top: 6px;
|
||
font-size: 12px;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
// 暗色主题下未开通卡片
|
||
&.disabled {
|
||
background: #222;
|
||
border-color: #333;
|
||
|
||
.card-title {
|
||
color: #666 !important;
|
||
}
|
||
|
||
.card-icon-wrapper {
|
||
color: #555;
|
||
}
|
||
|
||
.card-divider {
|
||
border-color: #2e2e2e;
|
||
}
|
||
|
||
.card-locked .lock-icon {
|
||
color: #555;
|
||
}
|
||
|
||
.card-locked .lock-tip {
|
||
color: #666;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
.modules-section .module-card.is-locked {
|
||
background: #1f1f1f;
|
||
|
||
.card-title {
|
||
color: #8c8c8c !important;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.card-desc {
|
||
color: #6b6b6b !important;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</style>
|