refactor(layout): 优化后台布局样式与主题适配

- 优化顶部工具栏、通知入口和页签操作按钮的图标颜色继承
- 调整混合布局菜单激活态与暗黑模式下的视觉表现
- 重构设置面板分组与底部操作区,精简无效配置文案
- 统一列表页公共布局样式,优化表格和页面容器边界
- 优化控制台卡片、图表和暗黑模式背景表现
- 移除布局中重复的页签高度覆盖和无用 mixin
This commit is contained in:
Ray.Hao committed 2026-06-18 23:39:31 +08:00
1 parent 0e992ae16f
commit 03a9c03401
15 files changed
+357 -280

No files matched your search

+8 -3
View File
@@ -122,6 +122,7 @@ function handleVisibleChange(visible: boolean) {
justify-content: center; justify-content: center;
width: 100%; width: 100%;
height: 100%; height: 100%;
color: inherit;
} }
&__trigger { &__trigger {
@@ -130,6 +131,7 @@ function handleVisibleChange(visible: boolean) {
justify-content: center; justify-content: center;
width: 100%; width: 100%;
height: 100%; height: 100%;
color: inherit;
} }
&__icon { &__icon {
@@ -139,17 +141,20 @@ function handleVisibleChange(visible: boolean) {
justify-content: center; justify-content: center;
width: 28px; width: 28px;
height: 28px; height: 28px;
color: currentColor;
border-radius: 6px; border-radius: 6px;
} }
&__bell { &__bell {
color: var(--el-text-color-regular); --color: currentColor;
color: currentColor;
} }
&__pulse { &__pulse {
position: absolute; position: absolute;
top: 4px; top: 3px;
right: 5px; right: 4px;
width: 6px; width: 6px;
height: 6px; height: 6px;
background: var(--el-color-danger); background: var(--el-color-danger);
+3
View File
@@ -67,6 +67,7 @@
"theme": "Theme", "theme": "Theme",
"interface": "Interface", "interface": "Interface",
"navigation": "Navigation", "navigation": "Navigation",
"assist": "Assist",
"themePalette": "Color System", "themePalette": "Color System",
"customColors": "Custom Colors", "customColors": "Custom Colors",
"customPalette": "Custom", "customPalette": "Custom",
@@ -102,6 +103,8 @@
"sidebarColorScheme": "Sidebar Color Scheme", "sidebarColorScheme": "Sidebar Color Scheme",
"showWatermark": "Show Watermark", "showWatermark": "Show Watermark",
"pageSwitchingAnimation": "Page Switching Animation", "pageSwitchingAnimation": "Page Switching Animation",
"grayMode": "Gray Mode",
"colorWeak": "Color Weak",
"none": "None", "none": "None",
"fade": "Fade", "fade": "Fade",
"fade-slide": "Fade Slide", "fade-slide": "Fade Slide",
+2
View File
@@ -104,6 +104,8 @@
"showAppLogo": "显示Logo", "showAppLogo": "显示Logo",
"showWatermark": "显示水印", "showWatermark": "显示水印",
"pageSwitchingAnimation": "页面切换动画", "pageSwitchingAnimation": "页面切换动画",
"grayMode": "灰色模式",
"colorWeak": "色弱模式",
"none": "无动画", "none": "无动画",
"fade": "淡入淡出", "fade": "淡入淡出",
"fade-slide": "平滑切换", "fade-slide": "平滑切换",
-9
View File
@@ -51,7 +51,6 @@
<main <main
class="layout__main" class="layout__main"
:class="{ :class="{
hasTagsView: showTagsView,
'layout__main--collapsed': !isSidebarOpen, 'layout__main--collapsed': !isSidebarOpen,
'layout__main--fullscreen': appStore.contentFullscreen, 'layout__main--fullscreen': appStore.contentFullscreen,
}" }"
@@ -84,8 +83,6 @@ const { MenuIcon, topMenuItems, activeTopMenuPath, sideMenuRoutes, handleTopMenu
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@use "@/styles/mixins" as *;
$double-sidebar-width: $sidebar-primary-width + $sidebar-secondary-width; $double-sidebar-width: $sidebar-primary-width + $sidebar-secondary-width;
.layout { .layout {
@@ -305,12 +302,6 @@ $double-sidebar-width: $sidebar-primary-width + $sidebar-secondary-width;
} }
} }
.hasTagsView {
:deep(.app-main) {
@include app-main-height-with-tags;
}
}
.mobile { .mobile {
.layout__double-sidebar { .layout__double-sidebar {
width: $double-sidebar-width; width: $double-sidebar-width;
-7
View File
@@ -18,7 +18,6 @@
<div <div
class="layout__main" class="layout__main"
:class="{ :class="{
hasTagsView: showTagsView,
'layout__main--collapsed': !isSidebarOpen, 'layout__main--collapsed': !isSidebarOpen,
'layout__main--fullscreen': appStore.contentFullscreen, 'layout__main--fullscreen': appStore.contentFullscreen,
}" }"
@@ -130,10 +129,4 @@ const appStore = useAppStore();
margin-left: 0 !important; margin-left: 0 !important;
} }
} }
.hasTagsView {
:deep(.app-main) {
@include app-main-height-with-tags;
}
}
</style> </style>
+38 -25
View File
@@ -66,7 +66,7 @@
</div> </div>
<!-- 主内容区 --> <!-- 主内容区 -->
<div :class="{ hasTagsView: showTagsView }" class="layout__main"> <div class="layout__main">
<LayoutTagsView v-if="showTagsView" /> <LayoutTagsView v-if="showTagsView" />
<LayoutMain /> <LayoutMain />
</div> </div>
@@ -171,27 +171,43 @@ const useMenuColors = computed(
.el-menu-item { .el-menu-item {
position: relative; position: relative;
height: 100%; height: 100%;
padding: 0 12px; padding: 0 14px;
line-height: $navbar-height; line-height: $navbar-height;
border-bottom: none; border-bottom: none !important;
border-radius: 0; border-radius: 0;
transition: transition:
color 0.16s ease, color 0.16s ease,
background-color 0.16s ease, opacity 0.16s ease;
border-color 0.16s ease;
&::after {
position: absolute;
right: 12px;
bottom: 0;
left: 12px;
height: 2px;
content: "";
background: var(--el-color-primary);
border-radius: 999px 999px 0 0;
opacity: 0;
transform: scaleX(0.4);
transition:
opacity 0.16s ease,
transform 0.16s ease;
}
&:hover { &:hover {
background-color: var(--menu-hover) !important; color: var(--el-color-primary) !important;
background-color: transparent !important;
} }
&.is-active { &.is-active {
color: var(--el-color-primary) !important; color: var(--el-color-primary) !important;
background-color: color-mix( background-color: transparent !important;
in srgb, }
var(--el-color-primary) 10%,
var(--el-bg-color-overlay) &.is-active::after {
) !important; opacity: 1;
border-bottom: 2px solid var(--el-color-primary); transform: scaleX(1);
} }
} }
} }
@@ -289,23 +305,20 @@ const useMenuColors = computed(
} }
} }
:deep(.hasTagsView) {
.app-main {
@include app-main-height-with-tags;
}
}
:global(html.dark), :global(html.dark),
:global(html.sidebar-color-blue) { :global(html.sidebar-color-blue) {
.layout__header-menu { .layout__header-menu {
:deep(.el-menu--horizontal) { :deep(.el-menu--horizontal) {
.el-menu-item.is-active { .el-menu-item {
color: var(--menu-active-text) !important; &:hover {
background-color: color-mix( color: color-mix(in srgb, var(--el-color-primary) 24%, var(--el-color-white)) !important;
in srgb, background-color: transparent !important;
var(--el-color-primary) 18%, }
var(--menu-background)
) !important; &.is-active {
color: color-mix(in srgb, var(--el-color-primary) 38%, var(--el-color-white)) !important;
background-color: transparent !important;
}
} }
} }
} }
+1 -7
View File
@@ -16,7 +16,7 @@
</div> </div>
<!-- 主内容区 --> <!-- 主内容区 -->
<div :class="{ hasTagsView: showTagsView }" class="layout__main"> <div class="layout__main">
<LayoutTagsView v-if="showTagsView" /> <LayoutTagsView v-if="showTagsView" />
<LayoutMain /> <LayoutMain />
</div> </div>
@@ -149,10 +149,4 @@ const isLogoCollapsed = computed(() => width.value < 768);
overflow-y: auto; overflow-y: auto;
} }
} }
.hasTagsView {
:deep(.app-main) {
@include app-main-height-with-tags;
}
}
</style> </style>
+154 -110
View File
@@ -8,10 +8,10 @@
> >
<div class="settings-content"> <div class="settings-content">
<section class="config-section"> <section class="config-section">
<el-divider>{{ t("settings.theme") }}</el-divider> <div class="section-title">{{ t("settings.theme") }}</div>
<div class="flex-center"> <div class="theme-mode">
<el-radio-group v-model="themeMode" class="theme-mode-group"> <el-radio-group v-model="themeMode">
<el-radio-button :value="ThemeMode.LIGHT"> <el-radio-button :value="ThemeMode.LIGHT">
{{ t("login.light") }} {{ t("login.light") }}
</el-radio-button> </el-radio-button>
@@ -24,9 +24,9 @@
</el-radio-group> </el-radio-group>
</div> </div>
<div class="config-block config-block--tight"> <div class="config-card">
<div class="config-item__header"> <div class="config-card__header">
<span class="text-xs">{{ t("settings.themePalette") }}</span> <span>{{ t("settings.themePalette") }}</span>
<button <button
type="button" type="button"
:class="['custom-color-trigger', { 'is-open': isCustomColorsOpen }]" :class="['custom-color-trigger', { 'is-open': isCustomColorsOpen }]"
@@ -90,7 +90,7 @@
</section> </section>
<section class="config-section"> <section class="config-section">
<el-divider>{{ t("settings.navigation") }}</el-divider> <div class="section-title">{{ t("settings.navigation") }}</div>
<div class="layout-select"> <div class="layout-select">
<div class="layout-grid"> <div class="layout-grid">
@@ -140,7 +140,7 @@
<el-radio-group <el-radio-group
v-model="sidebarColor" v-model="sidebarColor"
class="config-item__control" class="config-item__control"
@change="changeSidebarColor" @change="setSidebarColor"
> >
<el-radio :value="SidebarColor.MINIMAL_WHITE"> <el-radio :value="SidebarColor.MINIMAL_WHITE">
{{ t("settings.minimalWhite") }} {{ t("settings.minimalWhite") }}
@@ -153,7 +153,7 @@
</section> </section>
<section class="config-section"> <section class="config-section">
<el-divider>{{ t("settings.interface") }}</el-divider> <div class="section-title">{{ t("settings.interface") }}</div>
<div class="config-item flex-x-between"> <div class="config-item flex-x-between">
<span class="text-xs">{{ t("settings.showTagsView") }}</span> <span class="text-xs">{{ t("settings.showTagsView") }}</span>
@@ -161,7 +161,7 @@
</div> </div>
<div v-if="settingsStore.showTagsView" class="config-item config-item--block"> <div v-if="settingsStore.showTagsView" class="config-item config-item--block">
<div class="config-item__header"> <div class="config-card__header">
<span class="text-xs">{{ t("settings.tagsViewStyle") }}</span> <span class="text-xs">{{ t("settings.tagsViewStyle") }}</span>
</div> </div>
<div class="tags-style-grid"> <div class="tags-style-grid">
@@ -191,11 +191,6 @@
<el-switch v-model="settingsStore.showAppLogo" /> <el-switch v-model="settingsStore.showAppLogo" />
</div> </div>
<div class="config-item flex-x-between">
<span class="text-xs">{{ t("settings.showWatermark") }}</span>
<el-switch v-model="settingsStore.showWatermark" />
</div>
<div class="config-item flex-x-between"> <div class="config-item flex-x-between">
<span class="text-xs">{{ t("settings.pageSwitchingAnimation") }}</span> <span class="text-xs">{{ t("settings.pageSwitchingAnimation") }}</span>
<el-select v-model="settingsStore.pageSwitchingAnimation" style="width: 150px"> <el-select v-model="settingsStore.pageSwitchingAnimation" style="width: 150px">
@@ -207,43 +202,46 @@
/> />
</el-select> </el-select>
</div> </div>
</section>
<section class="config-section">
<div class="section-title">{{ t("settings.assist") }}</div>
<div class="config-item flex-x-between"> <div class="config-item flex-x-between">
<span class="text-xs">灰色模式</span> <span class="text-xs">{{ t("settings.showWatermark") }}</span>
<el-switch v-model="settingsStore.showWatermark" />
</div>
<div class="config-item flex-x-between">
<span class="text-xs">{{ t("settings.grayMode") }}</span>
<el-switch v-model="settingsStore.grayMode" /> <el-switch v-model="settingsStore.grayMode" />
</div> </div>
<div class="config-item flex-x-between"> <div class="config-item flex-x-between">
<span class="text-xs">色弱模式</span> <span class="text-xs">{{ t("settings.colorWeak") }}</span>
<el-switch v-model="settingsStore.colorWeak" /> <el-switch v-model="settingsStore.colorWeak" />
</div> </div>
</section> </section>
</div> </div>
<template #footer> <template #footer>
<div class="action-buttons"> <div class="settings-footer">
<el-tooltip content="复制当前 defaults 片段,方便覆盖到 src/settings.ts" placement="top"> <el-button
<el-button type="primary"
type="primary" :icon="copyIcon"
size="default" :loading="copyLoading"
:icon="copyIcon" @click="copyCurrentSettings"
:loading="copyLoading" >
@click="handleCopySettings" {{ copyLoading ? "复制中..." : t("settings.copyConfig") }}
> </el-button>
{{ copyLoading ? "复制中..." : t("settings.copyConfig") }} <el-button
</el-button> type="default"
</el-tooltip> :icon="resetIcon"
<el-tooltip content="重置将恢复所有设置为默认值" placement="top"> :loading="resetLoading"
<el-button @click="resetSettingsToDefault"
type="warning" >
size="default" {{ resetLoading ? "重置中..." : t("settings.resetConfig") }}
:icon="resetIcon" </el-button>
:loading="resetLoading"
@click="handleResetSettings"
>
{{ resetLoading ? "重置中..." : t("settings.resetConfig") }}
</el-button>
</el-tooltip>
</div> </div>
</template> </template>
</el-drawer> </el-drawer>
@@ -331,19 +329,23 @@ const drawerVisible = computed({
set: (value) => (settingsStore.settingsVisible = value), set: (value) => (settingsStore.settingsVisible = value),
}); });
const getPaletteColors = (colors: ThemeColorMap) => colorOptions.map((item) => colors[item.name]); function getPaletteColors(colors: ThemeColorMap) {
return colorOptions.map((item) => colors[item.name]);
}
const getPaletteName = (palette: ThemePalettePreset) => { function getPaletteName(palette: ThemePalettePreset) {
const key = paletteI18nKeys[palette.id]; const key = paletteI18nKeys[palette.id];
return key ? t(`settings.themePalettes.${key}.name`) : palette.name; return key ? t(`settings.themePalettes.${key}.name`) : palette.name;
}; }
const getPaletteDescription = (palette: ThemePalettePreset) => { function getPaletteDescription(palette: ThemePalettePreset) {
const key = paletteI18nKeys[palette.id]; const key = paletteI18nKeys[palette.id];
return key ? t(`settings.themePalettes.${key}.description`) : palette.description; return key ? t(`settings.themePalettes.${key}.description`) : palette.description;
}; }
const getColorLabel = (name: ThemeColorName) => t(`settings.themeColorNames.${name}`); function getColorLabel(name: ThemeColorName) {
return t(`settings.themeColorNames.${name}`);
}
const activePaletteName = computed(() => const activePaletteName = computed(() =>
settingsStore.activeThemePalette settingsStore.activeThemePalette
@@ -351,32 +353,47 @@ const activePaletteName = computed(() =>
: t("settings.customPalette") : t("settings.customPalette")
); );
const toggleCustomColors = () => { /**
* 展开或收起自定义颜色
*/
function toggleCustomColors(): void {
isCustomColorsOpen.value = !isCustomColorsOpen.value; isCustomColorsOpen.value = !isCustomColorsOpen.value;
}; }
const handleThemeColorChange = (name: ThemeColorName, color: string | null) => { /**
* 更新单个主题色
*/
function handleThemeColorChange(name: ThemeColorName, color: string | null): void {
if (!color) return; if (!color) return;
settingsStore.updateThemeColor(name, color); settingsStore.updateThemeColor(name, color);
}; }
const changeSidebarColor = (val: string | number | boolean | undefined) => { /**
if (val !== SidebarColor.CLASSIC_BLUE && val !== SidebarColor.MINIMAL_WHITE) return; * 切换侧边栏配色
*/
function setSidebarColor(value: string | number | boolean | undefined): void {
if (value !== SidebarColor.CLASSIC_BLUE && value !== SidebarColor.MINIMAL_WHITE) return;
settingsStore.sidebarColorScheme = val; settingsStore.sidebarColorScheme = value;
}; }
const handleLayoutChange = (layout: LayoutMode) => { /**
* 切换导航布局
*/
function handleLayoutChange(layout: LayoutMode): void {
if (settingsStore.layout === layout) return; if (settingsStore.layout === layout) return;
settingsStore.layout = layout; settingsStore.layout = layout;
}; }
const handleCopySettings = async () => { /**
* 复制当前 settings 默认配置片段
*/
async function copyCurrentSettings(): Promise<void> {
try { try {
copyLoading.value = true; copyLoading.value = true;
const configCode = generateSettingsCode(); const configCode = buildDefaultsCode();
await navigator.clipboard.writeText(configCode); await navigator.clipboard.writeText(configCode);
@@ -389,26 +406,36 @@ const handleCopySettings = async () => {
} finally { } finally {
copyLoading.value = false; copyLoading.value = false;
} }
}; }
const handleResetSettings = async () => {
resetLoading.value = true;
/**
* 恢复所有设置为默认值
*/
async function resetSettingsToDefault(): Promise<void> {
try { try {
await ElMessageBox.confirm(t("settings.confirmReset"), t("settings.resetConfig"), {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
});
resetLoading.value = true;
settingsStore.resetSettings(); settingsStore.resetSettings();
sidebarColor.value = settingsStore.sidebarColorScheme; sidebarColor.value = settingsStore.sidebarColorScheme;
ElMessage.success(t("settings.resetSuccess")); ElMessage.success(t("settings.resetSuccess"));
} catch { } catch {
ElMessage.error("重置配置失败"); // 用户取消时不提示
} finally { } finally {
resetLoading.value = false; resetLoading.value = false;
} }
}; }
// 这里只生成 defaults,避免复制出来的代码和 src/settings.ts 的真实结构对不上。 /**
const generateSettingsCode = (): string => { * 生成 src/settings.ts 中 defaults 的配置片段
*/
function buildDefaultsCode(): string {
const themeColorsCode = JSON.stringify(settingsStore.themeColors, null, 4) const themeColorsCode = JSON.stringify(settingsStore.themeColors, null, 4)
.replace(/"([^"]+)":/g, "$1:") .replace(/"([^"]+)":/g, "$1:")
.replace(/^/gm, " "); .replace(/^/gm, " ");
@@ -445,11 +472,11 @@ const generateSettingsCode = (): string => {
showSettings: ${settings.showSettings}, showSettings: ${settings.showSettings},
watermarkContent: ${settings.watermarkContent}, watermarkContent: ${settings.watermarkContent},
} as const;`; } as const;`;
}; }
const handleCloseDrawer = () => { function handleCloseDrawer(): void {
settingsStore.settingsVisible = false; settingsStore.settingsVisible = false;
}; }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -463,51 +490,38 @@ const handleCloseDrawer = () => {
overflow: hidden; overflow: hidden;
color: var(--el-text-color-regular); color: var(--el-text-color-regular);
} }
:deep(.el-drawer__footer) {
padding: 12px 18px 16px;
border-top: 1px solid var(--el-border-color-lighter);
}
} }
.settings-content { .settings-content {
flex: 1 1 auto; flex: 1 1 auto;
padding: 16px 18px; padding: 16px 18px 20px;
overflow-y: auto; overflow-y: auto;
color: var(--el-text-color-regular); color: var(--el-text-color-regular);
} }
.action-buttons {
display: flex;
& > .el-button {
flex: 1;
font-size: 14px;
border-radius: 8px;
transition: box-shadow 0.16s ease;
&:hover {
box-shadow: none;
}
}
}
.config-section { .config-section {
margin-bottom: 16px; & + & {
margin-top: 20px;
}
.config-item { .config-item {
display: flex; display: flex;
gap: 12px; gap: 12px;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 10px 0; min-height: 42px;
border-bottom: 1px solid var(--el-border-color-light); padding: 8px 0;
transition: background-color 0.16s ease; border-bottom: 1px solid var(--el-border-color-lighter);
&:last-child { &:last-child {
border-bottom: none; border-bottom: none;
} }
&:hover {
background-color: var(--el-fill-color-light);
border-radius: 6px;
}
> .text-xs { > .text-xs {
flex-shrink: 0; flex-shrink: 0;
color: var(--el-text-color-regular); color: var(--el-text-color-regular);
@@ -519,9 +533,9 @@ const handleCloseDrawer = () => {
display: grid; display: grid;
grid-template-columns: auto minmax(0, 1fr); grid-template-columns: auto minmax(0, 1fr);
column-gap: 14px; column-gap: 14px;
padding: 14px 12px; padding: 12px;
margin-top: 4px; margin-top: 12px;
background: var(--el-fill-color-lighter); background: var(--el-fill-color-extra-light);
border: 1px solid var(--el-border-color-lighter); border: 1px solid var(--el-border-color-lighter);
border-radius: 8px; border-radius: 8px;
@@ -555,32 +569,62 @@ const handleCloseDrawer = () => {
.config-item--block { .config-item--block {
display: block; display: block;
min-height: 0;
padding-top: 10px;
padding-bottom: 12px;
&:hover { border-bottom: 1px solid var(--el-border-color-lighter);
padding-right: 0;
padding-left: 0;
margin: 0;
background-color: transparent;
}
} }
} }
.config-block { .section-title {
margin-top: 12px; display: flex;
gap: 10px;
align-items: center;
margin-bottom: 12px;
font-size: 13px;
font-weight: 600;
line-height: 1;
color: var(--el-text-color-primary);
&::before,
&::after {
flex: 1;
height: 1px;
content: "";
background: var(--el-border-color-lighter);
}
} }
.config-block--tight { .theme-mode {
margin-top: 10px; display: flex;
justify-content: center;
} }
.config-item__header { .config-card {
margin-top: 14px;
}
.config-card__header {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
margin-bottom: 8px; margin-bottom: 8px;
font-size: 12px;
color: var(--el-text-color-regular);
> .text-xs { > span {
color: var(--el-text-color-regular); font-weight: 500;
}
}
.settings-footer {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
.el-button {
margin: 0;
} }
} }
+47 -52
View File
@@ -41,22 +41,30 @@
</div> </div>
</el-scrollbar> </el-scrollbar>
<!-- 右侧操作 -->
<div class="tags-actions"> <div class="tags-actions">
<!-- 刷新当前 --> <button
<span class="tags-actions__btn" @click="refreshSelectedTag(currentTag)"> type="button"
class="tags-actions__btn"
aria-label="刷新当前"
title="刷新当前"
@click="refreshSelectedTag(currentTag)"
>
<el-icon :size="16"><Refresh /></el-icon> <el-icon :size="16"><Refresh /></el-icon>
</span> </button>
<!-- 内容全屏 --> <button
<span class="tags-actions__btn" @click="appStore.toggleContentFullscreen()"> type="button"
class="tags-actions__btn"
aria-label="内容全屏"
title="内容全屏"
@click="appStore.toggleContentFullscreen()"
>
<div v-if="!appStore.contentFullscreen" class="i-svg:fullscreen icon-16" /> <div v-if="!appStore.contentFullscreen" class="i-svg:fullscreen icon-16" />
<div v-else class="i-svg:fullscreen-exit icon-16" /> <div v-else class="i-svg:fullscreen-exit icon-16" />
</span> </button>
<!-- 下拉菜单 -->
<el-dropdown trigger="click" @command="handleActionCommand"> <el-dropdown trigger="click" @command="handleActionCommand">
<span class="tags-actions__btn"> <button type="button" class="tags-actions__btn" aria-label="页签操作" title="页签操作">
<el-icon :size="16"><ArrowDown /></el-icon> <el-icon :size="16"><ArrowDown /></el-icon>
</span> </button>
<template #dropdown> <template #dropdown>
<el-dropdown-menu> <el-dropdown-menu>
<el-dropdown-item command="refresh"> <el-dropdown-item command="refresh">
@@ -174,10 +182,8 @@ const tagsStyleClass = computed(() => {
} }
}); });
// 图标兼容:判断是否为 Element Plus 图标 (el-icon-xxx)
const isEpIcon = (icon: string) => icon.startsWith("el-icon"); const isEpIcon = (icon: string) => icon.startsWith("el-icon");
// 图标兼容:el-icon-xxx → PascalCase 组件名
const toEpIconName = (icon: string) => const toEpIconName = (icon: string) =>
icon.replace("el-icon-", "").replace(/(^|-)\w/g, (s) => s.slice(-1).toUpperCase()); icon.replace("el-icon-", "").replace(/(^|-)\w/g, (s) => s.slice(-1).toUpperCase());
@@ -202,7 +208,6 @@ const contextMenuStyle = computed(() => {
}; };
}); });
// 标签栏右侧操作按钮分发
const handleActionCommand = (command: string) => { const handleActionCommand = (command: string) => {
switch (command) { switch (command) {
case "refresh": case "refresh":
@@ -226,7 +231,6 @@ const handleActionCommand = (command: string) => {
} }
}; };
// path → TagView 映射,用于快速查找标签
const routePathMap = computed(() => { const routePathMap = computed(() => {
const map = new Map<string, TagView>(); const map = new Map<string, TagView>();
visitedViews.value.forEach((tag) => { visitedViews.value.forEach((tag) => {
@@ -235,7 +239,6 @@ const routePathMap = computed(() => {
return map; return map;
}); });
// 判断右键选中标签是否为最左侧(不可关闭左侧)
const isFirstView = computed(() => { const isFirstView = computed(() => {
if (!selectedTag.value) return false; if (!selectedTag.value) return false;
return ( return (
@@ -244,13 +247,11 @@ const isFirstView = computed(() => {
); );
}); });
// 判断右键选中标签是否为最右侧(不可关闭右侧)
const isLastView = computed(() => { const isLastView = computed(() => {
if (!selectedTag.value) return false; if (!selectedTag.value) return false;
return selectedTag.value.fullPath === visitedViews.value[visitedViews.value.length - 1]?.fullPath; return selectedTag.value.fullPath === visitedViews.value[visitedViews.value.length - 1]?.fullPath;
}); });
// 从路由表中提取所有 affix 固定标签
const extractAffixTags = (routes: RouteRecordRaw[], basePath = "/"): TagView[] => { const extractAffixTags = (routes: RouteRecordRaw[], basePath = "/"): TagView[] => {
const affixTags: TagView[] = []; const affixTags: TagView[] = [];
@@ -341,7 +342,6 @@ const closeContextMenu = () => {
contextMenu.visible = false; contextMenu.visible = false;
}; };
// 鼠标滚轮横向滚动(标签栏超出可视区时生效)
const handleScroll = (event: WheelEvent) => { const handleScroll = (event: WheelEvent) => {
closeContextMenu(); closeContextMenu();
@@ -358,7 +358,6 @@ const handleScroll = (event: WheelEvent) => {
scrollbarRef.value.setScrollLeft(newScrollLeft); scrollbarRef.value.setScrollLeft(newScrollLeft);
}; };
// 刷新指定标签页:移除缓存后跳转 redirect 中转页重新加载
const refreshSelectedTag = (tag: TagView | null) => { const refreshSelectedTag = (tag: TagView | null) => {
if (!tag) return; if (!tag) return;
@@ -368,7 +367,6 @@ const refreshSelectedTag = (tag: TagView | null) => {
}); });
}; };
// 关闭指定标签页,若关闭的是当前激活标签则跳转到最后一个
const closeSelectedTag = (tag: TagView | null) => { const closeSelectedTag = (tag: TagView | null) => {
if (!tag) return; if (!tag) return;
@@ -379,18 +377,10 @@ const closeSelectedTag = (tag: TagView | null) => {
}); });
}; };
// 批量关闭标签(左/右/其它方向)
/** /**
* 按方向批量关闭标签的通用执行器。 * 关闭指定方向的标签
*
* closeLeftTagsForActive / closeRightTagsForActive / closeLeftTags /
* closeRightTags 四个公开函数均委托此函数,仅 source(标签来源 ref)
* 和 batchFn(调用的 store 方法)不同。
*
* 关闭后若当前路由对应的标签已被移除,自动跳转到最后一个剩余标签。
*/ */
function closeDirectional( function closeDirectionalTags(
source: Ref<TagView | null>, source: Ref<TagView | null>,
batchFn: (tag: TagView) => Promise<{ visitedViews: TagView[] }> batchFn: (tag: TagView) => Promise<{ visitedViews: TagView[] }>
) { ) {
@@ -406,7 +396,7 @@ function closeDirectional(
} }
/** /**
* 关闭当前激活标签之外的其它标签。 * 关闭当前激活标签之外的其它标签
*/ */
const closeOtherTagsForActive = () => { const closeOtherTagsForActive = () => {
if (!currentTag.value) return; if (!currentTag.value) return;
@@ -415,16 +405,11 @@ const closeOtherTagsForActive = () => {
}); });
}; };
// 关闭左侧(当前激活标签) const closeLeftTagsForActive = () => closeDirectionalTags(currentTag, tagsViewStore.delLeftViews);
const closeLeftTagsForActive = () => closeDirectional(currentTag, tagsViewStore.delLeftViews); const closeRightTagsForActive = () => closeDirectionalTags(currentTag, tagsViewStore.delRightViews);
// 关闭右侧(当前激活标签) const closeLeftTags = () => closeDirectionalTags(selectedTag, tagsViewStore.delLeftViews);
const closeRightTagsForActive = () => closeDirectional(currentTag, tagsViewStore.delRightViews); const closeRightTags = () => closeDirectionalTags(selectedTag, tagsViewStore.delRightViews);
// 关闭左侧(右键选中标签)
const closeLeftTags = () => closeDirectional(selectedTag, tagsViewStore.delLeftViews);
// 关闭右侧(右键选中标签)
const closeRightTags = () => closeDirectional(selectedTag, tagsViewStore.delRightViews);
// 关闭右键选中标签以外的其他标签
const closeOtherTags = () => { const closeOtherTags = () => {
if (!selectedTag.value) return; if (!selectedTag.value) return;
router.push(selectedTag.value); router.push(selectedTag.value);
@@ -433,14 +418,12 @@ const closeOtherTags = () => {
}); });
}; };
// 关闭所有标签页
const closeAllTags = (tag: TagView | null) => { const closeAllTags = (tag: TagView | null) => {
tagsViewStore.delAllViews().then((result) => { tagsViewStore.delAllViews().then((result) => {
tagsViewStore.toLastView(result.visitedViews, tag || undefined); tagsViewStore.toLastView(result.visitedViews, tag || undefined);
}); });
}; };
// 右键菜单生命周期:显示时监听外部点击关闭,卸载时移除监听
const useContextMenuManager = () => { const useContextMenuManager = () => {
const handleOutsideClick = () => { const handleOutsideClick = () => {
closeContextMenu(); closeContextMenu();
@@ -459,7 +442,6 @@ const useContextMenuManager = () => {
}); });
}; };
// 路由变化时同步添加标签并更新激活状态
watch( watch(
route, route,
() => { () => {
@@ -469,7 +451,6 @@ watch(
{ immediate: true } { immediate: true }
); );
// 初始化固定标签(affix)
onMounted(() => { onMounted(() => {
initAffixTags(); initAffixTags();
}); });
@@ -708,22 +689,36 @@ useContextMenuManager();
justify-content: center; justify-content: center;
width: 30px; width: 30px;
height: 30px; height: 30px;
padding: 0;
font: inherit;
color: var(--el-text-color-regular);
appearance: none;
cursor: pointer; cursor: pointer;
background: transparent;
border: 0;
border-radius: 6px; border-radius: 6px;
transition: background-color 0.15s ease; transition:
color 0.15s ease,
background-color 0.15s ease;
&:hover { &:hover {
background-color: var(--el-fill-color); color: var(--el-color-primary);
background-color: color-mix(in srgb, var(--el-color-primary) 10%, transparent);
} }
:deep(.el-icon), &:focus-visible {
:deep(div) { outline: 2px solid var(--el-color-primary-light-5);
opacity: 0.45; outline-offset: 1px;
} }
&:hover :deep(.el-icon), :deep(.el-icon) {
&:hover :deep(div) { color: currentcolor;
opacity: 0.7; --color: currentcolor;
}
.icon-16 {
color: currentcolor;
background-color: currentcolor;
} }
} }
} }
+68 -52
View File
@@ -3,7 +3,7 @@
<!-- 桌面端工具项 --> <!-- 桌面端工具项 -->
<template v-if="isDesktop"> <template v-if="isDesktop">
<!-- 搜索 --> <!-- 搜索 -->
<div class="navbar-actions__item"> <div class="navbar-actions__item navbar-actions__item--search">
<CommandPalette /> <CommandPalette />
</div> </div>
@@ -34,7 +34,7 @@
</template> </template>
<!-- 用户菜单 --> <!-- 用户菜单 -->
<div class="navbar-actions__item"> <div class="navbar-actions__item navbar-actions__item--profile">
<el-dropdown trigger="click"> <el-dropdown trigger="click">
<div class="user-profile"> <div class="user-profile">
<div style="width: 28px; height: 28px; overflow: hidden; border-radius: 50%"> <div style="width: 28px; height: 28px; overflow: hidden; border-radius: 50%">
@@ -110,8 +110,9 @@ function handleTenantChange(tenantId: number) {
ElMessage.success("切换租户成功"); ElMessage.success("切换租户成功");
window.location.href = "/"; window.location.href = "/";
}, },
(error: any) => { (error: unknown) => {
ElMessage.error(error.message || "切换租户失败"); const message = error instanceof Error ? error.message : "切换租户失败";
ElMessage.error(message);
} }
); );
} }
@@ -177,6 +178,10 @@ function handleSettingsClick() {
<style lang="scss" scoped> <style lang="scss" scoped>
.navbar-actions { .navbar-actions {
--navbar-action-color: var(--el-text-color-secondary);
--navbar-action-hover-color: var(--el-color-primary);
--navbar-action-hover-bg: var(--el-fill-color-light);
display: flex; display: flex;
gap: 4px; gap: 4px;
align-items: center; align-items: center;
@@ -190,9 +195,10 @@ function handleSettingsClick() {
min-width: 32px; min-width: 32px;
height: 32px; height: 32px;
padding: 0 6px; padding: 0 6px;
color: var(--navbar-action-color);
text-align: center; text-align: center;
cursor: pointer; cursor: pointer;
border-radius: 8px; border-radius: 6px;
transition: transition:
background-color 0.16s, background-color 0.16s,
color 0.16s; color 0.16s;
@@ -212,6 +218,14 @@ function handleSettingsClick() {
justify-content: center; justify-content: center;
width: 100%; width: 100%;
height: 32px; height: 32px;
color: inherit !important;
}
:deep(.el-tooltip__trigger),
:deep(.fullscreen-trigger),
:deep(.size-trigger),
:deep(.notice__trigger) {
color: inherit;
} }
:deep(.i-svg\:language) { :deep(.i-svg\:language) {
@@ -223,20 +237,40 @@ function handleSettingsClick() {
background-size: 16px 16px; background-size: 16px 16px;
} }
// 图标样式 :deep([class*="i-svg:"]),
:deep([class*="i-svg:"]) { :deep(.el-icon) {
--color: currentColor;
font-size: 16px; font-size: 16px;
line-height: 1; line-height: 1;
color: var(--el-text-color-secondary); color: currentColor !important;
transition: color 0.2s; transition: color 0.16s;
}
:deep([class*="i-svg:"]) {
background-color: currentColor !important;
} }
&:hover { &:hover {
background: var(--el-fill-color); color: var(--navbar-action-hover-color);
background: var(--navbar-action-hover-bg);
}
}
:deep([class*="i-svg:"]) { &__item--search {
color: var(--el-color-primary); color: var(--el-text-color-secondary);
}
&:hover {
background: transparent;
}
}
&__item--profile {
padding-right: 4px;
padding-left: 4px;
&:hover {
background: transparent;
} }
} }
@@ -257,7 +291,7 @@ function handleSettingsClick() {
&__name { &__name {
margin-left: 8px; margin-left: 8px;
font-size: 13px; font-size: 13px;
color: var(--el-text-color-regular); color: currentColor;
white-space: nowrap; white-space: nowrap;
transition: color 0.3s; transition: color 0.3s;
} }
@@ -266,73 +300,55 @@ function handleSettingsClick() {
// 白色文字样式(用于深色背景:暗黑主题、顶部布局、混合布局等) // 白色文字样式(用于深色背景:暗黑主题、顶部布局、混合布局等)
.navbar-actions--white-text { .navbar-actions--white-text {
.navbar-actions__item { --navbar-action-color: var(--menu-text);
:deep([class*="i-svg:"]) { --navbar-action-hover-color: var(--menu-active-text);
color: color-mix(in srgb, var(--el-color-white) 85%, transparent); --navbar-action-hover-bg: var(--menu-hover);
}
&:hover {
background: color-mix(in srgb, var(--el-color-white) 10%, transparent);
:deep([class*="i-svg:"]) {
color: var(--el-color-white);
}
}
}
.user-profile__name { .user-profile__name {
color: color-mix(in srgb, var(--el-color-white) 85%, transparent); color: currentColor;
} }
// 租户选择器在白色文字模式下的样式 // 租户选择器在白色文字模式下的样式
::v-deep(.tenant-switcher__trigger) { ::v-deep(.tenant-switcher__trigger) {
color: color-mix(in srgb, var(--el-color-white) 85%, transparent); color: currentColor;
background: transparent;
} }
::v-deep(.tenant-switcher__trigger .tenant-switcher__icon) { ::v-deep(.tenant-switcher__trigger .tenant-switcher__icon) {
color: color-mix(in srgb, var(--el-color-white) 85%, transparent); color: currentColor;
} }
::v-deep(.tenant-switcher__trigger:hover) { ::v-deep(.tenant-switcher__trigger:hover) {
color: var(--el-color-white); color: var(--navbar-action-hover-color);
background: color-mix(in srgb, var(--el-color-white) 10%, transparent); background: var(--navbar-action-hover-bg);
} }
::v-deep(.tenant-switcher__trigger:hover .tenant-switcher__icon) { ::v-deep(.tenant-switcher__trigger:hover .tenant-switcher__icon) {
color: var(--el-color-white); color: currentColor;
} }
} }
// 深色文字样式(用于浅色背景:明亮主题下的左侧布局等) // 深色文字样式(用于浅色背景:明亮主题下的左侧布局等)
.navbar-actions--dark-text { .navbar-actions--dark-text {
.navbar-actions__item { --navbar-action-color: var(--el-text-color-secondary);
:deep([class*="i-svg:"]) { --navbar-action-hover-color: var(--el-color-primary);
color: var(--el-text-color-secondary) !important; --navbar-action-hover-bg: var(--el-fill-color-light);
}
&:hover {
background: rgba(0, 0, 0, 0.04);
:deep([class*="i-svg:"]) {
color: var(--el-color-primary) !important;
}
}
}
.user-profile__name { .user-profile__name {
color: var(--el-text-color-regular) !important; color: var(--el-text-color-regular);
} }
// 租户选择器在深色文字模式下的样式 // 租户选择器在深色文字模式下的样式
::v-deep(.tenant-switcher__trigger) { ::v-deep(.tenant-switcher__trigger) {
color: var(--el-text-color-regular) !important; color: var(--el-text-color-regular);
background: transparent;
} }
::v-deep(.tenant-switcher__trigger .tenant-switcher__icon) { ::v-deep(.tenant-switcher__trigger .tenant-switcher__icon) {
color: var(--el-text-color-regular) !important; color: currentColor;
} }
::v-deep(.tenant-switcher__trigger:hover) { ::v-deep(.tenant-switcher__trigger:hover) {
color: var(--el-color-primary) !important; color: var(--navbar-action-hover-color);
background: var(--el-fill-color-light); background: var(--navbar-action-hover-bg);
} }
::v-deep(.tenant-switcher__trigger:hover .tenant-switcher__icon) { ::v-deep(.tenant-switcher__trigger:hover .tenant-switcher__icon) {
color: var(--el-color-primary) !important; color: currentColor;
} }
} }
+15 -6
View File
@@ -149,7 +149,7 @@
} }
.el-table { .el-table {
--el-table-border-color: var(--border-color); --el-table-border-color: var(--el-border-color-extra-light);
--el-table-header-bg-color: #f8fafc; --el-table-header-bg-color: #f8fafc;
--el-table-row-hover-bg-color: #f8fbff; --el-table-row-hover-bg-color: #f8fbff;
--el-table-current-row-bg-color: #eef5ff; --el-table-current-row-bg-color: #eef5ff;
@@ -164,27 +164,31 @@
} }
&.el-table--border::after { &.el-table--border::after {
display: block; width: 1px;
background-color: var(--border-color); background-color: var(--el-table-border-color);
} }
.el-table__inner-wrapper::before { .el-table__inner-wrapper::before {
height: 0; height: 0;
} }
.el-table__inner-wrapper::after {
background-color: var(--el-table-border-color);
}
.el-table__header th { .el-table__header th {
height: 44px; height: 44px;
padding: 0; padding: 0;
font-weight: 500; font-weight: 500;
color: var(--el-text-color-regular); color: var(--el-text-color-regular);
background: var(--el-table-header-bg-color); background: var(--el-table-header-bg-color);
border-bottom: 1px solid var(--border-color); border-bottom: 1px solid var(--el-table-border-color);
} }
.el-table__body td { .el-table__body td {
height: 46px; height: 46px;
padding: 0; padding: 0;
border-bottom: 1px solid #eef1f6; border-bottom: 1px solid var(--el-table-border-color);
} }
.cell { .cell {
@@ -192,7 +196,12 @@
} }
.el-table__fixed-right { .el-table__fixed-right {
box-shadow: -1px 0 0 var(--border-color); box-shadow: -1px 0 0 var(--el-table-border-color);
}
.el-table-fixed-column--left.is-last-column::before,
.el-table-fixed-column--right.is-first-column::before {
box-shadow: none;
} }
&.is-empty { &.is-empty {
-4
View File
@@ -4,10 +4,6 @@
@use "variables" as *; @use "variables" as *;
@mixin app-main-height-with-tags {
height: calc(100vh - $navbar-height - $tags-view-height) !important;
}
@mixin app-main-height { @mixin app-main-height {
height: calc(100vh - $navbar-height); height: calc(100vh - $navbar-height);
} }
+19 -2
View File
@@ -68,26 +68,43 @@
} }
.page-table-wrapper { .page-table-wrapper {
--page-table-border-color: var(--el-border-color-extra-light);
min-height: 0; min-height: 0;
overflow: hidden; overflow: hidden;
background: var(--content-bg); background: var(--content-bg);
border: 1px solid var(--border-color); border: 1px solid var(--page-table-border-color);
border-radius: var(--card-radius); border-radius: var(--card-radius);
} }
.page-table { .page-table {
--el-table-border-color: var(--page-table-border-color);
width: 100%; width: 100%;
height: 100%; height: 100%;
min-height: 0; min-height: 0;
border-radius: 0; border-radius: 0;
&.el-table--border { &.el-table--border {
border: 0; border: 0 !important;
} }
.el-table__inner-wrapper { .el-table__inner-wrapper {
height: 100%; height: 100%;
} }
&::before,
&.el-table--border::before,
&.el-table--border::after,
&.el-table--border .el-table__inner-wrapper::after {
display: none;
}
.el-table__border-left-patch,
.el-table__border-right-patch,
.el-table__border-bottom-patch {
display: none;
}
} }
.page-pagination { .page-pagination {
+1 -1
View File
@@ -30,7 +30,7 @@ html.sidebar-color-blue {
--menu-background: #304156; --menu-background: #304156;
--menu-text: #bfcbd9; --menu-text: #bfcbd9;
--menu-active-text: #ffffff; --menu-active-text: #ffffff;
--menu-active-bg: rgb(22 93 255 / 18%); --menu-active-bg: rgb(64 158 255 / 22%);
--menu-hover: rgb(255 255 255 / 7%); --menu-hover: rgb(255 255 255 / 7%);
--menu-border: rgb(15 23 42 / 22%); --menu-border: rgb(15 23 42 / 22%);
--sidebar-logo-background: #2d3748; --sidebar-logo-background: #2d3748;
+1 -2
View File
@@ -594,8 +594,7 @@ $pad: 10px;
flex-direction: column; flex-direction: column;
gap: $gap; gap: $gap;
padding: $pad; padding: $pad;
background: background: var(--page-bg);
linear-gradient(180deg, rgb(255 255 255 / 62%), rgb(255 255 255 / 0%) 240px), var(--page-bg);
} }
// Header // Header