@quiteer/naive-extra 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +86 -0
- package/dist/components/breadcrumb/index.d.ts +0 -0
- package/dist/components/breadcrumb/index.vue.d.ts +3 -0
- package/dist/components/breadcrumb/props.d.ts +0 -0
- package/dist/components/button/action/index.d.ts +3 -0
- package/dist/components/button/action/index.vue.d.ts +118 -0
- package/dist/components/button/action/props.d.ts +63 -0
- package/dist/components/button/action/utils.d.ts +8 -0
- package/dist/components/button/base/index.d.ts +3 -0
- package/dist/components/button/base/index.vue.d.ts +36 -0
- package/dist/components/button/base/props.d.ts +27 -0
- package/dist/components/button/index.d.ts +4 -0
- package/dist/components/button/types.d.ts +2 -0
- package/dist/components/form/helper.d.ts +11 -0
- package/dist/components/form/index.d.ts +3 -0
- package/dist/components/form/index.vue.d.ts +642 -0
- package/dist/components/form/props.d.ts +34 -0
- package/dist/components/icon/IconPicker.vue.d.ts +13 -0
- package/dist/components/icon/iconify.d.ts +25 -0
- package/dist/components/icon/index.d.ts +3 -0
- package/dist/components/icon/index.vue.d.ts +12 -0
- package/dist/components/layout/const.d.ts +22 -0
- package/dist/components/layout/context.d.ts +77 -0
- package/dist/components/layout/index.d.ts +5 -0
- package/dist/components/layout/index.vue.d.ts +80 -0
- package/dist/components/layout/layout-parts/AppBreadcrumb.vue.d.ts +3 -0
- package/dist/components/layout/layout-parts/AppFooter.vue.d.ts +18 -0
- package/dist/components/layout/layout-parts/AppHeader.vue.d.ts +18 -0
- package/dist/components/layout/layout-parts/AppLeftLogoInfo.vue.d.ts +3 -0
- package/dist/components/layout/layout-parts/AppMain.vue.d.ts +18 -0
- package/dist/components/layout/layout-parts/AppSidebar.vue.d.ts +4067 -0
- package/dist/components/layout/layout-parts/LayoutTransition.vue.d.ts +58 -0
- package/dist/components/layout/mode.d.ts +0 -0
- package/dist/components/layout/props.d.ts +35 -0
- package/dist/components/layout/types.d.ts +59 -0
- package/dist/components/layout/utils.d.ts +97 -0
- package/dist/components/provider/index.d.ts +3 -0
- package/dist/components/provider/index.vue.d.ts +19 -0
- package/dist/components/provider/props.d.ts +33 -0
- package/dist/components/search-bar/index.d.ts +3 -0
- package/dist/components/search-bar/index.vue.d.ts +1288 -0
- package/dist/components/search-bar/props.d.ts +15 -0
- package/dist/components/table/TableSetting.vue.d.ts +15 -0
- package/dist/components/table/index.d.ts +4 -0
- package/dist/components/table/index.vue.d.ts +17246 -0
- package/dist/components/table/props.d.ts +26 -0
- package/dist/components/table/useColumn.d.ts +15 -0
- package/dist/components/upload/enum.d.ts +18 -0
- package/dist/components/upload/index.d.ts +4 -0
- package/dist/components/upload/index.vue.d.ts +17 -0
- package/dist/components/upload/props.d.ts +7 -0
- package/dist/const/defaults.d.ts +7 -0
- package/dist/const/index.d.ts +2 -0
- package/dist/const/types.d.ts +134 -0
- package/dist/context/color.d.ts +13 -0
- package/dist/context/common.d.ts +117 -0
- package/dist/context/index.d.ts +41 -0
- package/dist/context/layout.d.ts +52 -0
- package/dist/context/loading-bar.d.ts +14 -0
- package/dist/context/locale.d.ts +143 -0
- package/dist/context/menu.d.ts +212 -0
- package/dist/context/message.d.ts +14 -0
- package/dist/context/notification.d.ts +14 -0
- package/dist/context/table.d.ts +917 -0
- package/dist/context/theme.d.ts +20 -0
- package/dist/hooks/index.d.ts +6 -0
- package/dist/hooks/useAdmin.d.ts +0 -0
- package/dist/hooks/useForm.d.ts +54 -0
- package/dist/hooks/useLayout.d.ts +116 -0
- package/dist/hooks/useProviderContext.d.ts +17 -0
- package/dist/hooks/useTable.d.ts +66 -0
- package/dist/hooks/useThemeOverrides.d.ts +8 -0
- package/dist/hooks/useUpload.d.ts +22 -0
- package/dist/index.css +36 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +6771 -0
- package/dist/share/compact.d.ts +16 -0
- package/dist/share/index.d.ts +2 -0
- package/dist/share/menu.d.ts +0 -0
- package/dist/share/route.d.ts +0 -0
- package/dist/share/slot.d.ts +6 -0
- package/dist/utils/form.d.ts +0 -0
- package/dist/utils/index.d.ts +0 -0
- package/dist/utils/transformRoutes.d.ts +67 -0
- package/dist/utils/tree.d.ts +6 -0
- package/package.json +53 -0
- package/src/auto-imports.d.ts +73 -0
- package/src/components/breadcrumb/index.ts +0 -0
- package/src/components/breadcrumb/index.vue +0 -0
- package/src/components/breadcrumb/props.ts +0 -0
- package/src/components/button/action/index.ts +4 -0
- package/src/components/button/action/index.vue +313 -0
- package/src/components/button/action/props.ts +78 -0
- package/src/components/button/action/utils.ts +122 -0
- package/src/components/button/base/index.ts +4 -0
- package/src/components/button/base/index.vue +156 -0
- package/src/components/button/base/props.ts +29 -0
- package/src/components/button/index.ts +4 -0
- package/src/components/button/types.ts +2 -0
- package/src/components/form/helper.ts +73 -0
- package/src/components/form/index.ts +5 -0
- package/src/components/form/index.vue +243 -0
- package/src/components/form/props.ts +75 -0
- package/src/components/icon/IconPicker.vue +255 -0
- package/src/components/icon/iconify.ts +80 -0
- package/src/components/icon/index.ts +7 -0
- package/src/components/icon/index.vue +23 -0
- package/src/components/layout/const.ts +102 -0
- package/src/components/layout/context.ts +189 -0
- package/src/components/layout/index.ts +8 -0
- package/src/components/layout/index.vue +64 -0
- package/src/components/layout/layout-parts/AppBreadcrumb.vue +108 -0
- package/src/components/layout/layout-parts/AppFooter.vue +26 -0
- package/src/components/layout/layout-parts/AppHeader.vue +112 -0
- package/src/components/layout/layout-parts/AppLeftLogoInfo.vue +30 -0
- package/src/components/layout/layout-parts/AppMain.vue +34 -0
- package/src/components/layout/layout-parts/AppSidebar.vue +174 -0
- package/src/components/layout/layout-parts/LayoutTransition.vue +366 -0
- package/src/components/layout/mode.ts +0 -0
- package/src/components/layout/props.ts +36 -0
- package/src/components/layout/types.ts +79 -0
- package/src/components/layout/utils.ts +201 -0
- package/src/components/provider/index.ts +5 -0
- package/src/components/provider/index.vue +69 -0
- package/src/components/provider/props.ts +45 -0
- package/src/components/search-bar/index.ts +5 -0
- package/src/components/search-bar/index.vue +282 -0
- package/src/components/search-bar/props.ts +26 -0
- package/src/components/table/TableSetting.vue +253 -0
- package/src/components/table/index.ts +14 -0
- package/src/components/table/index.vue +179 -0
- package/src/components/table/props.ts +29 -0
- package/src/components/table/useColumn.ts +104 -0
- package/src/components/upload/enum.ts +21 -0
- package/src/components/upload/index.ts +9 -0
- package/src/components/upload/index.vue +267 -0
- package/src/components/upload/props.ts +8 -0
- package/src/components.d.ts +154 -0
- package/src/const/defaults.ts +94 -0
- package/src/const/index.ts +2 -0
- package/src/const/types.ts +139 -0
- package/src/context/color.ts +53 -0
- package/src/context/common.ts +27 -0
- package/src/context/index.ts +141 -0
- package/src/context/layout.ts +34 -0
- package/src/context/loading-bar.ts +26 -0
- package/src/context/locale.ts +22 -0
- package/src/context/menu.ts +26 -0
- package/src/context/message.ts +30 -0
- package/src/context/notification.ts +29 -0
- package/src/context/table.ts +32 -0
- package/src/context/theme.ts +35 -0
- package/src/hooks/index.ts +6 -0
- package/src/hooks/useAdmin.ts +0 -0
- package/src/hooks/useForm.ts +272 -0
- package/src/hooks/useLayout.ts +300 -0
- package/src/hooks/useProviderContext.ts +47 -0
- package/src/hooks/useTable.ts +241 -0
- package/src/hooks/useThemeOverrides.ts +18 -0
- package/src/hooks/useUpload.ts +82 -0
- package/src/index.ts +59 -0
- package/src/share/compact.ts +35 -0
- package/src/share/index.ts +2 -0
- package/src/share/menu.ts +0 -0
- package/src/share/route.ts +0 -0
- package/src/share/slot.ts +29 -0
- package/src/utils/form.ts +0 -0
- package/src/utils/index.ts +0 -0
- package/src/utils/transformRoutes.ts +163 -0
- package/src/utils/tree.ts +31 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { MenuInst, MenuOption } from 'naive-ui'
|
|
3
|
+
// 解决 TS2742: The inferred type cannot be named without a reference to ...
|
|
4
|
+
// 显式引用这些类型以确保生成的声明文件是可移植的
|
|
5
|
+
import type {} from 'treemate'
|
|
6
|
+
import type { Slots } from 'vue'
|
|
7
|
+
import type {} from 'vueuc'
|
|
8
|
+
import { computed, ref, unref, useSlots, watch } from 'vue'
|
|
9
|
+
import { useRouter } from 'vue-router'
|
|
10
|
+
import { hasSlotContent } from '../../../share/slot'
|
|
11
|
+
import { SIDE_GROUP_LAYOUT_TYPES, SIDE_MIXED_LAYOUT_TYPES } from '../const'
|
|
12
|
+
import { useContext } from '../context'
|
|
13
|
+
|
|
14
|
+
import { findNodeByKey, renderMenuLabel, resolveMainSubFromActive, updateMenuTree } from '../utils'
|
|
15
|
+
import AppLeftLogoInfo from './AppLeftLogoInfo.vue'
|
|
16
|
+
|
|
17
|
+
const { isCollapsed, collapsedWidth, siderWidth, headerHeight, bordered, inverted, isLeftMain, isTopMain, activeKey, mainActiveKey, subActiveKey, type, menuOptions: options, mainMenuOptions, subMenuOptions, updateActiveKey, updateIsCollapsed, accordion, hasSiderLayout } = useContext()!
|
|
18
|
+
|
|
19
|
+
const menuInstRef = ref<MenuInst | null>(null)
|
|
20
|
+
|
|
21
|
+
const width = computed<number>(() => {
|
|
22
|
+
if (unref(isCollapsed))
|
|
23
|
+
return unref(collapsedWidth)!
|
|
24
|
+
|
|
25
|
+
return unref(siderWidth)!
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
function handleUpdateCollapsed(v: boolean) {
|
|
29
|
+
updateIsCollapsed(v)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const isSideMixedMenu = computed<boolean>(() => SIDE_MIXED_LAYOUT_TYPES.includes(unref(type)))
|
|
33
|
+
const isSideGroupMenu = computed<boolean>(() => SIDE_GROUP_LAYOUT_TYPES.includes(unref(type)))
|
|
34
|
+
|
|
35
|
+
const active = ref('')
|
|
36
|
+
const expandedKeys = ref<string[]>([])
|
|
37
|
+
|
|
38
|
+
function handleUpdateExpandedKeys(keys: string[]) {
|
|
39
|
+
expandedKeys.value = keys
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 统一处理激活状态和滚动/展开
|
|
43
|
+
watch(() => unref(activeKey), (newKey) => {
|
|
44
|
+
if (!newKey)
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
const { mainKey, subKey } = resolveMainSubFromActive(unref(options) as any[], newKey)
|
|
48
|
+
mainActiveKey.value = mainKey || ''
|
|
49
|
+
subActiveKey.value = subKey || ''
|
|
50
|
+
|
|
51
|
+
if (unref(type) === 'side-menu' || unref(type) === 'side-group-menu' || unref(type) === 'side-mixed-menu') {
|
|
52
|
+
active.value = newKey
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
if (unref(isLeftMain)) {
|
|
56
|
+
active.value = mainKey || ''
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (unref(isTopMain)) {
|
|
60
|
+
active.value = subKey || ''
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 只有在激活项变化时才调用 showOption,避免干扰手动展开/收起
|
|
65
|
+
if (active.value) {
|
|
66
|
+
menuInstRef.value?.showOption(active.value)
|
|
67
|
+
}
|
|
68
|
+
}, { immediate: true })
|
|
69
|
+
|
|
70
|
+
const router = useRouter()
|
|
71
|
+
function handleUpdateValue(key: string) {
|
|
72
|
+
const node = findNodeByKey(unref(options) as any[], key)
|
|
73
|
+
if (node?.href || /^https?:\/\//.test(key)) {
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
updateActiveKey(key)
|
|
78
|
+
active.value = key
|
|
79
|
+
menuInstRef.value?.showOption(key)
|
|
80
|
+
|
|
81
|
+
if (unref(isLeftMain) && hasSiderLayout.value) {
|
|
82
|
+
const { subKey } = resolveMainSubFromActive(unref(options) as any[], key)
|
|
83
|
+
if (subKey) {
|
|
84
|
+
if (subKey.startsWith('/')) {
|
|
85
|
+
router.push(subKey)
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
router.push({ name: subKey })
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const menuOptions = computed(() => {
|
|
95
|
+
const transformToGroups = (items: MenuOption[]): MenuOption[] => {
|
|
96
|
+
return updateMenuTree(items as any[], (item) => {
|
|
97
|
+
if (item.children && item.children.length > 0) {
|
|
98
|
+
return { ...item, type: 'group' } as MenuOption
|
|
99
|
+
}
|
|
100
|
+
return item
|
|
101
|
+
}) as MenuOption[]
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 1. 获取基础菜单项
|
|
105
|
+
let baseOptions: MenuOption[] = []
|
|
106
|
+
const currentType = unref(type)
|
|
107
|
+
if (currentType === 'side-menu' || currentType === 'side-group-menu' || currentType === 'side-mixed-menu') {
|
|
108
|
+
// 如果是左侧主菜单类布局(非混合顶部),使用完整选项
|
|
109
|
+
baseOptions = unref(options) || []
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
// 否则(如 top-menu/2, top-group-menu/2 等),根据主次关系选择子菜单
|
|
113
|
+
baseOptions = unref(isLeftMain) ? unref(mainMenuOptions) : unref(subMenuOptions)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 2. 根据布局类型进行转换
|
|
117
|
+
if (isSideGroupMenu.value) {
|
|
118
|
+
return transformToGroups(baseOptions)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (isSideMixedMenu.value) {
|
|
122
|
+
// 混合菜单:第一层保持原样(通常是图标),第二层开始转为 group
|
|
123
|
+
return baseOptions.map((item) => {
|
|
124
|
+
if (item.children && item.children.length) {
|
|
125
|
+
return {
|
|
126
|
+
...item,
|
|
127
|
+
children: transformToGroups(item.children)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return item
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return baseOptions
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
const slots: Slots = useSlots()
|
|
138
|
+
const hasDefaultSlot = computed<boolean>(() => hasSlotContent(slots.default))
|
|
139
|
+
</script>
|
|
140
|
+
|
|
141
|
+
<template>
|
|
142
|
+
<n-layout-sider
|
|
143
|
+
class="transition-all duration-300 relative"
|
|
144
|
+
position="absolute"
|
|
145
|
+
collapse-mode="width"
|
|
146
|
+
show-trigger="bar"
|
|
147
|
+
:width="width"
|
|
148
|
+
:collapsed-width="collapsedWidth"
|
|
149
|
+
:bordered="bordered"
|
|
150
|
+
:inverted="inverted"
|
|
151
|
+
:collapsed="isCollapsed"
|
|
152
|
+
:native-scrollbar="false"
|
|
153
|
+
:style="{ top: unref(isTopMain) ? `${unref(headerHeight)}px` : 0 }"
|
|
154
|
+
@update:collapsed="handleUpdateCollapsed"
|
|
155
|
+
>
|
|
156
|
+
<div v-if="isLeftMain">
|
|
157
|
+
<slot v-if="hasDefaultSlot" />
|
|
158
|
+
<AppLeftLogoInfo v-else />
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
<n-menu
|
|
162
|
+
ref="menuInstRef"
|
|
163
|
+
:value="active"
|
|
164
|
+
:expanded-keys="expandedKeys"
|
|
165
|
+
:options="menuOptions"
|
|
166
|
+
:render-label="renderMenuLabel"
|
|
167
|
+
:collapsed-width="collapsedWidth"
|
|
168
|
+
:inverted="inverted"
|
|
169
|
+
:accordion="accordion"
|
|
170
|
+
@update:value="handleUpdateValue"
|
|
171
|
+
@update:expanded-keys="handleUpdateExpandedKeys"
|
|
172
|
+
/>
|
|
173
|
+
</n-layout-sider>
|
|
174
|
+
</template>
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* 通用布局过渡组件
|
|
4
|
+
*
|
|
5
|
+
* 为侧边栏、头部、底部、内容区域提供统一的进场/出场动画能力,
|
|
6
|
+
* 通过 `mode` 控制动画类型,适配宽度/高度/位移/透明等常见过渡。
|
|
7
|
+
*
|
|
8
|
+
* @param props.mode - 动画模式:'fade' | 'slide-x' | 'slide-y' | 'scale' | 'width' | 'height'
|
|
9
|
+
* @param props.duration - 过渡时长(毫秒),默认 250
|
|
10
|
+
* @param props.easing - 缓动函数,默认 cubic-bezier(0.25,0.46,0.45,0.94)
|
|
11
|
+
* @returns 无返回值(渲染一个 <Transition> 包裹的插槽)
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```vue
|
|
15
|
+
* <LayoutTransition mode="width">
|
|
16
|
+
* <AppSidebar v-if="showSider" />
|
|
17
|
+
* </LayoutTransition>
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* @remarks
|
|
21
|
+
* - width/height 模式使用 scrollWidth/scrollHeight 进行展开动画,before-leave 固定当前尺寸避免抖动
|
|
22
|
+
* - slide-x/slide-y 使用 transform 与 opacity 联合过渡
|
|
23
|
+
* - scale 使用轻微缩放与透明度过渡
|
|
24
|
+
*
|
|
25
|
+
* @security
|
|
26
|
+
* - 仅修改元素内联样式;不涉及外部副作用
|
|
27
|
+
*
|
|
28
|
+
* @performance
|
|
29
|
+
* - 依赖浏览器原生过渡;时间复杂度与节点尺寸无关
|
|
30
|
+
*/
|
|
31
|
+
const props = withDefaults(defineProps<{
|
|
32
|
+
mode?: 'fade' | 'slide-x' | 'slide-y' | 'scale' | 'width' | 'height'
|
|
33
|
+
duration?: number
|
|
34
|
+
easing?: string
|
|
35
|
+
appear?: boolean
|
|
36
|
+
}>(), {
|
|
37
|
+
mode: 'fade',
|
|
38
|
+
duration: 250,
|
|
39
|
+
easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
|
40
|
+
appear: true
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 设置通用过渡样式
|
|
45
|
+
* 将 transition 应用于节点,按需组合属性。
|
|
46
|
+
*/
|
|
47
|
+
function setTransition(node: HTMLElement, propsList: string[]) {
|
|
48
|
+
node.style.transition = propsList.map(p => `${p} ${props.duration}ms ${props.easing}`).join(', ')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 宽度入场动画(展开)
|
|
53
|
+
*
|
|
54
|
+
* 初始 width=0,随后过渡到 scrollWidth,实现展开效果。
|
|
55
|
+
*
|
|
56
|
+
* @param el - 进入的元素
|
|
57
|
+
*/
|
|
58
|
+
function widthEnter(el: Element) {
|
|
59
|
+
const node = el as HTMLElement
|
|
60
|
+
const computedStyle = window.getComputedStyle(node)
|
|
61
|
+
const target
|
|
62
|
+
= Number.parseFloat(computedStyle.width || '0')
|
|
63
|
+
|| node.getBoundingClientRect().width
|
|
64
|
+
|| node.offsetWidth
|
|
65
|
+
|| node.scrollWidth
|
|
66
|
+
if (!target || target <= 0) {
|
|
67
|
+
node.style.transition = ''
|
|
68
|
+
node.style.overflow = ''
|
|
69
|
+
node.style.width = ''
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
node.style.overflow = 'hidden'
|
|
73
|
+
node.style.width = '0px'
|
|
74
|
+
setTransition(node, ['width'])
|
|
75
|
+
requestAnimationFrame(() => {
|
|
76
|
+
node.style.width = `${target}px`
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 宽度离场前动画(收起)
|
|
82
|
+
*
|
|
83
|
+
* 固定当前宽度,然后过渡到 0。
|
|
84
|
+
*
|
|
85
|
+
* @param el - 离场的元素
|
|
86
|
+
*/
|
|
87
|
+
function widthBeforeLeave(el: Element) {
|
|
88
|
+
const node = el as HTMLElement
|
|
89
|
+
const current = node.offsetWidth
|
|
90
|
+
node.style.overflow = 'hidden'
|
|
91
|
+
node.style.width = `${current}px`
|
|
92
|
+
setTransition(node, ['width'])
|
|
93
|
+
requestAnimationFrame(() => {
|
|
94
|
+
node.style.width = '0px'
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 高度入场动画(展开)
|
|
100
|
+
*
|
|
101
|
+
* 初始 height=0,随后过渡到 scrollHeight,实现展开效果。
|
|
102
|
+
*
|
|
103
|
+
* @param el - 进入的元素
|
|
104
|
+
*/
|
|
105
|
+
function heightEnter(el: Element) {
|
|
106
|
+
const node = el as HTMLElement
|
|
107
|
+
const target = node.scrollHeight
|
|
108
|
+
node.style.overflow = 'hidden'
|
|
109
|
+
node.style.height = '0px'
|
|
110
|
+
setTransition(node, ['height'])
|
|
111
|
+
requestAnimationFrame(() => {
|
|
112
|
+
node.style.height = `${target}px`
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* 高度离场前动画(收起)
|
|
118
|
+
*
|
|
119
|
+
* 固定当前高度,然后过渡到 0。
|
|
120
|
+
*
|
|
121
|
+
* @param el - 离场的元素
|
|
122
|
+
*/
|
|
123
|
+
function heightBeforeLeave(el: Element) {
|
|
124
|
+
const node = el as HTMLElement
|
|
125
|
+
const current = node.offsetHeight
|
|
126
|
+
node.style.overflow = 'hidden'
|
|
127
|
+
node.style.height = `${current}px`
|
|
128
|
+
setTransition(node, ['height'])
|
|
129
|
+
requestAnimationFrame(() => {
|
|
130
|
+
node.style.height = '0px'
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* 透明入场动画
|
|
136
|
+
*
|
|
137
|
+
* opacity: 0 -> 1
|
|
138
|
+
*
|
|
139
|
+
* @param el - 进入的元素
|
|
140
|
+
*/
|
|
141
|
+
function fadeEnter(el: Element) {
|
|
142
|
+
const node = el as HTMLElement
|
|
143
|
+
node.style.opacity = '0'
|
|
144
|
+
setTransition(node, ['opacity'])
|
|
145
|
+
requestAnimationFrame(() => {
|
|
146
|
+
node.style.opacity = '1'
|
|
147
|
+
})
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* 透明离场前动画
|
|
152
|
+
*
|
|
153
|
+
* opacity: 1 -> 0
|
|
154
|
+
*
|
|
155
|
+
* @param el - 离场的元素
|
|
156
|
+
*/
|
|
157
|
+
function fadeBeforeLeave(el: Element) {
|
|
158
|
+
const node = el as HTMLElement
|
|
159
|
+
node.style.opacity = '1'
|
|
160
|
+
setTransition(node, ['opacity'])
|
|
161
|
+
requestAnimationFrame(() => {
|
|
162
|
+
node.style.opacity = '0'
|
|
163
|
+
})
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* 横向位移入场动画
|
|
168
|
+
*
|
|
169
|
+
* transform: translateX(-8px) -> 0, opacity: 0 -> 1
|
|
170
|
+
*
|
|
171
|
+
* @param el - 进入的元素
|
|
172
|
+
*/
|
|
173
|
+
function slideXEnter(el: Element) {
|
|
174
|
+
const node = el as HTMLElement
|
|
175
|
+
node.style.opacity = '0'
|
|
176
|
+
node.style.transform = 'translateX(-8px)'
|
|
177
|
+
setTransition(node, ['transform', 'opacity'])
|
|
178
|
+
requestAnimationFrame(() => {
|
|
179
|
+
node.style.opacity = '1'
|
|
180
|
+
node.style.transform = 'translateX(0)'
|
|
181
|
+
})
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* 横向位移离场前动画
|
|
186
|
+
*
|
|
187
|
+
* transform: 0 -> translateX(-8px), opacity: 1 -> 0
|
|
188
|
+
*
|
|
189
|
+
* @param el - 离场的元素
|
|
190
|
+
*/
|
|
191
|
+
function slideXBeforeLeave(el: Element) {
|
|
192
|
+
const node = el as HTMLElement
|
|
193
|
+
node.style.opacity = '1'
|
|
194
|
+
node.style.transform = 'translateX(0)'
|
|
195
|
+
setTransition(node, ['transform', 'opacity'])
|
|
196
|
+
requestAnimationFrame(() => {
|
|
197
|
+
node.style.opacity = '0'
|
|
198
|
+
node.style.transform = 'translateX(-8px)'
|
|
199
|
+
})
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* 纵向位移入场动画
|
|
204
|
+
*
|
|
205
|
+
* transform: translateY(-6px) -> 0, opacity: 0 -> 1
|
|
206
|
+
*
|
|
207
|
+
* @param el - 进入的元素
|
|
208
|
+
*/
|
|
209
|
+
function slideYEnter(el: Element) {
|
|
210
|
+
const node = el as HTMLElement
|
|
211
|
+
node.style.opacity = '0'
|
|
212
|
+
node.style.transform = 'translateY(-6px)'
|
|
213
|
+
setTransition(node, ['transform', 'opacity'])
|
|
214
|
+
requestAnimationFrame(() => {
|
|
215
|
+
node.style.opacity = '1'
|
|
216
|
+
node.style.transform = 'translateY(0)'
|
|
217
|
+
})
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* 纵向位移离场前动画
|
|
222
|
+
*
|
|
223
|
+
* transform: 0 -> translateY(-6px), opacity: 1 -> 0
|
|
224
|
+
*
|
|
225
|
+
* @param el - 离场的元素
|
|
226
|
+
*/
|
|
227
|
+
function slideYBeforeLeave(el: Element) {
|
|
228
|
+
const node = el as HTMLElement
|
|
229
|
+
node.style.opacity = '1'
|
|
230
|
+
node.style.transform = 'translateY(0)'
|
|
231
|
+
setTransition(node, ['transform', 'opacity'])
|
|
232
|
+
requestAnimationFrame(() => {
|
|
233
|
+
node.style.opacity = '0'
|
|
234
|
+
node.style.transform = 'translateY(-6px)'
|
|
235
|
+
})
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* 轻微缩放入场动画
|
|
240
|
+
*
|
|
241
|
+
* scale: 0.98 -> 1, opacity: 0 -> 1
|
|
242
|
+
*
|
|
243
|
+
* @param el - 进入的元素
|
|
244
|
+
*/
|
|
245
|
+
function scaleEnter(el: Element) {
|
|
246
|
+
const node = el as HTMLElement
|
|
247
|
+
node.style.opacity = '0'
|
|
248
|
+
node.style.transform = 'scale(0.98)'
|
|
249
|
+
setTransition(node, ['transform', 'opacity'])
|
|
250
|
+
requestAnimationFrame(() => {
|
|
251
|
+
node.style.opacity = '1'
|
|
252
|
+
node.style.transform = 'scale(1)'
|
|
253
|
+
})
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* 轻微缩放离场前动画
|
|
258
|
+
*
|
|
259
|
+
* scale: 1 -> 0.98, opacity: 1 -> 0
|
|
260
|
+
*
|
|
261
|
+
* @param el - 离场的元素
|
|
262
|
+
*/
|
|
263
|
+
function scaleBeforeLeave(el: Element) {
|
|
264
|
+
const node = el as HTMLElement
|
|
265
|
+
node.style.opacity = '1'
|
|
266
|
+
node.style.transform = 'scale(1)'
|
|
267
|
+
setTransition(node, ['transform', 'opacity'])
|
|
268
|
+
requestAnimationFrame(() => {
|
|
269
|
+
node.style.opacity = '0'
|
|
270
|
+
node.style.transform = 'scale(0.98)'
|
|
271
|
+
})
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* 清理动画样式
|
|
276
|
+
*
|
|
277
|
+
* 移除过渡、溢出、尺寸、透明度、位移等,避免影响后续布局。
|
|
278
|
+
*
|
|
279
|
+
* @param el - 离场后的元素
|
|
280
|
+
*/
|
|
281
|
+
function afterLeave(el: Element) {
|
|
282
|
+
const node = el as HTMLElement
|
|
283
|
+
node.style.transition = ''
|
|
284
|
+
node.style.overflow = ''
|
|
285
|
+
node.style.width = ''
|
|
286
|
+
node.style.height = ''
|
|
287
|
+
node.style.opacity = ''
|
|
288
|
+
node.style.transform = ''
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* 统一进场处理
|
|
293
|
+
*
|
|
294
|
+
* 根据 `mode` 分派具体动画。
|
|
295
|
+
*
|
|
296
|
+
* @param el - 进入元素
|
|
297
|
+
*/
|
|
298
|
+
function handleEnter(el: Element) {
|
|
299
|
+
switch (props.mode) {
|
|
300
|
+
case 'width':
|
|
301
|
+
widthEnter(el)
|
|
302
|
+
break
|
|
303
|
+
case 'height':
|
|
304
|
+
heightEnter(el)
|
|
305
|
+
break
|
|
306
|
+
case 'slide-x':
|
|
307
|
+
slideXEnter(el)
|
|
308
|
+
break
|
|
309
|
+
case 'slide-y':
|
|
310
|
+
slideYEnter(el)
|
|
311
|
+
break
|
|
312
|
+
case 'scale':
|
|
313
|
+
scaleEnter(el)
|
|
314
|
+
break
|
|
315
|
+
default:
|
|
316
|
+
fadeEnter(el)
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* 统一离场前处理
|
|
322
|
+
*
|
|
323
|
+
* 根据 `mode` 分派具体动画。
|
|
324
|
+
*
|
|
325
|
+
* @param el - 离场元素
|
|
326
|
+
*/
|
|
327
|
+
function handleBeforeLeave(el: Element) {
|
|
328
|
+
switch (props.mode) {
|
|
329
|
+
case 'width':
|
|
330
|
+
widthBeforeLeave(el)
|
|
331
|
+
break
|
|
332
|
+
case 'height':
|
|
333
|
+
heightBeforeLeave(el)
|
|
334
|
+
break
|
|
335
|
+
case 'slide-x':
|
|
336
|
+
slideXBeforeLeave(el)
|
|
337
|
+
break
|
|
338
|
+
case 'slide-y':
|
|
339
|
+
slideYBeforeLeave(el)
|
|
340
|
+
break
|
|
341
|
+
case 'scale':
|
|
342
|
+
scaleBeforeLeave(el)
|
|
343
|
+
break
|
|
344
|
+
default:
|
|
345
|
+
fadeBeforeLeave(el)
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
</script>
|
|
349
|
+
|
|
350
|
+
<template>
|
|
351
|
+
<Transition
|
|
352
|
+
:appear="props.appear"
|
|
353
|
+
@enter="handleEnter"
|
|
354
|
+
@before-leave="handleBeforeLeave"
|
|
355
|
+
@after-leave="afterLeave"
|
|
356
|
+
>
|
|
357
|
+
<slot />
|
|
358
|
+
</Transition>
|
|
359
|
+
<!-- 用法:
|
|
360
|
+
<LayoutTransition mode="width"><AppSidebar v-if="hasSider"/></LayoutTransition>
|
|
361
|
+
<LayoutTransition mode="height"><AppHeader v-if="showHeader"/></LayoutTransition>
|
|
362
|
+
<LayoutTransition mode="fade"><AppMain v-if="ready"/></LayoutTransition>
|
|
363
|
+
-->
|
|
364
|
+
</template>
|
|
365
|
+
|
|
366
|
+
<style scoped></style>
|
|
File without changes
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { MenuOption } from 'naive-ui'
|
|
2
|
+
import type { RouteRecordRaw } from 'vue-router'
|
|
3
|
+
import type { LayoutType } from './types'
|
|
4
|
+
|
|
5
|
+
export type Props = Partial<{
|
|
6
|
+
/** @description 布局类型 */
|
|
7
|
+
type: LayoutType
|
|
8
|
+
/** @description 是否显示边框 */
|
|
9
|
+
bordered: boolean
|
|
10
|
+
/** @description 反色样式(Header/Sider/Footer/Menu) */
|
|
11
|
+
inverted: boolean
|
|
12
|
+
/** @description 是否折叠侧边栏 */
|
|
13
|
+
isCollapsed: boolean
|
|
14
|
+
/** @description 是否显示底部 */
|
|
15
|
+
showFooter: boolean
|
|
16
|
+
/** @description 头部高度 */
|
|
17
|
+
headerHeight: number
|
|
18
|
+
/** @description 底部高度 */
|
|
19
|
+
footerHeight: number
|
|
20
|
+
/** @description 底部是否占满宽度 */
|
|
21
|
+
footerFull: boolean
|
|
22
|
+
/** @description 侧边栏宽度 */
|
|
23
|
+
siderWidth: number
|
|
24
|
+
/** @description 侧边栏混合模式的宽度 */
|
|
25
|
+
siderMixedWidth: number
|
|
26
|
+
/** @description 折叠侧边栏宽度 */
|
|
27
|
+
collapsedWidth: number
|
|
28
|
+
/** @description 当前激活的路由键 */
|
|
29
|
+
activeKey: string
|
|
30
|
+
/** @description 菜单路由 */
|
|
31
|
+
menuOptions: MenuOption[]
|
|
32
|
+
/** @description 基础路由 */
|
|
33
|
+
baseRoutes: RouteRecordRaw[]
|
|
34
|
+
/** @description 是否开启手风琴模式 */
|
|
35
|
+
accordion: boolean
|
|
36
|
+
}>
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 应用布局类型枚举(格式:[主要]-[侧边栏模式]-[菜单选项])
|
|
3
|
+
* 用于动态切换页面整体结构,省略 "layout" 后缀以保持简洁
|
|
4
|
+
*/
|
|
5
|
+
export type LayoutType
|
|
6
|
+
= | 'side-menu'
|
|
7
|
+
| 'side-menu/2'
|
|
8
|
+
| 'side-mixed-menu'
|
|
9
|
+
| 'side-group-menu'
|
|
10
|
+
| 'top-menu'
|
|
11
|
+
| 'top-menu/2'
|
|
12
|
+
| 'top-group-menu/2'
|
|
13
|
+
| 'top-mixed-menu/2'
|
|
14
|
+
| 'blank'
|
|
15
|
+
|
|
16
|
+
export interface RouteMeta {
|
|
17
|
+
/**
|
|
18
|
+
* 页面标题(用于 document.title 或 tab 标签)
|
|
19
|
+
*/
|
|
20
|
+
title?: string
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 是否需要认证(true: 需登录,false: 免登录)
|
|
24
|
+
* @default true
|
|
25
|
+
*/
|
|
26
|
+
requiresAuth?: boolean
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 所需权限(字符串数组或字符串)
|
|
30
|
+
*/
|
|
31
|
+
permissions?: string | string[]
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 所属角色(如 'admin', 'user')
|
|
35
|
+
*/
|
|
36
|
+
roles?: string[]
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 是否在侧边栏菜单中显示
|
|
40
|
+
* @default false
|
|
41
|
+
*/
|
|
42
|
+
hideMenu?: boolean
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 侧边栏菜单选中
|
|
46
|
+
* @default string
|
|
47
|
+
*/
|
|
48
|
+
activeMenu?: import('vue-router').RouteRecordRedirectOption
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 菜单图标(Iconify 格式,如 'mdi:home')
|
|
52
|
+
*/
|
|
53
|
+
icon?: string
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 使用的布局名称(如 'blank', 'main')
|
|
57
|
+
*/
|
|
58
|
+
layout?: LayoutType
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 是否开启 keep-alive 缓存
|
|
62
|
+
*/
|
|
63
|
+
keepAlive?: boolean
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 页面过渡动画名
|
|
67
|
+
*/
|
|
68
|
+
transition?: string
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 排序
|
|
72
|
+
*/
|
|
73
|
+
order?: number
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 外链
|
|
77
|
+
*/
|
|
78
|
+
href?: string
|
|
79
|
+
}
|