Files
yunzerwebsiteallinone/backend/src/views/home/index.vue
T

1276 lines
32 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"
: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>