@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,272 @@
|
|
|
1
|
+
import type { Ref } from 'vue'
|
|
2
|
+
import type { FormSchema } from '../components/form/props'
|
|
3
|
+
|
|
4
|
+
interface Option { label: string, value: any, disabled?: boolean }
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 基于表单 `schemas` 提供便捷的查询与更新工具
|
|
8
|
+
*
|
|
9
|
+
* 适用于外部通过接口动态配置下拉选项、默认值、组件属性等场景,
|
|
10
|
+
* 对传入的 `schemas` 进行就地更新(保持响应式)。
|
|
11
|
+
*
|
|
12
|
+
* @param schemas - 表单项描述数组,建议传入响应式引用
|
|
13
|
+
* @returns 返回一组对 `schemas` 的操作方法
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* const { setOptions, setComponentProps } = useForm(schemas)
|
|
18
|
+
* setOptions('status', [{ label: '启用', value: 1 }])
|
|
19
|
+
* setComponentProps('status', { clearable: true })
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* @remarks
|
|
23
|
+
* - 所有更新均为原地更新,适配 Vue 的响应式数组/对象
|
|
24
|
+
* - 仅对存在的字段进行更新,不会抛异常
|
|
25
|
+
*
|
|
26
|
+
* @security
|
|
27
|
+
* 请勿在 `componentProps` 中写入敏感信息
|
|
28
|
+
*
|
|
29
|
+
* @performance
|
|
30
|
+
* 操作复杂度均为 O(n) 或更低,适合一般表单规模
|
|
31
|
+
*/
|
|
32
|
+
export function useForm(schemas: FormSchema[], formRef?: Ref<any>) {
|
|
33
|
+
/**
|
|
34
|
+
* 查找指定字段的 schema
|
|
35
|
+
*
|
|
36
|
+
* @param field - 字段名
|
|
37
|
+
* @returns 命中的 `FormSchema` 或 `undefined`
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```ts
|
|
41
|
+
* const schema = useItemByField('status')
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
function useItemByField(field: string) {
|
|
45
|
+
return schemas.find(item => item.field === field)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 为 Select/RadioGroup/Checkbox 等具备 `options` 的组件设置选项
|
|
50
|
+
*
|
|
51
|
+
* @param field - 字段名
|
|
52
|
+
* @param options - 选项数组,包含 `label` 与 `value`
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```ts
|
|
56
|
+
* setOptions('status', [
|
|
57
|
+
* { label: '未上架', value: 0 },
|
|
58
|
+
* { label: '已上架', value: 1 }
|
|
59
|
+
* ])
|
|
60
|
+
* ```
|
|
61
|
+
*
|
|
62
|
+
* @remarks
|
|
63
|
+
* - 支持的组件:`NSelect`、`NRadioGroup`、`NCheckbox`
|
|
64
|
+
*/
|
|
65
|
+
function setOptions(field: string, options: Option[]) {
|
|
66
|
+
const target = useItemByField(field)
|
|
67
|
+
if (!target)
|
|
68
|
+
return
|
|
69
|
+
const comp = target.component
|
|
70
|
+
const allow = comp === 'NSelect' || comp === 'NRadioGroup' || comp === 'NCheckbox'
|
|
71
|
+
if (!allow)
|
|
72
|
+
return
|
|
73
|
+
if (!target.componentProps || typeof target.componentProps !== 'object') {
|
|
74
|
+
target.componentProps = {}
|
|
75
|
+
}
|
|
76
|
+
target.componentProps.options = options
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 获取指定字段的选项数组
|
|
81
|
+
*
|
|
82
|
+
* @param field - 字段名
|
|
83
|
+
* @returns 选项数组或 `undefined`
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* ```ts
|
|
87
|
+
* const opts = getOptions('status')
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
function getOptions(field: string): Option[] | undefined {
|
|
91
|
+
const target = useItemByField(field)
|
|
92
|
+
return target?.componentProps?.options
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 合并更新组件属性 `componentProps`
|
|
97
|
+
*
|
|
98
|
+
* @param field - 字段名
|
|
99
|
+
* @param props - 待合并的属性对象
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* ```ts
|
|
103
|
+
* setComponentProps('status', { clearable: true })
|
|
104
|
+
* ```
|
|
105
|
+
*/
|
|
106
|
+
function setComponentProps(field: string, props: Record<string, any>) {
|
|
107
|
+
const target = useItemByField(field)
|
|
108
|
+
if (!target)
|
|
109
|
+
return
|
|
110
|
+
if (!target.componentProps || typeof target.componentProps !== 'object') {
|
|
111
|
+
target.componentProps = {}
|
|
112
|
+
}
|
|
113
|
+
Object.assign(target.componentProps, props)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* 设置单个字段的默认值
|
|
118
|
+
*
|
|
119
|
+
* @param field - 字段名
|
|
120
|
+
* @param value - 默认值
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* ```ts
|
|
124
|
+
* setDefaultValue('status', 1)
|
|
125
|
+
* ```
|
|
126
|
+
*/
|
|
127
|
+
function setDefaultValue(field: string, value: any) {
|
|
128
|
+
const target = useItemByField(field)
|
|
129
|
+
if (!target)
|
|
130
|
+
return
|
|
131
|
+
target.defaultValue = value
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* 局部更新指定字段的 schema
|
|
136
|
+
*
|
|
137
|
+
* @param field - 字段名
|
|
138
|
+
* @param patch - 局部更新内容
|
|
139
|
+
*
|
|
140
|
+
* @example
|
|
141
|
+
* ```ts
|
|
142
|
+
* replaceSchema('status', { label: '上架状态', component: 'NSelect' })
|
|
143
|
+
* ```
|
|
144
|
+
*/
|
|
145
|
+
function replaceSchema(field: string, patch: Partial<FormSchema>) {
|
|
146
|
+
const target = useItemByField(field)
|
|
147
|
+
if (!target)
|
|
148
|
+
return
|
|
149
|
+
Object.assign(target, patch)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* 新增或替换(按 `field` 唯一)一个 schema
|
|
154
|
+
*
|
|
155
|
+
* @param schema - 完整的表单项描述
|
|
156
|
+
*
|
|
157
|
+
* @example
|
|
158
|
+
* ```ts
|
|
159
|
+
* upsertSchema({ field: 'title', label: '标题', component: 'NInput' })
|
|
160
|
+
* ```
|
|
161
|
+
*/
|
|
162
|
+
function upsertSchema(schema: FormSchema) {
|
|
163
|
+
const idx = schemas.findIndex(s => s.field === schema.field)
|
|
164
|
+
if (idx >= 0) {
|
|
165
|
+
schemas[idx] = { ...schemas[idx], ...schema }
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
schemas.push(schema)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* 移除指定字段的 schema
|
|
174
|
+
*
|
|
175
|
+
* @param field - 字段名
|
|
176
|
+
* @returns 是否删除成功
|
|
177
|
+
*
|
|
178
|
+
* @example
|
|
179
|
+
* ```ts
|
|
180
|
+
* const ok = removeSchema('status')
|
|
181
|
+
* ```
|
|
182
|
+
*/
|
|
183
|
+
function removeSchema(field: string): boolean {
|
|
184
|
+
const idx = schemas.findIndex(s => s.field === field)
|
|
185
|
+
if (idx < 0)
|
|
186
|
+
return false
|
|
187
|
+
schemas.splice(idx, 1)
|
|
188
|
+
return true
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* 列出所有字段名
|
|
193
|
+
*
|
|
194
|
+
* @returns 字段名数组
|
|
195
|
+
*
|
|
196
|
+
* @example
|
|
197
|
+
* ```ts
|
|
198
|
+
* const fields = listFields()
|
|
199
|
+
* ```
|
|
200
|
+
*/
|
|
201
|
+
function listFields(): string[] {
|
|
202
|
+
return schemas.map(s => String(s.field))
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* 从 `schemas` 生成规则映射(`path -> rules[]`)
|
|
207
|
+
*
|
|
208
|
+
* @returns 规则映射对象
|
|
209
|
+
*
|
|
210
|
+
* @example
|
|
211
|
+
* ```ts
|
|
212
|
+
* const rules = toRulesMap()
|
|
213
|
+
* ```
|
|
214
|
+
*/
|
|
215
|
+
function toRulesMap(): Record<string, object[]> {
|
|
216
|
+
const acc: Record<string, object[]> = {}
|
|
217
|
+
schemas.forEach((s) => {
|
|
218
|
+
if (Array.isArray(s.rules) && s.rules.length) {
|
|
219
|
+
acc[String(s.field)] = s.rules
|
|
220
|
+
}
|
|
221
|
+
})
|
|
222
|
+
return acc
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function getFormRef() {
|
|
226
|
+
return formRef?.value
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function bindFormRef(ref: Ref<any>) {
|
|
230
|
+
;(formRef as any) = ref
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function validate(): Promise<void> | undefined {
|
|
234
|
+
return formRef?.value?.validate()
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function resetFields(): Promise<void> | undefined {
|
|
238
|
+
return formRef?.value?.resetFields()
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function clearValidate(): Promise<void> | undefined {
|
|
242
|
+
return formRef?.value?.clearValidate()
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function getFieldsValue(): Record<string, any> | undefined {
|
|
246
|
+
return formRef?.value?.getFieldsValue()
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function setFieldsValue(values: Record<string, any>): void {
|
|
250
|
+
formRef?.value?.setFieldsValue(values)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
useItemByField,
|
|
255
|
+
setOptions,
|
|
256
|
+
getOptions,
|
|
257
|
+
setComponentProps,
|
|
258
|
+
setDefaultValue,
|
|
259
|
+
replaceSchema,
|
|
260
|
+
upsertSchema,
|
|
261
|
+
removeSchema,
|
|
262
|
+
listFields,
|
|
263
|
+
toRulesMap,
|
|
264
|
+
getFormRef,
|
|
265
|
+
bindFormRef,
|
|
266
|
+
validate,
|
|
267
|
+
resetFields,
|
|
268
|
+
clearValidate,
|
|
269
|
+
getFieldsValue,
|
|
270
|
+
setFieldsValue
|
|
271
|
+
}
|
|
272
|
+
}
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
/** 提供给用户的布局上下文以及一些协助使用的函数 */
|
|
2
|
+
|
|
3
|
+
import type { MenuOption } from 'naive-ui'
|
|
4
|
+
import type { ComputedRef, Ref, WritableComputedRef } from 'vue'
|
|
5
|
+
import type { RouteRecordRaw } from 'vue-router'
|
|
6
|
+
import type { LayoutType, RouteMeta } from '../components/layout/types'
|
|
7
|
+
import { computed, isRef, onUnmounted, reactive, ref, watch } from 'vue'
|
|
8
|
+
import { useRoute, useRouter } from 'vue-router'
|
|
9
|
+
import { DEFAULT_LAYOUT_PROPS } from '../components/layout/const'
|
|
10
|
+
import { filterRouteTree, normalizeAndRedirect, sortRouteTree, toRouteTree } from '../utils/transformRoutes'
|
|
11
|
+
|
|
12
|
+
// 仅在此文件内用简化上下文类型,避免类型实例化过深
|
|
13
|
+
export interface UseLayoutContext {
|
|
14
|
+
type: 'left-menu' | string
|
|
15
|
+
bordered: boolean
|
|
16
|
+
inverted: boolean
|
|
17
|
+
isCollapsed: boolean
|
|
18
|
+
headerHeight: number
|
|
19
|
+
footerHeight: number
|
|
20
|
+
showFooter: boolean
|
|
21
|
+
siderWidth: number
|
|
22
|
+
siderMixedWidth: number
|
|
23
|
+
collapsedWidth: number
|
|
24
|
+
routes: RouteLike[]
|
|
25
|
+
activeKey: string
|
|
26
|
+
menuOptions: MenuOption[]
|
|
27
|
+
accordion: boolean
|
|
28
|
+
homePath?: string
|
|
29
|
+
excludePaths?: string[]
|
|
30
|
+
updateIsCollapsed: (v: boolean) => void
|
|
31
|
+
updateInverted: (v: boolean) => void
|
|
32
|
+
updateActiveKey: (v: string) => void
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface UseLayoutReturn {
|
|
36
|
+
collapsed: Ref<boolean>
|
|
37
|
+
baseRoutes: any
|
|
38
|
+
addRoute: (route: RouteRecordRaw) => void
|
|
39
|
+
addRoutes: (routes: RouteRecordRaw[]) => void
|
|
40
|
+
removeRoute: (id: string) => void
|
|
41
|
+
toggle: () => void
|
|
42
|
+
setCollapsed: (v: boolean) => void
|
|
43
|
+
activeKey: Ref<string>
|
|
44
|
+
setActiveKey: (key: string) => void
|
|
45
|
+
menuOptions: ComputedRef<MenuOption[]>
|
|
46
|
+
context: UseLayoutContext
|
|
47
|
+
type: WritableComputedRef<LayoutType>
|
|
48
|
+
bordered: Ref<boolean>
|
|
49
|
+
inverted: Ref<boolean>
|
|
50
|
+
headerHeight: Ref<number>
|
|
51
|
+
footerHeight: Ref<number>
|
|
52
|
+
siderWidth: Ref<number>
|
|
53
|
+
siderMixedWidth: Ref<number>
|
|
54
|
+
collapsedWidth: Ref<number>
|
|
55
|
+
accordion: Ref<boolean>
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 获取扁平路由列表(基于 Vue Router)
|
|
60
|
+
*
|
|
61
|
+
* 读取 `useRouter().getRoutes()` 并转换为简化的 `RouteRecordRaw[]`(扁平结构)。
|
|
62
|
+
* 自动补充 `meta.title` 兜底为 `name`,并过滤 `meta.showInMenu === false` 的项。
|
|
63
|
+
*
|
|
64
|
+
* @returns 扁平路由数组,适配菜单转换
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* ```ts
|
|
68
|
+
* const routes = getRoutes()
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
export interface RouteLike {
|
|
72
|
+
path: string
|
|
73
|
+
name?: string
|
|
74
|
+
meta?: RouteMeta
|
|
75
|
+
children?: RouteLike[]
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function useActiveKey(initialKey = ''): {
|
|
79
|
+
activeKey: Ref<string>
|
|
80
|
+
setActiveKey: (key: string) => void
|
|
81
|
+
} {
|
|
82
|
+
const activeKey = ref<string>(initialKey)
|
|
83
|
+
const setActiveKey = (key: string) => {
|
|
84
|
+
activeKey.value = key
|
|
85
|
+
}
|
|
86
|
+
return { activeKey, setActiveKey }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 创建布局辅助方法集合
|
|
91
|
+
*
|
|
92
|
+
* 提供统一的布局状态管理(折叠、激活键)、菜单生成以及用于 provide/inject 的上下文对象构建。
|
|
93
|
+
*
|
|
94
|
+
* @param option - 布局初始化与参数配置
|
|
95
|
+
* @param option.routes - 路由数组或其 Ref(用于生成菜单)
|
|
96
|
+
* @param option.initialCollapsed - 初始折叠状态,默认 false
|
|
97
|
+
* @param option.initialActiveKey - 初始激活菜单键,默认 ''
|
|
98
|
+
* @param option.bordered - 是否显示边框,支持布尔或 Ref
|
|
99
|
+
* @param option.inverted - 是否启用反色样式,支持布尔或 Ref
|
|
100
|
+
* @param option.headerHeight - 头部高度,支持 number 或 Ref<number>
|
|
101
|
+
* @param option.footerHeight - 底部高度,支持 number 或 Ref<number>
|
|
102
|
+
* @param option.siderWidth - 侧栏宽度,支持 number 或 Ref<number>
|
|
103
|
+
* @param option.collapsedWidth - 折叠宽度,支持 number 或 Ref<number>
|
|
104
|
+
* @returns 返回包含状态控制、菜单选项与上下文的辅助对象
|
|
105
|
+
* @throws {TypeError} 在编译期校验参数类型,不在运行时抛错
|
|
106
|
+
*
|
|
107
|
+
* @remarks
|
|
108
|
+
* - 统一创建 ComputedRef 包裹的配置,确保在 provide/inject 下保持响应式
|
|
109
|
+
* - `routes` 支持数组或 Ref,内部自动转换为响应式
|
|
110
|
+
*
|
|
111
|
+
* @security
|
|
112
|
+
* - 无副作用,仅管理 UI 状态与映射
|
|
113
|
+
*
|
|
114
|
+
* @performance
|
|
115
|
+
* - 主要开销为菜单映射与少量 computed,通常可忽略
|
|
116
|
+
*/
|
|
117
|
+
|
|
118
|
+
export function useLayout(option: {
|
|
119
|
+
baseRoutes: Readonly<Ref<RouteRecordRaw[]> | RouteRecordRaw[]>
|
|
120
|
+
menuOptions?: Ref<MenuOption[]> | MenuOption[]
|
|
121
|
+
initialCollapsed?: boolean
|
|
122
|
+
initialActiveKey?: string
|
|
123
|
+
type?: LayoutType
|
|
124
|
+
bordered?: boolean
|
|
125
|
+
inverted?: boolean
|
|
126
|
+
headerHeight?: number
|
|
127
|
+
footerHeight?: number
|
|
128
|
+
showFooter?: boolean
|
|
129
|
+
siderWidth?: number
|
|
130
|
+
siderMixedWidth?: number
|
|
131
|
+
collapsedWidth?: number
|
|
132
|
+
accordion?: boolean
|
|
133
|
+
homePath?: string
|
|
134
|
+
excludePaths?: string[]
|
|
135
|
+
}): UseLayoutReturn {
|
|
136
|
+
const route = useRoute()
|
|
137
|
+
const router = useRouter()
|
|
138
|
+
|
|
139
|
+
// 状态管理
|
|
140
|
+
const collapsed = ref(option.initialCollapsed ?? false)
|
|
141
|
+
const { activeKey, setActiveKey } = useActiveKey(option.initialActiveKey ?? option.homePath ?? '/')
|
|
142
|
+
|
|
143
|
+
// 全局布局类型(当页面没有指定 meta.layout 时使用)
|
|
144
|
+
const globalType = ref<LayoutType>(option.type ?? DEFAULT_LAYOUT_PROPS.type)
|
|
145
|
+
|
|
146
|
+
// 最终使用的布局类型(优先使用页面 meta.layout)
|
|
147
|
+
const type = computed<LayoutType>({
|
|
148
|
+
get: () => (route.meta.layout as LayoutType) || globalType.value,
|
|
149
|
+
set: (v) => {
|
|
150
|
+
globalType.value = v
|
|
151
|
+
}
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
// 配置项全部转为 ref(统一响应式)
|
|
155
|
+
const bordered = ref(option.bordered ?? DEFAULT_LAYOUT_PROPS.bordered)
|
|
156
|
+
const inverted = ref(option.inverted ?? DEFAULT_LAYOUT_PROPS.inverted)
|
|
157
|
+
const headerHeight = ref(option.headerHeight ?? DEFAULT_LAYOUT_PROPS.headerHeight)
|
|
158
|
+
const footerHeight = ref(option.footerHeight ?? DEFAULT_LAYOUT_PROPS.footerHeight)
|
|
159
|
+
const showFooter = ref(option.showFooter ?? DEFAULT_LAYOUT_PROPS.showFooter)
|
|
160
|
+
const siderWidth = ref(option.siderWidth ?? DEFAULT_LAYOUT_PROPS.siderWidth)
|
|
161
|
+
const siderMixedWidth = ref(option.siderMixedWidth ?? DEFAULT_LAYOUT_PROPS.siderMixedWidth)
|
|
162
|
+
const collapsedWidth = ref(option.collapsedWidth ?? DEFAULT_LAYOUT_PROPS.collapsedWidth)
|
|
163
|
+
const accordion = ref(option.accordion ?? DEFAULT_LAYOUT_PROPS.accordion)
|
|
164
|
+
|
|
165
|
+
// 基础路由处理
|
|
166
|
+
const baseRoutes = isRef(option.baseRoutes) ? option.baseRoutes : ref(option.baseRoutes)
|
|
167
|
+
const normalizedBaseRoutes = computed(() => normalizeAndRedirect(baseRoutes.value as RouteRecordRaw[]))
|
|
168
|
+
const routesTree = computed<RouteLike[]>(() => {
|
|
169
|
+
const tree = toRouteTree(normalizedBaseRoutes.value) as any
|
|
170
|
+
const sorted = sortRouteTree(tree)
|
|
171
|
+
const filtered = filterRouteTree(sorted as any, option.excludePaths ?? [])
|
|
172
|
+
return filtered as any
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
const menuOptions = computed<MenuOption[]>(() => {
|
|
176
|
+
if (option.menuOptions)
|
|
177
|
+
return isRef(option.menuOptions) ? option.menuOptions.value : option.menuOptions
|
|
178
|
+
|
|
179
|
+
return []
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
// ✅ 构建响应式 context:直接放 ref/computed,不要 .value!
|
|
183
|
+
const context = reactive({
|
|
184
|
+
type,
|
|
185
|
+
// 响应式字段(ref 会被 reactive 自动 unwrap 使用,但保持响应链接)
|
|
186
|
+
bordered,
|
|
187
|
+
inverted,
|
|
188
|
+
isCollapsed: collapsed,
|
|
189
|
+
headerHeight,
|
|
190
|
+
footerHeight,
|
|
191
|
+
showFooter,
|
|
192
|
+
siderWidth,
|
|
193
|
+
collapsedWidth,
|
|
194
|
+
activeKey,
|
|
195
|
+
menuOptions,
|
|
196
|
+
accordion,
|
|
197
|
+
routes: routesTree as unknown as any,
|
|
198
|
+
homePath: option.homePath,
|
|
199
|
+
excludePaths: option.excludePaths,
|
|
200
|
+
|
|
201
|
+
// 方法
|
|
202
|
+
updateIsCollapsed(v: boolean) {
|
|
203
|
+
collapsed.value = v
|
|
204
|
+
},
|
|
205
|
+
updateInverted(v: boolean) {
|
|
206
|
+
inverted.value = v
|
|
207
|
+
},
|
|
208
|
+
updateActiveKey(v: string) {
|
|
209
|
+
setActiveKey(v)
|
|
210
|
+
if (/^https?:\/\//.test(v))
|
|
211
|
+
return
|
|
212
|
+
|
|
213
|
+
if (v.startsWith('/')) {
|
|
214
|
+
router.push(v)
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
router.push({ name: v })
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}) as unknown as UseLayoutContext
|
|
221
|
+
|
|
222
|
+
// 监听路由变化,同步更新 activeKey
|
|
223
|
+
const stop = watch(
|
|
224
|
+
() => [route.name, route.path, route.meta.activeMenu],
|
|
225
|
+
([name, path, activeMenu]) => {
|
|
226
|
+
setActiveKey((activeMenu as string) || (name as string) || (path as string))
|
|
227
|
+
},
|
|
228
|
+
{ immediate: true }
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
const stopSync = watch(
|
|
232
|
+
normalizedBaseRoutes,
|
|
233
|
+
(list) => {
|
|
234
|
+
for (const r of list as RouteRecordRaw[]) {
|
|
235
|
+
const name = r.name as string | undefined
|
|
236
|
+
if (name && router.hasRoute(name)) {
|
|
237
|
+
router.removeRoute(name)
|
|
238
|
+
}
|
|
239
|
+
router.addRoute(r)
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
{ immediate: true, deep: true }
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
onUnmounted(() => stop())
|
|
246
|
+
onUnmounted(() => stopSync())
|
|
247
|
+
|
|
248
|
+
function addRoute(route: RouteRecordRaw) {
|
|
249
|
+
const existing = new Set<string>(router.getRoutes().map(r => String(r.name ?? '')))
|
|
250
|
+
const name = route.name as string | undefined
|
|
251
|
+
if (name && existing.has(name))
|
|
252
|
+
return
|
|
253
|
+
const [rec] = normalizeAndRedirect([route])
|
|
254
|
+
if (rec) {
|
|
255
|
+
router.addRoute(rec)
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function addRoutes(routes: RouteRecordRaw[]) {
|
|
260
|
+
const existing = new Set<string>(router.getRoutes().map(r => String(r.name ?? '')))
|
|
261
|
+
const toAdd = routes.filter((r) => {
|
|
262
|
+
const n = r.name as string | undefined
|
|
263
|
+
return !n || !existing.has(n)
|
|
264
|
+
})
|
|
265
|
+
if (!toAdd.length)
|
|
266
|
+
return
|
|
267
|
+
const recs = normalizeAndRedirect(toAdd)
|
|
268
|
+
for (const rec of recs) router.addRoute(rec)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function removeRoute(id: string) {
|
|
272
|
+
const name = id
|
|
273
|
+
if (router.hasRoute(name)) {
|
|
274
|
+
router.removeRoute(name)
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
collapsed,
|
|
280
|
+
baseRoutes,
|
|
281
|
+
addRoute,
|
|
282
|
+
addRoutes,
|
|
283
|
+
removeRoute,
|
|
284
|
+
toggle: () => (collapsed.value = !collapsed.value),
|
|
285
|
+
setCollapsed: (v: boolean) => (collapsed.value = v),
|
|
286
|
+
activeKey,
|
|
287
|
+
setActiveKey,
|
|
288
|
+
menuOptions,
|
|
289
|
+
context,
|
|
290
|
+
type,
|
|
291
|
+
bordered,
|
|
292
|
+
inverted,
|
|
293
|
+
headerHeight,
|
|
294
|
+
footerHeight,
|
|
295
|
+
siderWidth,
|
|
296
|
+
siderMixedWidth,
|
|
297
|
+
collapsedWidth,
|
|
298
|
+
accordion
|
|
299
|
+
}
|
|
300
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { NaiveExtraThemeConfig } from '../const'
|
|
2
|
+
import type { NaiveExtraContext } from '../context/index'
|
|
3
|
+
import { computed, inject, ref } from 'vue'
|
|
4
|
+
import { DEFAULT_THEME_CONFIG } from '../const'
|
|
5
|
+
import { NAIVE_EXTRA_CONTEXT_KEY } from '../context/index'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 消费主题配置上下文
|
|
9
|
+
*
|
|
10
|
+
* 为用户提供访问和更新全局配置的能力。
|
|
11
|
+
*
|
|
12
|
+
* @returns {NaiveExtraContext} 包含配置状态、合并后的配置、以及更新方法
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* const { mergedConfig, updateConfig } = useProviderContext()
|
|
17
|
+
*
|
|
18
|
+
* // 更新品牌色
|
|
19
|
+
* updateConfig({ palette: { primary: '#ff0000' } })
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export function useProviderContext(): NaiveExtraContext {
|
|
23
|
+
const context = inject(NAIVE_EXTRA_CONTEXT_KEY, null)
|
|
24
|
+
if (context)
|
|
25
|
+
return context
|
|
26
|
+
|
|
27
|
+
// 兜底逻辑
|
|
28
|
+
const config = ref(DEFAULT_THEME_CONFIG)
|
|
29
|
+
const isDark = computed(() => DEFAULT_THEME_CONFIG.themeMode === 'dark')
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
config,
|
|
33
|
+
mergedConfig: computed(() => DEFAULT_THEME_CONFIG as Required<NaiveExtraThemeConfig>),
|
|
34
|
+
providerProps: computed(() => ({})),
|
|
35
|
+
loadingBarProviderProps: computed(() => ({})),
|
|
36
|
+
messageProviderProps: computed(() => ({})),
|
|
37
|
+
notificationProviderProps: computed(() => ({})),
|
|
38
|
+
isDark,
|
|
39
|
+
updateConfig: () => console.warn('[NaiveExtra] 尝试在未挂载 Provider 的情况下更新配置,操作无效。'),
|
|
40
|
+
setThemeMode: () => {},
|
|
41
|
+
toggleTheme: () => {},
|
|
42
|
+
setLocaleMode: () => {},
|
|
43
|
+
toggleLocale: () => {},
|
|
44
|
+
setPrimaryColor: () => {},
|
|
45
|
+
setBorderRadius: () => {}
|
|
46
|
+
}
|
|
47
|
+
}
|