wip: use kingdee style header

This commit is contained in:
zzs 2025-03-13 17:35:20 +08:00
parent 0ed5dd9d06
commit 11908d5812
9 changed files with 498 additions and 17 deletions

View File

@ -1,3 +1,4 @@
import BasicMenu from './src/BasicMenu.vue'
import KingdeeBaseMenu from './src/KingdeeBasicMenu.vue'
export { BasicMenu }
export { BasicMenu, KingdeeBaseMenu }

View File

@ -0,0 +1,147 @@
<script lang="ts" setup>
import { computed, reactive, ref, toRefs, unref, watch } from 'vue'
import type { MenuProps } from 'ant-design-vue'
import { Menu } from 'ant-design-vue'
import type { RouteLocationNormalizedLoaded } from 'vue-router'
import { useRouter } from 'vue-router'
import type { MenuState } from './types'
import KingdeeBasicSubMenuItem from './components/KingdeeBasicSubMenuItem.vue'
import { useOpenKeys } from './useOpenKeys'
import { basicProps } from './props'
import { MenuModeEnum, MenuTypeEnum } from '@/enums/menuEnum'
import { isFunction } from '@/utils/is'
import { useMenuSetting } from '@/hooks/setting/useMenuSetting'
import { REDIRECT_NAME } from '@/router/constant'
import { useDesign } from '@/hooks/web/useDesign'
import { getCurrentParentPath } from '@/router/menus'
import { listenerRouteChange } from '@/logics/mitt/routeChange'
import { getAllParentPath } from '@/router/helper/menuHelper'
defineOptions({ name: 'BasicMenu' })
const props = defineProps(basicProps)
const emit = defineEmits(['menuClick'])
const isClickGo = ref(false)
const currentActiveMenu = ref('')
const menuState = reactive<MenuState>({
defaultSelectedKeys: [],
openKeys: [],
selectedKeys: [],
collapsedOpenKeys: [],
})
const { prefixCls } = useDesign('basic-menu')
const { items, mode, accordion } = toRefs(props)
const { getCollapsed, getTopMenuAlign, getSplit } = useMenuSetting()
const { currentRoute } = useRouter()
const { handleOpenChange, setOpenKeys, getOpenKeys } = useOpenKeys(menuState, items, mode as any, accordion)
const getIsTopMenu = computed(() => {
const { type, mode } = props
return (type === MenuTypeEnum.TOP_MENU && mode === MenuModeEnum.HORIZONTAL) || (props.isHorizontal && unref(getSplit))
})
const getMenuClass = computed(() => {
const align = props.isHorizontal && unref(getSplit) ? 'start' : unref(getTopMenuAlign)
return [
prefixCls,
`justify-${align}`,
{
[`${prefixCls}__second`]: !props.isHorizontal && unref(getSplit),
[`${prefixCls}__sidebar-hor`]: unref(getIsTopMenu),
},
]
})
const getInlineCollapseOptions = computed(() => {
const isInline = props.mode === MenuModeEnum.INLINE
const inlineCollapseOptions: { inlineCollapsed?: boolean } = {}
if (isInline)
inlineCollapseOptions.inlineCollapsed = props.mixSider ? false : unref(getCollapsed)
return inlineCollapseOptions
})
listenerRouteChange((route) => {
if (route.name === REDIRECT_NAME)
return
handleMenuChange(route)
currentActiveMenu.value = route.meta?.currentActiveMenu as string
if (unref(currentActiveMenu)) {
menuState.selectedKeys = [unref(currentActiveMenu)]
setOpenKeys(unref(currentActiveMenu))
}
})
!props.mixSider
&& watch(
() => props.items,
() => {
handleMenuChange()
},
)
const handleMenuClick: MenuProps['onClick'] = async ({ key }) => {
const { beforeClickFn } = props
if (beforeClickFn && isFunction(beforeClickFn)) {
const flag = await beforeClickFn(key)
if (!flag)
return
}
emit('menuClick', key)
isClickGo.value = true
menuState.selectedKeys = [key]
}
async function handleMenuChange(route?: RouteLocationNormalizedLoaded) {
if (unref(isClickGo)) {
isClickGo.value = false
return
}
const path = (route || unref(currentRoute)).meta?.currentActiveMenu || (route || unref(currentRoute)).path
setOpenKeys(path)
if (unref(currentActiveMenu))
return
if (props.isHorizontal && unref(getSplit)) {
const parentPath = await getCurrentParentPath(path)
menuState.selectedKeys = [parentPath]
}
else {
const parentPaths = await getAllParentPath(props.items, path)
menuState.selectedKeys = parentPaths
}
}
</script>
<template>
<Menu
:selected-keys="menuState.selectedKeys"
:default-selected-keys="menuState.defaultSelectedKeys"
:mode="mode"
:open-keys="getOpenKeys"
:inline-indent="inlineIndent"
:theme="theme"
:class="getMenuClass"
:sub-menu-open-delay="0.2"
v-bind="getInlineCollapseOptions"
@open-change="handleOpenChange"
@click="handleMenuClick"
>
<template v-for="item in items" :key="item.path">
<KingdeeBasicSubMenuItem :item="item" :theme="theme" :is-horizontal="isHorizontal" />
</template>
</Menu>
</template>
<style lang="less">
@import './index.less';
</style>

