【修复】条件节点前fromNodeId传值问题

【修复】部署参数默认错误问题
【测设】部分项目代码结构
【同步】前端项目代码
This commit is contained in:
chudong
2025-05-09 18:44:33 +08:00
parent 6e2fe8cf52
commit d147bc7a82
237 changed files with 8705 additions and 8741 deletions

View 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,
}

View 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,
}
}

View 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)
}

View 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,
})

View 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,
}
}

View 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

View 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

View 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

View 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 }

View 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,
}
}