mirror of
https://gitee.com/mirrors/AllinSSL.git
synced 2026-03-17 19:52:02 +08:00
【修复】条件节点前fromNodeId传值问题
【修复】部署参数默认错误问题 【测设】部分项目代码结构 【同步】前端项目代码
This commit is contained in:
43
frontend/packages/vue/naive-ui/src/hooks/index.tsx
Normal file
43
frontend/packages/vue/naive-ui/src/hooks/index.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import useForm, { useFormHooks } from './useForm' // 表单
|
||||
import useTable, { useTablePage, useTableOperation } from './useTable' // 表格
|
||||
import useTabs from './useTabs' // 标签页
|
||||
import useDialog from './useDialog' // 对话框
|
||||
import useMessage from './useMessage' // 消息
|
||||
import useLoadingMask from './useLoadingMask' // 加载遮罩
|
||||
import useModal, {
|
||||
useModalHooks,
|
||||
useModalOptions,
|
||||
useModalClose,
|
||||
useModalConfirm,
|
||||
useModalCancel,
|
||||
useModalCloseable,
|
||||
useModalMessage,
|
||||
useModalLoading,
|
||||
useModalUseDiscrete,
|
||||
} from './useModal' // 模态框
|
||||
import useBatchTable from './useBatch' // 批量表格
|
||||
import useFullScreen from './useFullScreen' // 全屏
|
||||
|
||||
export {
|
||||
useForm,
|
||||
useTable,
|
||||
useTabs,
|
||||
useDialog,
|
||||
useMessage,
|
||||
useModal,
|
||||
useModalHooks,
|
||||
useModalOptions,
|
||||
useModalClose,
|
||||
useModalConfirm,
|
||||
useModalCancel,
|
||||
useModalCloseable,
|
||||
useModalMessage,
|
||||
useModalLoading,
|
||||
useModalUseDiscrete,
|
||||
useFormHooks,
|
||||
useBatchTable,
|
||||
useFullScreen,
|
||||
useLoadingMask,
|
||||
useTablePage,
|
||||
useTableOperation,
|
||||
}
|
||||
147
frontend/packages/vue/naive-ui/src/hooks/useBatch.tsx
Normal file
147
frontend/packages/vue/naive-ui/src/hooks/useBatch.tsx
Normal file
@@ -0,0 +1,147 @@
|
||||
import { ref, computed, type Ref } from 'vue'
|
||||
import { NButton, NSelect, NCheckbox } from 'naive-ui'
|
||||
import { translation, TranslationLocale, TranslationModule } from '../locals/translation'
|
||||
|
||||
interface BatchOptions {
|
||||
label: string
|
||||
value: string
|
||||
callback?: (rows: Ref<any[]>, rowKeys: Ref<(string | number)[]>) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量操作表格 Hook
|
||||
* @param options 表格配置选项
|
||||
* @returns 表格实例,包含批量操作相关功能
|
||||
*/
|
||||
export default function useBatchTable<T = any>(tableOptions: any, batchOptions: BatchOptions[]) {
|
||||
// 获取当前语言
|
||||
const currentLocale = localStorage.getItem('locale-active') || 'zhCN'
|
||||
// 获取翻译文本
|
||||
const hookT = (key: string, params?: string) => {
|
||||
const locale = currentLocale.replace('-', '_').replace(/"/g, '') as TranslationLocale
|
||||
const translationFn =
|
||||
(translation[locale as TranslationLocale] as TranslationModule).useForm[
|
||||
key as keyof TranslationModule['useForm']
|
||||
] || translation.zhCN.useForm[key as keyof typeof translation.zhCN.useForm]
|
||||
return typeof translationFn === 'function' ? translationFn(params || '') : translationFn
|
||||
}
|
||||
|
||||
// 表格组件
|
||||
const { TableComponent, tableRef, ...tableAttrs } = useTable(tableOptions)
|
||||
const batchTableRef = ref<any>(null)
|
||||
// 选中项状态
|
||||
const selectedRows = ref<T[]>([])
|
||||
const selectedRowKeys = ref<(string | number)[]>([])
|
||||
const totalData = computed(() => batchTableRef.value?.data || [])
|
||||
|
||||
// 计算全选状态
|
||||
const isAllSelected = computed(() => {
|
||||
return totalData.value.length > 0 && selectedRowKeys.value.length === totalData.value.length
|
||||
})
|
||||
|
||||
// 计算半选状态
|
||||
const isIndeterminate = computed(() => {
|
||||
return selectedRowKeys.value.length > 0 && selectedRowKeys.value.length < totalData.value.length
|
||||
})
|
||||
|
||||
/**
|
||||
* 处理选择变化
|
||||
* @param rowKeys 选中的行键值
|
||||
* @param rows 选中的行数据
|
||||
*/
|
||||
const handleSelectionChange: any = (rowKeys: (string | number)[], rows: T[]) => {
|
||||
selectedRowKeys.value = rowKeys
|
||||
selectedRows.value = rows
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理全选变化
|
||||
*/
|
||||
const handleCheckAll = (checked: boolean) => {
|
||||
if (checked) {
|
||||
selectedRows.value = [...totalData.value]
|
||||
selectedRowKeys.value = totalData.value.map((item: T) => batchTableRef.value.rowKey(item))
|
||||
} else {
|
||||
clearSelection()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取表格组件
|
||||
*/
|
||||
const BatchTableComponent = (props: any, context: any) => {
|
||||
return TableComponent(
|
||||
{
|
||||
ref: batchTableRef,
|
||||
rowKey: (row: T) => (row as any).id,
|
||||
checkedRowKeys: selectedRowKeys.value,
|
||||
onUpdateCheckedRowKeys: handleSelectionChange,
|
||||
...props,
|
||||
},
|
||||
context,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量操作组件
|
||||
*/
|
||||
const selectedAction = ref<string | null>(null)
|
||||
|
||||
const BatchOperationComponent = () => {
|
||||
const setValue = (value: string) => {
|
||||
selectedAction.value = value
|
||||
}
|
||||
|
||||
const startBatch = async () => {
|
||||
const option = batchOptions.find((item) => item.value === selectedAction.value)
|
||||
if (option) {
|
||||
const batchStatus = await option.callback?.(selectedRows, selectedRowKeys)
|
||||
if (batchStatus) {
|
||||
// 重置选择
|
||||
selectedAction.value = null
|
||||
clearSelection()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="batch-operation" style="display: flex; align-items: center; gap: 16px;">
|
||||
<NCheckbox
|
||||
checked={isAllSelected.value}
|
||||
indeterminate={isIndeterminate.value}
|
||||
onUpdateChecked={handleCheckAll}
|
||||
></NCheckbox>
|
||||
|
||||
<NSelect
|
||||
options={batchOptions}
|
||||
value={selectedAction.value}
|
||||
onUpdateValue={setValue}
|
||||
placeholder={hookT('placeholder')}
|
||||
style="width: 120px"
|
||||
disabled={selectedRows.value.length === 0}
|
||||
/>
|
||||
<NButton type="primary" disabled={selectedRows.value.length === 0} onClick={startBatch}>
|
||||
{hookT('startBatch')}
|
||||
</NButton>
|
||||
<span>{hookT('selectedItems')(selectedRows.value.length)}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空选择
|
||||
*/
|
||||
const clearSelection = () => {
|
||||
selectedRowKeys.value = []
|
||||
selectedRows.value = []
|
||||
}
|
||||
|
||||
return {
|
||||
selectedRows,
|
||||
clearSelection,
|
||||
BatchTableComponent,
|
||||
BatchOperationComponent,
|
||||
...tableAttrs,
|
||||
tableRef: batchTableRef,
|
||||
}
|
||||
}
|
||||
164
frontend/packages/vue/naive-ui/src/hooks/useDialog.tsx
Normal file
164
frontend/packages/vue/naive-ui/src/hooks/useDialog.tsx
Normal file
@@ -0,0 +1,164 @@
|
||||
import { getCurrentInstance, h, ref, shallowRef } from 'vue'
|
||||
import { useDialog as useNaiveDialog, createDiscreteApi, type DialogOptions, NIcon } from 'naive-ui'
|
||||
import { Info24Filled, ErrorCircle24Filled, CheckmarkCircle24Filled } from '@vicons/fluent'
|
||||
import { themeProvider as ThemeProvider } from '../components/customProvider'
|
||||
import type { CustomDialogOptions } from '../types/dialog'
|
||||
|
||||
// 自定义Dialog钩子函数
|
||||
export default function useDialog(options?: CustomDialogOptions) {
|
||||
// 判断是否在setup中使用
|
||||
const instance = getCurrentInstance()
|
||||
// 创建响应式数据
|
||||
const optionsRef = ref<CustomDialogOptions>(options || {})
|
||||
// 创建Dialog实例
|
||||
const dialogInstance = shallowRef()
|
||||
// 创建Dialog方法
|
||||
const create = (optionsNew: CustomDialogOptions) => {
|
||||
const {
|
||||
type = 'warning',
|
||||
title,
|
||||
area,
|
||||
content,
|
||||
draggable = true,
|
||||
confirmText = '确定',
|
||||
cancelText = '取消',
|
||||
confirmButtonProps = { type: 'primary' },
|
||||
cancelButtonProps = { type: 'default' },
|
||||
maskClosable = false,
|
||||
closeOnEsc = false,
|
||||
autoFocus = false,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
onClose,
|
||||
onMaskClick,
|
||||
...dialogOptions
|
||||
} = optionsNew
|
||||
|
||||
// 转换area
|
||||
const areaConvert = () => {
|
||||
if (!area) return { width: '35rem', height: 'auto' }
|
||||
if (typeof area === 'string') return { width: area, height: 'auto' }
|
||||
return { width: area[0], height: area[1] }
|
||||
}
|
||||
|
||||
// 转换content
|
||||
const contentConvert = () => {
|
||||
if (!content) return ''
|
||||
const Icon = (type: string) => {
|
||||
const typeIcon = {
|
||||
info: [<Info24Filled class="text-primary" />],
|
||||
success: [<CheckmarkCircle24Filled class="text-success" />],
|
||||
warning: [<Info24Filled class="text-warning" />],
|
||||
error: [<ErrorCircle24Filled class="text-error" />],
|
||||
}
|
||||
return h(NIcon, { size: 30, class: `n-dialog__icon` }, () => typeIcon[type as keyof typeof typeIcon][0])
|
||||
}
|
||||
const contentNew = h('div', { class: 'flex pt-[0.4rem]' }, [
|
||||
Icon(type),
|
||||
h('div', { class: 'w-full pt-1 flex items-center' }, typeof content === 'string' ? content : content()),
|
||||
])
|
||||
// 如果不在setup中使用
|
||||
if (!instance) return h(ThemeProvider, { type }, () => contentNew)
|
||||
return contentNew
|
||||
}
|
||||
|
||||
// 合并Dialog配置
|
||||
const config: DialogOptions = {
|
||||
title,
|
||||
content: () => contentConvert(),
|
||||
style: areaConvert(),
|
||||
draggable,
|
||||
maskClosable,
|
||||
showIcon: false,
|
||||
closeOnEsc,
|
||||
autoFocus,
|
||||
positiveText: confirmText,
|
||||
negativeText: cancelText,
|
||||
positiveButtonProps: confirmButtonProps,
|
||||
negativeButtonProps: cancelButtonProps,
|
||||
onPositiveClick: onConfirm,
|
||||
onNegativeClick: onCancel,
|
||||
onClose,
|
||||
onMaskClick,
|
||||
...dialogOptions,
|
||||
}
|
||||
if (instance) {
|
||||
// 创建Dialog实例
|
||||
const naiveDialog = useNaiveDialog()
|
||||
dialogInstance.value = naiveDialog.create(config)
|
||||
return dialogInstance.value
|
||||
}
|
||||
|
||||
// 创建discreteDialog实例
|
||||
const { dialog } = createDiscreteApi(['dialog'])
|
||||
dialogInstance.value = dialog.create(config)
|
||||
return dialogInstance.value
|
||||
}
|
||||
|
||||
/**
|
||||
* 成功-对话框
|
||||
* @param options - 提示配置
|
||||
* @returns 提示实例
|
||||
*/
|
||||
const success = (content: string, options: CustomDialogOptions = {}) => {
|
||||
return create({ ...options, type: 'success', content, showIcon: true })
|
||||
}
|
||||
|
||||
/**
|
||||
* 警告-对话框
|
||||
* @param options - 提示配置
|
||||
* @returns 提示实例
|
||||
*/
|
||||
const warning = (content: string, options: CustomDialogOptions = {}) => {
|
||||
return create({ ...options, type: 'warning', content })
|
||||
}
|
||||
|
||||
/**
|
||||
* 错误 - 对话框
|
||||
* @param options - 提示配置
|
||||
* @returns 提示实例
|
||||
*/
|
||||
const error = (content: string, options: CustomDialogOptions = {}) => {
|
||||
return create({ ...options, type: 'error', content })
|
||||
}
|
||||
|
||||
/**
|
||||
* 信息提示
|
||||
* @param options - 提示配置
|
||||
* @returns 提示实例
|
||||
*/
|
||||
const info = (content: string, options: CustomDialogOptions = {}) => {
|
||||
return create({ ...options, type: 'info', content })
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新Dialog实例
|
||||
* @param options - 提示配置
|
||||
* @returns 提示实例
|
||||
*/
|
||||
const update = (options: CustomDialogOptions) => {
|
||||
optionsRef.value = options
|
||||
return create(options)
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求结果提示
|
||||
* @param options - 提示配置
|
||||
* @param data - 请求结果
|
||||
* @returns 提示实例
|
||||
*/
|
||||
const request = (data: Record<string, unknown>, options: CustomDialogOptions = {}) => {
|
||||
return create({ ...options, type: data.status ? 'success' : 'error', content: data.message as string })
|
||||
}
|
||||
|
||||
// 销毁所有Dialog实例方法
|
||||
const destroyAll = () => {
|
||||
dialogInstance.value?.destroyAll()
|
||||
}
|
||||
|
||||
const newReturn = { create, options: optionsRef, update, success, warning, error, info, request, destroyAll }
|
||||
// 如果配置为空
|
||||
if (!options) return newReturn
|
||||
|
||||
return Object.assign(create(options), newReturn)
|
||||
}
|
||||
809
frontend/packages/vue/naive-ui/src/hooks/useForm.tsx
Normal file
809
frontend/packages/vue/naive-ui/src/hooks/useForm.tsx
Normal file
@@ -0,0 +1,809 @@
|
||||
import { ref, Ref, toRef, effectScope, onScopeDispose, shallowRef, toRefs, watch, isRef } from 'vue'
|
||||
import {
|
||||
NForm,
|
||||
NFormItem,
|
||||
NGrid,
|
||||
NInput,
|
||||
NInputNumber,
|
||||
NInputGroup,
|
||||
NSelect,
|
||||
NRadio,
|
||||
NRadioGroup,
|
||||
NRadioButton,
|
||||
NCheckbox,
|
||||
NCheckboxGroup,
|
||||
NSwitch,
|
||||
NDatePicker,
|
||||
NTimePicker,
|
||||
NColorPicker,
|
||||
NSlider,
|
||||
NRate,
|
||||
NTransfer,
|
||||
NMention,
|
||||
NDynamicInput,
|
||||
NDynamicTags,
|
||||
NAutoComplete,
|
||||
NCascader,
|
||||
NTreeSelect,
|
||||
NUpload,
|
||||
NUploadDragger,
|
||||
type FormInst,
|
||||
NFormItemGi,
|
||||
NIcon,
|
||||
NDivider,
|
||||
type InputProps,
|
||||
type InputNumberProps,
|
||||
type SelectProps,
|
||||
type RadioProps,
|
||||
type RadioButtonProps,
|
||||
type SwitchProps,
|
||||
type DatePickerProps,
|
||||
type TimePickerProps,
|
||||
type SliderProps,
|
||||
type SelectOption,
|
||||
type FormProps,
|
||||
type FormItemProps,
|
||||
type CheckboxGroupProps,
|
||||
SwitchSlots,
|
||||
} from 'naive-ui'
|
||||
import { LeftOutlined, DownOutlined } from '@vicons/antd'
|
||||
import { translation, TranslationModule, type TranslationLocale } from '../locals/translation'
|
||||
import type {
|
||||
FormInstanceWithComponent,
|
||||
UseFormOptions,
|
||||
FormItemConfig,
|
||||
GridItemConfig,
|
||||
FormElement,
|
||||
SlotFormElement,
|
||||
RenderFormElement,
|
||||
FormElementType,
|
||||
BaseFormElement,
|
||||
FormItemGiConfig,
|
||||
RadioOptionItem,
|
||||
CheckboxOptionItem,
|
||||
FormConfig,
|
||||
FormElementPropsMap,
|
||||
} from '../types/form'
|
||||
|
||||
// 获取当前语言
|
||||
const currentLocale = localStorage.getItem('locale-active') || 'zhCN'
|
||||
|
||||
// 获取翻译文本
|
||||
const hookT = (key: string, params?: string) => {
|
||||
const locale = currentLocale.replace('-', '_').replace(/"/g, '') as TranslationLocale
|
||||
const translationFn =
|
||||
(translation[locale as TranslationLocale] as TranslationModule).useForm[
|
||||
key as keyof TranslationModule['useForm']
|
||||
] || translation.zhCN.useForm[key as keyof typeof translation.zhCN.useForm]
|
||||
return typeof translationFn === 'function' ? translationFn(params || '') : translationFn
|
||||
}
|
||||
|
||||
/**
|
||||
* 组件映射表:将表单元素类型映射到对应的 Naive UI 组件
|
||||
* 包含所有支持的表单控件组件
|
||||
*/
|
||||
const componentMap = {
|
||||
input: NInput, // 输入框
|
||||
inputNumber: NInputNumber, // 数字输入框
|
||||
inputGroup: NInputGroup, // 输入框组
|
||||
select: NSelect, // 选择器
|
||||
radio: NRadio, // 单选框组
|
||||
radioButton: NRadioButton, // 单选按钮
|
||||
checkbox: NCheckbox, // 复选框组
|
||||
switch: NSwitch, // 开关
|
||||
datepicker: NDatePicker, // 日期选择器
|
||||
timepicker: NTimePicker, // 时间选择器
|
||||
colorPicker: NColorPicker, // 颜色选择器
|
||||
slider: NSlider, // 滑块
|
||||
rate: NRate, // 评分
|
||||
transfer: NTransfer, // 穿梭框
|
||||
mention: NMention, // 提及
|
||||
dynamicInput: NDynamicInput, // 动态输入
|
||||
dynamicTags: NDynamicTags, // 动态标签
|
||||
autoComplete: NAutoComplete, // 自动完成
|
||||
cascader: NCascader, // 级联选择
|
||||
treeSelect: NTreeSelect, // 树选择
|
||||
upload: NUpload, // 上传
|
||||
uploadDragger: NUploadDragger, // 拖拽上传
|
||||
} as const
|
||||
|
||||
/**
|
||||
* 表单插槽类型定义
|
||||
* 用于定义表单中可以使用的插槽,每个插槽都是一个函数
|
||||
* 函数接收表单数据和表单实例引用作为参数,返回JSX元素
|
||||
*/
|
||||
type FormSlots<T> = Record<string, (formData: Ref<T>, formRef: Ref<FormInst | null>) => JSX.Element>
|
||||
|
||||
/**
|
||||
* 处理表单项的前缀和后缀插槽
|
||||
* @param slot 插槽配置对象
|
||||
* @returns 处理后的前缀和后缀元素数组
|
||||
*/
|
||||
const processFormItemSlots = (slot?: { prefix?: Array<() => JSX.Element>; suffix?: Array<() => JSX.Element> }) => {
|
||||
const prefixElements = slot?.prefix
|
||||
? slot.prefix.map((item: () => JSX.Element) => ({
|
||||
type: 'render' as const,
|
||||
render: item,
|
||||
}))
|
||||
: []
|
||||
|
||||
const suffixElements = slot?.suffix
|
||||
? slot.suffix.map((item: () => JSX.Element) => ({
|
||||
type: 'render' as const,
|
||||
render: item,
|
||||
}))
|
||||
: []
|
||||
|
||||
return { prefixElements, suffixElements }
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建标准表单项配置
|
||||
* @param label 标签文本
|
||||
* @param key 表单字段名
|
||||
* @param type 表单元素类型
|
||||
* @param props 组件属性
|
||||
* @param itemAttrs 表单项属性
|
||||
* @param slot 插槽配置
|
||||
* @returns 标准化的表单项配置
|
||||
*/
|
||||
const createFormItem = <T extends keyof typeof componentMap>(
|
||||
label: string,
|
||||
key: string,
|
||||
type: T,
|
||||
props: FormElementPropsMap[T],
|
||||
itemAttrs?: FormItemProps,
|
||||
slot?: { prefix?: Array<() => JSX.Element>; suffix?: Array<() => JSX.Element> },
|
||||
) => {
|
||||
const { prefixElements, suffixElements } = processFormItemSlots(slot)
|
||||
return {
|
||||
type: 'formItem' as const,
|
||||
label,
|
||||
path: key,
|
||||
required: true,
|
||||
children: [
|
||||
...prefixElements,
|
||||
{
|
||||
type,
|
||||
field: key,
|
||||
...(type === 'input' ? { placeholder: hookT('placeholder', label) } : {}),
|
||||
...props,
|
||||
},
|
||||
...suffixElements,
|
||||
],
|
||||
...itemAttrs,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 表单钩子函数
|
||||
* 用于创建一个动态表单实例,提供表单的状态管理和渲染能力
|
||||
* @param options 表单配置选项,包含表单配置、请求函数和默认值等
|
||||
* @returns 返回统一的表单实例接口
|
||||
*/
|
||||
export default function useForm<T>(options: UseFormOptions<T>) {
|
||||
// 创建 effectScope 用于管理响应式副作用
|
||||
const scope = effectScope()
|
||||
return scope.run(() => {
|
||||
const { config, request, defaultValue = {}, rules: rulesVal } = options
|
||||
|
||||
// 表单响应式状态
|
||||
const loading = ref(false) // 表单加载状态
|
||||
const formRef = ref<FormInst | null>(null) // 表单实例引用
|
||||
const data = isRef(defaultValue) ? (defaultValue as Ref<T>) : ref(defaultValue as T) // 使用ref而不是reactive,避免响应丢失
|
||||
const formConfig = ref<FormConfig>(config) // 表单配置
|
||||
const rules = shallowRef({ ...rulesVal }) // 表单验证规则
|
||||
|
||||
// 表单属性配置
|
||||
const props = ref<FormProps>({
|
||||
labelPlacement: 'left',
|
||||
labelWidth: '8rem',
|
||||
// 其他可配置的表单属性
|
||||
})
|
||||
|
||||
/**
|
||||
* 渲染基础表单元素
|
||||
* 根据配置渲染对应的Naive UI表单控件
|
||||
* @param element 基础表单元素配置,包含类型、字段名和组件属性等
|
||||
* @returns 返回渲染后的JSX元素,如果找不到对应组件则返回null
|
||||
*/
|
||||
const renderBaseElement = <T extends FormElementType, K extends Record<string, any>>(
|
||||
element: BaseFormElement<T>,
|
||||
) => {
|
||||
let type = element.type
|
||||
if (['textarea', 'password'].includes(type)) type = 'input'
|
||||
// 获取对应的 Naive UI 组件
|
||||
const Component = componentMap[type as keyof typeof componentMap]
|
||||
if (!Component) return null
|
||||
|
||||
// 解构出组件属性,分离类型和字段名
|
||||
const { field, ...componentProps } = element
|
||||
// 处理Radio、Checkbox
|
||||
if (['radio', 'radioButton'].includes(type)) {
|
||||
// 类型断言以访问options属性
|
||||
const radioElement = element as BaseFormElement<'radio' | 'radioButton'> & { options?: RadioOptionItem[] }
|
||||
return (
|
||||
<NRadioGroup
|
||||
value={getNestedValue(data.value as K, field)}
|
||||
onUpdateValue={(val: any) => {
|
||||
setNestedValue(data.value as K, field, val)
|
||||
}}
|
||||
>
|
||||
{radioElement.options?.map((option: RadioOptionItem) =>
|
||||
type === 'radio' ? (
|
||||
<NRadio value={option.value} {...componentProps}>
|
||||
{option.label}
|
||||
</NRadio>
|
||||
) : (
|
||||
<NRadioButton value={option.value} {...componentProps}>
|
||||
{option.label}
|
||||
</NRadioButton>
|
||||
),
|
||||
)}
|
||||
</NRadioGroup>
|
||||
)
|
||||
}
|
||||
if (['checkbox'].includes(type)) {
|
||||
// 类型断言以访问options属性
|
||||
const checkboxElement = element as BaseFormElement<'checkbox'> & {
|
||||
options?: CheckboxOptionItem[]
|
||||
}
|
||||
return (
|
||||
<NCheckboxGroup
|
||||
value={getNestedValue(data.value as K, field)}
|
||||
onUpdateValue={(val: any) => {
|
||||
setNestedValue(data.value as K, field, val)
|
||||
}}
|
||||
{...componentProps}
|
||||
>
|
||||
{checkboxElement.options?.map((option: CheckboxOptionItem) => (
|
||||
<NCheckbox value={option.value} {...componentProps}>
|
||||
{option.label}
|
||||
</NCheckbox>
|
||||
))}
|
||||
</NCheckboxGroup>
|
||||
)
|
||||
}
|
||||
// 根据是否有字段名决定是否使用v-model双向绑定
|
||||
return (
|
||||
<Component
|
||||
value={getNestedValue(data.value as K, field)}
|
||||
onUpdateValue={(val: any) => {
|
||||
setNestedValue(data.value as K, field, val)
|
||||
}}
|
||||
{...componentProps}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染表单元素
|
||||
* 统一处理所有类型的表单元素,包括插槽、自定义渲染和基础表单元素
|
||||
* @param element 表单元素配置
|
||||
* @param slots 插槽配置对象
|
||||
* @returns 返回渲染后的JSX元素或null
|
||||
*/
|
||||
const renderFormElement = (element: FormElement, slots?: FormSlots<T>): JSX.Element | null => {
|
||||
// 是否是插槽元素
|
||||
const isSlotElement = (el: FormElement): el is SlotFormElement => el.type === 'slot'
|
||||
// 是否是渲染函数
|
||||
const isRenderElement = (el: FormElement): el is RenderFormElement => el.type === 'custom'
|
||||
// 是否是自定义渲染元素
|
||||
const isBaseElement = (el: FormElement): el is BaseFormElement => !isSlotElement(el) && !isRenderElement(el)
|
||||
|
||||
// 处理插槽元素:使用配置的插槽函数渲染内容
|
||||
if (isSlotElement(element)) {
|
||||
return slots?.[element.slot]?.(data as unknown as Ref<T>, formRef) ?? null
|
||||
}
|
||||
|
||||
// 处理自定义渲染元素:调用自定义渲染函数
|
||||
if (isRenderElement(element)) {
|
||||
console.log(data, 'data')
|
||||
return element.render(data as unknown as Ref<T>, formRef)
|
||||
}
|
||||
// 处理基础表单元素:使用组件映射表渲染对应组件
|
||||
if (isBaseElement(element)) return renderBaseElement(element)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染表单项
|
||||
* 创建表单项容器,可以是普通表单项或栅格布局中的表单项
|
||||
* @param item 表单项配置,包含子元素和属性
|
||||
* @param slots 插槽配置对象
|
||||
* @returns 返回渲染后的表单项JSX元素
|
||||
*/
|
||||
const renderFormItem = (
|
||||
item: FormItemConfig | FormItemGiConfig | RenderFormElement | SlotFormElement,
|
||||
slots?: FormSlots<T>,
|
||||
) => {
|
||||
if (item.type === 'custom') return item.render(data as Ref<T>, formRef)
|
||||
if (item.type === 'slot') return renderFormElement(item, slots)
|
||||
const { children, type, ...itemProps } = item
|
||||
if (type === 'formItemGi') {
|
||||
return <NFormItemGi {...itemProps}>{children.map((child) => renderFormElement(child, slots))}</NFormItemGi>
|
||||
}
|
||||
return <NFormItem {...itemProps}>{children.map((child) => renderFormElement(child, slots))}</NFormItem>
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染栅格布局
|
||||
* 创建栅格布局容器,并渲染其中的表单项
|
||||
* @param grid 栅格配置,包含布局属性和子元素
|
||||
* @param slots 插槽配置对象
|
||||
* @returns 返回渲染后的栅格布局JSX元素
|
||||
*/
|
||||
const renderGrid = (grid: GridItemConfig, slots?: FormSlots<T>) => {
|
||||
const { children, ...gridProps } = grid
|
||||
return <NGrid {...gridProps}>{children.map((item) => renderFormItem(item, slots))}</NGrid>
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染完整表单组件
|
||||
* 创建最外层的表单容器,并根据配置渲染内部的栅格或表单项
|
||||
* @param attrs 组件属性,包含插槽配置
|
||||
* @param context 组件上下文
|
||||
* @returns 返回渲染后的完整表单JSX元素
|
||||
*/
|
||||
const component = (attrs: FormProps, context: { slots?: FormSlots<T> }) => (
|
||||
<NForm ref={formRef} model={data.value} rules={rules.value} labelPlacement="left" {...props} {...attrs}>
|
||||
{formConfig.value.map((item: FormConfig[0]) =>
|
||||
item.type === 'grid' ? renderGrid(item, context.slots) : renderFormItem(item, context.slots),
|
||||
)}
|
||||
</NForm>
|
||||
)
|
||||
|
||||
/**
|
||||
* 验证表单
|
||||
* 触发表单的验证流程,检查所有字段的有效性
|
||||
* @returns 返回一个Promise,解析为验证是否通过的布尔值
|
||||
*/
|
||||
const validate = async () => {
|
||||
if (!formRef.value) return false
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交表单
|
||||
* 验证表单并调用提交请求函数
|
||||
* @returns 返回一个Promise,解析为请求的响应结果
|
||||
*/
|
||||
const fetch = async () => {
|
||||
if (!request) return
|
||||
try {
|
||||
loading.value = true
|
||||
const valid = await validate()
|
||||
if (!valid) throw new Error('表单验证失败')
|
||||
return await request(data.value, formRef)
|
||||
} catch (error) {
|
||||
throw new Error('表单验证失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置表单
|
||||
* 清除表单的验证状态,并将所有字段值重置为默认值
|
||||
*/
|
||||
const reset = () => {
|
||||
formRef.value?.restoreValidation()
|
||||
data.value = Object.assign({}, isRef(defaultValue) ? defaultValue.value : defaultValue) // 重置为默认值,使用新对象以确保触发响应
|
||||
}
|
||||
|
||||
// 当组件卸载时,清理所有副作用
|
||||
onScopeDispose(() => {
|
||||
scope.stop()
|
||||
})
|
||||
|
||||
// 返回标准化的表单实例接口
|
||||
return {
|
||||
component, // 表单渲染组件
|
||||
example: formRef, // 当前组件实例
|
||||
data, // 响应式数据
|
||||
loading, // 加载状态
|
||||
config: formConfig, // 表单配置
|
||||
props, // 表单属性
|
||||
rules, // 验证规则
|
||||
dataToRef: () => toRefs(data.value), // 响应式数据转ref
|
||||
fetch, // 提交方法
|
||||
reset, // 重置方法
|
||||
validate, // 验证方法
|
||||
}
|
||||
}) as FormInstanceWithComponent<T>
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个表单输入项
|
||||
* @param {string} label 标签文本
|
||||
* @param {string} key 表单字段名
|
||||
* @param {InputProps & { class?: string }} other 输入框的额外属性
|
||||
* @param {FormItemProps & { class?: string }} itemAttrs 表单项的额外属性
|
||||
* @param {Object} slot 插槽配置
|
||||
* @returns {FormItemConfig} 表单项配置
|
||||
*/
|
||||
const useFormInput = (
|
||||
label: string,
|
||||
key: string,
|
||||
other?: InputProps & { class?: string },
|
||||
itemAttrs?: FormItemProps & { class?: string },
|
||||
slot?: { prefix?: Array<() => JSX.Element>; suffix?: Array<() => JSX.Element> },
|
||||
) => createFormItem(label, key, 'input', { placeholder: hookT('placeholder', label), ...other }, itemAttrs, slot)
|
||||
|
||||
/**
|
||||
* 创建一个表单textarea
|
||||
* @param {string} label 标签文本
|
||||
* @param {string} key 表单字段名
|
||||
* @param {InputProps & { class?: string }} other 输入框的额外属性
|
||||
* @param {FormItemProps & { class?: string }} itemAttrs 表单项的额外属性
|
||||
* @param {Object} slot 插槽配置
|
||||
* @returns {FormItemConfig} 表单项配置
|
||||
*/
|
||||
const useFormTextarea = (
|
||||
label: string,
|
||||
key: string,
|
||||
other?: InputProps & { class?: string },
|
||||
itemAttrs?: FormItemProps & { class?: string },
|
||||
slot?: { prefix?: Array<() => JSX.Element>; suffix?: Array<() => JSX.Element> },
|
||||
) =>
|
||||
createFormItem(
|
||||
label,
|
||||
key,
|
||||
'input',
|
||||
{ type: 'textarea', placeholder: hookT('placeholder', label), ...other },
|
||||
itemAttrs,
|
||||
slot,
|
||||
)
|
||||
|
||||
/**
|
||||
* 创建一个表单密码输入项
|
||||
* @param {string} label 标签文本
|
||||
* @param {string} key 表单字段名
|
||||
* @param {InputProps & { class?: string }} other 输入框的额外属性
|
||||
* @param {FormItemProps & { class?: string }} itemAttrs 表单项的额外属性
|
||||
* @param {Object} slot 插槽配置
|
||||
* @returns {FormItemConfig} 表单项配置
|
||||
*/
|
||||
const useFormPassword = (
|
||||
label: string,
|
||||
key: string,
|
||||
other?: InputProps & { class?: string },
|
||||
itemAttrs?: FormItemProps & { class?: string },
|
||||
slot?: { prefix?: Array<() => JSX.Element>; suffix?: Array<() => JSX.Element> },
|
||||
) =>
|
||||
createFormItem(
|
||||
label,
|
||||
key,
|
||||
'input',
|
||||
{ type: 'password', placeholder: hookT('placeholder', label), ...other },
|
||||
itemAttrs,
|
||||
slot,
|
||||
)
|
||||
|
||||
/**
|
||||
* 创建一个表单数字输入项
|
||||
* @param {string} label 标签文本
|
||||
* @param {string} key 表单字段名
|
||||
* @param {InputNumberProps & { class?: string }} other 输入框的额外属性
|
||||
* @param {FormItemProps & { class?: string }} itemAttrs 表单项的额外属性
|
||||
* @param {Object} slot 插槽配置
|
||||
* @returns {FormItemConfig} 表单项配置
|
||||
*/
|
||||
const useFormInputNumber = (
|
||||
label: string,
|
||||
key: string,
|
||||
other?: InputNumberProps & { class?: string },
|
||||
itemAttrs?: FormItemProps & { class?: string },
|
||||
slot?: { prefix?: Array<() => JSX.Element>; suffix?: Array<() => JSX.Element> },
|
||||
) => createFormItem(label, key, 'inputNumber', { showButton: false, ...other }, itemAttrs, slot)
|
||||
|
||||
/**
|
||||
* 定义嵌套值获取函数用于控制台日志
|
||||
* @param {Record<string, any>} obj 对象
|
||||
* @param {string} path 路径
|
||||
* @returns {any} 嵌套对象的值
|
||||
*/
|
||||
function getNestedValue(obj: Record<string, any>, path: string): any {
|
||||
return path.includes('.')
|
||||
? path.split('.').reduce((prev, curr) => (prev && prev[curr] !== undefined ? prev[curr] : undefined), obj)
|
||||
: obj[path]
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置嵌套对象的值
|
||||
* @param obj 对象
|
||||
* @param path 路径
|
||||
* @param value 要设置的值
|
||||
*/
|
||||
const setNestedValue = (obj: Record<string, any>, path: string, value: any): void => {
|
||||
if (path.includes('.')) {
|
||||
const parts = path.split('.')
|
||||
const lastPart = parts.pop()!
|
||||
const target = parts.reduce((prev, curr) => {
|
||||
if (prev[curr] === undefined) {
|
||||
prev[curr] = {}
|
||||
}
|
||||
return prev[curr]
|
||||
}, obj)
|
||||
target[lastPart] = value
|
||||
} else {
|
||||
obj[path] = value
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个表单组
|
||||
* @param group 表单项组
|
||||
*/
|
||||
const useFormGroup = <T extends Record<string, any>>(group: Record<string, any>[]) => {
|
||||
return {
|
||||
type: 'custom',
|
||||
render: (formData: Ref<T>, formRef: Ref<FormInst | null>) => {
|
||||
return (
|
||||
<div class="flex">
|
||||
{group.map((item) => {
|
||||
if (item.type === 'custom') return item.render(formData, formRef)
|
||||
const { children, ...itemProps } = item
|
||||
return (
|
||||
<NFormItem {...itemProps}>
|
||||
{children.map((child: BaseFormElement | RenderFormElement | SlotFormElement) => {
|
||||
if (child.type === 'render' || child.type === 'custom')
|
||||
return (child as RenderFormElement).render(formData, formRef)
|
||||
// 获取对应的 Naive UI 组件
|
||||
const Component = componentMap[child.type as keyof typeof componentMap]
|
||||
if (!Component) return null
|
||||
// 解构出组件属性,分离类型和字段名
|
||||
const { field, ...componentProps } = child as BaseFormElement
|
||||
return (
|
||||
<Component
|
||||
value={getNestedValue(formData.value as T, field)}
|
||||
onUpdateValue={(val: any) => {
|
||||
setNestedValue(formData.value as T, field, val)
|
||||
}}
|
||||
{...componentProps}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</NFormItem>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个表单选择器
|
||||
* @param label 标签文本
|
||||
* @param key 表单字段名
|
||||
* @param other 选择器的额外属性
|
||||
* @param itemAttrs 表单项的额外属性
|
||||
*/
|
||||
const useFormSelect = (
|
||||
label: string,
|
||||
key: string,
|
||||
options: SelectOption[],
|
||||
other?: SelectProps & { class?: string },
|
||||
itemAttrs?: FormItemProps & { class?: string },
|
||||
slot?: { prefix?: Array<() => JSX.Element>; suffix?: Array<() => JSX.Element> },
|
||||
) => {
|
||||
return createFormItem(label, key, 'select', { options, ...other }, itemAttrs, slot)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个表单插槽
|
||||
* @param label 标签文本
|
||||
* @param key 表单字段名
|
||||
*/
|
||||
const useFormSlot = (key?: string) => {
|
||||
return {
|
||||
type: 'slot',
|
||||
slot: key || 'default',
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个表单自定义渲染
|
||||
* @param render 自定义渲染函数
|
||||
*/
|
||||
const useFormCustom = <T,>(render: (formData: Ref<T>, formRef: Ref<FormInst | null>) => JSX.Element) => {
|
||||
return {
|
||||
type: 'custom' as const,
|
||||
render,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个表单单选框
|
||||
* @param label 标签文本
|
||||
* @param key 表单字段名
|
||||
*/
|
||||
const useFormRadio = (
|
||||
label: string,
|
||||
key: string,
|
||||
options: RadioOptionItem[],
|
||||
other?: RadioProps & { class?: string },
|
||||
itemAttrs?: FormItemProps & { class?: string },
|
||||
slot?: { prefix?: Array<() => JSX.Element>; suffix?: Array<() => JSX.Element> },
|
||||
) => {
|
||||
return createFormItem(label, key, 'radio', { options, ...other }, itemAttrs, slot)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个表单单选按钮
|
||||
* @param label 标签文本
|
||||
* @param key 表单字段名
|
||||
*/
|
||||
const useFormRadioButton = (
|
||||
label: string,
|
||||
key: string,
|
||||
options: RadioOptionItem[],
|
||||
other?: RadioButtonProps & { class?: string },
|
||||
itemAttrs?: FormItemProps & { class?: string },
|
||||
slot?: { prefix?: Array<() => JSX.Element>; suffix?: Array<() => JSX.Element> },
|
||||
) => {
|
||||
return createFormItem(label, key, 'radioButton', { options, ...other }, itemAttrs, slot)
|
||||
}
|
||||
/**
|
||||
* 创建一个表单复选框
|
||||
* @param label 标签文本
|
||||
* @param key 表单字段名
|
||||
*/
|
||||
const useFormCheckbox = (
|
||||
label: string,
|
||||
key: string,
|
||||
options: CheckboxOptionItem[],
|
||||
other?: Partial<CheckboxGroupProps> & { class?: string },
|
||||
itemAttrs?: FormItemProps & { class?: string },
|
||||
slot?: { prefix?: Array<() => JSX.Element>; suffix?: Array<() => JSX.Element> },
|
||||
) => {
|
||||
return createFormItem(label, key, 'checkbox', { options, ...other } as any, itemAttrs, slot)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个表单开关
|
||||
* @param label 标签文本
|
||||
* @param key 表单字段名
|
||||
*/
|
||||
const useFormSwitch = (
|
||||
label: string,
|
||||
key: string,
|
||||
other?: SwitchProps & { class?: string },
|
||||
itemAttrs?: FormItemProps & { class?: string },
|
||||
slot?: SwitchSlots,
|
||||
) => {
|
||||
return createFormItem(label, key, 'switch', { ...other }, itemAttrs, slot)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个表单日期选择器
|
||||
* @param label 标签文本
|
||||
* @param key 表单字段名
|
||||
*/
|
||||
const useFormDatepicker = (
|
||||
label: string,
|
||||
key: string,
|
||||
other?: DatePickerProps & { class?: string },
|
||||
itemAttrs?: FormItemProps & { class?: string },
|
||||
slot?: { prefix?: Array<() => JSX.Element>; suffix?: Array<() => JSX.Element> },
|
||||
) => {
|
||||
return createFormItem(label, key, 'datepicker', { ...other }, itemAttrs, slot)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个表单时间选择器
|
||||
* @param label 标签文本
|
||||
* @param key 表单字段名
|
||||
*/
|
||||
const useFormTimepicker = (
|
||||
label: string,
|
||||
key: string,
|
||||
other?: TimePickerProps & { class?: string },
|
||||
itemAttrs?: FormItemProps & { class?: string },
|
||||
slot?: { prefix?: Array<() => JSX.Element>; suffix?: Array<() => JSX.Element> },
|
||||
) => {
|
||||
return createFormItem(label, key, 'timepicker', { ...other }, itemAttrs, slot)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个表单滑块
|
||||
* @param label 标签文本
|
||||
* @param key 表单字段名
|
||||
*/
|
||||
const useFormSlider = (
|
||||
label: string,
|
||||
key: string,
|
||||
other?: SliderProps & { class?: string },
|
||||
itemAttrs?: FormItemProps & { class?: string },
|
||||
slot?: { prefix?: Array<() => JSX.Element>; suffix?: Array<() => JSX.Element> },
|
||||
) => {
|
||||
return createFormItem(label, key, 'slider', { ...other }, itemAttrs, slot)
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 表单行hook 更多配置
|
||||
* @param { Ref<boolean> } isMore 是否展开
|
||||
* @param { string } content 内容
|
||||
*/
|
||||
const useFormMore = (isMore: Ref<boolean>, content?: string) => {
|
||||
const color = `var(--n-color-target)`
|
||||
return {
|
||||
type: 'custom',
|
||||
render: () => (
|
||||
<NDivider
|
||||
class="cursor-pointer w-full"
|
||||
onClick={() => {
|
||||
isMore.value = !isMore.value
|
||||
}}
|
||||
>
|
||||
<div class="flex items-center w-full" style={{ color }}>
|
||||
<span class="mr-[4px]">
|
||||
{!isMore.value ? hookT('expand') : hookT('collapse')}
|
||||
{content || hookT('moreConfig')}
|
||||
</span>
|
||||
<NIcon>{isMore.value ? <DownOutlined /> : <LeftOutlined />}</NIcon>
|
||||
</div>
|
||||
</NDivider>
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 表单行hook 帮助文档
|
||||
* @param { Ref<boolean> } isMore 是否展开
|
||||
* @param { string } content 内容
|
||||
*/
|
||||
const useFormHelp = (
|
||||
options: { content: string | JSX.Element; isHtml?: boolean }[],
|
||||
other?: { listStyle?: string },
|
||||
) => {
|
||||
const helpList = toRef(options)
|
||||
return {
|
||||
type: 'custom',
|
||||
render: () => (
|
||||
<ul
|
||||
class={`text-[#777] mt-[2px] leading-[2rem] text-[12px] ml-[20px] list-${other?.listStyle || 'disc'}`}
|
||||
style="color: var(--n-close-icon-color);"
|
||||
{...other}
|
||||
>
|
||||
{helpList.value.map(
|
||||
(
|
||||
item: {
|
||||
content: string | JSX.Element
|
||||
isHtml?: boolean
|
||||
},
|
||||
index: number,
|
||||
) => (item.isHtml ? <li key={index} v-html={item.content}></li> : <li key={index}>{item.content}</li>),
|
||||
)}
|
||||
</ul>
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// 导出所有表单钩子函数
|
||||
export const useFormHooks = () => ({
|
||||
useFormInput,
|
||||
useFormTextarea,
|
||||
useFormPassword,
|
||||
useFormInputNumber,
|
||||
useFormSelect,
|
||||
useFormSlot,
|
||||
useFormCustom,
|
||||
useFormGroup,
|
||||
useFormRadio,
|
||||
useFormRadioButton,
|
||||
useFormCheckbox,
|
||||
useFormSwitch,
|
||||
useFormDatepicker,
|
||||
useFormTimepicker,
|
||||
useFormSlider,
|
||||
useFormMore,
|
||||
useFormHelp,
|
||||
})
|
||||
203
frontend/packages/vue/naive-ui/src/hooks/useFullScreen.tsx
Normal file
203
frontend/packages/vue/naive-ui/src/hooks/useFullScreen.tsx
Normal file
@@ -0,0 +1,203 @@
|
||||
import { h, ref, Ref, defineComponent, onBeforeUnmount } from 'vue'
|
||||
import { NButton } from 'naive-ui'
|
||||
|
||||
/**
|
||||
* 扩展Document接口以支持各浏览器的全屏API
|
||||
* 处理不同浏览器前缀版本的全屏元素获取和退出全屏方法
|
||||
*/
|
||||
interface FullscreenDocument extends Document {
|
||||
webkitFullscreenElement?: Element // Webkit浏览器的全屏元素属性
|
||||
mozFullScreenElement?: Element // Mozilla浏览器的全屏元素属性
|
||||
msFullscreenElement?: Element // IE浏览器的全屏元素属性
|
||||
webkitExitFullscreen?: () => Promise<void> // Webkit浏览器退出全屏方法
|
||||
mozCancelFullScreen?: () => Promise<void> // Mozilla浏览器退出全屏方法
|
||||
msExitFullscreen?: () => Promise<void> // IE浏览器退出全屏方法
|
||||
}
|
||||
|
||||
/**
|
||||
* 扩展HTMLElement接口以支持各浏览器的全屏请求方法
|
||||
* 处理不同浏览器前缀版本的请求全屏方法
|
||||
*/
|
||||
interface FullscreenElement extends HTMLElement {
|
||||
webkitRequestFullscreen?: () => Promise<void> // Webkit浏览器请求全屏方法
|
||||
mozRequestFullScreen?: () => Promise<void> // Mozilla浏览器请求全屏方法
|
||||
msRequestFullscreen?: () => Promise<void> // IE浏览器请求全屏方法
|
||||
}
|
||||
|
||||
/**
|
||||
* 全屏钩子配置选项接口
|
||||
*/
|
||||
interface UseFullScreenOptions {
|
||||
onEnter?: () => void // 进入全屏时的回调函数
|
||||
onExit?: () => void // 退出全屏时的回调函数
|
||||
}
|
||||
|
||||
/**
|
||||
* 全屏钩子返回值接口
|
||||
*/
|
||||
interface UseFullScreenReturn {
|
||||
isFullscreen: Ref<boolean> // 是否处于全屏状态
|
||||
toggle: () => void // 切换全屏状态的方法
|
||||
FullScreenButton: ReturnType<typeof defineComponent> // 全屏切换按钮组件
|
||||
}
|
||||
|
||||
/**
|
||||
* 全屏功能钩子函数
|
||||
* 提供全屏状态管理、切换控制和内置全屏按钮组件
|
||||
*
|
||||
* @param targetRef - 目标元素引用,可以是HTMLElement或包含$el属性的Vue组件实例
|
||||
* @param options - 配置选项,包含进入和退出全屏的回调函数
|
||||
* @returns 包含全屏状态、切换方法和按钮组件的对象
|
||||
*/
|
||||
export default function useFullScreen(
|
||||
targetRef: Ref<HTMLElement | null | { $el: HTMLElement }>,
|
||||
options: UseFullScreenOptions = {},
|
||||
): UseFullScreenReturn {
|
||||
const isFullscreen = ref(false) // 全屏状态引用
|
||||
const fullscreenDoc = document as FullscreenDocument
|
||||
|
||||
/**
|
||||
* 获取当前处于全屏状态的元素
|
||||
* 兼容不同浏览器的全屏API
|
||||
* @returns 全屏元素或null
|
||||
*/
|
||||
const getFullscreenElement = (): Element | null => {
|
||||
return (
|
||||
fullscreenDoc.fullscreenElement ||
|
||||
fullscreenDoc.webkitFullscreenElement ||
|
||||
fullscreenDoc.mozFullScreenElement ||
|
||||
fullscreenDoc.msFullscreenElement ||
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 进入全屏模式
|
||||
* 尝试使用不同浏览器支持的方法请求全屏
|
||||
*/
|
||||
const enterFullscreen = async (): Promise<void> => {
|
||||
// 处理Vue组件实例和普通DOM元素两种情况
|
||||
const target = targetRef.value && '$el' in targetRef.value ? targetRef.value.$el : targetRef.value
|
||||
if (!target) return
|
||||
|
||||
try {
|
||||
const element = target as FullscreenElement
|
||||
const requestMethods = [
|
||||
element.requestFullscreen,
|
||||
element.webkitRequestFullscreen,
|
||||
element.mozRequestFullScreen,
|
||||
element.msRequestFullscreen,
|
||||
]
|
||||
|
||||
// 找到并使用第一个可用的方法
|
||||
for (const method of requestMethods) {
|
||||
if (method) {
|
||||
await method.call(element)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
isFullscreen.value = true
|
||||
// 调用进入全屏回调(如果提供)
|
||||
options.onEnter?.()
|
||||
} catch (error) {
|
||||
console.error('Failed to enter fullscreen:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 退出全屏模式
|
||||
* 尝试使用不同浏览器支持的方法退出全屏
|
||||
*/
|
||||
const exitFullscreen = async (): Promise<void> => {
|
||||
try {
|
||||
const exitMethods = [
|
||||
fullscreenDoc.exitFullscreen,
|
||||
fullscreenDoc.webkitExitFullscreen,
|
||||
fullscreenDoc.mozCancelFullScreen,
|
||||
fullscreenDoc.msExitFullscreen,
|
||||
]
|
||||
|
||||
// 找到并使用第一个可用的方法
|
||||
for (const method of exitMethods) {
|
||||
if (method) {
|
||||
await method.call(document)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
isFullscreen.value = false
|
||||
// 调用退出全屏回调(如果提供)
|
||||
options.onExit?.()
|
||||
} catch (error) {
|
||||
console.error('Failed to exit fullscreen:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换全屏状态
|
||||
* 根据当前状态决定是进入还是退出全屏
|
||||
*/
|
||||
const toggle = (): void => {
|
||||
if (isFullscreen.value) {
|
||||
exitFullscreen()
|
||||
} else {
|
||||
enterFullscreen()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理全屏状态变化事件
|
||||
* 在全屏状态变化时更新状态并调用回调
|
||||
*/
|
||||
const handleFullscreenChange = (): void => {
|
||||
isFullscreen.value = !!getFullscreenElement()
|
||||
// 当退出全屏时调用退出回调
|
||||
if (!isFullscreen.value) {
|
||||
options.onExit?.()
|
||||
}
|
||||
}
|
||||
|
||||
// 支持不同浏览器的全屏变化事件名称
|
||||
const fullscreenEvents = ['fullscreenchange', 'webkitfullscreenchange', 'mozfullscreenchange', 'MSFullscreenChange']
|
||||
|
||||
// 为所有全屏事件添加监听器
|
||||
fullscreenEvents.forEach((event) => {
|
||||
document.addEventListener(event, handleFullscreenChange)
|
||||
})
|
||||
|
||||
// 组件卸载前清理:移除所有事件监听器
|
||||
onBeforeUnmount(() => {
|
||||
fullscreenEvents.forEach((event) => {
|
||||
document.removeEventListener(event, handleFullscreenChange)
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* 全屏切换按钮组件
|
||||
* 提供一个内置的UI组件用于切换全屏状态
|
||||
*/
|
||||
const FullScreenButton = defineComponent({
|
||||
name: 'FullScreenButton',
|
||||
setup() {
|
||||
return () =>
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
onClick: toggle,
|
||||
type: 'primary',
|
||||
ghost: true,
|
||||
},
|
||||
// 根据全屏状态显示不同的按钮文本
|
||||
() => (isFullscreen.value ? '退出全屏' : '进入全屏'),
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
// 返回钩子的公开API
|
||||
return {
|
||||
isFullscreen,
|
||||
toggle,
|
||||
FullScreenButton,
|
||||
}
|
||||
}
|
||||
222
frontend/packages/vue/naive-ui/src/hooks/useLoadingMask.tsx
Normal file
222
frontend/packages/vue/naive-ui/src/hooks/useLoadingMask.tsx
Normal file
@@ -0,0 +1,222 @@
|
||||
import { ref, createVNode, render, type VNode } from 'vue'
|
||||
import { NSpin } from 'naive-ui'
|
||||
import { LoadingMaskOptions, LoadingMaskInstance } from '../types/loadingMask'
|
||||
|
||||
/**
|
||||
* 默认配置选项
|
||||
*/
|
||||
const defaultOptions: LoadingMaskOptions = {
|
||||
text: '正在加载中,请稍后 ...',
|
||||
description: '',
|
||||
color: '',
|
||||
size: 'small',
|
||||
stroke: '',
|
||||
show: true,
|
||||
fullscreen: true,
|
||||
background: 'rgba(0, 0, 0, 0.5)',
|
||||
zIndex: 2000,
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载遮罩钩子函数
|
||||
* @param options 遮罩层初始配置
|
||||
* @returns LoadingMaskInstance 遮罩层控制实例
|
||||
*/
|
||||
const useLoadingMask = (options: LoadingMaskOptions = {}): LoadingMaskInstance => {
|
||||
// 合并配置
|
||||
const mergedOptions = ref<LoadingMaskOptions>({
|
||||
...defaultOptions,
|
||||
...options,
|
||||
})
|
||||
|
||||
// 控制显示状态
|
||||
const visible = ref(false)
|
||||
// VNode实例
|
||||
let loadingInstance: VNode | null = null
|
||||
// 挂载容器
|
||||
let container: HTMLElement | null = null
|
||||
|
||||
/**
|
||||
* 创建遮罩层DOM元素
|
||||
* 负责创建和配置遮罩层的容器元素
|
||||
*/
|
||||
const createLoadingElement = () => {
|
||||
// 如果已经存在,先销毁
|
||||
if (container) {
|
||||
document.body.removeChild(container)
|
||||
container = null
|
||||
}
|
||||
container = document.createElement('div')
|
||||
const targetElement = getTargetElement()
|
||||
|
||||
// 设置样式
|
||||
const style: Record<string, unknown> = {
|
||||
position: mergedOptions.value.fullscreen ? 'fixed' : 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: mergedOptions.value.background,
|
||||
zIndex: mergedOptions.value.zIndex,
|
||||
...(mergedOptions.value.customStyle || {}),
|
||||
}
|
||||
|
||||
// 非全屏模式下,计算目标元素的位置和尺寸
|
||||
if (!mergedOptions.value.fullscreen && targetElement && targetElement !== document.body) {
|
||||
const rect = targetElement.getBoundingClientRect()
|
||||
Object.assign(style, {
|
||||
top: `${rect.top}px`,
|
||||
left: `${rect.left}px`,
|
||||
width: `${rect.width}px`,
|
||||
height: `${rect.height}px`,
|
||||
position: 'fixed',
|
||||
})
|
||||
}
|
||||
|
||||
// 应用样式
|
||||
Object.keys(style).forEach((key) => {
|
||||
container!.style[key as any] = style[key] as string
|
||||
})
|
||||
|
||||
// 添加自定义类名
|
||||
if (mergedOptions.value.customClass) {
|
||||
container.className = mergedOptions.value.customClass
|
||||
}
|
||||
document.body.appendChild(container)
|
||||
return container
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取目标元素
|
||||
* 根据配置返回目标DOM元素,如果没有指定或找不到则返回body
|
||||
*/
|
||||
const getTargetElement = (): HTMLElement => {
|
||||
const { target } = mergedOptions.value
|
||||
if (!target) {
|
||||
return document.body
|
||||
}
|
||||
if (typeof target === 'string') {
|
||||
const element = document.querySelector(target) as HTMLElement
|
||||
return element || document.body
|
||||
}
|
||||
return target
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染遮罩层
|
||||
* 创建NSpin组件并渲染到容器中
|
||||
*/
|
||||
const renderLoading = () => {
|
||||
if (!visible.value) return
|
||||
const container = createLoadingElement()
|
||||
// 创建内容容器
|
||||
const contentContainer = createVNode(
|
||||
'div',
|
||||
{
|
||||
style: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '16px 24px',
|
||||
backgroundColor: '#fff',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',
|
||||
},
|
||||
},
|
||||
[
|
||||
// 加载组件VNode
|
||||
createVNode(NSpin, {
|
||||
description: mergedOptions.value.description,
|
||||
size: mergedOptions.value.size,
|
||||
stroke: mergedOptions.value.stroke,
|
||||
style: { marginRight: '12px' },
|
||||
...(mergedOptions.value.spinProps || {}),
|
||||
}),
|
||||
// 文字内容
|
||||
createVNode(
|
||||
'span',
|
||||
{
|
||||
style: {
|
||||
fontSize: '14px',
|
||||
color: '#333',
|
||||
},
|
||||
},
|
||||
mergedOptions.value.text,
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
loadingInstance = contentContainer
|
||||
|
||||
// 渲染到容器
|
||||
render(loadingInstance, container)
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开遮罩层
|
||||
* @param newOptions 可选的新配置,会与现有配置合并
|
||||
*/
|
||||
const open = (newOptions?: LoadingMaskOptions) => {
|
||||
if (newOptions) {
|
||||
mergedOptions.value = {
|
||||
...mergedOptions.value,
|
||||
...newOptions,
|
||||
}
|
||||
}
|
||||
|
||||
visible.value = true
|
||||
renderLoading()
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭遮罩层
|
||||
* 隐藏并移除DOM元素,同时调用onClose回调
|
||||
*/
|
||||
const close = () => {
|
||||
console.log('close', '测试内容')
|
||||
visible.value = false
|
||||
|
||||
if (container) {
|
||||
render(null, container)
|
||||
document.body.removeChild(container)
|
||||
container = null
|
||||
}
|
||||
|
||||
mergedOptions.value.onClose?.()
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新遮罩层配置
|
||||
* @param newOptions 新的配置选项
|
||||
*/
|
||||
const update = (newOptions: LoadingMaskOptions) => {
|
||||
mergedOptions.value = {
|
||||
...mergedOptions.value,
|
||||
...newOptions,
|
||||
}
|
||||
|
||||
if (visible.value) {
|
||||
renderLoading()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁遮罩层实例
|
||||
* 关闭并清理资源
|
||||
*/
|
||||
const destroy = () => {
|
||||
close()
|
||||
loadingInstance = null
|
||||
}
|
||||
|
||||
return {
|
||||
open,
|
||||
close,
|
||||
update,
|
||||
destroy,
|
||||
}
|
||||
}
|
||||
|
||||
export default useLoadingMask
|
||||
55
frontend/packages/vue/naive-ui/src/hooks/useMessage.tsx
Normal file
55
frontend/packages/vue/naive-ui/src/hooks/useMessage.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import { computed, getCurrentInstance } from 'vue'
|
||||
import { useMessage as useNaiveMessage, createDiscreteApi, type MessageOptions } from 'naive-ui'
|
||||
import { useTheme } from '../theme'
|
||||
import type { MessageApiExtended } from '../types/message'
|
||||
|
||||
/**
|
||||
* 消息提示钩子函数,兼容组件内和非组件环境
|
||||
*
|
||||
* 在组件中使用时,使用 Naive UI 的 useMessage
|
||||
* 在非组件环境中,使用 createDiscreteApi 创建消息实例
|
||||
*/
|
||||
export function useMessage(): MessageApiExtended {
|
||||
// 判断是否在setup中使用
|
||||
const instance = getCurrentInstance()
|
||||
|
||||
// 在setup中使用原生useMessage
|
||||
if (instance && instance?.setupContext) {
|
||||
const naiveMessage = useNaiveMessage()
|
||||
return {
|
||||
...naiveMessage,
|
||||
request: (data: { status: boolean; message: string }, options?: MessageOptions) => {
|
||||
if (data.status) {
|
||||
return naiveMessage.success(data.message, options)
|
||||
} else {
|
||||
return naiveMessage.error(data.message, options)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// 在非组件环境中使用createDiscreteApi
|
||||
const { theme, themeOverrides } = useTheme()
|
||||
|
||||
// 创建configProviderProps
|
||||
const configProviderProps = computed(() => ({
|
||||
theme: theme.value,
|
||||
themeOverrides: themeOverrides.value,
|
||||
}))
|
||||
|
||||
// 创建discreteMessage实例
|
||||
const { message } = createDiscreteApi(['message'], { configProviderProps })
|
||||
|
||||
return {
|
||||
...message,
|
||||
request: (data: { status: boolean; message: string }, options?: MessageOptions) => {
|
||||
if (data.status) {
|
||||
return message.success(data.message, options)
|
||||
} else {
|
||||
return message.error(data.message, options)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default useMessage
|
||||
470
frontend/packages/vue/naive-ui/src/hooks/useModal.tsx
Normal file
470
frontend/packages/vue/naive-ui/src/hooks/useModal.tsx
Normal file
@@ -0,0 +1,470 @@
|
||||
import {
|
||||
type Component,
|
||||
type VNodeChild,
|
||||
ref,
|
||||
h,
|
||||
getCurrentInstance,
|
||||
provide,
|
||||
defineComponent,
|
||||
inject,
|
||||
App,
|
||||
Ref,
|
||||
computed,
|
||||
} from 'vue'
|
||||
import {
|
||||
useModal as useNaiveModal,
|
||||
createDiscreteApi,
|
||||
type ButtonProps,
|
||||
type ModalReactive,
|
||||
NButton,
|
||||
ModalOptions,
|
||||
} from 'naive-ui'
|
||||
import { isBoolean, isFunction } from '@baota/utils/type'
|
||||
import { useTheme } from '../theme'
|
||||
import { translation } from '../locals/translation'
|
||||
|
||||
import customProvider from '../components/customProvider'
|
||||
|
||||
// 定义provide/inject的key
|
||||
export const MODAL_CLOSE_KEY = Symbol('modal-close')
|
||||
export const MODAL_CLOSEABLE_KEY = Symbol('modal-closeable')
|
||||
export const MODAL_LOADING_KEY = Symbol('modal-loading')
|
||||
export const MODAL_CONFIRM_KEY = Symbol('modal-confirm')
|
||||
export const MODAL_CANCEL_KEY = Symbol('modal-cancel')
|
||||
// export const MODAL_I18N_KEY = Symbol('modal-i18n')e')
|
||||
export const MODAL_MESSAGE_KEY = Symbol('modal-message')
|
||||
export const MODAL_OPTIONS_KEY = Symbol('modal-options')
|
||||
// 自定义Modal配置类型
|
||||
export interface CustomModalOptions {
|
||||
title?: string | (() => VNodeChild) // 标题
|
||||
area?: string | string[] | number | number[] // 视图大小
|
||||
maskClosable?: boolean // 是否可通过遮罩层关闭
|
||||
destroyOnClose?: boolean // 是否在关闭时销毁
|
||||
draggable?: boolean // 是否可拖拽
|
||||
closable?: boolean // 是否显示关闭按钮
|
||||
footer?: boolean | (() => VNodeChild) // 是否显示底部按钮
|
||||
confirmText?: string // 确认按钮文本
|
||||
cancelText?: string // 取消按钮文本
|
||||
modalStyle?: Record<string, any> // 弹窗样式
|
||||
confirmButtonProps?: ButtonProps // 确认按钮props
|
||||
cancelButtonProps?: ButtonProps // 取消按钮props
|
||||
component?: (() => Promise<Component>) | Component // 组件
|
||||
componentProps?: Record<string, unknown> // 组件props
|
||||
onConfirm?: (close: () => void) => Promise<unknown> | void // 确认回调
|
||||
onCancel?: (close: () => void) => Promise<unknown> | void // 取消回调
|
||||
onClose?: (close: () => void) => void // 关闭回调
|
||||
onUpdateShow?: (show: boolean) => void // 更新显示状态回调
|
||||
modelOptions?: ModalOptions // Modal配置
|
||||
'z-index'?: number
|
||||
}
|
||||
|
||||
const appsUseList = {
|
||||
router: null,
|
||||
i18n: null,
|
||||
pinia: null,
|
||||
}
|
||||
|
||||
// 挂载资源
|
||||
const mountApps = (app: App, resources: any) => {
|
||||
if (app && resources) app.use(resources)
|
||||
}
|
||||
|
||||
// 自定义Modal钩子函数
|
||||
const useModal = (options: CustomModalOptions) => {
|
||||
const { theme, themeOverrides } = useTheme()
|
||||
|
||||
// 创建discreteModal实例 - 这个可以在任何地方使用
|
||||
const { modal, message, unmount, app } = createDiscreteApi(['modal', 'message'], {
|
||||
configProviderProps: { theme: theme.value, themeOverrides: themeOverrides.value },
|
||||
})
|
||||
|
||||
mountApps(app, appsUseList['i18n'])
|
||||
mountApps(app, appsUseList['router'])
|
||||
mountApps(app, appsUseList['pinia'])
|
||||
|
||||
// 判断是否在setup中使用
|
||||
const instance = getCurrentInstance()
|
||||
// 控制Modal显示状态
|
||||
const visible = ref(false)
|
||||
// Modal实例引用
|
||||
const modalInstance = ref<ModalReactive | null>(null)
|
||||
// 获取naiveModal实例 - 只在setup中使用
|
||||
const getNaiveModal = () => {
|
||||
if (instance) {
|
||||
return useNaiveModal()
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// const naiveModal = getNaiveModal()
|
||||
// 获取组件实例引用
|
||||
const wrapperRef = ref()
|
||||
// 创建Modal方法
|
||||
const create = async (optionsNew: CustomModalOptions) => {
|
||||
const {
|
||||
component,
|
||||
componentProps,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
footer = false,
|
||||
confirmText,
|
||||
cancelText,
|
||||
confirmButtonProps = { type: 'primary' },
|
||||
cancelButtonProps = { type: 'default' },
|
||||
...modelOptions
|
||||
} = optionsNew
|
||||
|
||||
const optionsRef = ref({ footer, confirmText, cancelText, confirmButtonProps, cancelButtonProps })
|
||||
|
||||
// 处理视图高度和宽度
|
||||
const getViewSize = (areaNew: string | string[] | number | number[] = '50%') => {
|
||||
if (Array.isArray(areaNew)) {
|
||||
return {
|
||||
width: typeof areaNew[0] === 'number' ? areaNew[0] + 'px' : areaNew[0],
|
||||
height: typeof areaNew[1] === 'number' ? areaNew[1] + 'px' : areaNew[1],
|
||||
}
|
||||
}
|
||||
return {
|
||||
width: typeof areaNew === 'number' ? areaNew + 'px' : areaNew,
|
||||
height: 'auto',
|
||||
}
|
||||
}
|
||||
|
||||
// 处理组件
|
||||
const content = async () => {
|
||||
if (typeof component === 'function') {
|
||||
try {
|
||||
// 处理异步组件函数
|
||||
const syncComponent = await (component as () => Promise<Component>)()
|
||||
return syncComponent.default || syncComponent
|
||||
} catch (e) {
|
||||
// 处理普通函数组件
|
||||
return component
|
||||
}
|
||||
}
|
||||
return component
|
||||
}
|
||||
|
||||
// 组件
|
||||
const componentNew = (await content()) as Component
|
||||
// 视图大小
|
||||
const { width, height } = await getViewSize(optionsNew.area)
|
||||
|
||||
// 存储组件内部注册的方法
|
||||
const confirmHandler = ref<(close: () => void) => Promise<void> | void>()
|
||||
const cancelHandler = ref<(close: () => void) => Promise<void> | void>()
|
||||
const closeable = ref(true)
|
||||
const loading = ref(false)
|
||||
|
||||
// 获取当前语言
|
||||
const currentLocale = localStorage.getItem('activeLocales') || '"zhCN"'
|
||||
// 获取翻译文本
|
||||
const hookT = (key: string) => {
|
||||
const locale = currentLocale.replace('-', '_').replace(/"/g, '')
|
||||
return (
|
||||
translation[locale as keyof typeof translation]?.useModal?.[key as keyof typeof translation.zhCN.useModal] ||
|
||||
translation.zhCN.useModal[key as keyof typeof translation.zhCN.useModal]
|
||||
)
|
||||
}
|
||||
|
||||
const closeMessage = ref(hookT('cannotClose'))
|
||||
|
||||
// 合并Modal配置
|
||||
const config: ModalOptions = {
|
||||
preset: 'card',
|
||||
style: { width, height, ...modelOptions.modalStyle },
|
||||
closeOnEsc: false,
|
||||
maskClosable: false,
|
||||
onClose: () => {
|
||||
if (!closeable.value || loading.value) {
|
||||
message.error(closeMessage.value)
|
||||
return false
|
||||
}
|
||||
// 调用组件内注册的取消方法
|
||||
cancelHandler.value?.()
|
||||
// 调用外部传入的取消回调
|
||||
onCancel?.(() => {})
|
||||
// unmount() // 卸载
|
||||
return true
|
||||
},
|
||||
content: () => {
|
||||
const Wrapper = defineComponent({
|
||||
setup() {
|
||||
// 提供Modal配置
|
||||
provide(MODAL_OPTIONS_KEY, optionsRef)
|
||||
|
||||
// 提供关闭方法
|
||||
provide(MODAL_CLOSE_KEY, close)
|
||||
|
||||
// 提供信息方法
|
||||
provide(MODAL_MESSAGE_KEY, message)
|
||||
|
||||
// 模块-确认按钮
|
||||
provide(MODAL_CONFIRM_KEY, (handler: (close: () => void) => Promise<void> | void) => {
|
||||
confirmHandler.value = handler
|
||||
})
|
||||
|
||||
// 模块-取消按钮
|
||||
provide(MODAL_CANCEL_KEY, (handler: (close: () => void) => Promise<void> | void) => {
|
||||
cancelHandler.value = handler
|
||||
})
|
||||
|
||||
// 模块 - 可关闭状态
|
||||
provide(MODAL_CLOSEABLE_KEY, (canClose: boolean) => {
|
||||
closeable.value = canClose
|
||||
})
|
||||
|
||||
// 模块-过度
|
||||
provide(MODAL_LOADING_KEY, (loadStatus: boolean, closeMsg?: string) => {
|
||||
loading.value = loadStatus
|
||||
closeMessage.value = closeMsg || hookT('cannotClose')
|
||||
})
|
||||
|
||||
// 暴露给父级使用
|
||||
return {
|
||||
confirmHandler,
|
||||
cancelHandler,
|
||||
render: () => h(componentNew as Component, { ...componentProps }),
|
||||
}
|
||||
},
|
||||
render() {
|
||||
return this.render()
|
||||
},
|
||||
})
|
||||
|
||||
const wrapper = instance ? h(Wrapper) : h(customProvider, {}, () => h(Wrapper))
|
||||
|
||||
return h(wrapper, { ref: wrapperRef })
|
||||
},
|
||||
// onAfterLeave: () => {
|
||||
// // 调用组件内注册的取消方法
|
||||
// cancelHandler.value?.()
|
||||
// // 调用外部传入的取消回调
|
||||
// onCancel?.(() => {})
|
||||
// },
|
||||
}
|
||||
const footerComp = computed(() => {
|
||||
if (isBoolean(optionsRef.value.footer) && optionsRef.value.footer) {
|
||||
// 确认事件
|
||||
const confirmEvent = async () => {
|
||||
await confirmHandler.value?.(close)
|
||||
// 调用外部传入的确认回调
|
||||
await onConfirm?.(close)
|
||||
}
|
||||
// 取消事件
|
||||
const cancelEvent = async () => {
|
||||
await cancelHandler.value?.(close)
|
||||
// 调用外部传入的取消回调
|
||||
await onCancel?.(close)
|
||||
if (!cancelHandler.value && !onCancel) {
|
||||
close()
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div class="flex justify-end">
|
||||
<NButton
|
||||
disabled={loading.value}
|
||||
{...cancelButtonProps}
|
||||
style={{ marginRight: '8px' }}
|
||||
onClick={cancelEvent}
|
||||
>
|
||||
{optionsRef.value.cancelText || hookT('cancel')}
|
||||
</NButton>
|
||||
<NButton disabled={loading.value} {...confirmButtonProps} onClick={confirmEvent}>
|
||||
{optionsRef.value.confirmText || hookT('confirm')}
|
||||
</NButton>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return null
|
||||
})
|
||||
// 底部按钮配置
|
||||
if (optionsRef.value.footer) config.footer = () => footerComp.value
|
||||
// 合并Modal配置
|
||||
Object.assign(config, modelOptions)
|
||||
if (instance) {
|
||||
const currentNaiveModal = getNaiveModal()
|
||||
if (currentNaiveModal) {
|
||||
modalInstance.value = currentNaiveModal.create(config)
|
||||
return modalInstance.value
|
||||
}
|
||||
}
|
||||
// 使用createDiscreteApi创建
|
||||
const discreteModal = modal.create(config)
|
||||
modalInstance.value = discreteModal
|
||||
options.onUpdateShow?.(true)
|
||||
return discreteModal
|
||||
}
|
||||
|
||||
// 关闭Modal方法
|
||||
const close = () => {
|
||||
visible.value = false
|
||||
if (modalInstance.value) {
|
||||
modalInstance.value.destroy()
|
||||
}
|
||||
options.onUpdateShow?.(false)
|
||||
}
|
||||
|
||||
// 销毁所有Modal实例方法
|
||||
const destroyAll = () => {
|
||||
// 销毁当前实例
|
||||
if (modalInstance.value) {
|
||||
modalInstance.value.destroy()
|
||||
modalInstance.value = null
|
||||
}
|
||||
visible.value = false
|
||||
// 销毁所有实例
|
||||
const currentNaiveModal = getNaiveModal()
|
||||
if (currentNaiveModal) {
|
||||
currentNaiveModal.destroyAll()
|
||||
} else {
|
||||
modal.destroyAll()
|
||||
}
|
||||
}
|
||||
|
||||
// 更新显示状态
|
||||
const updateShow = (show: boolean) => {
|
||||
visible.value = show
|
||||
}
|
||||
|
||||
return {
|
||||
...create(options),
|
||||
updateShow,
|
||||
close,
|
||||
destroyAll,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 重新设置Modal配置的钩子函数
|
||||
* @returns {Object} Modal配置
|
||||
*/
|
||||
export const useModalOptions = (): Ref<CustomModalOptions> => {
|
||||
return inject(MODAL_OPTIONS_KEY, ref({}))
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 获取Modal关闭方法的钩子函数
|
||||
*/
|
||||
export const useModalClose = () =>
|
||||
inject(MODAL_CLOSE_KEY, () => {
|
||||
console.warn('useModalClose 必须在 Modal 组件内部使用')
|
||||
})
|
||||
|
||||
/**
|
||||
* @description 注册Modal确认按钮点击处理方法的钩子函数
|
||||
* @param handler 确认按钮处理函数,接收一个关闭Modal的函数作为参数
|
||||
* @returns void
|
||||
*/
|
||||
export const useModalConfirm = (handler: (close: () => void) => Promise<any> | void) => {
|
||||
const registerConfirm = inject(MODAL_CONFIRM_KEY, (fn: (close: () => void) => Promise<void> | void) => {
|
||||
console.warn('useModalConfirm 必须在 Modal 组件内部使用')
|
||||
return
|
||||
})
|
||||
// 注册确认处理方法
|
||||
registerConfirm(handler)
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 注册Modal取消按钮点击处理方法的钩子函数
|
||||
* @param handler 取消按钮处理函数,接收一个关闭Modal的函数作为参数
|
||||
* @returns void
|
||||
*/
|
||||
export const useModalCancel = (handler: (close: () => void) => Promise<void> | void) => {
|
||||
const registerCancel = inject(MODAL_CANCEL_KEY, (fn: (close: () => void) => Promise<void> | void) => {
|
||||
console.warn('useModalCancel 必须在 Modal 组件内部使用')
|
||||
return
|
||||
})
|
||||
// 注册取消处理方法
|
||||
registerCancel(handler)
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 控制Modal是否可关闭的钩子函数
|
||||
* @returns {(canClose: boolean) => void} 设置Modal可关闭状态的函数
|
||||
*/
|
||||
export const useModalCloseable = () => {
|
||||
const registerCloseable = inject(MODAL_CLOSEABLE_KEY, (canClose: boolean) => {
|
||||
console.warn('useModalCloseable 必须在 Modal 组件内部使用')
|
||||
return
|
||||
})
|
||||
return registerCloseable
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 获取Modal消息提示实例的钩子函数
|
||||
* @returns {Object} Message消息实例,包含loading, success, error, warning, info等方法
|
||||
*/
|
||||
export const useModalMessage = () => {
|
||||
const message = inject(MODAL_MESSAGE_KEY, {
|
||||
loading: (str: string) => {},
|
||||
success: (str: string) => {},
|
||||
error: (str: string) => {},
|
||||
warning: (str: string) => {},
|
||||
info: (str: string) => {},
|
||||
})
|
||||
return message
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 控制Modal加载状态的钩子函数
|
||||
* @returns {(loadStatus: boolean, closeMsg?: string) => void} 设置加载状态的函数,
|
||||
* loadStatus为true时显示加载状态并禁止关闭,closeMsg为自定义禁止关闭时的提示消息
|
||||
*/
|
||||
export const useModalLoading = () => {
|
||||
const registerLoading = inject(MODAL_LOADING_KEY, (loadStatus: boolean, closeMsg?: string) => {
|
||||
console.warn('useModalLoading 必须在 Modal 组件内部使用')
|
||||
return
|
||||
})
|
||||
return registerLoading
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 获取Modal所有钩子函数的集合
|
||||
* @returns {Object} 包含所有Modal相关钩子函数的对象
|
||||
*/
|
||||
export const useModalHooks = () => ({
|
||||
/**
|
||||
* 设置Modal配置,用于修改Modal的配置
|
||||
*/
|
||||
options: useModalOptions,
|
||||
|
||||
/**
|
||||
* 关闭当前Modal的函数
|
||||
*/
|
||||
close: useModalClose,
|
||||
|
||||
/**
|
||||
* 注册Modal确认按钮点击处理方法
|
||||
*/
|
||||
confirm: useModalConfirm,
|
||||
|
||||
/**
|
||||
* 注册Modal取消按钮点击处理方法
|
||||
*/
|
||||
cancel: useModalCancel,
|
||||
|
||||
/**
|
||||
* 设置Modal是否可关闭的状态控制函数
|
||||
*/
|
||||
closeable: useModalCloseable,
|
||||
|
||||
/**
|
||||
* 获取Modal内部可用的消息提示实例
|
||||
*/
|
||||
message: useModalMessage,
|
||||
|
||||
/**
|
||||
* 设置Modal加载状态的控制函数
|
||||
*/
|
||||
loading: useModalLoading,
|
||||
})
|
||||
|
||||
// 设置资源
|
||||
export const useModalUseDiscrete = ({ router, i18n, pinia }: any) => {
|
||||
appsUseList['i18n'] = i18n
|
||||
appsUseList['router'] = router
|
||||
appsUseList['pinia'] = pinia
|
||||
}
|
||||
|
||||
export default useModal
|
||||
286
frontend/packages/vue/naive-ui/src/hooks/useTable.tsx
Normal file
286
frontend/packages/vue/naive-ui/src/hooks/useTable.tsx
Normal file
@@ -0,0 +1,286 @@
|
||||
import { ref, shallowRef, ShallowRef, Ref, effectScope, watch, onUnmounted, isRef, computed } from 'vue'
|
||||
import {
|
||||
type DataTableProps,
|
||||
type DataTableSlots,
|
||||
type PaginationProps,
|
||||
type PaginationSlots,
|
||||
NDataTable,
|
||||
NPagination,
|
||||
NButton,
|
||||
} from 'naive-ui'
|
||||
import { translation, TranslationModule, type TranslationLocale } from '../locals/translation'
|
||||
import { useMessage } from './useMessage'
|
||||
|
||||
import type {
|
||||
UseTableOptions,
|
||||
TableInstanceWithComponent,
|
||||
TableResponse,
|
||||
TablePageInstanceWithComponent,
|
||||
TablePageProps,
|
||||
} from '../types/table'
|
||||
|
||||
// 获取当前语言
|
||||
const currentLocale = localStorage.getItem('locale-active') || 'zhCN'
|
||||
|
||||
// 获取翻译文本
|
||||
const hookT = (key: string, params?: string) => {
|
||||
const locale = currentLocale.replace('-', '_').replace(/"/g, '') as TranslationLocale
|
||||
const translationFn =
|
||||
(translation[locale as TranslationLocale] as TranslationModule).useTable[
|
||||
key as keyof TranslationModule['useTable']
|
||||
] || translation.zhCN.useTable[key as keyof typeof translation.zhCN.useTable]
|
||||
return typeof translationFn === 'function' ? translationFn(params || '') : translationFn
|
||||
}
|
||||
/**
|
||||
* 表格钩子函数
|
||||
* @param options 表格配置选项
|
||||
* @returns 表格实例,包含表格状态和方法
|
||||
*/
|
||||
export default function useTable<T = Record<string, any>, Z extends Record<string, any> = Record<string, any>>({
|
||||
config, // 表格列配置
|
||||
request, // 数据请求函数
|
||||
defaultValue = ref({}) as Ref<Z>, // 默认请求参数,支持响应式
|
||||
watchValue = false, // 监听参数
|
||||
}: UseTableOptions<T, Z>) {
|
||||
const scope = effectScope() // 创建一个作用域,用于管理副作用
|
||||
|
||||
return scope.run(() => {
|
||||
// 表格状态
|
||||
const columns = shallowRef(config) // 表格列配置
|
||||
const loading = ref(false) // 加载状态
|
||||
const data = ref({ list: [], total: 0 }) as Ref<{ list: T[]; total: number }> // 表格数据
|
||||
const alias = ref({ total: 'total', list: 'list' }) // 表格别名
|
||||
const example = ref() // 表格引用
|
||||
const param = (isRef(defaultValue) ? defaultValue : ref({ ...(defaultValue as Z) })) as Ref<Z> // 表格请求参数
|
||||
const total = ref(0) // 分页参数
|
||||
const props = shallowRef({}) as ShallowRef<DataTableProps> // 表格属性
|
||||
// const watchData = ref([]) // 监听参数
|
||||
const { error: errorMsg } = useMessage()
|
||||
|
||||
/**
|
||||
* 获取表格数据
|
||||
*/
|
||||
const fetchData = async <T,>() => {
|
||||
try {
|
||||
loading.value = true
|
||||
const rdata: TableResponse<T> = await request(param.value)
|
||||
total.value = rdata[alias.value.total as keyof TableResponse<T>] as number
|
||||
data.value = {
|
||||
list: rdata[alias.value.list as keyof TableResponse<T>] as [],
|
||||
total: rdata[alias.value.total as keyof TableResponse<T>] as number,
|
||||
}
|
||||
return data.value
|
||||
} catch (error: any) {
|
||||
errorMsg(error.message)
|
||||
console.error('请求数据失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置表格状态和数据
|
||||
*/
|
||||
const reset = async <T,>() => {
|
||||
param.value = defaultValue.value
|
||||
return await fetchData<T>()
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染表格组件
|
||||
*/
|
||||
const component = (props: DataTableProps, context: { slots?: DataTableSlots }) => {
|
||||
const { slots, ...attrs } = props as any
|
||||
const s2 = context
|
||||
return (
|
||||
<NDataTable
|
||||
remote
|
||||
ref={example}
|
||||
loading={loading.value}
|
||||
data={data.value.list}
|
||||
columns={columns.value}
|
||||
{...props}
|
||||
{...attrs}
|
||||
>
|
||||
{{
|
||||
empty: () => (slots?.empty || s2?.slots?.empty ? slots?.empty() || s2?.slots?.empty() : null),
|
||||
loading: () => (slots?.loading || s2?.slots?.loading ? slots?.loading() || s2?.slots?.loading() : null),
|
||||
}}
|
||||
</NDataTable>
|
||||
)
|
||||
}
|
||||
|
||||
// 检测到参数变化时,重新请求数据
|
||||
if (Array.isArray(watchValue)) {
|
||||
// 只监听指定的字段
|
||||
const source = computed(() => watchValue.map((key) => param.value[key]))
|
||||
watch(source, fetchData, { deep: true })
|
||||
}
|
||||
// 检测到默认参数变化时,合并参数
|
||||
// watch(defaultValue, () => (param.value = { ...defaultValue.value, ...param.value }), { deep: true })
|
||||
|
||||
onUnmounted(() => {
|
||||
scope.stop() // 停止作用域
|
||||
}) // 清理副作用
|
||||
|
||||
// 返回表格实例
|
||||
return {
|
||||
loading,
|
||||
example,
|
||||
data,
|
||||
alias,
|
||||
param,
|
||||
total,
|
||||
reset: reset<T>,
|
||||
fetch: fetchData<T>,
|
||||
component,
|
||||
config: columns,
|
||||
props,
|
||||
}
|
||||
}) as TableInstanceWithComponent<T, Z>
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 扩展表格实例方法
|
||||
*/
|
||||
const useTablePage = <T extends Record<string, any> = Record<string, any>>({
|
||||
param,
|
||||
total,
|
||||
alias = { page: 'page', pageSize: 'page_size' }, // 字段别名映射
|
||||
props = {},
|
||||
slot = {},
|
||||
refresh = () => {},
|
||||
}: TablePageProps<T> & { refresh?: () => void }) => {
|
||||
const scope = effectScope() // 创建一个作用域,用于管理副作用
|
||||
return scope.run(() => {
|
||||
const { page, pageSize } = { ...{ page: 'page', pageSize: 'page_size' }, ...alias }
|
||||
const pageSizeOptionsRef = ref([10, 20, 50, 100, 200]) // 当前页码
|
||||
const propsRef = ref({ ...props })
|
||||
|
||||
// 如果分页参数不存在,则设置默认值
|
||||
if (!(param.value as Record<string, unknown>)[page]) {
|
||||
;(param.value as Record<string, unknown>)[page] = 1 // 当前页码
|
||||
}
|
||||
|
||||
// 如果分页参数不存在,则设置默认值
|
||||
if (!(param.value as Record<string, unknown>)[pageSize]) {
|
||||
;(param.value as Record<string, unknown>)[pageSize] = 20 // 每页条数
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 更新页码
|
||||
* @param {number} currentPage 当前页码
|
||||
*/
|
||||
const handlePageChange = (currentPage: number) => {
|
||||
param.value = {
|
||||
...param.value,
|
||||
[page]: currentPage,
|
||||
}
|
||||
if (refresh) {
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 更新每页条数
|
||||
* @param {number} size 每页条数
|
||||
*/
|
||||
const handlePageSizeChange = (size: number) => {
|
||||
param.value = {
|
||||
...param.value,
|
||||
[page]: 1, // 重置页码为1
|
||||
[pageSize]: size,
|
||||
}
|
||||
if (refresh) {
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染分页组件
|
||||
const component = (props: PaginationProps, context: { slots?: PaginationSlots }) => {
|
||||
// 处理插槽
|
||||
const mergedSlots = {
|
||||
...slot,
|
||||
...(context.slots || {}),
|
||||
}
|
||||
return (
|
||||
<NPagination
|
||||
page={param.value[page]}
|
||||
pageSize={param.value[pageSize]}
|
||||
itemCount={total.value}
|
||||
pageSizes={pageSizeOptionsRef.value}
|
||||
showSizePicker={true}
|
||||
onUpdatePage={handlePageChange}
|
||||
onUpdatePageSize={handlePageSizeChange}
|
||||
{...propsRef.value}
|
||||
{...props}
|
||||
v-slots={mergedSlots}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
// 组件卸载
|
||||
onUnmounted(() => {
|
||||
scope.stop() // 停止作用域
|
||||
}) // 清理副作用
|
||||
|
||||
return {
|
||||
component,
|
||||
handlePageChange,
|
||||
handlePageSizeChange,
|
||||
pageSizeOptions: pageSizeOptionsRef,
|
||||
}
|
||||
}) as TablePageInstanceWithComponent
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 表格列hook--操作列
|
||||
*/
|
||||
const useTableOperation = (
|
||||
options: {
|
||||
title: string
|
||||
onClick: (row: any) => void
|
||||
isHide?: boolean | ((row: any) => boolean)
|
||||
}[],
|
||||
others?: any,
|
||||
) => {
|
||||
return {
|
||||
title: hookT('operation'),
|
||||
key: 'CreatedAt',
|
||||
width: 180,
|
||||
fixed: 'right' as const,
|
||||
align: 'right' as const,
|
||||
render: (row: any) => {
|
||||
const buttons: JSX.Element[] = []
|
||||
for (let index = 0; index < options.length; index++) {
|
||||
const option = options[index]
|
||||
const isHide =
|
||||
typeof option.isHide === 'function'
|
||||
? option.isHide(row)
|
||||
: typeof option.isHide === 'boolean'
|
||||
? option.isHide
|
||||
: false
|
||||
if (isHide) continue
|
||||
buttons.push(
|
||||
<NButton size="small" text type="primary" onClick={() => option.onClick(row)}>
|
||||
{option.title}
|
||||
</NButton>,
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="flex justify-end">
|
||||
{buttons.map((button, index) => (
|
||||
<>
|
||||
{button}
|
||||
{index < buttons.length - 1 && <span class="mx-[.8rem] text-[#dcdfe6]">|</span>}
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
...others,
|
||||
}
|
||||
}
|
||||
|
||||
export { useTablePage, useTableOperation }
|
||||
86
frontend/packages/vue/naive-ui/src/hooks/useTabs.tsx
Normal file
86
frontend/packages/vue/naive-ui/src/hooks/useTabs.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import { NTabs, NTabPane } from 'naive-ui'
|
||||
import { useRoute, useRouter, type RouteRecordRaw } from 'vue-router'
|
||||
|
||||
/**
|
||||
* 标签页配置项接口
|
||||
*/
|
||||
export interface UseTabsOptions {
|
||||
/** 是否在初始化时自动选中第一个标签 */
|
||||
defaultToFirst?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* 标签页实例接口
|
||||
*/
|
||||
export interface TabsInstance {
|
||||
/** 当前激活的标签值 */
|
||||
activeKey: string
|
||||
/** 子路由列表 */
|
||||
childRoutes: RouteRecordRaw[]
|
||||
/** 切换标签页方法 */
|
||||
handleTabChange: (key: string) => void
|
||||
/** 标签页渲染组件 */
|
||||
TabsComponent: () => JSX.Element
|
||||
}
|
||||
|
||||
/**
|
||||
* 标签页钩子函数
|
||||
* 用于处理二级路由的标签页导航
|
||||
*/
|
||||
export default function useTabs(options: UseTabsOptions = {}): TabsInstance {
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const { defaultToFirst = true } = options
|
||||
|
||||
// 当前激活的标签值
|
||||
const activeKey = ref(route.name as string)
|
||||
|
||||
// 获取当前路由的子路由配置
|
||||
const childRoutes = computed(() => {
|
||||
const parentRoute = router.getRoutes().find((r) => r.path === route.matched[0]?.path)
|
||||
return parentRoute?.children || []
|
||||
})
|
||||
|
||||
/**
|
||||
* 处理标签切换
|
||||
* @param key 目标路由名称
|
||||
*/
|
||||
const handleTabChange = (key: string) => {
|
||||
const targetRoute = childRoutes.value.find((route) => route.name === key)
|
||||
if (targetRoute) {
|
||||
router.push({ name: key })
|
||||
activeKey.value = key
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 标签页组件
|
||||
* 渲染标签页导航和对应的视图
|
||||
*/
|
||||
const TabsComponent = () => (
|
||||
<div class="tabs-container">
|
||||
<NTabs value={activeKey.value} onUpdateValue={handleTabChange} type="line" class="tabs-nav">
|
||||
{childRoutes.value.map((route: RouteRecordRaw) => (
|
||||
<NTabPane key={route.name as string} name={route.name as string} tab={route.meta?.title || route.name} />
|
||||
))}
|
||||
</NTabs>
|
||||
<div class="tabs-content">
|
||||
<router-view />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
// 初始化时自动选中第一个标签
|
||||
if (defaultToFirst && childRoutes.value.length > 0 && !route.name) {
|
||||
const firstRoute = childRoutes.value[0]
|
||||
handleTabChange(firstRoute.name as string)
|
||||
}
|
||||
|
||||
return {
|
||||
activeKey: activeKey.value,
|
||||
childRoutes: childRoutes.value,
|
||||
handleTabChange,
|
||||
TabsComponent,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user