View File

@ -0,0 +1,56 @@
<script lang="ts" setup>
import { computed } from 'vue'
import { Menu } from 'ant-design-vue'
import { useRouter } from 'vue-router'
import { itemProps } from '../props'
import BasicMenuItem from './BasicMenuItem.vue'
import MenuItemContent from './MenuItemContent.vue'
import { useDesign } from '@/hooks/web/useDesign'
import type { Menu as MenuType } from '@/router/types'
defineOptions({ name: 'BasicSubMenuItem' })
const props = defineProps(itemProps)
const SubMenu = Menu.SubMenu
const { prefixCls } = useDesign('basic-menu-item')
const getShowMenu = computed(() => !props.item.meta?.hideMenu)
function menuHasChildren(menuTreeItem: MenuType): boolean {
return (
!menuTreeItem.meta?.hideChildrenInMenu
&& Reflect.has(menuTreeItem, 'children')
&& !!menuTreeItem.children
&& menuTreeItem.children.length > 0
)
}
const router = useRouter()
function handleSwitchCloud(item: MenuType) {
router.push(getFirstPathOfLastChild(item))
}
function getFirstPathOfLastChild(item: MenuType): string {
// path
if (!item.children || item.children.length === 0)
return item.path
// path
return getFirstPathOfLastChild(item.children[0])
}
</script>
<template>
<BasicMenuItem v-if="!menuHasChildren(item) && getShowMenu" v-bind="$props" :class="prefixCls" />
<SubMenu v-if="menuHasChildren(item) && getShowMenu" :key="`submenu-${item.path}`" :class="[theme]" popup-class-name="app-top-menu-popup">
<template #title>
<MenuItemContent v-bind="$props" :item="item" @click="handleSwitchCloud(item)" />
</template>
<!-- <template v-for="childrenItem in item.children || []" :key="childrenItem.path"> -->
<!-- <BasicSubMenuItem v-bind="$props" :item="childrenItem" /> -->
<!-- </template> -->
</SubMenu>
</template>

View File

