2025/2/8第一次更新

This commit is contained in:
爱吃咸鱼小猫咪
2025-02-08 18:50:38 +08:00
commit d7af560866
26519 changed files with 5046029 additions and 0 deletions
@@ -0,0 +1,15 @@
import { computed } from 'vue'
import { useNamespace } from '../../../../hooks'
export const useFormCustomStyle = () => {
const ns = useNamespace('form')
const formClass = computed<string>(() => {
const cls: string[] = [ns.b()]
return cls.join(' ')
})
return {
formClass,
}
}
@@ -0,0 +1,104 @@
import { computed, inject, ref } from 'vue'
import { formContextKey } from '../../../../tokens'
import { formatDomSizeValue, generateId } from '../../../../utils'
import { useNamespace, useSelectorQuery } from '../../../../hooks'
import { useFormSize } from './use-form-common-props'
import type { CSSProperties, Ref } from 'vue'
import type { FormItemProps } from '../form-item'
export const useFormItemCustomStyle = (
props: FormItemProps,
hasLabel: Ref<boolean>,
isRequired: Ref<boolean>
) => {
const form = inject(formContextKey, undefined)
const ns = useNamespace('form-item')
const size = useFormSize(undefined, { formItem: false })
const { getSelectorNodeInfo } = useSelectorQuery()
// 标签的宽度
const labelWidth = computed(() =>
formatDomSizeValue(props.labelWidth || form?.labelWidth || '')
)
// 标签的位置
const labelPosition = computed(
() => props.labelPosition || form?.labelPosition || 'right'
)
// 是否隐藏必填星号
const hideRequiredAsterisk = computed(
() => form?.hideRequiredAsterisk || false
)
// 必填星号的位置
const requireAsteriskPosition = computed(
() => form?.requireAsteriskPosition || 'left'
)
// label标签容器宽度
const labelContainerWidth = ref(0)
const labelId = `label-${generateId()}`
// 获取label标签的宽度
const initLabelContainerWidth = () => {
if (!hasLabel.value) return
getSelectorNodeInfo(`#${labelId}`).then((res) => {
labelContainerWidth.value = res?.width || 0
})
}
// formItem所属类
const formItemClass = computed<string>(() => {
const cls: string[] = [ns.b()]
if (size.value) cls.push(ns.m(size.value))
if (labelPosition.value) cls.push(ns.m(`label-${labelPosition.value}`))
return cls.join(' ')
})
// formItemLabel所属类
const formItemLabelClass = computed<string>(() => {
const cls: string[] = [ns.e('label')]
if (!hideRequiredAsterisk.value && isRequired.value) {
cls.push(
ns.em('label', 'required'),
ns.em('label', `asterisk-${requireAsteriskPosition.value}`)
)
}
return cls.join(' ')
})
// formItemLabel所属样式
const formItemLabelStyle = computed<CSSProperties>(() => {
const style: CSSProperties = {}
if (labelPosition.value !== 'top' && labelWidth.value)
style.width = labelWidth.value
return style
})
// formItemErrorMessage所属样式
const formItemErrorMessageStyle = computed<CSSProperties>(() => {
const style: CSSProperties = {}
if (labelPosition.value !== 'top' && hasLabel.value) {
style.paddingLeft = `${labelContainerWidth.value}px`
}
return style
})
return {
ns,
labelId,
formItemClass,
formItemLabelClass,
formItemLabelStyle,
formItemErrorMessageStyle,
initLabelContainerWidth,
}
}
@@ -0,0 +1,6 @@
export * from './form-custom'
export * from './form-item-custom'
export * from './use-form-common-props'
export * from './use-form-item'
export * from './use-form-item-operation'
export * from './use-form'
@@ -0,0 +1,36 @@
import { computed, inject, ref, unref } from 'vue'
import { useProp } from '../../../../hooks'
import { formContextKey, formItemContextKey } from '../../../../tokens'
import type { FormComponentSize } from '../../../../constants'
import type { MaybeRef } from '../../../../utils'
/* 表单尺寸 */
export const useFormSize = (
fallback?: MaybeRef<FormComponentSize | undefined>,
ignore: Partial<Record<'prop' | 'form' | 'formItem' | 'global', boolean>> = {}
) => {
const emptyRef = ref(undefined)
const size = ignore.prop ? emptyRef : useProp<FormComponentSize>('size')
const form = ignore.form
? { size: undefined }
: inject(formContextKey, undefined)
const formItem = ignore.formItem
? { size: undefined }
: inject(formItemContextKey, undefined)
return computed(
(): FormComponentSize =>
size.value || unref(fallback) || formItem?.size || form?.size || ''
)
}
/* 表单是否禁用 */
export const useFormDisabled = (fallback?: MaybeRef<boolean | undefined>) => {
const disabled = useProp<boolean>('disabled')
const form = inject(formContextKey, undefined)
return computed(
() => disabled.value || unref(fallback) || form?.disabled || false
)
}
@@ -0,0 +1,278 @@
import { computed, inject, nextTick, ref, watch } from 'vue'
import AsyncValidator from '../../../../libs/async-validator'
import { castArray, debounce } from '../../../../libs/lodash'
import { formContextKey } from '../../../../tokens'
import {
cloneDeep,
getProp,
isEmptyVariableInDefault,
isFunction,
isString,
} from '../../../../utils'
import type { Slots } from 'vue'
import type { RuleItem } from '../../../../libs/async-validator'
import type { FormItemProps, FormItemValidateStates } from '../form-item'
import type {
FormItemContext,
FormItemRule,
FormValidateFailure,
} from '../types'
import type { Arrayable } from '../../../../utils'
export const useFormItemOperation = (props: FormItemProps, slots: Slots) => {
const formContext = inject(formContextKey, undefined)
// 初始化的值
let initialValue: any = undefined
// 是否重置字段校验
let isResettingField = false
// 校验状态
const validateState = ref<FormItemValidateStates>('')
const validateStateDebounced = ref<FormItemValidateStates>('')
// 错误信息
const validateMessage = ref('')
// 是否有标签
const hasLabel = computed(() => {
return !!(props.label || slots.label)
})
// 当前标签的值
const currentLabel = computed(
() => `${props.label || ''}${formContext?.labelSuffix || ''}`
)
// formItem field字段名称
const fieldValue = computed(() => {
const model = formContext?.model
if (!model || !props.prop) {
return
}
return getProp(model, props.prop).value
})
// formItem prop字段名称
const propString = computed(() => {
if (!props.prop) return ''
return isString(props.prop) ? props.prop : props.prop.join('.')
})
// 校验规则
const normalizedRules = computed(() => {
const rules: FormItemRule[] = []
// 如果设置了rules,则直接使用rules
if (props.rules) rules.push(...castArray(props.rules))
// 如果设置了prop,则根据prop从formContext中获取rules
const formRules = formContext?.rules
if (formRules && props.prop) {
const _rules = getProp<Arrayable<FormItemRule> | undefined>(
formRules,
props.prop
).value
if (_rules) rules.push(...castArray(_rules))
}
// 如果设置了required,则根据required的值来设置校验规则
if (props.required !== undefined) {
const requiredRules = rules
.map((rule, index) => [rule, index] as const)
.filter(([rule]) => Object.keys(rule).includes('required'))
if (requiredRules.length) {
for (const [rule, index] of requiredRules) {
if (rule.required === props.required) continue
rules[index] = { ...rule, required: props.required }
}
} else {
rules.push({ required: props.required })
}
}
return rules
})
// 是否需要校验(开启校验)
const validateEnabled = computed(() => normalizedRules.value.length > 0)
// 是否为必填
const isRequired = computed(() =>
normalizedRules.value.some((rule) => rule.required)
)
// 是否显示错误信息
const shouldShowError = computed(
() =>
validateStateDebounced.value === 'error' &&
props.showMessage &&
isEmptyVariableInDefault(formContext?.showMessage, true)
)
// 设置校验状态
const setValidateState = (state: FormItemValidateStates) => {
validateState.value = state
}
// 获取校验规则
const getFilterRule = (trigger: string) => {
const rules = normalizedRules.value
return (
rules
.filter((rule) => {
if (!rule.trigger || !trigger) return true
if (Array.isArray(rule.trigger)) {
return rule.trigger.includes(trigger)
} else {
return rule.trigger === trigger
}
})
// eslint-disable-next-line @typescript-eslint/no-unused-vars
.map(({ trigger, ...rule }): RuleItem => rule)
)
}
// 校验失败
const onValidationFailed = (error: FormValidateFailure) => {
const { errors, fields } = error
if (!errors || !fields) {
console.error(error)
}
setValidateState('error')
validateMessage.value = errors
? isEmptyVariableInDefault(errors?.[0]?.message, `${props.prop} 为必填项`)
: ''
formContext?.emits('validate', props.prop!, false, validateMessage.value)
}
// 校验通过
const onValidationSucceded = () => {
setValidateState('success')
validateMessage.value = ''
formContext?.emits('validate', props.prop!, true, '')
}
// 进行校验操作
const doValidate = async (rules: RuleItem[]): Promise<true> => {
const modelName = propString.value
const validator = new AsyncValidator({
[modelName]: rules,
})
return validator
.validate({ [modelName]: fieldValue.value }, { firstFields: true })
.then(() => {
onValidationSucceded()
return true as const
})
.catch((err: FormValidateFailure) => {
onValidationFailed(err as FormValidateFailure)
return Promise.reject(err)
})
}
// 校验
const validate: FormItemContext['validate'] = async (trigger, callback) => {
// 重置字段后跳过校验
if (isResettingField || !props.prop) return false
const hasCallback = isFunction(callback)
if (!validateEnabled.value) {
callback?.(false)
return false
}
const rules = getFilterRule(trigger)
if (rules.length === 0) {
callback?.(true)
return true
}
setValidateState('validating')
return doValidate(rules)
.then(() => {
callback?.(true)
return true as const
})
.catch((err: FormValidateFailure) => {
const { fields } = err
callback?.(false, fields)
return hasCallback ? false : Promise.reject(fields)
})
}
// 清除校验信息
const clearValidate: FormItemContext['clearValidate'] = () => {
setValidateState('')
validateMessage.value = ''
isResettingField = false
}
// 重置字段
const resetField: FormItemContext['resetField'] = async () => {
const model = formContext?.model
if (!model || !props.prop) return
const computedValue = getProp(model, props.prop)
// 阻止触发校验
isResettingField = true
computedValue.value = cloneDeep(initialValue)
await nextTick()
clearValidate()
isResettingField = false
}
// 设置初始化的值
const initFieldValue = () => {
initialValue = cloneDeep(fieldValue.value)
}
const validateStateDebouncedUpdater = debounce(() => {
validateStateDebounced.value = validateState.value
}, 100)
watch(
() => validateState.value,
() => validateStateDebouncedUpdater()
)
watch(
() => props.error,
(val) => {
validateMessage.value = val || ''
setValidateState(val ? 'error' : '')
},
{
immediate: true,
}
)
watch(
() => props.validateStatus,
(val) => {
setValidateState(val || '')
}
)
return {
formContext,
hasLabel,
currentLabel,
validateState,
validateMessage,
isRequired,
shouldShowError,
doValidate,
validate,
clearValidate,
resetField,
initFieldValue,
}
}
@@ -0,0 +1,12 @@
import { inject } from 'vue'
import { formContextKey, formItemContextKey } from '../../../../tokens'
export const useFormItem = () => {
const form = inject(formContextKey, undefined)
const formItem = inject(formItemContextKey, undefined)
return {
form,
formItem,
}
}
@@ -0,0 +1,130 @@
import { computed } from 'vue'
import { filterFields } from '../utils'
import { isFunction } from '../../../../utils'
import type { ValidateFieldsError } from '../../../../libs/async-validator'
import type { FormItemProp } from '../form-item'
import type { FormProps } from '../form'
import type {
FormContext,
FormItemContext,
FormValidationCallback,
FormValidationResult,
} from '../types'
import type { Arrayable } from '../../../../utils'
export const useForm = (props: FormProps) => {
// formItem信息
const fields: FormItemContext[] = []
// 添加formItem信息
const addField: FormContext['addField'] = (field) => {
fields.push(field)
}
// 移除formItem信息
const removeField: FormContext['removeField'] = (field) => {
if (field.prop) {
fields.splice(fields.indexOf(field), 1)
}
}
// 重置formItem
const resetFields: FormContext['resetFields'] = (properties = []) => {
if (!props.model) {
// eslint-disable-next-line no-console
return console.warn('[TnForm] model参数未定义')
}
filterFields(fields, properties).forEach((field) => field.resetField())
}
// 清除formItem验证
const clearValidate: FormContext['clearValidate'] = (props = []) => {
filterFields(fields, props).forEach((field) => field.clearValidate())
}
// 是否可以进行校验
const isValidatable = computed(() => {
const hasModel = !!props.model
if (!hasModel) {
// eslint-disable-next-line no-console
console.warn('[TnForm] model参数未定义')
}
return hasModel
})
// 获取需要校验的字段
const obtainValidateFields = (props: Arrayable<FormItemProp>) => {
if (fields.length === 0) return []
const filteredFields = filterFields(fields, props)
if (!filteredFields.length) {
// eslint-disable-next-line no-console
console.warn('[TnForm] 未找到需要校验的字段')
return []
}
return filteredFields
}
// 校验
const validate = async (
callback?: FormValidationCallback
): FormValidationResult => validateField(undefined, callback)
// 开始校验字段
const doValidateField = async (
props: Arrayable<FormItemProp>
): Promise<boolean> => {
if (!isValidatable.value) return false
const fields = obtainValidateFields(props)
if (fields.length === 0) return false
let validationErrors: ValidateFieldsError = {}
for (const field of fields) {
try {
await field.validate('')
} catch (fields) {
validationErrors = {
...validationErrors,
...(fields as ValidateFieldsError),
}
}
}
if (Object.keys(validationErrors).length === 0) return true
return Promise.reject(validationErrors)
}
// 校验字段
const validateField: FormContext['validateField'] = async (
modelProps = [],
callback
) => {
const shouldThrow = !isFunction(callback)
try {
const result = await doValidateField(modelProps)
// 如果结果为false则说明当前校验不通过
if (result === true) {
callback?.(true)
}
return result
} catch (e) {
if (e instanceof Error) throw e
const invalidFields = e as ValidateFieldsError
callback?.(false, invalidFields)
return shouldThrow && Promise.reject(invalidFields)
}
}
return {
addField,
removeField,
resetFields,
clearValidate,
validate,
validateField,
}
}
+83
View File
@@ -0,0 +1,83 @@
import { formComponentSizes } from '../../../constants'
import { buildProps, definePropType } from '../../../utils'
import type { ExtractPropTypes } from 'vue'
import type { Arrayable } from '../../../utils'
import type { FormItemRule } from './types'
export const formItemValidateStates = [
'',
'error',
'validating',
'success',
] as const
export const formItemProps = buildProps({
/**
* @description label文本
*/
label: String,
/**
* @description label的宽度,默认单位为rpx,支持传入数字和auto
*/
labelWidth: {
type: [String, Number],
default: '',
},
/**
* @description label标签位置
*/
labelPosition: {
type: String,
values: ['left', 'right', 'top'],
default: '',
},
/**
* @description model中的key,如果需要使用校验,该字段为必填,可以是一个路径数组(['user', 'name', 0])
*/
prop: {
type: definePropType<FormItemProp>([String, Array]),
},
/**
* @description 标记字段是否为必填,如果不填写则根据校验规则自动生成
*/
required: {
type: Boolean,
default: undefined,
},
/**
* @description 表单校验规则
*/
rules: {
type: definePropType<Arrayable<FormItemRule>>([Object, Array]),
},
/**
* @description 字段错误信息,如果设置了该字段则校验状态会变成error,并显示该字段的内容
*/
error: String,
/**
* @description 校验状态
*/
validateStatus: {
type: String,
values: formItemValidateStates,
},
/**
* @description 是否显示校验结果
*/
showMessage: {
type: Boolean,
default: true,
},
/**
* @description 控制表单组件尺寸
*/
size: {
type: String,
values: formComponentSizes,
},
})
export type FormItemProps = ExtractPropTypes<typeof formItemProps>
export type FormItemValidateStates = (typeof formItemValidateStates)[number]
export type FormItemProp = Arrayable<string>
+136
View File
@@ -0,0 +1,136 @@
<script lang="ts" setup>
import {
nextTick,
onBeforeUnmount,
onMounted,
provide,
reactive,
toRefs,
useSlots,
} from 'vue'
import { formItemContextKey } from '../../../tokens'
import { formItemProps } from './form-item'
import {
useFormItemCustomStyle,
useFormItemOperation,
useFormSize,
} from './composables'
import type { FormItemContext } from './types'
const props = defineProps(formItemProps)
const slots = useSlots()
const {
formContext,
validateState,
validateMessage,
hasLabel,
currentLabel,
shouldShowError,
isRequired,
resetField,
clearValidate,
validate,
initFieldValue,
} = useFormItemOperation(props, slots)
const {
ns: formItemNs,
labelId,
formItemClass,
formItemLabelClass,
formItemLabelStyle,
formItemErrorMessageStyle,
initLabelContainerWidth,
} = useFormItemCustomStyle(props, hasLabel, isRequired)
const _size = useFormSize(undefined, { formItem: false })
const context: FormItemContext = reactive({
...toRefs(props),
size: _size,
validateState,
hasLabel,
resetField,
clearValidate,
validate,
})
onMounted(() => {
if (props.prop) {
formContext?.addField(context)
initFieldValue()
}
nextTick(() => {
initLabelContainerWidth()
})
})
onBeforeUnmount(() => {
formContext?.removeField(context)
})
provide(formItemContextKey, context)
defineExpose({
/**
* @description 表单尺寸
*/
size: _size,
/**
* @description 校验信息
*/
validateMessage,
/**
* @description 校验状态
*/
validateState,
/**
* @description 对表单Item的内容进行验证。 接收一个回调函数或返回Promise
*/
validate,
/**
* @description 重置当前字段信息
*/
resetField,
/**
* @description 清除表单字段验证
*/
clearValidate,
})
</script>
<template>
<view :class="[formItemClass]">
<view :class="[formItemNs.e('wrapper')]">
<!-- label -->
<view
v-if="hasLabel"
:id="labelId"
:class="[formItemLabelClass]"
:style="formItemLabelStyle"
>
<slot name="label">
{{ currentLabel }}
</slot>
</view>
<!-- 表单组件 -->
<view :class="[formItemNs.e('content')]">
<slot />
</view>
</view>
<!-- 错误信息 -->
<view
v-if="shouldShowError"
class="tn-red_text"
:class="[formItemNs.e('error-message')]"
:style="formItemErrorMessageStyle"
>
{{ validateMessage }}
</view>
</view>
</template>
<style lang="scss" scoped>
@import '../../../theme-chalk/src/form-item.scss';
</style>
+88
View File
@@ -0,0 +1,88 @@
import {
buildProps,
definePropType,
isArray,
isBoolean,
isString,
} from '../../../utils'
import { formMetaProps } from '../../base/common-props/form-meta'
import type { ExtractPropTypes } from 'vue'
import type { FormItemProp } from './form-item'
import type { FormRules } from './types'
export const formProps = buildProps({
...formMetaProps,
/**
* @description 表单数据对象
*/
model: Object,
/**
* @description 表单校验规则
*/
rules: {
type: definePropType<FormRules>([Object, Array]),
},
/**
* @description label标签位置
*/
labelPosition: {
type: String,
values: ['left', 'right', 'top'],
default: 'right',
},
/**
* @description 必填星号显示位置
*/
requireAsteriskPosition: {
type: String,
values: ['left', 'right'],
default: 'left',
},
/**
* @description label的宽度,默认单位为rpx,支持传入数字、带单位的数值和auto
*/
labelWidth: {
type: [String, Number],
default: '',
},
/**
* @description 表单域标签的后缀
*/
labelSuffix: {
type: String,
default: '',
},
/**
* @description 是否在输入框中显示校验结果反馈图标
*/
statusIcon: Boolean,
/**
* @description 是否显示校验结果
*/
showMessage: {
type: Boolean,
default: true,
},
/**
* @description 是否在校验规则修改后立马触发一次校验
*/
validateOnRuleChange: {
type: Boolean,
default: true,
},
/**
* @description 是否隐藏必填星号
*/
hideRequiredAsterisk: Boolean,
})
export const formEmits = {
validate: (prop: FormItemProp, isValid: boolean, message: string) =>
(isArray(prop) || isString(prop)) &&
isBoolean(isValid) &&
isString(message),
}
export type FormProps = ExtractPropTypes<typeof formProps>
export type FormMetaProps = ExtractPropTypes<typeof formMetaProps>
export type FormEmits = typeof formEmits
+72
View File
@@ -0,0 +1,72 @@
<script lang="ts" setup>
import { provide, reactive, toRefs, watch } from 'vue'
import { formContextKey } from '../../../tokens'
import { formEmits, formProps } from './form'
import { useForm, useFormCustomStyle } from './composables'
const props = defineProps(formProps)
const emits = defineEmits(formEmits)
const { formClass } = useFormCustomStyle()
const {
addField,
removeField,
resetFields,
clearValidate,
validate,
validateField,
} = useForm(props)
watch(
() => props.rules,
() => {
if (props.validateOnRuleChange) validate()
},
{
deep: true,
}
)
provide(
formContextKey,
reactive({
...toRefs(props),
emits,
resetFields,
clearValidate,
validateField,
addField,
removeField,
})
)
defineExpose({
/**
* @description 对整个表单的内容进行验证。 接收一个回调函数或返回Promise
*/
validate,
/**
* @description 验证具体的某个字段
*/
validateField,
/**
* @description 重置表单
*/
resetFields,
/**
* @description 清除表单验证
*/
clearValidate,
})
</script>
<template>
<view :class="[formClass]">
<slot />
</view>
</template>
<style lang="scss" scoped>
@import '../../../theme-chalk/src/form.scss';
</style>
@@ -0,0 +1,5 @@
import type Form from './form.vue'
import type FormItem from './form-item.vue'
export type TnFormInstance = InstanceType<typeof Form>
export type TnFormItemInstance = InstanceType<typeof FormItem>
+56
View File
@@ -0,0 +1,56 @@
import type { SetupContext } from 'vue'
import type {
RuleItem,
ValidateError,
ValidateFieldsError,
} from '../../../libs/async-validator'
import type { FormComponentSize } from '../../../constants'
import type { Arrayable } from '../../../utils'
import type {
FormItemProp,
FormItemProps,
FormItemValidateStates,
} from './form-item'
import type { FormEmits, FormProps } from './form'
export interface FormItemRule extends RuleItem {
trigger?: Arrayable<string>
}
export type FormRules = Partial<Record<string, Arrayable<FormItemRule>>>
export type FormValidationResult = Promise<boolean>
export type FormValidationCallback = (
isValid: boolean,
invalidFields?: ValidateFieldsError
) => void
export interface FormValidateFailure {
errors: ValidateError[] | null
fields: ValidateFieldsError
}
export type FormContext = FormProps & {
emits: SetupContext<FormEmits>['emit']
//expose
addField: (field: FormItemContext) => void
removeField: (field: FormItemContext) => void
resetFields: (props?: Arrayable<FormItemProp>) => void
clearValidate: (props?: Arrayable<FormItemProp>) => void
validateField: (
props?: Arrayable<FormItemProp>,
callback?: FormValidationCallback
) => FormValidationResult
}
export interface FormItemContext extends FormItemProps {
size: FormComponentSize
validateState: FormItemValidateStates
hasLabel: boolean
validate: (
trigger: string,
callback?: FormValidationCallback
) => FormValidationResult
resetField: () => void
clearValidate: () => void
}
+15
View File
@@ -0,0 +1,15 @@
import { castArray } from '../../../libs/lodash'
import type { Arrayable } from '../../../utils'
import type { FormItemContext } from './types'
import type { FormItemProp } from './form-item'
export const filterFields = (
fields: FormItemContext[],
props: Arrayable<FormItemProp>
) => {
const normalized = castArray(props)
return normalized.length > 0
? fields.filter((field) => field.prop && normalized.includes(field.prop))
: fields
}