@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,189 @@
|
|
|
1
|
+
import type { MenuOption } from 'naive-ui'
|
|
2
|
+
import type { ComputedRef, Reactive, Ref } from 'vue'
|
|
3
|
+
import type { RouteRecordRaw } from 'vue-router'
|
|
4
|
+
import type { Props } from './props'
|
|
5
|
+
import type { LayoutType } from './types'
|
|
6
|
+
import { computed, inject, provide, reactive, ref, toRef, unref, watch } from 'vue'
|
|
7
|
+
import { useRoute } from 'vue-router'
|
|
8
|
+
import { normalizeAndRedirect } from '../../utils/transformRoutes'
|
|
9
|
+
import { BREADCRUMB_LAYOUT_TYPES } from './const'
|
|
10
|
+
|
|
11
|
+
export interface LayoutEmits {
|
|
12
|
+
/** @description 更新是否折叠侧边栏 */
|
|
13
|
+
updateIsCollapsed: (value: boolean) => void
|
|
14
|
+
/** @description 更新反色样式 */
|
|
15
|
+
updateInverted: (value: boolean) => void
|
|
16
|
+
/** @description 更新当前激活的路由键 */
|
|
17
|
+
updateActiveKey: (value: string) => void
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const LayoutContextKey = Symbol('LayoutContext')
|
|
21
|
+
export const LayoutEmitsKey = Symbol('LayoutEmits')
|
|
22
|
+
|
|
23
|
+
export interface LayoutContextState extends Props {
|
|
24
|
+
mainActiveKey: string
|
|
25
|
+
subActiveKey: string
|
|
26
|
+
hasSiderLayout: boolean
|
|
27
|
+
hasBreadcrumb: boolean
|
|
28
|
+
isLeftMain: boolean
|
|
29
|
+
isTopMain: boolean
|
|
30
|
+
sideWidth: number
|
|
31
|
+
accordion?: boolean
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function provideLayoutContext(props: Required<Props>, extra?: { hasSiderLayout: ComputedRef<boolean> }) {
|
|
35
|
+
const route = useRoute()
|
|
36
|
+
const mainActiveKey = ref(props.activeKey)
|
|
37
|
+
const subActiveKey = ref(props.activeKey)
|
|
38
|
+
|
|
39
|
+
// 内部维护一个可变的布局类型,初始值为 props.type
|
|
40
|
+
const internalLayoutType = ref<LayoutType>(props.type)
|
|
41
|
+
|
|
42
|
+
// 最终使用的布局类型:优先使用路由 meta.layout,否则使用内部状态
|
|
43
|
+
const effectiveLayoutType = computed(() => (route?.meta?.layout as LayoutType) || internalLayoutType.value)
|
|
44
|
+
|
|
45
|
+
// 最终使用的激活键:优先使用路由 meta.activeMenu,否则使用 props.activeKey
|
|
46
|
+
const effectiveActiveKey = computed(() => (route?.meta?.activeMenu as string) || props.activeKey)
|
|
47
|
+
|
|
48
|
+
// 监听 props.type 的变化并同步到内部状态
|
|
49
|
+
watch(() => props.type, (newType) => {
|
|
50
|
+
internalLayoutType.value = newType
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
const hasBreadcrumb = computed(() => BREADCRUMB_LAYOUT_TYPES.includes(unref(effectiveLayoutType)))
|
|
54
|
+
|
|
55
|
+
const isLeftMain = computed(() => unref(effectiveLayoutType).includes('side'))
|
|
56
|
+
|
|
57
|
+
const isTopMain = computed(() => unref(effectiveLayoutType).includes('top'))
|
|
58
|
+
|
|
59
|
+
const sideWidth = computed(() => props.siderWidth)
|
|
60
|
+
|
|
61
|
+
const context = reactive({
|
|
62
|
+
// 使用 effectiveLayoutType 和 effectiveActiveKey 替代直接使用 props
|
|
63
|
+
type: effectiveLayoutType,
|
|
64
|
+
bordered: toRef(props, 'bordered'),
|
|
65
|
+
inverted: toRef(props, 'inverted'),
|
|
66
|
+
isCollapsed: toRef(props, 'isCollapsed'),
|
|
67
|
+
headerHeight: toRef(props, 'headerHeight'),
|
|
68
|
+
footerHeight: toRef(props, 'footerHeight'),
|
|
69
|
+
showFooter: toRef(props, 'showFooter'),
|
|
70
|
+
siderWidth: toRef(props, 'siderWidth'),
|
|
71
|
+
siderMixedWidth: toRef(props, 'siderMixedWidth'),
|
|
72
|
+
collapsedWidth: toRef(props, 'collapsedWidth'),
|
|
73
|
+
activeKey: effectiveActiveKey,
|
|
74
|
+
menuOptions: toRef(props, 'menuOptions'),
|
|
75
|
+
accordion: toRef(props, 'accordion'),
|
|
76
|
+
baseRoutes: computed(() => normalizeAndRedirect(unref((props as any).baseRoutes))),
|
|
77
|
+
mainActiveKey,
|
|
78
|
+
subActiveKey,
|
|
79
|
+
hasSiderLayout: extra?.hasSiderLayout,
|
|
80
|
+
hasBreadcrumb,
|
|
81
|
+
isLeftMain,
|
|
82
|
+
isTopMain,
|
|
83
|
+
sideWidth
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
provide(LayoutContextKey, context)
|
|
87
|
+
return context
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface UseContextReturn extends LayoutEmits {
|
|
91
|
+
ctx: Reactive<LayoutContextState>
|
|
92
|
+
type: ComputedRef<LayoutType>
|
|
93
|
+
bordered: ComputedRef<boolean>
|
|
94
|
+
inverted: ComputedRef<boolean>
|
|
95
|
+
isCollapsed: ComputedRef<boolean>
|
|
96
|
+
headerHeight: ComputedRef<number>
|
|
97
|
+
footerHeight: ComputedRef<number>
|
|
98
|
+
showFooter: ComputedRef<boolean>
|
|
99
|
+
siderWidth: ComputedRef<number>
|
|
100
|
+
siderMixedWidth: ComputedRef<number>
|
|
101
|
+
collapsedWidth: ComputedRef<number>
|
|
102
|
+
activeKey: ComputedRef<string>
|
|
103
|
+
subActiveKey: Ref<string>
|
|
104
|
+
mainActiveKey: Ref<string>
|
|
105
|
+
menuOptions: ComputedRef<MenuOption[]>
|
|
106
|
+
accordion: ComputedRef<boolean>
|
|
107
|
+
baseRoutes: ComputedRef<RouteRecordRaw[]>
|
|
108
|
+
mainMenuOptions: ComputedRef<MenuOption[]>
|
|
109
|
+
subMenuOptions: ComputedRef<MenuOption[]>
|
|
110
|
+
hasSiderLayout: Ref<boolean>
|
|
111
|
+
hasBreadcrumb: Ref<boolean>
|
|
112
|
+
isLeftMain: ComputedRef<boolean>
|
|
113
|
+
isTopMain: ComputedRef<boolean>
|
|
114
|
+
sideWidth: ComputedRef<number>
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function useContext(): UseContextReturn {
|
|
118
|
+
const context = inject<Reactive<LayoutContextState>>(LayoutContextKey)!
|
|
119
|
+
const layoutEmit = inject<Reactive<LayoutEmits>>(LayoutEmitsKey)!
|
|
120
|
+
|
|
121
|
+
const type = computed(() => unref(context.type)!)
|
|
122
|
+
const bordered = computed(() => unref(context.bordered)!)
|
|
123
|
+
const inverted = computed(() => unref(context.inverted)!)
|
|
124
|
+
const isCollapsed = computed(() => unref(context.isCollapsed)!)
|
|
125
|
+
const headerHeight = computed(() => unref(context.headerHeight)!)
|
|
126
|
+
const footerHeight = computed(() => unref(context.footerHeight)!)
|
|
127
|
+
const showFooter = computed(() => unref(context.showFooter)!)
|
|
128
|
+
const siderWidth = computed(() => unref(context.siderWidth)!)
|
|
129
|
+
const siderMixedWidth = computed(() => unref(context.siderMixedWidth)!)
|
|
130
|
+
const collapsedWidth = computed(() => unref(context.collapsedWidth)!)
|
|
131
|
+
const baseRoutes = computed(() => unref((context as any).baseRoutes) ?? []) as ComputedRef<RouteRecordRaw[]>
|
|
132
|
+
|
|
133
|
+
const activeKey = computed(() => unref(context.activeKey)!)
|
|
134
|
+
const menuOptions = computed(() => unref(context.menuOptions)!)
|
|
135
|
+
const accordion = computed(() => unref(context.accordion)!)
|
|
136
|
+
|
|
137
|
+
const mainActiveKey = toRef(context, 'mainActiveKey')
|
|
138
|
+
const subActiveKey = toRef(context, 'subActiveKey')
|
|
139
|
+
|
|
140
|
+
const hasSiderLayout = computed(() => unref(context.hasSiderLayout))
|
|
141
|
+
const hasBreadcrumb = computed(() => unref(context.hasBreadcrumb))
|
|
142
|
+
const isLeftMain = computed(() => unref(context.isLeftMain))
|
|
143
|
+
const isTopMain = computed(() => unref(context.isTopMain))
|
|
144
|
+
|
|
145
|
+
const sideWidth = computed(() => unref(context.sideWidth))
|
|
146
|
+
|
|
147
|
+
const mainMenuOptions = computed(() => {
|
|
148
|
+
const opts = unref(menuOptions) as any[] || []
|
|
149
|
+
return opts.map(o => ({ key: o.key, label: o.label, icon: o.icon, href: o.href }))
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
const subMenuOptions = computed(() => {
|
|
153
|
+
const changeRange = mainMenuOptions.value.map(o => o.key)
|
|
154
|
+
const opts = unref(menuOptions) as any[] || []
|
|
155
|
+
const key = String(unref(mainActiveKey) ?? '')
|
|
156
|
+
const topKey = changeRange.find(k => key === k || key.startsWith(`${k}/`)) ?? changeRange[0] ?? ''
|
|
157
|
+
const parent = (opts || []).find(o => o.key === topKey)
|
|
158
|
+
|
|
159
|
+
return parent?.children ?? []
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
ctx: context,
|
|
164
|
+
type,
|
|
165
|
+
bordered,
|
|
166
|
+
inverted,
|
|
167
|
+
isCollapsed,
|
|
168
|
+
headerHeight,
|
|
169
|
+
footerHeight,
|
|
170
|
+
showFooter,
|
|
171
|
+
siderWidth,
|
|
172
|
+
siderMixedWidth,
|
|
173
|
+
collapsedWidth,
|
|
174
|
+
activeKey,
|
|
175
|
+
subActiveKey,
|
|
176
|
+
mainActiveKey,
|
|
177
|
+
menuOptions,
|
|
178
|
+
accordion,
|
|
179
|
+
baseRoutes,
|
|
180
|
+
mainMenuOptions,
|
|
181
|
+
subMenuOptions,
|
|
182
|
+
hasSiderLayout,
|
|
183
|
+
hasBreadcrumb,
|
|
184
|
+
isLeftMain,
|
|
185
|
+
isTopMain,
|
|
186
|
+
sideWidth,
|
|
187
|
+
...layoutEmit
|
|
188
|
+
} as UseContextReturn
|
|
189
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { Props } from './props'
|
|
3
|
+
import type { LayoutType } from './types'
|
|
4
|
+
import { computed, unref } from 'vue'
|
|
5
|
+
import { useRoute } from 'vue-router'
|
|
6
|
+
import { DEFAULT_LAYOUT_PROPS, SIDE_LAYOUT_TYPES } from './const'
|
|
7
|
+
import { LayoutEmitsKey, provideLayoutContext } from './context'
|
|
8
|
+
import AppFooter from './layout-parts/AppFooter.vue'
|
|
9
|
+
import AppHeader from './layout-parts/AppHeader.vue'
|
|
10
|
+
import AppMain from './layout-parts/AppMain.vue'
|
|
11
|
+
import AppSidebar from './layout-parts/AppSidebar.vue'
|
|
12
|
+
import LayoutTransition from './layout-parts/LayoutTransition.vue'
|
|
13
|
+
|
|
14
|
+
const props = withDefaults(defineProps<Props>(), { ...DEFAULT_LAYOUT_PROPS, menuOptions: () => [], baseRoutes: () => [] })
|
|
15
|
+
const emit = defineEmits<{
|
|
16
|
+
'update:isCollapsed': [value: boolean]
|
|
17
|
+
'update:inverted': [value: boolean]
|
|
18
|
+
'update:activeKey': [value: string]
|
|
19
|
+
}>()
|
|
20
|
+
|
|
21
|
+
const route = useRoute()
|
|
22
|
+
const effectiveType = computed(() => (route?.meta?.layout as LayoutType) || props.type)
|
|
23
|
+
const hasSiderLayout = computed(() => SIDE_LAYOUT_TYPES.includes(unref(effectiveType)))
|
|
24
|
+
const isBlank = computed(() => unref(effectiveType) === 'blank')
|
|
25
|
+
|
|
26
|
+
provideLayoutContext(props, { hasSiderLayout })
|
|
27
|
+
provide(LayoutEmitsKey, {
|
|
28
|
+
updateIsCollapsed(value: boolean) {
|
|
29
|
+
emit('update:isCollapsed', value)
|
|
30
|
+
},
|
|
31
|
+
updateInverted(value: boolean) {
|
|
32
|
+
emit('update:inverted', value)
|
|
33
|
+
},
|
|
34
|
+
updateActiveKey(value: string) {
|
|
35
|
+
emit('update:activeKey', value)
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
<template>
|
|
41
|
+
<n-layout :has-sider="hasSiderLayout" position="absolute" :native-scrollbar="false" :inverted="inverted">
|
|
42
|
+
<LayoutTransition mode="fade">
|
|
43
|
+
<AppSidebar v-if="hasSiderLayout">
|
|
44
|
+
<slot name="logo" />
|
|
45
|
+
</AppSidebar>
|
|
46
|
+
</LayoutTransition>
|
|
47
|
+
|
|
48
|
+
<LayoutTransition mode="height">
|
|
49
|
+
<AppHeader v-if="!isBlank">
|
|
50
|
+
<slot name="logo" />
|
|
51
|
+
</AppHeader>
|
|
52
|
+
</LayoutTransition>
|
|
53
|
+
|
|
54
|
+
<AppMain>
|
|
55
|
+
<slot />
|
|
56
|
+
</AppMain>
|
|
57
|
+
|
|
58
|
+
<LayoutTransition mode="height">
|
|
59
|
+
<AppFooter v-if="!isBlank && props.showFooter">
|
|
60
|
+
<slot name="footer" />
|
|
61
|
+
</AppFooter>
|
|
62
|
+
</LayoutTransition>
|
|
63
|
+
</n-layout>
|
|
64
|
+
</template>
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { Icon } from '@iconify/vue'
|
|
3
|
+
import { computed, h } from 'vue'
|
|
4
|
+
import { useRoute, useRouter } from 'vue-router'
|
|
5
|
+
|
|
6
|
+
const route = useRoute()
|
|
7
|
+
const router = useRouter()
|
|
8
|
+
|
|
9
|
+
const DEFAULT_ICON = 'icon-park-solid:web-page'
|
|
10
|
+
|
|
11
|
+
const crumbs = computed(() => {
|
|
12
|
+
// 使用 route.matched 获取当前路由匹配到的所有嵌套路由记录
|
|
13
|
+
// 这天然支持 404、动态路由等所有已注册的路由
|
|
14
|
+
const matched = route.matched
|
|
15
|
+
|
|
16
|
+
// 过滤掉重定向路由或无意义的根路径(视情况而定,这里先不过滤,完全遵照用户指示)
|
|
17
|
+
// 通常我们过滤掉没有 meta.title 的布局路由,但用户要求“没有就取name或者path”,说明要尽可能展示
|
|
18
|
+
|
|
19
|
+
return matched
|
|
20
|
+
.filter(item => item.meta?.breadcrumb !== false) // 支持通过 meta.breadcrumb: false 隐藏
|
|
21
|
+
.map((item) => {
|
|
22
|
+
// 优先使用 meta 中的 icon,否则使用默认图标
|
|
23
|
+
const iconName = (item.meta?.icon as string) || DEFAULT_ICON
|
|
24
|
+
const icon = () => h(Icon, { icon: iconName, width: '18px', height: '18px' })
|
|
25
|
+
|
|
26
|
+
// 优先使用 meta.title,其次 name,最后 path
|
|
27
|
+
const label = (item.meta?.title as string) || (item.name as string) || item.path
|
|
28
|
+
|
|
29
|
+
// 获取当前层级的子路由,用于下拉菜单
|
|
30
|
+
// item 是 RouteRecordNormalized,它的 children 包含了所有子路由配置
|
|
31
|
+
const children = (item.children || [])
|
|
32
|
+
.filter(child => child.meta?.hideMenu !== true) // 过滤掉 hideMenu: true 的路由
|
|
33
|
+
.map((child) => {
|
|
34
|
+
const childIconName = (child.meta?.icon as string) || DEFAULT_ICON
|
|
35
|
+
const childIcon = () => h(Icon, { icon: childIconName, width: '18px', height: '18px' })
|
|
36
|
+
return {
|
|
37
|
+
label: (child.meta?.title as string) || (child.name as string) || child.path,
|
|
38
|
+
key: (child.name as string) || child.path, // 优先用 name 跳转
|
|
39
|
+
icon: childIcon
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
label,
|
|
45
|
+
path: item.path, // 这里的 path 是绝对路径或匹配模式
|
|
46
|
+
name: item.name,
|
|
47
|
+
icon,
|
|
48
|
+
clickable: true, // 默认可点击
|
|
49
|
+
children
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
// 过滤掉 label 为空的项(防止空布局占位)
|
|
53
|
+
.filter(item => item.label)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
function onSelectChild(key: string | number) {
|
|
57
|
+
// 查找目标路由
|
|
58
|
+
// 由于 key 可能是 name 或 path,我们需要在当前 crumbs 的 children 中寻找
|
|
59
|
+
// 但这里简单处理,直接尝试跳转
|
|
60
|
+
if (typeof key === 'string') {
|
|
61
|
+
// 尝试解析路由
|
|
62
|
+
try {
|
|
63
|
+
// 如果 key 是 name
|
|
64
|
+
router.push({ name: key }).catch(() => {
|
|
65
|
+
// 如果失败,尝试作为 path
|
|
66
|
+
router.push(key)
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
catch (_) {
|
|
70
|
+
router.push(key)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
</script>
|
|
75
|
+
|
|
76
|
+
<template>
|
|
77
|
+
<n-breadcrumb class="px-2 w-full">
|
|
78
|
+
<n-breadcrumb-item v-for="item in crumbs" :key="item.path">
|
|
79
|
+
<n-flex :size="4">
|
|
80
|
+
<component :is="item.icon" v-if="item.icon" />
|
|
81
|
+
<n-dropdown
|
|
82
|
+
v-if="item.children.length"
|
|
83
|
+
:options="item.children"
|
|
84
|
+
trigger="hover"
|
|
85
|
+
@select="onSelectChild"
|
|
86
|
+
>
|
|
87
|
+
<n-flex
|
|
88
|
+
align="center"
|
|
89
|
+
:class="item.clickable ? 'cursor-pointer' : 'cursor-default opacity-80'"
|
|
90
|
+
>
|
|
91
|
+
<component :is="item.label" v-if="typeof item.label === 'function'" />
|
|
92
|
+
<span v-else>{{ item.label }}</span>
|
|
93
|
+
</n-flex>
|
|
94
|
+
</n-dropdown>
|
|
95
|
+
<n-flex
|
|
96
|
+
v-else
|
|
97
|
+
align="center"
|
|
98
|
+
:class="item.clickable ? 'cursor-pointer' : 'cursor-default opacity-80'"
|
|
99
|
+
>
|
|
100
|
+
<component :is="item.label" v-if="typeof item.label === 'function'" />
|
|
101
|
+
<span v-else>{{ item.label }}</span>
|
|
102
|
+
</n-flex>
|
|
103
|
+
</n-flex>
|
|
104
|
+
</n-breadcrumb-item>
|
|
105
|
+
</n-breadcrumb>
|
|
106
|
+
</template>
|
|
107
|
+
|
|
108
|
+
<style scoped></style>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { Slots } from 'vue'
|
|
3
|
+
import { computed, useSlots } from 'vue'
|
|
4
|
+
import { hasSlotContent } from '../../../share/slot'
|
|
5
|
+
import { SIDE_LAYOUT_TYPES } from '../const'
|
|
6
|
+
import { useContext } from '../context'
|
|
7
|
+
|
|
8
|
+
const { bordered, inverted, footerHeight, sideWidth, type } = useContext()!
|
|
9
|
+
const slots: Slots = useSlots()
|
|
10
|
+
const hasDefaultSlot = computed<boolean>(() => hasSlotContent(slots.default))
|
|
11
|
+
|
|
12
|
+
const footerStyles = computed(() => ({
|
|
13
|
+
height: `${unref(footerHeight)}px`,
|
|
14
|
+
left: SIDE_LAYOUT_TYPES.includes(unref(type)) ? `${unref(sideWidth)}px` : '0px',
|
|
15
|
+
zIndex: 1
|
|
16
|
+
}))
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<template>
|
|
20
|
+
<n-layout-footer position="absolute" :bordered="bordered" :inverted="inverted" :style="footerStyles">
|
|
21
|
+
<slot v-if="hasDefaultSlot" />
|
|
22
|
+
<n-flex v-else align="center" justify="center">
|
|
23
|
+
<span>这里是底部信息,可使用 {{ '<slot name="footer" />' }}覆盖此内容</span>
|
|
24
|
+
</n-flex>
|
|
25
|
+
</n-layout-footer>
|
|
26
|
+
</template>
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { Slots } from 'vue'
|
|
3
|
+
import { computed, ref, unref, useSlots, watch } from 'vue'
|
|
4
|
+
import { useRouter } from 'vue-router'
|
|
5
|
+
import { hasSlotContent } from '../../../share/slot'
|
|
6
|
+
import { TOP_LAYOUT_TYPES } from '../const'
|
|
7
|
+
import { useContext } from '../context'
|
|
8
|
+
import { findNodeByKey, renderMenuLabel, resolveMainSubFromActive } from '../utils'
|
|
9
|
+
import AppBreadcrumb from './AppBreadcrumb.vue'
|
|
10
|
+
import AppLeftLogoInfo from './AppLeftLogoInfo.vue'
|
|
11
|
+
|
|
12
|
+
const { type, bordered, inverted, headerHeight, sideWidth, collapsedWidth, activeKey, mainActiveKey, subActiveKey, hasSiderLayout, hasBreadcrumb, isLeftMain, isTopMain, isCollapsed, menuOptions: options, mainMenuOptions, subMenuOptions, updateActiveKey } = useContext()!
|
|
13
|
+
|
|
14
|
+
const left = computed<string | number>(() => {
|
|
15
|
+
if (unref(isTopMain))
|
|
16
|
+
return 0
|
|
17
|
+
|
|
18
|
+
if (unref(hasSiderLayout)) {
|
|
19
|
+
if (unref(isCollapsed))
|
|
20
|
+
return `${unref(collapsedWidth)}px`
|
|
21
|
+
|
|
22
|
+
return `${unref(sideWidth)}px`
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return 0
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
const headerStyle = computed(() => ({
|
|
29
|
+
height: `${unref(headerHeight)}px`,
|
|
30
|
+
zIndex: 1,
|
|
31
|
+
left: left.value,
|
|
32
|
+
padding: unref(hasSiderLayout) ? '0' : '0 16px'
|
|
33
|
+
}))
|
|
34
|
+
|
|
35
|
+
const showMenu = computed<boolean>(() => TOP_LAYOUT_TYPES.includes(unref(type)!))
|
|
36
|
+
const isTopMenu = computed<boolean>(() => unref(type) === 'top-menu')
|
|
37
|
+
|
|
38
|
+
const active = ref('')
|
|
39
|
+
|
|
40
|
+
const menuOptions = computed(() => {
|
|
41
|
+
if (isTopMenu.value)
|
|
42
|
+
return unref(options)
|
|
43
|
+
return unref(isTopMain) ? unref(mainMenuOptions) : unref(subMenuOptions)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
// 统一处理激活状态
|
|
47
|
+
watch(() => unref(activeKey), (newKey) => {
|
|
48
|
+
if (!newKey)
|
|
49
|
+
return
|
|
50
|
+
|
|
51
|
+
const { mainKey, subKey } = resolveMainSubFromActive(unref(options) as any[], newKey)
|
|
52
|
+
mainActiveKey.value = mainKey || ''
|
|
53
|
+
subActiveKey.value = subKey || ''
|
|
54
|
+
|
|
55
|
+
if (isTopMenu.value) {
|
|
56
|
+
active.value = newKey
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
if (unref(isTopMain)) {
|
|
60
|
+
active.value = mainKey || ''
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (unref(isLeftMain)) {
|
|
64
|
+
active.value = subKey || ''
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}, { immediate: true })
|
|
68
|
+
|
|
69
|
+
const router = useRouter()
|
|
70
|
+
function handleUpdateValue(key: string) {
|
|
71
|
+
const node = findNodeByKey(unref(options) as any[], key)
|
|
72
|
+
if (node?.href || /^https?:\/\//.test(key)) {
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
updateActiveKey(key)
|
|
77
|
+
active.value = key
|
|
78
|
+
if (unref(isTopMain) && !isTopMenu.value) {
|
|
79
|
+
const { subKey } = resolveMainSubFromActive(unref(options) as any[], key)
|
|
80
|
+
if (subKey) {
|
|
81
|
+
router.push({ name: subKey })
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const slots: Slots = useSlots()
|
|
87
|
+
const hasDefaultSlot = computed<boolean>(() => hasSlotContent(slots.default))
|
|
88
|
+
</script>
|
|
89
|
+
|
|
90
|
+
<template>
|
|
91
|
+
<n-layout-header position="absolute" :bordered="bordered" :inverted="inverted" :style="headerStyle">
|
|
92
|
+
<n-flex align="center" :wrap="false" class="w-full" :style="{ height: `${headerHeight}px` }">
|
|
93
|
+
<template v-if="isTopMain">
|
|
94
|
+
<slot v-if="hasDefaultSlot" />
|
|
95
|
+
<AppLeftLogoInfo v-else />
|
|
96
|
+
</template>
|
|
97
|
+
|
|
98
|
+
<AppBreadcrumb v-if="hasBreadcrumb" />
|
|
99
|
+
<n-menu
|
|
100
|
+
v-if="showMenu"
|
|
101
|
+
v-model:value="active"
|
|
102
|
+
class="flex-1"
|
|
103
|
+
mode="horizontal"
|
|
104
|
+
:options="menuOptions"
|
|
105
|
+
:render-label="renderMenuLabel"
|
|
106
|
+
responsive
|
|
107
|
+
:inverted="inverted"
|
|
108
|
+
@update:value="handleUpdateValue"
|
|
109
|
+
/>
|
|
110
|
+
</n-flex>
|
|
111
|
+
</n-layout-header>
|
|
112
|
+
</template>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { useContext } from '../context'
|
|
3
|
+
|
|
4
|
+
const { isCollapsed, headerHeight, isTopMain, hasSiderLayout, siderWidth } = useContext()!
|
|
5
|
+
|
|
6
|
+
const isMixed = computed(() => unref(isCollapsed))
|
|
7
|
+
|
|
8
|
+
function getContainerStyle(_: void) {
|
|
9
|
+
return {
|
|
10
|
+
height: `${unref(headerHeight)}px`,
|
|
11
|
+
minWidth: `${unref(siderWidth)}px`,
|
|
12
|
+
display: 'flex',
|
|
13
|
+
alignItems: 'center',
|
|
14
|
+
padding: isMixed.value && !isTopMain ? '0' : '0 20px',
|
|
15
|
+
justifyContent: isMixed.value ? 'center' : 'flex-start'
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<template>
|
|
21
|
+
<n-flex align="center" :wrap="false" :justify="isMixed && !isTopMain ? 'center' : 'flex-start'" :style="hasSiderLayout ? getContainerStyle() : {}" class="cursor-pointer select-none">
|
|
22
|
+
<i class=" i-skill-icons:vuejs-dark text-3xl" />
|
|
23
|
+
<div
|
|
24
|
+
class=" transition-width duration-200"
|
|
25
|
+
:class="{ 'opacity-0 w-0 absolute': isMixed && !isTopMain }"
|
|
26
|
+
>
|
|
27
|
+
明天会好的111
|
|
28
|
+
</div>
|
|
29
|
+
</n-flex>
|
|
30
|
+
</template>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { useContext } from '../context'
|
|
3
|
+
|
|
4
|
+
const { headerHeight, footerHeight, type, isCollapsed, collapsedWidth, sideWidth } = useContext()
|
|
5
|
+
|
|
6
|
+
const isBlank = computed(() => unref(type) === 'blank')
|
|
7
|
+
|
|
8
|
+
const left = computed(() => {
|
|
9
|
+
if (isBlank.value || unref(type) === 'top-menu')
|
|
10
|
+
return 0
|
|
11
|
+
if (unref(isCollapsed))
|
|
12
|
+
return `${unref(collapsedWidth)}px`
|
|
13
|
+
return `${unref(sideWidth)}px`
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
const mainStyle = computed(() => ({
|
|
17
|
+
top: isBlank.value ? 0 : `${unref(headerHeight)}px`,
|
|
18
|
+
left: left.value,
|
|
19
|
+
bottom: isBlank.value ? 0 : `${unref(footerHeight)}px`
|
|
20
|
+
}))
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<template>
|
|
24
|
+
<n-layout-content
|
|
25
|
+
position="absolute"
|
|
26
|
+
embedded
|
|
27
|
+
:style="mainStyle"
|
|
28
|
+
:native-scrollbar="false"
|
|
29
|
+
content-style="padding: 16px;"
|
|
30
|
+
>
|
|
31
|
+
<slot />
|
|
32
|
+
<RouterView />
|
|
33
|
+
</n-layout-content>
|
|
34
|
+
</template>
|