@ -16,6 +16,10 @@ import { useMultipleTabStore } from '@/store/modules/multipleTab'
defineOptions({ name: 'LayoutMultipleHeader' })
defineProps<{
scoped: boolean
}>()
const HEADER_HEIGHT = 48
const TABS_HEIGHT = 32
@ -90,7 +94,7 @@ const getClass = computed(() => {
/>
<div :style="getWrapStyle" :class="getClass">
<LayoutHeader v-if="getShowInsetHeaderRef" />
<MultipleTabs v-if="getShowTabs" :key="tabStore.getLastDragEndIndex" />
<MultipleTabs v-if="getShowTabs" :key="tabStore.getLastDragEndIndex" :scoped="scoped" />
</div>
</template>

View File

@ -2,8 +2,9 @@
import { computed, unref } from 'vue'
import { Layout } from 'ant-design-vue'
import { ErrorAction, FullScreen, LayoutBreadcrumb, Notify, UserDropDown } from '../../components'
import { ErrorAction, FullScreen, Notify, UserDropDown } from '../../components'
import LayoutTrigger from '../../../trigger/index.vue'
import KingdeeTopMenu from './KingdeeTopMenu.vue'
import { propTypes } from '@/utils/propTypes'
// import { AppLocalePicker, AppLogo, AppSearch, AppSizePicker } from '@/components/Application'
@ -19,7 +20,7 @@ import { useHeaderSetting } from '@/hooks/setting/useHeaderSetting'
import { useMenuSetting } from '@/hooks/setting/useMenuSetting'
import { useRootSetting } from '@/hooks/setting/useRootSetting'
// import { MenuModeEnum, MenuSplitTyeEnum } from '@/enums/menuEnum'
import { MenuModeEnum, MenuSplitTyeEnum } from '@/enums/menuEnum'
import { SettingButtonPositionEnum } from '@/enums/appEnum'
import { useAppInject } from '@/hooks/web/useAppInject'
@ -41,7 +42,7 @@ const { getShowHeaderTrigger, getSplit, getIsMixMode, getMenuWidth, getIsMixSide
// const { getIsMixMode, getMenuWidth } = useMenuSetting()
const { getUseErrorHandle, getShowSettingButton, getSettingButtonPosition } = useRootSetting()
const { getHeaderTheme, getShowFullScreen, getShowNotice, getShowContent, getShowBread, getShowHeaderLogo, getShowHeader, getShowSearch }
const { getHeaderTheme, getShowFullScreen, getShowNotice, getShowContent, getShowHeaderLogo, getShowHeader, getShowSearch }
= useHeaderSetting()
const { getShowLocalePicker } = useLocale()
@ -80,10 +81,10 @@ const getLogoWidth = computed(() => {
return { width: `${width}px` }
})
// const getSplitType = computed(() => {
// return unref(getSplit) ? MenuSplitTyeEnum.TOP : MenuSplitTyeEnum.NONE
// })
//
const getSplitType = computed(() => {
return unref(getSplit) ? MenuSplitTyeEnum.TOP : MenuSplitTyeEnum.NONE
})
// const getMenuMode = computed(() => {
// return unref(getSplit) ? MenuModeEnum.HORIZONTAL : null
// })
@ -107,14 +108,14 @@ const getLogoWidth = computed(() => {
v-if="(getShowContent && getShowHeaderTrigger && !getSplit && !getIsMixSidebar) || getIsMobile"
:theme="getHeaderTheme" :sider="false"
/>
<LayoutBreadcrumb v-if="getShowContent && getShowBread" :theme="getHeaderTheme" />
<!-- <LayoutBreadcrumb v-if="getShowContent && getShowBread" :theme="getHeaderTheme" /> -->
</div>
<!-- left end -->
<!-- menu start -->
<!-- <div v-if="getShowTopMenu && !getIsMobile" :class="`${prefixCls}-menu`"> -->
<!-- <LayoutMenu :is-horizontal="true" :theme="getHeaderTheme" :split-type="getSplitType" :menu-mode="getMenuMode" /> -->
<!-- </div> -->
<div v-if="!getIsMobile" :class="`${prefixCls}-menu`">
<KingdeeTopMenu :is-horizontal="true" :theme="getHeaderTheme" :split-type="getSplitType" :menu-mode="MenuModeEnum.HORIZONTAL" />
</div>
<!-- menu-end -->
<!-- action -->

View File

@ -0,0 +1,248 @@
<script lang="tsx">
import type { CSSProperties } from 'vue'
import { computed, defineComponent, toRef, unref } from 'vue'
import { useSplitMenu } from '@/layouts/default/menu/useLayoutMenu'
import { KingdeeBaseMenu } from '@/components/Menu'
import { SimpleMenu } from '@/components/SimpleMenu'
import { AppLogo } from '@/components/Application'
import { MenuModeEnum, MenuSplitTyeEnum } from '@/enums/menuEnum'
import { useMenuSetting } from '@/hooks/setting/useMenuSetting'
import { ScrollContainer } from '@/components/Container'
import { useGo } from '@/hooks/web/usePage'
import { openWindow } from '@/utils'
import { propTypes } from '@/utils/propTypes'
import { isHttpUrl } from '@/utils/is'
import { useRootSetting } from '@/hooks/setting/useRootSetting'
import { useAppInject } from '@/hooks/web/useAppInject'
import { useDesign } from '@/hooks/web/useDesign'
import type { Menu } from '@/router/types'
import { useMultipleTabStore } from '@/store/modules/multipleTab'
export default defineComponent({
name: 'KingdeeTopMenu',
props: {
theme: propTypes.oneOf(['light', 'dark']),
splitType: {
type: Number as PropType<MenuSplitTyeEnum>,
default: MenuSplitTyeEnum.NONE,
},
isHorizontal: propTypes.bool,
// menu Mode
menuMode: {
type: [String] as PropType<Nullable<MenuModeEnum>>,
default: '',
},
isScoped: {
type: Boolean,
default: false,
},
},
setup(props) {
const go = useGo()
const {
getMenuMode,
getMenuType,
getMenuTheme,
getCollapsed,
getCollapsedShowTitle,
getAccordion,
getIsHorizontal,
getIsSidebarType,
getSplit,
} = useMenuSetting()
const { getShowLogo } = useRootSetting()
const { prefixCls } = useDesign('layout-menu')
const { menusRef } = useSplitMenu(toRef(props, 'splitType'))
const { getIsMobile } = useAppInject()
const getComputedMenuMode = computed(() => (unref(getIsMobile) ? MenuModeEnum.INLINE : props.menuMode || unref(getMenuMode)))
const getComputedMenuTheme = computed(() => props.theme || unref(getMenuTheme))
const getIsShowLogo = computed(() => unref(getShowLogo) && unref(getIsSidebarType))
const getUseScroll = computed(() => {
return (
!unref(getIsHorizontal)
&& (unref(getIsSidebarType) || props.splitType === MenuSplitTyeEnum.LEFT || props.splitType === MenuSplitTyeEnum.NONE)
)
})
const getWrapperStyle = computed((): CSSProperties => {
return {
height: `calc(100% - ${unref(getIsShowLogo) ? '48px' : '0px'})`,
}
})
const getLogoClass = computed(() => {
return [
`${prefixCls}-logo`,
unref(getComputedMenuTheme),
{
[`${prefixCls}--mobile`]: unref(getIsMobile),
},
]
})
const tabStore = useMultipleTabStore()
const getTabsState = computed(() => {
return tabStore.getTabList.filter(item => !item.meta?.hideTab)
})
/**
* 只显示已经打开的一级菜单
* @param menus
* @param openedFullPath
*/
function filterMenus(menus: Menu[], openedFullPath: string[]): Menu[] {
return menus
.map((menu) => {
// openedFullPath
if (openedFullPath.includes(menu.path))
return menu
//
if (menu.children && menu.children.length > 0) {
const filteredChildren = filterMenus(menu.children, openedFullPath)
//
if (filteredChildren.length > 0) {
return {
...menu,
children: filteredChildren,
}
}
}
// openedFullPath
return null
})
.filter(menu => menu !== null) as Menu[]
}
function getFirstPathSegment(path: string): string {
const trimmedPath = path.replace(/^\/+/, '/')
const segments = trimmedPath.split('/')
const firstSegment = segments.find(segment => segment !== '')
return firstSegment ? `/${firstSegment}` : '/'
}
const getCommonProps = computed(() => {
const openedPath: string[] = []
const openedFullPath: string[] = []
const tabs = unref(getTabsState)
for (let i = 0; i < tabs.length; i++) {
openedPath.push(getFirstPathSegment(tabs[i].path))
openedFullPath.push(tabs[i].path)
}
const filterMenu = filterMenus(unref(menusRef), openedFullPath)
const menus = unref(menusRef)
return {
menus: filterMenu,
beforeClickFn: beforeMenuClickFn,
items: menus,
theme: unref(getComputedMenuTheme),
accordion: unref(getAccordion),
collapse: unref(getCollapsed),
collapsedShowTitle: unref(getCollapsedShowTitle),
onMenuClick: handleMenuClick,
}
})
/**
* click menu
* @param path
*/
function handleMenuClick(path: string) {
go(path)
}
/**
* before click menu
* @param path
*/
async function beforeMenuClickFn(path: string) {
if (!isHttpUrl(path))
return true
openWindow(path)
return false
}
function renderHeader() {
if (!unref(getIsShowLogo) && !unref(getIsMobile))
return null
return <AppLogo showTitle={!unref(getCollapsed)} class={unref(getLogoClass)} theme={unref(getComputedMenuTheme)} />
}
function renderMenu() {
const { menus, ...menuProps } = unref(getCommonProps)
if (!menus || !menus.length)
return null
return !props.isHorizontal
? (
<SimpleMenu {...menuProps} isSplitMenu={unref(getSplit)} items={menus} />
)
: (
<KingdeeBaseMenu
{...(menuProps as any)}
isHorizontal={props.isHorizontal}
type={unref(getMenuType)}
showLogo={unref(getIsShowLogo)}
mode={unref(getComputedMenuMode as any)}
items={menus}
/>
)
}
return () => {
return (
<>
{renderHeader()}
{unref(getUseScroll) ? <ScrollContainer style={unref(getWrapperStyle)}>{() => renderMenu()}</ScrollContainer> : renderMenu()}
</>
)
}
},
})
</script>
<style lang="less">
@prefix-cls: ~'@{namespace}-kd-layout-menu';
@logo-prefix-cls: ~'@{namespace}-kd-app-logo';
.@{prefix-cls} {
&-logo {
height: @header-height;
padding: 10px 4px 10px 10px;
img {
width: @logo-width;
height: @logo-width;
}
}
&--mobile {
.@{logo-prefix-cls} {
&__title {
opacity: 1;
}
}
}
}
</style>

View File

@ -52,7 +52,7 @@ const layoutClass = computed(() => {
<Layout :class="[layoutClass, `${prefixCls}-out`]">
<LayoutSideBar v-if="(getShowSidebar || getIsMobile)" />
<Layout :class="`${prefixCls}-main`">
<LayoutMultipleHeader />
<LayoutMultipleHeader :scoped="true" />
<LayoutContent />
<LayoutFooter />
</Layout>

View File

@ -123,8 +123,9 @@ export default defineComponent({
const getCommonProps = computed(() => {
const current = getFirstPathSegment(currentPath.path)
const menus = findChildrenByFirstPathSegment(unref(menusRef), current)
const scoped = props.isScoped
return {
menus,
menus: scoped ? menus : unref(menusRef),
beforeClickFn: beforeMenuClickFn,
items: menus,
theme: unref(getComputedMenuTheme),

View File

@ -6,7 +6,7 @@ import { useMouse } from '@vueuse/core'
import { computed, ref, unref } from 'vue'
import { Tabs } from 'ant-design-vue'
import { useRouter } from 'vue-router'
import { useRoute, useRouter } from 'vue-router'
import TabContent from './components/TabContent.vue'
import FoldButton from './components/FoldButton.vue'
import TabRedo from './components/TabRedo.vue'
@ -26,6 +26,10 @@ import { listenerRouteChange } from '@/logics/mitt/routeChange'
defineOptions({ name: 'MultipleTabs' })
const props = defineProps<{
scoped: boolean
}>()
const affixTextList = initAffixTabs()
const activeKeyRef = ref('')
@ -33,13 +37,32 @@ useTabsDrag(affixTextList)
const tabStore = useMultipleTabStore()
const userStore = useUserStore()
const router = useRouter()
const route = useRoute()
const { prefixCls } = useDesign('multiple-tabs')
const go = useGo()
const { getShowQuick, getShowRedo, getShowFold } = useMultipleTabSetting()
function getFirstPathSegment(path: string): string {
if (!path || typeof path !== 'string')
return '/'
const segments = path.split('/').filter(Boolean) //
return segments.length > 0 ? `/${segments[0]}` : '/'
}
const getTabsState = computed(() => {
return tabStore.getTabList.filter(item => !item.meta?.hideTab)
const currentPath = route.path
const currentCloud = getFirstPathSegment(currentPath)
if (props.scoped) {
return tabStore.getTabList.filter(item => !item.meta?.hideTab).filter((item) => {
const firstPath = getFirstPathSegment(item.path)
return firstPath === currentCloud
})
}
else { return tabStore.getTabList.filter(item => !item.meta?.hideTab) }
})
const unClose = computed(() => unref(getTabsState).length === 1)