@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.
Files changed (171) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +86 -0
  3. package/dist/components/breadcrumb/index.d.ts +0 -0
  4. package/dist/components/breadcrumb/index.vue.d.ts +3 -0
  5. package/dist/components/breadcrumb/props.d.ts +0 -0
  6. package/dist/components/button/action/index.d.ts +3 -0
  7. package/dist/components/button/action/index.vue.d.ts +118 -0
  8. package/dist/components/button/action/props.d.ts +63 -0
  9. package/dist/components/button/action/utils.d.ts +8 -0
  10. package/dist/components/button/base/index.d.ts +3 -0
  11. package/dist/components/button/base/index.vue.d.ts +36 -0
  12. package/dist/components/button/base/props.d.ts +27 -0
  13. package/dist/components/button/index.d.ts +4 -0
  14. package/dist/components/button/types.d.ts +2 -0
  15. package/dist/components/form/helper.d.ts +11 -0
  16. package/dist/components/form/index.d.ts +3 -0
  17. package/dist/components/form/index.vue.d.ts +642 -0
  18. package/dist/components/form/props.d.ts +34 -0
  19. package/dist/components/icon/IconPicker.vue.d.ts +13 -0
  20. package/dist/components/icon/iconify.d.ts +25 -0
  21. package/dist/components/icon/index.d.ts +3 -0
  22. package/dist/components/icon/index.vue.d.ts +12 -0
  23. package/dist/components/layout/const.d.ts +22 -0
  24. package/dist/components/layout/context.d.ts +77 -0
  25. package/dist/components/layout/index.d.ts +5 -0
  26. package/dist/components/layout/index.vue.d.ts +80 -0
  27. package/dist/components/layout/layout-parts/AppBreadcrumb.vue.d.ts +3 -0
  28. package/dist/components/layout/layout-parts/AppFooter.vue.d.ts +18 -0
  29. package/dist/components/layout/layout-parts/AppHeader.vue.d.ts +18 -0
  30. package/dist/components/layout/layout-parts/AppLeftLogoInfo.vue.d.ts +3 -0
  31. package/dist/components/layout/layout-parts/AppMain.vue.d.ts +18 -0
  32. package/dist/components/layout/layout-parts/AppSidebar.vue.d.ts +4067 -0
  33. package/dist/components/layout/layout-parts/LayoutTransition.vue.d.ts +58 -0
  34. package/dist/components/layout/mode.d.ts +0 -0
  35. package/dist/components/layout/props.d.ts +35 -0
  36. package/dist/components/layout/types.d.ts +59 -0
  37. package/dist/components/layout/utils.d.ts +97 -0
  38. package/dist/components/provider/index.d.ts +3 -0
  39. package/dist/components/provider/index.vue.d.ts +19 -0
  40. package/dist/components/provider/props.d.ts +33 -0
  41. package/dist/components/search-bar/index.d.ts +3 -0
  42. package/dist/components/search-bar/index.vue.d.ts +1288 -0
  43. package/dist/components/search-bar/props.d.ts +15 -0
  44. package/dist/components/table/TableSetting.vue.d.ts +15 -0
  45. package/dist/components/table/index.d.ts +4 -0
  46. package/dist/components/table/index.vue.d.ts +17246 -0
  47. package/dist/components/table/props.d.ts +26 -0
  48. package/dist/components/table/useColumn.d.ts +15 -0
  49. package/dist/components/upload/enum.d.ts +18 -0
  50. package/dist/components/upload/index.d.ts +4 -0
  51. package/dist/components/upload/index.vue.d.ts +17 -0
  52. package/dist/components/upload/props.d.ts +7 -0
  53. package/dist/const/defaults.d.ts +7 -0
  54. package/dist/const/index.d.ts +2 -0
  55. package/dist/const/types.d.ts +134 -0
  56. package/dist/context/color.d.ts +13 -0
  57. package/dist/context/common.d.ts +117 -0
  58. package/dist/context/index.d.ts +41 -0
  59. package/dist/context/layout.d.ts +52 -0
  60. package/dist/context/loading-bar.d.ts +14 -0
  61. package/dist/context/locale.d.ts +143 -0
  62. package/dist/context/menu.d.ts +212 -0
  63. package/dist/context/message.d.ts +14 -0
  64. package/dist/context/notification.d.ts +14 -0
  65. package/dist/context/table.d.ts +917 -0
  66. package/dist/context/theme.d.ts +20 -0
  67. package/dist/hooks/index.d.ts +6 -0
  68. package/dist/hooks/useAdmin.d.ts +0 -0
  69. package/dist/hooks/useForm.d.ts +54 -0
  70. package/dist/hooks/useLayout.d.ts +116 -0
  71. package/dist/hooks/useProviderContext.d.ts +17 -0
  72. package/dist/hooks/useTable.d.ts +66 -0
  73. package/dist/hooks/useThemeOverrides.d.ts +8 -0
  74. package/dist/hooks/useUpload.d.ts +22 -0
  75. package/dist/index.css +36 -0
  76. package/dist/index.d.ts +29 -0
  77. package/dist/index.js +6771 -0
  78. package/dist/share/compact.d.ts +16 -0
  79. package/dist/share/index.d.ts +2 -0
  80. package/dist/share/menu.d.ts +0 -0
  81. package/dist/share/route.d.ts +0 -0
  82. package/dist/share/slot.d.ts +6 -0
  83. package/dist/utils/form.d.ts +0 -0
  84. package/dist/utils/index.d.ts +0 -0
  85. package/dist/utils/transformRoutes.d.ts +67 -0
  86. package/dist/utils/tree.d.ts +6 -0
  87. package/package.json +53 -0
  88. package/src/auto-imports.d.ts +73 -0
  89. package/src/components/breadcrumb/index.ts +0 -0
  90. package/src/components/breadcrumb/index.vue +0 -0
  91. package/src/components/breadcrumb/props.ts +0 -0
  92. package/src/components/button/action/index.ts +4 -0
  93. package/src/components/button/action/index.vue +313 -0
  94. package/src/components/button/action/props.ts +78 -0
  95. package/src/components/button/action/utils.ts +122 -0
  96. package/src/components/button/base/index.ts +4 -0
  97. package/src/components/button/base/index.vue +156 -0
  98. package/src/components/button/base/props.ts +29 -0
  99. package/src/components/button/index.ts +4 -0
  100. package/src/components/button/types.ts +2 -0
  101. package/src/components/form/helper.ts +73 -0
  102. package/src/components/form/index.ts +5 -0
  103. package/src/components/form/index.vue +243 -0
  104. package/src/components/form/props.ts +75 -0
  105. package/src/components/icon/IconPicker.vue +255 -0
  106. package/src/components/icon/iconify.ts +80 -0
  107. package/src/components/icon/index.ts +7 -0
  108. package/src/components/icon/index.vue +23 -0
  109. package/src/components/layout/const.ts +102 -0
  110. package/src/components/layout/context.ts +189 -0
  111. package/src/components/layout/index.ts +8 -0
  112. package/src/components/layout/index.vue +64 -0
  113. package/src/components/layout/layout-parts/AppBreadcrumb.vue +108 -0
  114. package/src/components/layout/layout-parts/AppFooter.vue +26 -0
  115. package/src/components/layout/layout-parts/AppHeader.vue +112 -0
  116. package/src/components/layout/layout-parts/AppLeftLogoInfo.vue +30 -0
  117. package/src/components/layout/layout-parts/AppMain.vue +34 -0
  118. package/src/components/layout/layout-parts/AppSidebar.vue +174 -0
  119. package/src/components/layout/layout-parts/LayoutTransition.vue +366 -0
  120. package/src/components/layout/mode.ts +0 -0
  121. package/src/components/layout/props.ts +36 -0
  122. package/src/components/layout/types.ts +79 -0
  123. package/src/components/layout/utils.ts +201 -0
  124. package/src/components/provider/index.ts +5 -0
  125. package/src/components/provider/index.vue +69 -0
  126. package/src/components/provider/props.ts +45 -0
  127. package/src/components/search-bar/index.ts +5 -0
  128. package/src/components/search-bar/index.vue +282 -0
  129. package/src/components/search-bar/props.ts +26 -0
  130. package/src/components/table/TableSetting.vue +253 -0
  131. package/src/components/table/index.ts +14 -0
  132. package/src/components/table/index.vue +179 -0
  133. package/src/components/table/props.ts +29 -0
  134. package/src/components/table/useColumn.ts +104 -0
  135. package/src/components/upload/enum.ts +21 -0
  136. package/src/components/upload/index.ts +9 -0
  137. package/src/components/upload/index.vue +267 -0
  138. package/src/components/upload/props.ts +8 -0
  139. package/src/components.d.ts +154 -0
  140. package/src/const/defaults.ts +94 -0
  141. package/src/const/index.ts +2 -0
  142. package/src/const/types.ts +139 -0
  143. package/src/context/color.ts +53 -0
  144. package/src/context/common.ts +27 -0
  145. package/src/context/index.ts +141 -0
  146. package/src/context/layout.ts +34 -0
  147. package/src/context/loading-bar.ts +26 -0
  148. package/src/context/locale.ts +22 -0
  149. package/src/context/menu.ts +26 -0
  150. package/src/context/message.ts +30 -0
  151. package/src/context/notification.ts +29 -0
  152. package/src/context/table.ts +32 -0
  153. package/src/context/theme.ts +35 -0
  154. package/src/hooks/index.ts +6 -0
  155. package/src/hooks/useAdmin.ts +0 -0
  156. package/src/hooks/useForm.ts +272 -0
  157. package/src/hooks/useLayout.ts +300 -0
  158. package/src/hooks/useProviderContext.ts +47 -0
  159. package/src/hooks/useTable.ts +241 -0
  160. package/src/hooks/useThemeOverrides.ts +18 -0
  161. package/src/hooks/useUpload.ts +82 -0
  162. package/src/index.ts +59 -0
  163. package/src/share/compact.ts +35 -0
  164. package/src/share/index.ts +2 -0
  165. package/src/share/menu.ts +0 -0
  166. package/src/share/route.ts +0 -0
  167. package/src/share/slot.ts +29 -0
  168. package/src/utils/form.ts +0 -0
  169. package/src/utils/index.ts +0 -0
  170. package/src/utils/transformRoutes.ts +163 -0
  171. 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,8 @@
1
+ import QuiLayout from './index.vue'
2
+
3
+ export { DEFAULT_LAYOUT_TYPE } from './const'
4
+
5
+ export { QuiLayout }
6
+ export type { Props as LayoutProps } from './props'
7
+
8
+ export type { LayoutType, RouteMeta } from './types'
@@ -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>