优化backend前端布局
This commit is contained in:
1 parent
68617335f0
commit
e34543465a
11 files changed
+1899
-903
No files matched your search
Vendored
+2
@@ -11,6 +11,7 @@ export {}
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
AsideMenuItem: typeof import('./src/components/AsideMenuItem.vue')['default']
|
||||
CommonAside: typeof import('./src/components/CommonAside.vue')['default']
|
||||
CommonHeader: typeof import('./src/components/CommonHeader.vue')['default']
|
||||
ElAlert: typeof import('element-plus/es')['ElAlert']
|
||||
@@ -87,6 +88,7 @@ declare module 'vue' {
|
||||
ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect']
|
||||
ElUpload: typeof import('element-plus/es')['ElUpload']
|
||||
FlowCanvas: typeof import('./src/components/approval/FlowCanvas.vue')['default']
|
||||
MenuIcon: typeof import('./src/components/MenuIcon.vue')['default']
|
||||
MessageDetailDialog: typeof import('./src/components/MessageDetailDialog.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
|
||||
@@ -1,12 +1,51 @@
|
||||
// Element Plus Message z-index
|
||||
// 全局主题:浅蓝白后台框架
|
||||
// 自定义变量供顶栏 / 侧栏 / 内容区统一取色,Element Plus 主色同步收敛。
|
||||
:root {
|
||||
--el-message-z-index: 9999;
|
||||
|
||||
--el-color-primary: #2f6bff;
|
||||
--el-color-primary-light-3: #6f97ff;
|
||||
--el-color-primary-light-5: #97b5ff;
|
||||
--el-color-primary-light-7: #c5d8ff;
|
||||
--el-color-primary-light-8: #dce7ff;
|
||||
--el-color-primary-light-9: #eef4ff;
|
||||
--el-color-primary-dark-2: #2555cc;
|
||||
|
||||
--yz-primary: #2f6bff;
|
||||
--yz-primary-soft: #eef4ff;
|
||||
--yz-primary-softer: #f6f9ff;
|
||||
--yz-header-bg: #ffffff;
|
||||
--yz-aside-bg: #ffffff;
|
||||
--yz-page-bg: #f4f7fd;
|
||||
--yz-card-bg: #ffffff;
|
||||
--yz-border: #e8edf7;
|
||||
--yz-text: #24304a;
|
||||
--yz-text-regular: #5b6785;
|
||||
--yz-text-muted: #93a0b8;
|
||||
--yz-shadow: 0 2px 12px rgba(31, 61, 130, 0.06);
|
||||
--yz-header-height: 64px;
|
||||
}
|
||||
|
||||
// 暗色模式:只覆盖框架自定义变量,Element Plus 变量交给官方 dark css-vars
|
||||
html.dark {
|
||||
--yz-primary: #6f97ff;
|
||||
--yz-primary-soft: rgba(64, 110, 220, 0.2);
|
||||
--yz-primary-softer: rgba(64, 110, 220, 0.12);
|
||||
--yz-header-bg: #1b1f2b;
|
||||
--yz-aside-bg: #1b1f2b;
|
||||
--yz-page-bg: #131720;
|
||||
--yz-card-bg: #1f2431;
|
||||
--yz-border: #2c3344;
|
||||
--yz-text: #e8ecf6;
|
||||
--yz-text-regular: #b9c2d6;
|
||||
--yz-text-muted: #8692ac;
|
||||
--yz-shadow: 0 2px 12px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
// body 样式
|
||||
body {
|
||||
// background-color: #f5f7fa;
|
||||
color: #303133;
|
||||
color: var(--yz-text);
|
||||
background-color: var(--yz-page-bg);
|
||||
transition: background-color 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<el-menu-item
|
||||
v-if="!item.children || item.children.length === 0"
|
||||
:index="item.path || String(item.id)"
|
||||
:class="{ 'is-locked': item.locked }"
|
||||
>
|
||||
<MenuIcon :icon="item.icon" />
|
||||
<template #title>
|
||||
<span>{{ item.title }}</span>
|
||||
<i v-if="item.locked" class="fa-solid fa-lock lock-badge"></i>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
|
||||
<el-sub-menu
|
||||
v-else
|
||||
:index="item.path || String(item.id)"
|
||||
teleported
|
||||
:class="{ 'is-locked': item.locked }"
|
||||
>
|
||||
<template #title>
|
||||
<MenuIcon :icon="item.icon" />
|
||||
<span>{{ item.title }}</span>
|
||||
<i v-if="item.locked" class="fa-solid fa-lock lock-badge"></i>
|
||||
</template>
|
||||
<AsideMenuItem
|
||||
v-for="child in item.children"
|
||||
:key="child.id"
|
||||
:item="child"
|
||||
/>
|
||||
</el-sub-menu>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import MenuIcon from "@/components/MenuIcon.vue";
|
||||
|
||||
defineOptions({ name: "AsideMenuItem" });
|
||||
|
||||
defineProps({
|
||||
item: { type: Object, required: true },
|
||||
});
|
||||
</script>
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<i v-if="isFontAwesome" :class="icon" class="menu-icon"></i>
|
||||
<el-icon v-else class="menu-icon">
|
||||
<component :is="elementIconName" />
|
||||
</el-icon>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, getCurrentInstance } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
icon: { type: String, default: "" },
|
||||
});
|
||||
|
||||
const instance = getCurrentInstance();
|
||||
const isFontAwesome = computed(() => /fa-|(^|\s)fa[srlb]?\s/.test(props.icon || ""));
|
||||
|
||||
// main.js 已把所有 Element Plus 图标全局注册;数据库里存的是图标组件名,
|
||||
// 未能识别时统一回退到 Grid,避免渲染出空白图标。
|
||||
const elementIconName = computed(() => {
|
||||
const name = props.icon || "Grid";
|
||||
const components = instance?.appContext?.components || {};
|
||||
return components[name] ? name : "Grid";
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.menu-icon {
|
||||
flex-shrink: 0;
|
||||
margin-right: 8px;
|
||||
font-size: 16px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
text-align: center;
|
||||
color: inherit;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,318 @@
|
||||
import { computed } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { useMenuStore } from "@/stores/menu";
|
||||
import { pickDefaultChild, isWorkbenchMenu } from "@/router/menuDefaults";
|
||||
|
||||
/**
|
||||
* 后台框架导航模型。
|
||||
*
|
||||
* 层级约定:
|
||||
* 模块(从门户 /home 进入,如「OA管理」/apps/oa)
|
||||
* └─ 顶级菜单(顶部导航,如「仪表盘」「审批中心」)
|
||||
* └─ 左侧菜单(选中顶级菜单后的内容)
|
||||
*
|
||||
* 顶部导航展示的是当前模块下的顶级菜单,不是门户里的模块卡片。
|
||||
* 选中态直接由当前路由反推,因此标签页切换、刷新、深链接都能自动还原。
|
||||
*/
|
||||
|
||||
const fixedCmsMenu = {
|
||||
id: -200,
|
||||
path: "/apps/cms",
|
||||
title: "内容管理",
|
||||
icon: "Document",
|
||||
sort: -200,
|
||||
order: -200,
|
||||
children: [
|
||||
{
|
||||
id: -2001,
|
||||
path: "/apps/cms/analytics/content",
|
||||
title: "内容统计",
|
||||
icon: "DataAnalysis",
|
||||
sort: 0,
|
||||
order: 0,
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: -201,
|
||||
path: "/apps/cms/article",
|
||||
title: "文章管理",
|
||||
icon: "Document",
|
||||
sort: 1,
|
||||
order: 1,
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: -202,
|
||||
path: "/apps/cms/article/type",
|
||||
title: "文章分类",
|
||||
icon: "Folder",
|
||||
sort: 2,
|
||||
order: 2,
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: -203,
|
||||
path: "/apps/cms/products",
|
||||
title: "产品管理",
|
||||
icon: "Goods",
|
||||
sort: 3,
|
||||
order: 3,
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: -204,
|
||||
path: "/apps/cms/products/types",
|
||||
title: "产品分类",
|
||||
icon: "Folder",
|
||||
sort: 4,
|
||||
order: 4,
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: -205,
|
||||
path: "/apps/cms/solutions",
|
||||
title: "解决方案管理",
|
||||
icon: "Promotion",
|
||||
sort: 5,
|
||||
order: 5,
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: -206,
|
||||
path: "/apps/cms/solutions/types",
|
||||
title: "解决方案分类",
|
||||
icon: "Folder",
|
||||
sort: 6,
|
||||
order: 6,
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
function processMenus(menus) {
|
||||
return (menus || [])
|
||||
.filter((menu) => {
|
||||
// is_visible 只控制侧栏/顶部是否展示,不影响动态路由注册
|
||||
if (menu.is_visible !== undefined && Number(menu.is_visible) === 0) {
|
||||
return false;
|
||||
}
|
||||
if (menu.path && String(menu.path).trim() !== "") return true;
|
||||
return !!(menu.children && menu.children.length > 0);
|
||||
})
|
||||
.map((menu) => {
|
||||
const sort = Number(menu.sort) || 0;
|
||||
return {
|
||||
id: menu.id,
|
||||
pid: menu.pid || 0,
|
||||
path: menu.path,
|
||||
icon: menu.icon || "Grid",
|
||||
title: menu.title,
|
||||
component_path: menu.component_path || "",
|
||||
type: menu.type,
|
||||
locked: !!menu.locked,
|
||||
sort,
|
||||
order: sort,
|
||||
children: menu.children ? processMenus(menu.children) : [],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function mergeFixedCmsMenu(menus) {
|
||||
return menus.some((menu) => menu.path === fixedCmsMenu.path)
|
||||
? menus
|
||||
: [fixedCmsMenu, ...menus];
|
||||
}
|
||||
|
||||
function sortMenusRecursively(menus) {
|
||||
(menus || []).forEach((menu) => {
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
menu.children.sort((a, b) => {
|
||||
if (a.order !== b.order) return a.order - b.order;
|
||||
return (Number(a.id) || 0) - (Number(b.id) || 0);
|
||||
});
|
||||
sortMenusRecursively(menu.children);
|
||||
}
|
||||
});
|
||||
return menus;
|
||||
}
|
||||
|
||||
function findMenuItem(menus, targetIndex) {
|
||||
for (const menu of menus || []) {
|
||||
if (menu.path === targetIndex) return menu;
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
const found = findMenuItem(menu.children, targetIndex);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function isPathInSubtree(base, currentPath) {
|
||||
if (!base) return false;
|
||||
const normalized = base.replace(/\/+$/, "");
|
||||
return currentPath === normalized || currentPath.startsWith(`${normalized}/`);
|
||||
}
|
||||
|
||||
function findParentModule(menus, currentPath) {
|
||||
for (const menu of menus) {
|
||||
if (!menu.path || menu.path === "/home") {
|
||||
const found = findParentModule(menu.children || [], currentPath);
|
||||
if (found) return found;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isPathInSubtree(menu.path, currentPath)) return menu;
|
||||
|
||||
for (const child of menu.children || []) {
|
||||
if (isPathInSubtree(child.path, currentPath)) return menu;
|
||||
for (const grandchild of child.children || []) {
|
||||
if (isPathInSubtree(grandchild.path, currentPath)) return menu;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 在当前模块的顶级菜单里,找出路径最具体覆盖当前路由的那一项 */
|
||||
function findActiveTopMenu(topMenus, currentPath) {
|
||||
let best = null;
|
||||
for (const top of topMenus) {
|
||||
const stack = [top];
|
||||
while (stack.length) {
|
||||
const node = stack.pop();
|
||||
if (node.path && isPathInSubtree(node.path, currentPath)) {
|
||||
const length = node.path.length;
|
||||
if (!best || length > best.length) {
|
||||
best = { menu: top, length };
|
||||
}
|
||||
}
|
||||
if (node.children && node.children.length) {
|
||||
stack.push(...node.children);
|
||||
}
|
||||
}
|
||||
}
|
||||
return best?.menu || null;
|
||||
}
|
||||
|
||||
/** 点击顶级菜单时落到具体页面,避免多出一个「目录」标签页 */
|
||||
function resolveLandingMenu(menu) {
|
||||
if (!menu) return null;
|
||||
if (menu.locked) return menu.path ? menu : null;
|
||||
|
||||
let current = menu;
|
||||
const visited = new Set();
|
||||
while (current.children && current.children.length > 0 && !visited.has(current.id)) {
|
||||
visited.add(current.id);
|
||||
// 目录本身有页面时,访问目录路径即可(路由会渲染 index 子路由)
|
||||
if (current.component_path) break;
|
||||
const next = pickDefaultChild(current.children);
|
||||
if (!next) break;
|
||||
current = next;
|
||||
}
|
||||
return current.path ? current : menu.path ? menu : null;
|
||||
}
|
||||
|
||||
function flattenMenus(menus, parentTitle = "") {
|
||||
const result = [];
|
||||
for (const menu of menus || []) {
|
||||
// 只把「可直接打开的页面」放进搜索结果,纯目录交给路由 redirect,
|
||||
// 否则会同时产生「目录」和「落地页」两个标签页
|
||||
const isPage = !menu.children?.length || !!menu.component_path;
|
||||
if (menu.path && menu.path !== "/home" && isPage) {
|
||||
result.push({
|
||||
id: menu.id,
|
||||
path: menu.path,
|
||||
title: menu.title,
|
||||
icon: menu.icon,
|
||||
locked: menu.locked,
|
||||
parentTitle: parentTitle || menu.title,
|
||||
});
|
||||
}
|
||||
if (menu.children && menu.children.length) {
|
||||
result.push(...flattenMenus(menu.children, menu.title || parentTitle));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** 在当前模块下找到可直接打开的「仪表盘 / 工作台」页面,作为默认标签页 */
|
||||
function findWorkbenchPage(menus) {
|
||||
const stack = [...menus];
|
||||
const visited = new Set();
|
||||
while (stack.length) {
|
||||
const node = stack.shift();
|
||||
if (!node || visited.has(node.id)) continue;
|
||||
visited.add(node.id);
|
||||
if (
|
||||
node.path &&
|
||||
isWorkbenchMenu(node) &&
|
||||
(!node.children?.length || !!node.component_path)
|
||||
) {
|
||||
return node;
|
||||
}
|
||||
if (node.children?.length) stack.push(...node.children);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function useAdminNavigation() {
|
||||
const route = useRoute();
|
||||
const menuStore = useMenuStore();
|
||||
|
||||
const list = computed(() =>
|
||||
sortMenusRecursively(processMenus(mergeFixedCmsMenu(menuStore.menus || []))),
|
||||
);
|
||||
|
||||
const currentModule = computed(() => {
|
||||
const path = route.path;
|
||||
if (!path || path === "/" || path === "/home") return null;
|
||||
return findParentModule(list.value, path);
|
||||
});
|
||||
|
||||
const topMenus = computed(() => currentModule.value?.children || []);
|
||||
|
||||
const activeTopMenu = computed(() => {
|
||||
const menus = topMenus.value;
|
||||
if (!menus.length) return null;
|
||||
return (
|
||||
findActiveTopMenu(menus, route.path) ||
|
||||
pickDefaultChild(menus) ||
|
||||
menus[0] ||
|
||||
null
|
||||
);
|
||||
});
|
||||
|
||||
const sideMenus = computed(() => {
|
||||
const active = activeTopMenu.value;
|
||||
if (!active) return [];
|
||||
return active.children && active.children.length ? active.children : [active];
|
||||
});
|
||||
|
||||
const sideTitle = computed(
|
||||
() => activeTopMenu.value?.title || currentModule.value?.title || "功能菜单",
|
||||
);
|
||||
|
||||
const searchableMenus = computed(() => flattenMenus(list.value));
|
||||
|
||||
// 「首页」已移除:进入模块后默认展示该模块的仪表盘 / 工作台
|
||||
const defaultTab = computed(() => {
|
||||
const module = currentModule.value;
|
||||
if (!module) return null;
|
||||
const target =
|
||||
findWorkbenchPage(module.children || []) || resolveLandingMenu(module);
|
||||
return target?.path ? target : null;
|
||||
});
|
||||
|
||||
return {
|
||||
list,
|
||||
currentModule,
|
||||
topMenus,
|
||||
activeTopMenu,
|
||||
sideMenus,
|
||||
sideTitle,
|
||||
defaultTab,
|
||||
searchableMenus,
|
||||
findMenuItem,
|
||||
resolveLandingMenu,
|
||||
};
|
||||
}
|
||||
+50
-28
@@ -58,27 +58,23 @@ import { ref as vueRef } from 'vue';
|
||||
* }
|
||||
*/
|
||||
export const useTabsStore = defineTabsStore('tabs', () => {
|
||||
// 固定首页tab
|
||||
const defaultDashboardPath = '/home';
|
||||
// 默认标签页:不再固定「首页」,而是由 Main.vue 按当前模块设置成仪表盘
|
||||
const defaultDashboardPath = vueRef('');
|
||||
|
||||
// 从 localStorage 恢复 tabs 状态
|
||||
// 从 localStorage 恢复 tabs 状态(兼容旧版本里遗留的 /home 首页标签)
|
||||
function loadTabsFromStorage() {
|
||||
try {
|
||||
const savedTabs = localStorage.getItem('tabs_list');
|
||||
const savedActiveTab = localStorage.getItem('active_tab');
|
||||
if (savedTabs) {
|
||||
const tabs = JSON.parse(savedTabs);
|
||||
// 确保至少包含首页
|
||||
const hasHome = tabs.some(t => t.fullPath === defaultDashboardPath);
|
||||
if (!hasHome) {
|
||||
tabs.unshift({ title: '首页', fullPath: defaultDashboardPath, name: 'Home' });
|
||||
if (Array.isArray(tabs)) {
|
||||
return tabs.filter(t => t && t.fullPath && t.fullPath !== '/home');
|
||||
}
|
||||
return tabs;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('恢复 tabs 失败:', e);
|
||||
}
|
||||
return [{ title: '首页', fullPath: defaultDashboardPath, name: 'Home' }];
|
||||
return [];
|
||||
}
|
||||
|
||||
// 保存 tabs 到 localStorage
|
||||
@@ -95,7 +91,29 @@ export const useTabsStore = defineTabsStore('tabs', () => {
|
||||
|
||||
const tabList = vueRef(loadTabsFromStorage());
|
||||
const savedActiveTab = localStorage.getItem('active_tab');
|
||||
const activeTab = vueRef(savedActiveTab || defaultDashboardPath);
|
||||
const activeTab = vueRef(
|
||||
savedActiveTab && savedActiveTab !== '/home' ? savedActiveTab : ''
|
||||
);
|
||||
|
||||
// 设置当前模块的默认仪表盘标签:始终排在第一位且不可关闭
|
||||
function setDefaultTab(tab) {
|
||||
const path = tab?.fullPath || '';
|
||||
if (!path) return;
|
||||
defaultDashboardPath.value = path;
|
||||
|
||||
const idx = tabList.value.findIndex(t => t.fullPath === path);
|
||||
if (idx === -1) {
|
||||
tabList.value.unshift(tab);
|
||||
} else if (idx > 0) {
|
||||
const [exist] = tabList.value.splice(idx, 1);
|
||||
tabList.value.unshift(exist);
|
||||
}
|
||||
|
||||
if (!activeTab.value || !tabList.value.some(t => t.fullPath === activeTab.value)) {
|
||||
activeTab.value = path;
|
||||
}
|
||||
saveTabsToStorage(tabList.value, activeTab.value);
|
||||
}
|
||||
|
||||
// 添加tab,若已存在则激活
|
||||
function addTab(tab) {
|
||||
@@ -107,8 +125,9 @@ export const useTabsStore = defineTabsStore('tabs', () => {
|
||||
saveTabsToStorage(tabList.value, activeTab.value);
|
||||
}
|
||||
|
||||
// 删除指定tab并切换激活tab
|
||||
// 删除指定tab并切换激活tab(默认仪表盘标签不可关闭)
|
||||
function removeTab(fullPath) {
|
||||
if (fullPath === defaultDashboardPath.value) return;
|
||||
const idx = tabList.value.findIndex((t) => t.fullPath === fullPath);
|
||||
if (idx > -1) {
|
||||
tabList.value.splice(idx, 1);
|
||||
@@ -119,33 +138,31 @@ export const useTabsStore = defineTabsStore('tabs', () => {
|
||||
const newIdx = idx >= tabList.value.length ? tabList.value.length - 1 : idx;
|
||||
activeTab.value = tabList.value[newIdx].fullPath;
|
||||
} else {
|
||||
// 全部关闭,兜底首页
|
||||
activeTab.value = defaultDashboardPath;
|
||||
activeTab.value = defaultDashboardPath.value;
|
||||
}
|
||||
}
|
||||
saveTabsToStorage(tabList.value, activeTab.value);
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭其他,只留首页和当前激活tab
|
||||
// 关闭其他,只留默认仪表盘和当前激活tab
|
||||
function closeOthers() {
|
||||
tabList.value = tabList.value.filter(
|
||||
(t) => t.fullPath === defaultDashboardPath || t.fullPath === activeTab.value
|
||||
(t) => t.fullPath === defaultDashboardPath.value || t.fullPath === activeTab.value
|
||||
);
|
||||
saveTabsToStorage(tabList.value, activeTab.value);
|
||||
}
|
||||
|
||||
// 关闭左侧(关闭指定tab左侧的所有tab,保留首页和目标tab)
|
||||
// 关闭左侧(关闭指定tab左侧的所有tab,保留默认仪表盘和目标tab)
|
||||
function closeLeft(targetFullPath) {
|
||||
const targetIndex = tabList.value.findIndex((t) => t.fullPath === targetFullPath);
|
||||
if (targetIndex > -1) {
|
||||
// 保留首页和目标tab及其右侧的所有tab
|
||||
const beforeIndex = tabList.value.slice(0, targetIndex);
|
||||
const hasCloseableLeft = beforeIndex.some(t => t.fullPath !== defaultDashboardPath);
|
||||
const hasCloseableLeft = beforeIndex.some(t => t.fullPath !== defaultDashboardPath.value);
|
||||
|
||||
if (hasCloseableLeft) {
|
||||
tabList.value = tabList.value.filter((t, index) =>
|
||||
t.fullPath === defaultDashboardPath || index >= targetIndex
|
||||
t.fullPath === defaultDashboardPath.value || index >= targetIndex
|
||||
);
|
||||
// 如果关闭的tab中包含了当前激活的tab,则激活目标tab
|
||||
if (!tabList.value.find(t => t.fullPath === activeTab.value)) {
|
||||
@@ -156,17 +173,16 @@ export const useTabsStore = defineTabsStore('tabs', () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭右侧(关闭指定tab右侧的所有tab,保留首页和目标tab)
|
||||
// 关闭右侧(关闭指定tab右侧的所有tab,保留默认仪表盘和目标tab)
|
||||
function closeRight(targetFullPath) {
|
||||
const targetIndex = tabList.value.findIndex((t) => t.fullPath === targetFullPath);
|
||||
if (targetIndex > -1) {
|
||||
// 保留首页和目标tab及其左侧的所有tab
|
||||
const afterIndex = tabList.value.slice(targetIndex + 1);
|
||||
const hasCloseableRight = afterIndex.length > 0;
|
||||
|
||||
if (hasCloseableRight) {
|
||||
tabList.value = tabList.value.filter((t, index) =>
|
||||
t.fullPath === defaultDashboardPath || index <= targetIndex
|
||||
t.fullPath === defaultDashboardPath.value || index <= targetIndex
|
||||
);
|
||||
// 如果关闭的tab中包含了当前激活的tab,则激活目标tab
|
||||
if (!tabList.value.find(t => t.fullPath === activeTab.value)) {
|
||||
@@ -177,10 +193,13 @@ export const useTabsStore = defineTabsStore('tabs', () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭全部,只留首页
|
||||
// 关闭全部,只留默认仪表盘
|
||||
function closeAll() {
|
||||
tabList.value = tabList.value.filter((t) => t.fullPath === defaultDashboardPath);
|
||||
activeTab.value = defaultDashboardPath;
|
||||
const target = defaultDashboardPath.value || activeTab.value;
|
||||
tabList.value = target
|
||||
? tabList.value.filter((t) => t.fullPath === target)
|
||||
: [];
|
||||
activeTab.value = tabList.value.length ? target : '';
|
||||
saveTabsToStorage(tabList.value, activeTab.value);
|
||||
}
|
||||
|
||||
@@ -192,8 +211,9 @@ export const useTabsStore = defineTabsStore('tabs', () => {
|
||||
|
||||
// 重置 tabs store 到初始状态(登出时使用)
|
||||
function resetTabs() {
|
||||
tabList.value = [{ title: '首页', fullPath: defaultDashboardPath, name: 'Dashboard' }];
|
||||
activeTab.value = defaultDashboardPath;
|
||||
tabList.value = [];
|
||||
activeTab.value = '';
|
||||
defaultDashboardPath.value = '';
|
||||
// 清除 localStorage 中的 tabs 数据
|
||||
localStorage.removeItem('tabs_list');
|
||||
localStorage.removeItem('active_tab');
|
||||
@@ -202,7 +222,9 @@ export const useTabsStore = defineTabsStore('tabs', () => {
|
||||
return {
|
||||
tabList,
|
||||
activeTab,
|
||||
defaultDashboardPath,
|
||||
addTab,
|
||||
setDefaultTab,
|
||||
removeTab,
|
||||
closeOthers,
|
||||
closeLeft,
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* 全站主题模式:跟随系统 / 亮色 / 暗色。
|
||||
*
|
||||
* 顶栏个人中心菜单与门户页共用这一份状态,避免出现两处各自维护
|
||||
* 「是否暗色」而互相覆盖的情况。
|
||||
*/
|
||||
|
||||
const MODE_KEY = "yz-theme-mode";
|
||||
// 兼容旧版本:此前只保存最终的亮/暗结果
|
||||
const LEGACY_KEY = "element-plus-theme";
|
||||
|
||||
export const THEME_OPTIONS = [
|
||||
{ value: "system", label: "跟随系统" },
|
||||
{ value: "light", label: "亮色" },
|
||||
{ value: "dark", label: "暗色" },
|
||||
];
|
||||
|
||||
export function getThemeMode() {
|
||||
try {
|
||||
const mode = localStorage.getItem(MODE_KEY);
|
||||
if (["system", "light", "dark"].includes(mode)) return mode;
|
||||
const legacy = localStorage.getItem(LEGACY_KEY);
|
||||
if (legacy === "dark" || legacy === "light") return legacy;
|
||||
} catch (e) {
|
||||
// localStorage 不可用时退化为跟随系统
|
||||
}
|
||||
return "system";
|
||||
}
|
||||
|
||||
export function isDarkByMode(mode) {
|
||||
if (mode === "dark") return true;
|
||||
if (mode === "light") return false;
|
||||
return (
|
||||
typeof window !== "undefined" &&
|
||||
!!window.matchMedia &&
|
||||
window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
);
|
||||
}
|
||||
|
||||
/** 应用主题并返回当前是否为暗色 */
|
||||
export function applyThemeMode(mode) {
|
||||
const dark = isDarkByMode(mode);
|
||||
document.documentElement.classList.toggle("dark", dark);
|
||||
try {
|
||||
localStorage.setItem(MODE_KEY, mode);
|
||||
localStorage.setItem(LEGACY_KEY, dark ? "dark" : "light");
|
||||
} catch (e) {
|
||||
// 存储失败不影响本次主题生效
|
||||
}
|
||||
return dark;
|
||||
}
|
||||
|
||||
/** 监听系统配色变化,返回取消监听函数 */
|
||||
export function onSystemThemeChange(handler) {
|
||||
if (typeof window === "undefined" || !window.matchMedia) return () => {};
|
||||
const query = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
const listener = () => handler(query.matches);
|
||||
if (query.addEventListener) {
|
||||
query.addEventListener("change", listener);
|
||||
return () => query.removeEventListener("change", listener);
|
||||
}
|
||||
query.addListener(listener);
|
||||
return () => query.removeListener(listener);
|
||||
}
|
||||
+150
-21
@@ -7,12 +7,35 @@ import { useRouter, useRoute } from 'vue-router';
|
||||
import { ref, watch, reactive, nextTick, onMounted, onBeforeUnmount, computed } from 'vue';
|
||||
import { More, Close, CircleClose, ArrowUp, Fold, Expand } from '@element-plus/icons-vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { useAdminNavigation } from '@/composables/useAdminNavigation';
|
||||
|
||||
const tabsStore = useTabsStore();
|
||||
const dictStore = useDictStore();
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const defaultDashboardPath = '/home';
|
||||
const { defaultTab } = useAdminNavigation();
|
||||
|
||||
// 「首页」已移除:默认标签 = 当前模块的仪表盘,随模块切换自动更新
|
||||
const defaultDashboardPath = computed(
|
||||
() => tabsStore.defaultDashboardPath || route.fullPath
|
||||
);
|
||||
|
||||
let lastDefaultPath = '';
|
||||
watch(
|
||||
defaultTab,
|
||||
(tab) => {
|
||||
const path = tab?.path || '';
|
||||
if (!path || path === lastDefaultPath) return;
|
||||
lastDefaultPath = path;
|
||||
tabsStore.setDefaultTab({
|
||||
title: tab.title,
|
||||
fullPath: path,
|
||||
name: tab.title,
|
||||
icon: tab.icon,
|
||||
});
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// ========== 手机端侧栏抽屉控制 ==========
|
||||
const MOBILE_QUERY = '(max-width: 768px)';
|
||||
@@ -144,9 +167,9 @@ watch(
|
||||
const handleAsideMenuClick = async (menuItem) => {
|
||||
const targetPath = menuItem.path;
|
||||
|
||||
if (targetPath === '/home') {
|
||||
if (targetPath === '/home' || targetPath === '/') {
|
||||
tabsStore.closeAll();
|
||||
router.push('/home');
|
||||
router.push(defaultDashboardPath.value || '/');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -203,7 +226,7 @@ const closeOthers = () => {
|
||||
};
|
||||
const closeAll = () => {
|
||||
tabsStore.closeAll();
|
||||
router.push(defaultDashboardPath);
|
||||
router.push(defaultDashboardPath.value || route.fullPath);
|
||||
};
|
||||
// 主动监听tab激活,保证切tab时内容区切换(仅用于tab点击切换,菜单点击由handleAsideMenuClick处理)
|
||||
// 使用 immediate: false 避免初始化时触发,使用 flush: 'post' 确保在 DOM 更新后执行
|
||||
@@ -213,9 +236,9 @@ watch(
|
||||
// 如果新值和当前路由路径不同,且不是初始化(oldVal 不为 undefined),才进行跳转
|
||||
// 注意:这个watch主要用于处理tab点击切换,菜单点击由handleAsideMenuClick直接处理
|
||||
if (newVal && oldVal !== undefined && router.currentRoute.value.fullPath !== newVal) {
|
||||
// 如果切换到首页,关闭其他所有tab
|
||||
if (newVal === '/home') {
|
||||
tabsStore.closeAll();
|
||||
// 切回默认仪表盘时,清掉其它标签页
|
||||
if (newVal === defaultDashboardPath.value) {
|
||||
tabsStore.closeOthers();
|
||||
}
|
||||
|
||||
// 检查路由是否存在
|
||||
@@ -391,7 +414,7 @@ function hideContextMenu() {
|
||||
contextMenu.tab = null;
|
||||
}
|
||||
function closeTabContextTab() {
|
||||
if (contextMenu.tab && contextMenu.tab.fullPath !== defaultDashboardPath) {
|
||||
if (contextMenu.tab && contextMenu.tab.fullPath !== defaultDashboardPath.value) {
|
||||
tabsStore.removeTab(contextMenu.tab.fullPath);
|
||||
}
|
||||
hideContextMenu();
|
||||
@@ -436,15 +459,15 @@ function closeOthersContextTab() {
|
||||
function closeAllTabs() {
|
||||
tabsStore.closeAll();
|
||||
hideContextMenu();
|
||||
router.push(defaultDashboardPath);
|
||||
router.push(defaultDashboardPath.value || route.fullPath);
|
||||
}
|
||||
|
||||
// 计算是否可以关闭左侧/右侧
|
||||
const canCloseLeft = computed(() => {
|
||||
if (!contextMenu.tab) return false;
|
||||
const targetIndex = tabsStore.tabList.findIndex(t => t.fullPath === contextMenu.tab.fullPath);
|
||||
// 至少左侧有一个可关闭的tab(排除首页)
|
||||
return targetIndex > 0 && tabsStore.tabList.slice(0, targetIndex).some(t => t.fullPath !== defaultDashboardPath);
|
||||
// 至少左侧有一个可关闭的tab(排除默认仪表盘)
|
||||
return targetIndex > 0 && tabsStore.tabList.slice(0, targetIndex).some(t => t.fullPath !== defaultDashboardPath.value);
|
||||
});
|
||||
|
||||
const canCloseRight = computed(() => {
|
||||
@@ -478,16 +501,17 @@ const canCloseRight = computed(() => {
|
||||
></div>
|
||||
</transition>
|
||||
|
||||
<el-container class="main-container">
|
||||
<common-aside
|
||||
class="common-aside-wrapper"
|
||||
:class="{ 'mobile-open': sidebarVisible }"
|
||||
@menu-click="handleAsideMenuClick"
|
||||
/>
|
||||
<el-container>
|
||||
<el-header class="main-header">
|
||||
<common-header />
|
||||
</el-header>
|
||||
<el-container class="main-container" direction="vertical">
|
||||
<!-- 顶栏全宽:logo 位于页面真正的左上角 -->
|
||||
<el-header class="main-header">
|
||||
<common-header @menu-click="handleAsideMenuClick" />
|
||||
</el-header>
|
||||
<el-container class="main-body">
|
||||
<common-aside
|
||||
class="common-aside-wrapper"
|
||||
:class="{ 'mobile-open': sidebarVisible }"
|
||||
@menu-click="handleAsideMenuClick"
|
||||
/>
|
||||
<el-main class="right-main">
|
||||
<div class="multi-tabs-wrapper">
|
||||
<el-tabs
|
||||
@@ -887,3 +911,108 @@ const canCloseRight = computed(() => {
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- 浅色框架:白顶栏 + 浅蓝内容区 -->
|
||||
<style scoped lang="less">
|
||||
.common-layout,
|
||||
.main-container {
|
||||
background-color: var(--yz-page-bg) !important;
|
||||
}
|
||||
|
||||
// 顶栏全宽 + 下方「侧栏 / 内容」两栏,左侧保持一整块白色面板
|
||||
.main-container {
|
||||
flex-direction: column !important;
|
||||
}
|
||||
|
||||
.main-header {
|
||||
flex: 0 0 var(--yz-header-height);
|
||||
width: 100%;
|
||||
height: var(--yz-header-height) !important;
|
||||
background-color: var(--yz-header-bg) !important;
|
||||
border-bottom: none !important;
|
||||
}
|
||||
|
||||
.main-body {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
background-color: var(--yz-page-bg);
|
||||
}
|
||||
|
||||
:deep(.el-aside) {
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
.right-main {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
padding: 16px 20px 20px;
|
||||
background-color: var(--yz-page-bg) !important;
|
||||
}
|
||||
|
||||
.multi-tabs-wrapper {
|
||||
margin-bottom: 14px;
|
||||
padding: 6px 10px;
|
||||
border: 1px solid var(--yz-border) !important;
|
||||
border-radius: 12px;
|
||||
background: var(--yz-card-bg) !important;
|
||||
box-shadow: var(--yz-shadow) !important;
|
||||
}
|
||||
|
||||
.multi-tabs {
|
||||
:deep(.el-tabs__item) {
|
||||
border: 1px solid transparent !important;
|
||||
background: transparent !important;
|
||||
color: var(--yz-text-regular) !important;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__item:hover) {
|
||||
border-color: transparent !important;
|
||||
background: var(--yz-primary-softer) !important;
|
||||
color: var(--yz-primary) !important;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__item.is-active) {
|
||||
border-color: var(--el-color-primary-light-7) !important;
|
||||
background: var(--yz-primary-soft) !important;
|
||||
color: var(--yz-primary) !important;
|
||||
font-weight: 600;
|
||||
|
||||
.el-icon-close {
|
||||
color: var(--yz-primary) !important;
|
||||
|
||||
&:hover {
|
||||
color: var(--yz-primary) !important;
|
||||
background-color: var(--el-color-primary-light-7) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.main-panel {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.mobile-menu-btn {
|
||||
top: 12px;
|
||||
background: var(--yz-primary) !important;
|
||||
box-shadow: 0 4px 12px rgba(47, 107, 255, 0.3) !important;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.right-main {
|
||||
padding: 12px !important;
|
||||
}
|
||||
|
||||
// 抽屉从全宽顶栏下方展开,不遮住 logo
|
||||
.common-layout .common-aside-wrapper {
|
||||
top: var(--yz-header-height) !important;
|
||||
bottom: 0 !important;
|
||||
height: auto !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -3,6 +3,10 @@
|
||||
<div class="home-top">
|
||||
<div class="toolbar">
|
||||
<div class="toolbar-left">
|
||||
<div class="home-brand">
|
||||
<span class="home-logo"><i></i><i></i><i></i><i></i></span>
|
||||
<span class="home-brand-name">云泽管理平台</span>
|
||||
</div>
|
||||
<!-- 当前企业 + 一人多企业时可点击切换 -->
|
||||
<TenantSwitcher variant="plain" />
|
||||
</div>
|
||||
@@ -209,7 +213,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted } from "vue";
|
||||
import { ref, reactive, computed, onMounted, onUnmounted } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
|
||||
@@ -230,6 +234,7 @@ import { getCurrentUserProfile, logout } from "@/api/login";
|
||||
import { reloadMenusAndDynamicRoutes } from "@/router";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { useMenuStore } from "@/stores/menu";
|
||||
import { getThemeMode, applyThemeMode, onSystemThemeChange } from "@/utils/theme";
|
||||
import TenantSwitcher from "@/components/TenantSwitcher.vue";
|
||||
|
||||
interface ModuleItem {
|
||||
@@ -271,40 +276,27 @@ const uncategorizedModules = computed(() =>
|
||||
const moduleList = ref<ModuleItem[]>([]);
|
||||
const refreshLoading = ref(false);
|
||||
|
||||
const THEME_STORAGE_KEY = "element-plus-theme";
|
||||
// 与顶栏个人中心共用同一份主题模式(跟随系统 / 亮色 / 暗色)
|
||||
const themeMode = ref(getThemeMode());
|
||||
const isDark = ref(false);
|
||||
|
||||
const applyTheme = () => {
|
||||
isDark.value = applyThemeMode(themeMode.value);
|
||||
};
|
||||
|
||||
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;
|
||||
// 门户页的快捷按钮直接切换到明确的亮/暗模式
|
||||
themeMode.value = isDark.value ? "light" : "dark";
|
||||
applyTheme();
|
||||
};
|
||||
|
||||
const currentTheme = computed(() => (isDark.value ? "dark" : "light"));
|
||||
const themeIcon = computed(() => (isDark.value ? Sunny : Moon));
|
||||
let stopSystemThemeWatch = null;
|
||||
|
||||
const userName = computed(
|
||||
() => authStore.user?.name || authStore.user?.account || "用户",
|
||||
@@ -480,6 +472,9 @@ onMounted(() => {
|
||||
loadModules();
|
||||
syncReturnedPayment();
|
||||
initTheme();
|
||||
stopSystemThemeWatch = onSystemThemeChange(() => {
|
||||
if (themeMode.value === "system") applyTheme();
|
||||
});
|
||||
// 拉取当前登录用户的角色 / 部门 / 职位,用于右上角展示
|
||||
if (authStore.token) {
|
||||
fetchUserProfile();
|
||||
@@ -489,12 +484,16 @@ onMounted(() => {
|
||||
authStore.fetchCurrentUser();
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (stopSystemThemeWatch) stopSystemThemeWatch();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
// 定义一些可复用的变量(可选,建议根据项目规范调整)
|
||||
@primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
@bg-gradient: linear-gradient(135deg, #f5f7fa 0%, #e4e8eb 100%);
|
||||
@primary-gradient: linear-gradient(135deg, #4885ff 0%, #2f56e6 100%);
|
||||
@bg-gradient: linear-gradient(135deg, #f4f7fd 0%, #e8effc 100%);
|
||||
@text-main: #1a1a2e;
|
||||
@text-regular: #606266;
|
||||
|
||||
@@ -522,10 +521,50 @@ onMounted(() => {
|
||||
.toolbar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
|
||||
.home-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
}
|
||||
|
||||
.home-logo {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 8px);
|
||||
gap: 3px;
|
||||
place-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 9px;
|
||||
background: @primary-gradient;
|
||||
|
||||
i {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 2px;
|
||||
background: #fff;
|
||||
|
||||
&:nth-child(2) {
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
opacity: 0.75;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.home-brand-name {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: var(--yz-text);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
// 企业切换(TenantSwitcher 组件):样式由组件自带,这里只调楼宇图标颜色
|
||||
:deep(.tenant-chip .tenant-chip-icon) {
|
||||
color: #3973ff;
|
||||
color: var(--yz-primary);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -542,7 +581,7 @@ onMounted(() => {
|
||||
&:hover {
|
||||
background: #f0f2f5;
|
||||
border-color: #c0c4cc;
|
||||
color: #3973ff;
|
||||
color: var(--yz-primary);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -609,7 +648,7 @@ onMounted(() => {
|
||||
padding: 48px 56px;
|
||||
margin-bottom: 40px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 10px 40px rgba(102, 126, 234, 0.3);
|
||||
box-shadow: 0 10px 40px rgba(47, 107, 255, 0.3);
|
||||
|
||||
.welcome-content {
|
||||
position: relative;
|
||||
@@ -691,7 +730,7 @@ onMounted(() => {
|
||||
color: @text-main;
|
||||
margin: 0 0 20px 0;
|
||||
padding-left: 4px;
|
||||
border-left: 4px solid #667eea;
|
||||
border-left: 4px solid var(--yz-primary);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -723,7 +762,7 @@ onMounted(() => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #3973ff;
|
||||
color: var(--yz-primary);
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
@@ -773,16 +812,16 @@ onMounted(() => {
|
||||
.buy-btn {
|
||||
flex-shrink: 0;
|
||||
padding: 2px 10px;
|
||||
border: 1px solid #3973ff;
|
||||
border: 1px solid var(--yz-primary);
|
||||
border-radius: 12px;
|
||||
color: #3973ff;
|
||||
color: var(--yz-primary);
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: #3973ff;
|
||||
border-color: #3973ff;
|
||||
background: var(--yz-primary);
|
||||
border-color: var(--yz-primary);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
@@ -836,8 +875,8 @@ onMounted(() => {
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 24px rgba(102, 126, 234, 0.15);
|
||||
background-color: #3973ff;
|
||||
box-shadow: 0 8px 24px rgba(47, 107, 255, 0.15);
|
||||
background-color: var(--yz-primary);
|
||||
|
||||
.card-title,
|
||||
.card-desc {
|
||||
@@ -1170,7 +1209,7 @@ onMounted(() => {
|
||||
&:hover {
|
||||
background: #3d3d3d;
|
||||
border-color: #4d4d4d;
|
||||
color: #3973ff;
|
||||
color: var(--yz-primary);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1194,7 +1233,7 @@ onMounted(() => {
|
||||
.modules-section {
|
||||
.category-title {
|
||||
color: #f5f7fa;
|
||||
border-left-color: #667eea;
|
||||
border-left-color: var(--yz-primary);
|
||||
}
|
||||
|
||||
.module-card {
|
||||
@@ -1252,7 +1291,7 @@ onMounted(() => {
|
||||
&.dark {
|
||||
.modules-section .category-title {
|
||||
color: #ffffff !important;
|
||||
border-left-color: #667eea;
|
||||
border-left-color: var(--yz-primary);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user