提交
This commit is contained in:
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-14
@@ -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>
|
||||
+5
-1
@@ -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',
|
||||
|
||||
Reference in new issue
Block a user