This commit is contained in:
zlwu committed 2026-06-21 09:19:07 +08:00
1 parent c3be9d1dce
commit 299609706c
328 files changed
+144899 -13873

No files matched your search

@@ -353,7 +353,7 @@
i {
padding-left: 0;
font-size: 14px;
color: #999;
color: #1e293b;
}
.el-icon-circle-check {
@@ -10,19 +10,19 @@
<div
:class="
isClickable(item, index)
? 'c-p py-1 rounded tad-200 hover:bg-active-color hover:[&_span]:text-g-600'
? 'c-p py-1 rounded tad-200 hover:bg-active-color hover:[&_span]:text-[var(--mint-text)]'
: ''
"
@click="handleBreadcrumbClick(item, index)"
>
<span
class="block max-w-46 overflow-hidden text-ellipsis whitespace-nowrap px-1.5 text-sm text-g-600 dark:text-g-800"
class="block max-w-46 overflow-hidden text-ellipsis whitespace-nowrap px-1.5 text-sm text-[var(--mint-text)]"
>{{ formatMenuTitle(item.meta?.title as string) }}</span
>
</div>
<div
v-if="!isLastItem(index) && item.meta?.title"
class="mx-1 text-sm not-italic text-g-500"
class="mx-1 text-sm not-italic text-[var(--mint-text)]"
aria-hidden="true"
>
/
@@ -2,8 +2,8 @@
<template>
<div
class="layout-sidebar"
v-if="showLeftMenu || isDualMenu"
:class="{ 'no-border': menuList.length === 0 }"
:class="{ 'no-border': menuList.length === 0, 'layout-sidebar-portal': isPortalMenu }"
v-if="showLeftMenu || isDualMenu || isPortalMenu"
>
<!-- 双列菜单(左侧) -->
<div
@@ -36,16 +36,8 @@
}"
>
<ArtSvgIcon
v-if="menu.meta.icon"
class="menu-icon text-g-700 dark:text-g-800"
:icon="menu.meta.icon"
:style="{
marginBottom: dualMenuShowText ? '5px' : '0'
}"
/>
<div
v-else
class="dual-menu-icon-placeholder"
:icon="resolveSidebarMenuIcon(menu, Boolean(menu.children?.length))"
:style="{
marginBottom: dualMenuShowText ? '5px' : '0'
}"
@@ -70,8 +62,9 @@
:class="`menu-left-${getMenuTheme.theme} menu-left-${!menuOpen ? 'close' : 'open'}`"
:style="{ background: getMenuTheme.background }"
>
<!-- Logo、系统名称 -->
<!-- Logo、系统名称(门户布局顶栏已展示,侧栏隐藏) -->
<div
v-if="!isPortalMenu"
class="header"
@click="navigateToHome"
:style="{
@@ -109,6 +102,7 @@
<SidebarSubmenu
:list="menuList"
:isMobile="isMobileMode"
:level="isPortalMenu ? 1 : 0"
:theme="getMenuTheme"
@close="handleMenuClose"
/>
@@ -141,6 +135,7 @@
import { isIframe } from '@/utils/navigation'
import { handleMenuJump } from '@/utils/navigation'
import { filterSidebarRoutes } from '@/utils/router/sidebar-menu-filter'
import { resolveSidebarMenuIcon } from '@/utils/router/menu-icon'
import SidebarSubmenu from './widget/SidebarSubmenu.vue'
import { useCommon } from '@/hooks/core/useCommon'
import { useWindowSize, useTimeoutFn } from '@vueuse/core'
@@ -183,8 +178,12 @@
// 菜单类型判断
const isTopLeftMenu = computed(() => menuType.value === MenuTypeEnum.TOP_LEFT)
const isPortalMenu = computed(() => menuType.value === MenuTypeEnum.PORTAL)
const showLeftMenu = computed(
() => menuType.value === MenuTypeEnum.LEFT || menuType.value === MenuTypeEnum.TOP_LEFT
() =>
menuType.value === MenuTypeEnum.LEFT ||
menuType.value === MenuTypeEnum.TOP_LEFT ||
menuType.value === MenuTypeEnum.PORTAL
)
const isDualMenu = computed(() => menuType.value === MenuTypeEnum.DUAL_MENU)
@@ -204,8 +203,8 @@
const menuStore = useMenuStore()
const allMenus = menuStore.menuList
// 如果不是顶部左侧菜单或双列菜单,直接返回完整菜单列表
if (!isTopLeftMenu.value && !isDualMenu.value) {
// 顶部混合菜单、门户布局或双列菜单:仅展示当前一级模块下的子菜单
if (!isTopLeftMenu.value && !isDualMenu.value && !isPortalMenu.value) {
return allMenus
}
@@ -284,6 +283,14 @@
// 双列菜单收起时的滚动条样式
const scrollbarStyle = computed(() => {
if (isPortalMenu.value) {
return {
height: '100%',
transform: 'translateY(0)',
transition: 'transform 0.3s ease'
}
}
const isCollapsed = isDualMenu.value && !menuOpen.value
return {
transform: isCollapsed ? 'translateY(-50px)' : 'translateY(0)',
@@ -377,6 +384,12 @@
* 监听窗口尺寸变化,自动处理移动端菜单
*/
watch(width, (newWidth) => {
if (isPortalMenu.value) {
settingStore.setMenuOpen(true)
showMobileModal.value = false
return
}
if (newWidth < MOBILE_BREAKPOINT) {
settingStore.setMenuOpen(false)
if (!menuOpen.value) {
@@ -1,4 +1,4 @@
.layout-sidebar {
.layout-sidebar {
display: flex;
height: 100vh;
user-select: none;
@@ -6,6 +6,10 @@
border-right: none;
background: var(--mint-surface, #fff);
&.layout-sidebar-portal {
height: 100%;
}
&.no-border {
border-right: none !important;
}
@@ -158,8 +158,8 @@ $popup-menu-radius: 6px;
flex-shrink: 0;
font-size: $menu-font-size !important;
font-weight: 500;
// Fresh Mint:默认菜单项为中性色(demo `.nav a`)
color: var(--mint-text-muted, #64748b);
// Fresh Mint:菜单文字使用主文本色
color: var(--mint-text, #1e293b);
text-rendering: geometricprecision;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
@@ -170,7 +170,7 @@ $popup-menu-radius: 6px;
.el-sub-menu > .el-sub-menu__title {
font-size: 12px !important;
font-weight: 700;
color: var(--mint-text-muted, #64748b) !important;
color: var(--mint-text, #1e293b) !important;
letter-spacing: 0.02em;
}
@@ -223,7 +223,7 @@ $popup-menu-radius: 6px;
}
.el-sub-menu__icon-arrow {
color: var(--mint-text-muted, #64748b);
color: var(--mint-text);
}
}
@@ -265,7 +265,7 @@ $popup-menu-radius: 6px;
@include menu-hover($hover-bg-color);
.el-sub-menu__icon-arrow {
color: var(--mint-text-muted, #64748b);
color: var(--mint-text);
}
}
}
@@ -2,17 +2,12 @@
<template v-for="(item, index) in filteredMenuItems" :key="getUniqueKey(item, index)">
<ElSubMenu v-if="hasChildren(item)" :index="item.path || item.meta.title" :level="level">
<template #title>
<div
v-if="level === 0 && (item.meta.icon || item.meta.virtualMenuIcon)"
class="menu-icon flex-cc"
>
<div class="menu-icon flex-cc">
<ArtSvgIcon
v-if="item.meta.icon"
:icon="item.meta.icon"
:icon="resolveSidebarMenuIcon(item, true)"
:color="theme?.iconColor"
:style="{ color: theme.iconColor }"
/>
<span v-else class="menu-icon__virtual" aria-hidden="true" />
</div>
<span class="menu-name">
{{ formatMenuTitle(item.meta.title) }}
@@ -35,17 +30,12 @@
:level-item="level + 1"
@click="goPage(item)"
>
<div
v-if="level === 0 && (item.meta.icon || item.meta.virtualMenuIcon)"
class="menu-icon flex-cc"
>
<div class="menu-icon flex-cc">
<ArtSvgIcon
v-if="item.meta.icon"
:icon="item.meta.icon"
:icon="resolveSidebarMenuIcon(item, false)"
:color="theme?.iconColor"
:style="{ color: theme.iconColor }"
/>
<span v-else class="menu-icon__virtual" aria-hidden="true" />
</div>
<div
v-show="item.meta.showBadge && level === 0 && !menuOpen"
@@ -71,6 +61,7 @@
import type { AppRouteRecord } from '@/types/router'
import { formatMenuTitle } from '@/utils/router'
import { filterSidebarRoutes } from '@/utils/router/sidebar-menu-filter'
import { resolveSidebarMenuIcon } from '@/utils/router/menu-icon'
import { handleMenuJump } from '@/utils/navigation'
import { useSettingStore } from '@/store/modules/setting'
@@ -0,0 +1,133 @@
<!-- 一体化门户顶栏 -->
<template>
<header class="portal-header">
<div class="portal-header-brand" @click="toHome">
<ArtLogo class="portal-header-logo" />
<p class="portal-header-title">{{ AppConfig.systemInfo.name }}</p>
</div>
<ArtPortalTopNav class="portal-header-nav" />
<div class="portal-header-actions">
<ArtIconButton
v-if="shouldShowFullscreen"
icon="fullscreen"
:class="[!isFullscreen ? 'full-screen-btn' : 'exit-full-screen-btn']"
class="max-md:!hidden"
@click="toggleFullScreen"
/>
<ArtUserMenu />
</div>
</header>
</template>
<script setup lang="ts">
import { useRouter } from 'vue-router'
import { useFullscreen } from '@vueuse/core'
import AppConfig from '@/config'
import { useCommon } from '@/hooks/core/useCommon'
import { useHeaderBar } from '@/hooks/core/useHeaderBar'
import ArtUserMenu from '../art-header-bar/widget/ArtUserMenu.vue'
defineOptions({ name: 'ArtPortalHeader' })
const router = useRouter()
const { homePath } = useCommon()
const { shouldShowFullscreen } = useHeaderBar()
const { isFullscreen, toggle: toggleFullscreen } = useFullscreen()
const toHome = (): void => {
router.push(homePath.value)
}
const toggleFullScreen = (): void => {
toggleFullscreen()
}
</script>
<style lang="scss" scoped>
.portal-header {
box-sizing: border-box;
display: flex;
gap: 16px;
align-items: center;
width: 100%;
height: 72px;
padding: 0 20px;
user-select: none;
background: var(--default-bg-color, #fff);
border-bottom: 1px solid var(--art-card-border, #d9e8e2);
}
.portal-header-brand {
display: flex;
flex-shrink: 0;
gap: 10px;
align-items: center;
cursor: pointer;
}
.portal-header-logo {
flex-shrink: 0;
}
.portal-header-title {
margin: 0;
font-size: 18px;
font-weight: 600;
line-height: 1.2;
color: var(--mint-text, #1e293b);
white-space: nowrap;
}
.portal-header-nav {
flex: 1;
min-width: 0;
margin-left: 24px;
}
.portal-header-actions {
display: flex;
flex-shrink: 0;
gap: 8px;
align-items: center;
}
</style>
<style lang="scss" scoped>
.full-screen-btn:hover :deep(.art-svg-icon) {
animation: portal-expand 0.6s forwards;
}
.exit-full-screen-btn:hover :deep(.art-svg-icon) {
animation: portal-shrink 0.6s forwards;
}
@keyframes portal-expand {
0% {
transform: scale(1);
}
50% {
transform: scale(1.1);
}
100% {
transform: scale(1);
}
}
@keyframes portal-shrink {
0% {
transform: scale(1);
}
50% {
transform: scale(0.9);
}
100% {
transform: scale(1);
}
}
</style>
@@ -0,0 +1,155 @@
<!-- 门户顶栏一级模块卡片 -->
<template>
<div class="portal-top-nav">
<ElScrollbar v-if="moduleList.length" wrap-class="portal-top-nav-scroll">
<div class="portal-top-nav-list">
<button
v-for="item in moduleList"
:key="item.path"
type="button"
class="portal-top-nav-card"
:class="{ 'is-active': item.isActive }"
@click="handleModuleClick(item)"
>
<ArtSvgIcon
v-if="item.meta.icon"
:icon="item.meta.icon"
class="portal-top-nav-card__icon"
/>
<span class="portal-top-nav-card__label">{{ item.formattedTitle }}</span>
<div v-if="item.meta.showBadge" class="art-badge art-badge-mixed" />
</button>
</div>
</ElScrollbar>
<p v-else class="portal-top-nav-empty">暂无可用模块</p>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { storeToRefs } from 'pinia'
import { formatMenuTitle } from '@/utils/router'
import { useMenuStore } from '@/store/modules/menu'
import {
filterPortalTopModules,
getFirstAccessiblePath,
isPortalModuleActive
} from '@/utils/router/portal-menu'
import type { AppRouteRecord } from '@/types/router'
defineOptions({ name: 'ArtPortalTopNav' })
interface PortalModuleItem extends AppRouteRecord {
isActive: boolean
formattedTitle: string
}
const route = useRoute()
const router = useRouter()
const menuStore = useMenuStore()
const { menuList } = storeToRefs(menuStore)
const currentRoutePath = computed(() =>
String(route.meta.activePath || route.path)
)
const moduleList = computed<PortalModuleItem[]>(() => {
return filterPortalTopModules(menuList.value).map((item) => ({
...item,
isActive: isPortalModuleActive(item, currentRoutePath.value),
formattedTitle: formatMenuTitle(item.meta.title)
}))
})
const handleModuleClick = (item: PortalModuleItem): void => {
if (item.isActive) {
return
}
const target = getFirstAccessiblePath(item)
if (target && target !== route.path) {
router.push(target)
}
}
</script>
<style lang="scss" scoped>
.portal-top-nav {
flex: 1;
min-width: 0;
}
.portal-top-nav-empty {
margin: 0;
font-size: 13px;
line-height: 1.5;
color: var(--mint-text-muted, #64748b);
}
:deep(.portal-top-nav-scroll) {
flex: 1;
min-width: 0;
}
.portal-top-nav-list {
display: flex;
flex-wrap: nowrap;
gap: 10px;
align-items: center;
min-height: 56px;
padding: 4px 0;
}
.portal-top-nav-card {
position: relative;
display: inline-flex;
flex-direction: column;
flex-shrink: 0;
gap: 4px;
align-items: center;
justify-content: center;
min-width: 72px;
padding: 8px 12px;
cursor: pointer;
background: transparent;
border: none;
border-radius: 8px;
transition:
background-color 0.2s ease,
color 0.2s ease;
&__icon {
font-size: 22px;
color: var(--mint-text-muted, #64748b);
}
&__label {
max-width: 88px;
overflow: hidden;
text-overflow: ellipsis;
font-size: 12px;
line-height: 1.3;
color: var(--mint-text, #1e293b);
white-space: nowrap;
}
&:hover:not(.is-active) {
background: var(--primary-bg, #ecfdf5);
.portal-top-nav-card__icon,
.portal-top-nav-card__label {
color: var(--primary-dark, #059669);
}
}
&.is-active {
background: var(--primary, #10b981);
.portal-top-nav-card__icon,
.portal-top-nav-card__label {
color: #fff;
}
}
}
</style>
@@ -19,7 +19,11 @@ export function useSettingsState() {
// 菜单布局切换
const switchMenuLayouts = (type: MenuTypeEnum) => {
if (type === MenuTypeEnum.LEFT || type === MenuTypeEnum.TOP_LEFT) {
if (
type === MenuTypeEnum.LEFT ||
type === MenuTypeEnum.TOP_LEFT ||
type === MenuTypeEnum.PORTAL
) {
settingStore.setMenuOpen(true)
}
settingStore.switchMenuLayouts(type)
@@ -34,7 +34,8 @@
left: 'MenuTypeEnum.LEFT',
top: 'MenuTypeEnum.TOP',
'top-left': 'MenuTypeEnum.TOP_LEFT',
'dual-menu': 'MenuTypeEnum.DUAL_MENU'
'dual-menu': 'MenuTypeEnum.DUAL_MENU',
portal: 'MenuTypeEnum.PORTAL'
},
systemTheme: {
auto: 'SystemThemeEnum.AUTO',