refactor(projects): 页面布局调整为rdms风格

This commit is contained in:
2026-04-15 09:35:54 +08:00
parent a6fc7b48dc
commit e22f6550ae
21 changed files with 990 additions and 188 deletions

View File

@@ -16,18 +16,18 @@ defineOptions({ name: 'BaseLayout' });
const appStore = useAppStore();
const themeStore = useThemeStore();
const { childLevelMenus, isActiveFirstLevelMenuHasChildren } = setupMixMenuContext();
const { childLevelMenus } = setupMixMenuContext();
const GlobalMenu = defineAsyncComponent(() => import('../modules/global-menu/index.vue'));
const layoutMode = computed(() => {
const vertical: LayoutMode = 'vertical';
const horizontal: LayoutMode = 'horizontal';
return themeStore.layout.mode.includes(vertical) ? vertical : horizontal;
return themeStore.layoutMode.includes(vertical) ? vertical : horizontal;
});
const headerProps = computed(() => {
const { mode, reverseHorizontalMix } = themeStore.layout;
const mode = themeStore.layoutMode;
const headerPropsConfig: Record<UnionKey.ThemeLayoutMode, App.Global.HeaderProps> = {
vertical: {
@@ -48,31 +48,26 @@ const headerProps = computed(() => {
'horizontal-mix': {
showLogo: true,
showMenu: true,
showMenuToggler: reverseHorizontalMix && isActiveFirstLevelMenuHasChildren.value
showMenuToggler: false
}
};
return headerPropsConfig[mode];
});
const siderVisible = computed(() => themeStore.layout.mode !== 'horizontal');
const siderVisible = computed(() => themeStore.layoutMode !== 'horizontal');
const isVerticalMix = computed(() => themeStore.layout.mode === 'vertical-mix');
const isVerticalMix = computed(() => themeStore.layoutMode === 'vertical-mix');
const isHorizontalMix = computed(() => themeStore.layout.mode === 'horizontal-mix');
const isHorizontalMix = computed(() => themeStore.layoutMode === 'horizontal-mix');
const siderWidth = computed(() => getSiderWidth());
const siderCollapsedWidth = computed(() => getSiderCollapsedWidth());
function getSiderWidth() {
const { reverseHorizontalMix } = themeStore.layout;
const { width, mixWidth, mixChildMenuWidth } = themeStore.sider;
if (isHorizontalMix.value && reverseHorizontalMix) {
return isActiveFirstLevelMenuHasChildren.value ? width : 0;
}
let w = isVerticalMix.value || isHorizontalMix.value ? mixWidth : width;
if (isVerticalMix.value && appStore.mixSiderFixed && childLevelMenus.value.length) {
@@ -83,13 +78,8 @@ function getSiderWidth() {
}
function getSiderCollapsedWidth() {
const { reverseHorizontalMix } = themeStore.layout;
const { collapsedWidth, mixCollapsedWidth, mixChildMenuWidth } = themeStore.sider;
if (isHorizontalMix.value && reverseHorizontalMix) {
return isActiveFirstLevelMenuHasChildren.value ? collapsedWidth : 0;
}
let w = isVerticalMix.value || isHorizontalMix.value ? mixCollapsedWidth : collapsedWidth;
if (isVerticalMix.value && appStore.mixSiderFixed && childLevelMenus.value.length) {
@@ -110,7 +100,7 @@ function getSiderCollapsedWidth() {
:full-content="appStore.fullContent"
:fixed-top="themeStore.fixedHeaderAndTab"
:header-height="themeStore.header.height"
:tab-visible="themeStore.tab.visible"
:tab-visible="themeStore.tabVisible"
:tab-height="themeStore.tab.height"
:content-class="appStore.contentXScrollable ? 'overflow-x-hidden' : ''"
:sider-visible="siderVisible"

View File

@@ -8,7 +8,6 @@ import { useRouterPush } from '@/hooks/common/router';
export const { setupStore: setupMixMenuContext, useStore: useMixMenuContext } = useContext('mix-menu', useMixMenu);
function useMixMenu() {
const route = useRoute();
const routeStore = useRouteStore();
const { selectedKey } = useMenu();
@@ -19,9 +18,9 @@ function useMixMenu() {
}
function getActiveFirstLevelMenuKey() {
const [firstLevelRouteName] = selectedKey.value.split('_');
const [firstLevelMenuKey = ''] = routeStore.getSelectedMenuKeyPath(selectedKey.value);
setActiveFirstLevelMenuKey(firstLevelRouteName);
setActiveFirstLevelMenuKey(firstLevelMenuKey);
}
const allMenus = computed<App.Global.Menu[]>(() => routeStore.menus);
@@ -49,7 +48,7 @@ function useMixMenu() {
});
watch(
() => route.name,
[selectedKey, allMenus],
() => {
getActiveFirstLevelMenuKey();
},

View File

@@ -14,9 +14,12 @@ withDefaults(defineProps<Props>(), {
</script>
<template>
<RouterLink to="/" class="w-full flex-center nowrap-hidden">
<SystemLogo class="text-32px text-primary" />
<h2 v-show="showTitle" class="pl-8px text-16px text-primary font-bold transition duration-300 ease-in-out">
<RouterLink to="/" class="h-full w-full flex-y-center justify-start gap-8px nowrap-hidden px-12px">
<SystemLogo class="shrink-0 text-32px text-primary" />
<h2
v-show="showTitle"
class="min-w-0 flex-1-hidden ellipsis-text text-16px text-primary font-bold transition duration-300 ease-in-out"
>
{{ $t('system.title') }}
</h2>
</RouterLink>

View File

@@ -7,7 +7,6 @@ import VerticalMenu from './modules/vertical-menu.vue';
import VerticalMixMenu from './modules/vertical-mix-menu.vue';
import HorizontalMenu from './modules/horizontal-menu.vue';
import HorizontalMixMenu from './modules/horizontal-mix-menu.vue';
import ReversedHorizontalMixMenu from './modules/reversed-horizontal-mix-menu.vue';
defineOptions({
name: 'GlobalMenu'
@@ -21,13 +20,13 @@ const activeMenu = computed(() => {
vertical: VerticalMenu,
'vertical-mix': VerticalMixMenu,
horizontal: HorizontalMenu,
'horizontal-mix': themeStore.layout.reverseHorizontalMix ? ReversedHorizontalMixMenu : HorizontalMixMenu
'horizontal-mix': HorizontalMixMenu
};
return menuMap[themeStore.layout.mode];
return menuMap[themeStore.layoutMode];
});
const reRenderVertical = computed(() => themeStore.layout.mode === 'vertical' && appStore.isMobile);
const reRenderVertical = computed(() => themeStore.layoutMode === 'vertical' && appStore.isMobile);
</script>
<template>

View File

@@ -1,43 +1,111 @@
<script setup lang="ts">
import type { RouteKey } from '@elegant-router/types';
import { computed } from 'vue';
import { GLOBAL_HEADER_MENU_ID, GLOBAL_SIDER_MENU_ID } from '@/constants/app';
import { useAppStore } from '@/store/modules/app';
import { useRouteStore } from '@/store/modules/route';
import { useThemeStore } from '@/store/modules/theme';
import { useRouterPush } from '@/hooks/common/router';
import FirstLevelMenu from '../components/first-level-menu.vue';
import { useMenu, useMixMenuContext } from '../../../context';
import MenuItem from '../components/menu-item.vue';
defineOptions({
name: 'HorizontalMixMenu'
});
const appStore = useAppStore();
const routeStore = useRouteStore();
const themeStore = useThemeStore();
const { routerPushByKeyWithMetaQuery } = useRouterPush();
const { allMenus, childLevelMenus, activeFirstLevelMenuKey, setActiveFirstLevelMenuKey } = useMixMenuContext();
const { selectedKeyDummy, handleSelect } = useMenu();
const { selectedKey } = useMenu();
const activeFirstLevelMenu = computed(
() => allMenus.value.find(menu => menu.key === activeFirstLevelMenuKey.value) || null
);
const headerMenuHeight = computed(() => `${themeStore.header.height}px`);
const selectedMenuKeyPath = computed(() => routeStore.getSelectedMenuKeyPath(selectedKey.value));
function handleSelectMixMenu(menu: App.Global.Menu) {
setActiveFirstLevelMenuKey(menu.key);
if (!menu.children?.length) {
routerPushByKeyWithMetaQuery(menu.routeKey);
routerPushByKeyWithMetaQuery(menu.routeKey);
}
function handleClickNavItem(menu: App.Global.Menu) {
routerPushByKeyWithMetaQuery(menu.routeKey);
}
function handleClickDomainAnchor() {
if (!activeFirstLevelMenu.value) {
return;
}
routerPushByKeyWithMetaQuery(activeFirstLevelMenu.value.routeKey);
}
function isMenuActive(menu: App.Global.Menu) {
return selectedMenuKeyPath.value.includes(menu.key);
}
</script>
<template>
<Teleport :to="`#${GLOBAL_HEADER_MENU_ID}`">
<ElMenu
ellipsis
class="w-full"
mode="horizontal"
:default-active="selectedKeyDummy"
@select="val => handleSelect(val as RouteKey)"
>
<MenuItem v-for="item in childLevelMenus" :key="item.key" :item="item" :index="item.key" />
</ElMenu>
<div class="mix-header-nav size-full min-w-0 flex-y-center">
<button
v-if="activeFirstLevelMenu"
type="button"
class="domain-anchor h-full flex-y-center gap-8px px-8px text-left"
@click="handleClickDomainAnchor"
>
<component :is="activeFirstLevelMenu.icon" v-if="activeFirstLevelMenu.icon" class="text-icon" />
<span class="domain-anchor__label">{{ activeFirstLevelMenu.label }}</span>
</button>
<div v-if="childLevelMenus.length" class="mx-12px h-20px w-1px shrink-0 bg-[var(--el-border-color)]"></div>
<div v-if="childLevelMenus.length" class="header-nav-list h-full min-w-0 flex-1">
<template v-for="item in childLevelMenus" :key="item.key">
<button
v-if="!item.children?.length"
type="button"
class="header-nav-item"
:class="{ 'is-active': isMenuActive(item) }"
@click="handleClickNavItem(item)"
>
<span class="header-nav-item__label">{{ item.label }}</span>
</button>
<ElDropdown
v-else
trigger="hover"
placement="bottom"
popper-class="header-nav-dropdown"
:show-timeout="120"
:hide-timeout="120"
:teleported="true"
>
<button
type="button"
class="header-nav-item header-nav-item--dropdown"
:class="{ 'is-active': isMenuActive(item) }"
>
<span class="header-nav-item__label">{{ item.label }}</span>
<icon-ep:arrow-down class="header-nav-item__arrow" />
</button>
<template #dropdown>
<ElDropdownMenu>
<ElDropdownItem
v-for="child in item.children"
:key="child.key"
class="header-nav-dropdown__item"
:class="{ 'is-active-route': isMenuActive(child) }"
@click="handleClickNavItem(child)"
>
{{ child.label }}
</ElDropdownItem>
</ElDropdownMenu>
</template>
</ElDropdown>
</template>
</div>
</div>
</Teleport>
<Teleport :to="`#${GLOBAL_SIDER_MENU_ID}`">
<FirstLevelMenu
@@ -52,4 +120,138 @@ function handleSelectMixMenu(menu: App.Global.Menu) {
</Teleport>
</template>
<style scoped></style>
<style scoped>
.mix-header-nav {
height: v-bind(headerMenuHeight);
overflow: hidden;
}
.domain-anchor {
appearance: none;
-webkit-appearance: none;
border: none;
background: transparent;
margin: 0;
padding-top: 0;
padding-bottom: 0;
font: inherit;
flex-shrink: 0;
min-width: 0;
line-height: 1;
color: var(--el-text-color-primary);
}
.domain-anchor:hover {
color: var(--el-color-primary);
}
.domain-anchor__label {
display: inline-flex;
align-items: center;
max-width: 12rem;
line-height: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.header-nav-list {
display: flex;
align-items: center;
gap: 4px;
height: 100%;
overflow: hidden;
}
.header-nav-item {
appearance: none;
-webkit-appearance: none;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
position: relative;
margin: 0;
height: 100%;
flex-shrink: 0;
padding: 0 14px;
border: none;
background: transparent;
font: inherit;
line-height: 1;
color: var(--el-text-color-primary);
white-space: nowrap;
cursor: pointer;
}
.header-nav-item:hover {
color: var(--el-color-primary);
}
.header-nav-item__label {
display: inline-flex;
align-items: center;
line-height: 1;
}
.header-nav-item__arrow {
font-size: 12px;
line-height: 1;
}
.header-nav-item.is-active {
color: var(--el-color-primary);
}
.header-nav-item.is-active::after {
content: '';
position: absolute;
left: 12px;
right: 12px;
bottom: 0;
height: 2px;
border-radius: 999px;
background-color: var(--el-color-primary);
}
:global(.header-nav-dropdown.el-popper) {
padding: 0;
border: none;
border-radius: 14px;
background-color: rgb(255 255 255 / 98%);
box-shadow:
0 12px 28px rgb(15 23 42 / 10%),
0 2px 8px rgb(15 23 42 / 6%);
backdrop-filter: blur(8px);
}
:global(.header-nav-dropdown .el-popper__arrow) {
display: none;
}
:global(.header-nav-dropdown .el-dropdown-menu) {
padding: 8px;
border: 1px solid rgb(226 232 240 / 90%);
border-radius: 14px;
box-shadow: none;
}
:global(.header-nav-dropdown .el-dropdown-menu__item) {
height: 40px;
margin: 2px 0;
padding: 0 12px;
border-radius: 10px;
font-size: 14px;
line-height: 40px;
color: rgb(15 23 42 / 88%);
}
:global(.header-nav-dropdown .el-dropdown-menu__item:hover) {
background-color: rgb(99 102 241 / 8%);
}
:global(.header-nav-dropdown .el-dropdown-menu__item.is-active-route) {
color: var(--el-color-primary);
background-color: rgb(99 102 241 / 10%);
}
</style>

View File

@@ -10,8 +10,8 @@ defineOptions({ name: 'GlobalSider' });
const appStore = useAppStore();
const themeStore = useThemeStore();
const isVerticalMix = computed(() => themeStore.layout.mode === 'vertical-mix');
const isHorizontalMix = computed(() => themeStore.layout.mode === 'horizontal-mix');
const isVerticalMix = computed(() => themeStore.layoutMode === 'vertical-mix');
const isHorizontalMix = computed(() => themeStore.layoutMode === 'horizontal-mix');
const darkMenu = computed(() => !themeStore.darkMode && !isHorizontalMix.value && themeStore.sider.inverted);
const showLogo = computed(() => !isVerticalMix.value && !isHorizontalMix.value);
const menuWrapperClass = computed(() => (showLogo.value ? 'flex-1-hidden' : 'h-full'));

View File

@@ -2,7 +2,6 @@
import { useAppStore } from '@/store/modules/app';
import { $t } from '@/locales';
import DarkMode from './modules/dark-mode.vue';
import LayoutMode from './modules/layout-mode.vue';
import ThemeColor from './modules/theme-color.vue';
import PageFun from './modules/page-fun.vue';
import ConfigOperation from './modules/config-operation.vue';
@@ -15,7 +14,6 @@ const appStore = useAppStore();
<template>
<ElDrawer v-model="appStore.themeDrawerVisible" :title="$t('theme.themeDrawerTitle')" :size="360">
<DarkMode />
<LayoutMode />
<ThemeColor />
<PageFun />
<template #footer>

View File

@@ -27,7 +27,7 @@ function handleColourWeaknessChange(value: boolean) {
themeStore.setColourWeakness(value);
}
const showSiderInverted = computed(() => !themeStore.darkMode && themeStore.layout.mode.includes('vertical'));
const showSiderInverted = computed(() => !themeStore.darkMode && themeStore.layoutMode.includes('vertical'));
</script>
<template>

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import { computed } from 'vue';
import { themePageAnimationModeOptions, themeScrollModeOptions, themeTabModeOptions } from '@/constants/app';
import { themePageAnimationModeOptions, themeScrollModeOptions } from '@/constants/app';
import { useThemeStore } from '@/store/modules/theme';
import { translateOptions } from '@/utils/common';
import { $t } from '@/locales';
@@ -10,7 +10,7 @@ defineOptions({ name: 'PageFun' });
const themeStore = useThemeStore();
const layoutMode = computed(() => themeStore.layout.mode);
const layoutMode = computed(() => themeStore.layoutMode);
const isMixLayoutMode = computed(() => layoutMode.value.includes('mix'));
@@ -55,25 +55,6 @@ const isWrapperScrollMode = computed(() => themeStore.layout.scrollMode === 'wra
<SettingItem v-if="themeStore.header.breadcrumb.visible" key="4-1" :label="$t('theme.header.breadcrumb.showIcon')">
<ElSwitch v-model="themeStore.header.breadcrumb.showIcon" />
</SettingItem>
<SettingItem key="5" :label="$t('theme.tab.visible')">
<ElSwitch v-model="themeStore.tab.visible" />
</SettingItem>
<SettingItem v-if="themeStore.tab.visible" key="5-1" :label="$t('theme.tab.cache')">
<ElSwitch v-model="themeStore.tab.cache" />
</SettingItem>
<SettingItem v-if="themeStore.tab.visible" key="5-2" :label="$t('theme.tab.height')">
<ElInputNumber v-model="themeStore.tab.height" size="small" :step="1" class="w-120px" />
</SettingItem>
<SettingItem v-if="themeStore.tab.visible" key="5-3" :label="$t('theme.tab.mode.title')">
<ElSelect v-model="themeStore.tab.mode" size="small" class="w-120px">
<ElOption
v-for="{ label, value } in translateOptions(themeTabModeOptions)"
:key="value"
:label="label"
:value="value"
/>
</ElSelect>
</SettingItem>
<SettingItem v-if="layoutMode === 'vertical'" key="6-1" :label="$t('theme.sider.width')">
<ElInputNumber v-model="themeStore.sider.width" size="small" :step="1" class="w-120px" />
</SettingItem>