This commit is contained in:
2026-01-05 12:47:14 +08:00
commit 1fc846fae3
1614 changed files with 162035 additions and 0 deletions

View File

@@ -0,0 +1,54 @@
import { computed } from 'vue'
import type { ExtractPropTypes, Ref } from 'vue'
import { useInject } from '../_hooks'
import { commonProps, makeObjectProp, makeStringProp } from '../_utils'
import type { ErrorMessage, FormLabelPosition, FormRules, FormStarPosition } from './types'
export const FORM_KEY = Symbol('Form')
export const formProps = {
...commonProps,
/**
* @description 表单数据对象(使用表单校验时_必填_)
*/
modelValue: makeObjectProp({}),
/**
* @description 统一配置每个 `FormItem` 的 `rules`
*/
rules: makeObjectProp<FormRules>({}),
/**
* @description 禁用表单下的所有数据录入组件
*/
disabled: Boolean,
/**
* @description 表单项 label 的位置
*/
labelPosition: makeStringProp<FormLabelPosition>('left'),
/**
* @description 必填表单项 label 的红色星标位置
*/
starPosition: makeStringProp<FormStarPosition>('left'),
}
export type FormProps = ExtractPropTypes<typeof formProps>
export const formEmits = {
validate: (msg: ErrorMessage) => msg instanceof Object,
}
export type FormEmits = typeof formEmits
export function useFormDisabled(disabled: Ref<boolean | undefined>) {
const { parent } = useInject<{ props: { disabled: boolean } }>(FORM_KEY)
return computed(() => {
if (disabled.value != null)
return disabled.value
if (parent?.props.disabled != null)
return parent.props.disabled
return false
})
}

View File

@@ -0,0 +1,207 @@
<script setup lang="ts">
import type { VNode } from 'vue'
import { computed, defineComponent, reactive, watch } from 'vue'
import { PREFIX } from '../_constants'
import { useProvide } from '../_hooks'
import { getMainClass, getPropByPath, isObject, isPromise } from '../_utils'
import NutCellGroup from '../cellgroup/cellgroup.vue'
import type { FormItemRule } from '../formitem/types'
import { FORM_KEY, formEmits, formProps } from './form'
import type { ErrorMessage, FormRule, FormRules } from './types'
const props = defineProps(formProps)
const emit = defineEmits(formEmits)
defineExpose({ reset, submit, validate })
const formErrorTip = computed(() => reactive<any>({}))
const { internalChildren } = useProvide(
FORM_KEY,
'nut-form-item',
)({ props, formErrorTip })
const classes = computed(() => {
return getMainClass(props, componentName)
})
function clearErrorTips() {
Object.keys(formErrorTip.value).forEach((item) => {
formErrorTip.value[item] = ''
})
}
function reset() {
clearErrorTips()
}
watch(
() => props.modelValue,
() => {
clearErrorTips()
},
{ immediate: true },
)
function findFormItem(vnodes: VNode[]) {
let task: FormRule[] = []
vnodes.forEach((vnode: VNode) => {
let type = vnode.type
type = (type as any).name || type
if (type === 'nut-form-item' || type?.toString().endsWith('form-item')) {
task.push({
prop: vnode.props?.prop,
rules: vnode.props?.rules || [],
})
}
else if (Array.isArray(vnode.children) && vnode.children?.length) {
task = task.concat(findFormItem(vnode.children as VNode[]))
}
else if (isObject(vnode.children) && Object.keys(vnode.children)) {
// 异步节点获取
if ((vnode.children as any)?.default) {
vnode.children = (vnode.children as any).default()
task = task.concat(findFormItem(vnode.children as VNode[]))
}
}
else if (Array.isArray(vnode)) {
task = task.concat(findFormItem(vnode as VNode[]))
}
})
return task
}
function tipMessage(errorMsg: ErrorMessage) {
if (errorMsg.message)
emit('validate', errorMsg)
formErrorTip.value[errorMsg.prop] = errorMsg.message
}
async function checkRule(item: FormRule): Promise<ErrorMessage | boolean> {
const { rules, prop } = item
const _Promise = (errorMsg: ErrorMessage): Promise<ErrorMessage> => {
return new Promise((resolve, reject) => {
try {
tipMessage(errorMsg)
resolve(errorMsg)
}
catch (error) {
reject(error)
}
})
}
if (!prop)
console.warn('[NutUI] <FormItem> 使用 rules 校验规则时 , 必须设置 prop 参数')
const value = getPropByPath(props.modelValue, prop || '')
// clear tips
tipMessage({ prop, message: '' })
const formRules: FormRules = props.rules || {}
const _rules = [...(formRules?.[prop] || []), ...rules]
while (_rules.length) {
const rule = _rules.shift() as FormItemRule
const { validator, ...ruleWithoutValidator } = rule
const { required, regex, message } = ruleWithoutValidator
const errorMsg = { prop, message }
if (required) {
if (Array.isArray(value)) {
if (!value.length)
return _Promise(errorMsg)
}
else if (!value && value !== 0) {
return _Promise(errorMsg)
}
}
if (regex && !regex.test(String(value)))
return _Promise(errorMsg)
if (validator) {
const result = validator(value, ruleWithoutValidator)
if (isPromise(result)) {
try {
const value = await result
if (value === false)
return _Promise(errorMsg)
}
catch (error) {
const validateErrorMsg = { prop, message: error as string }
return _Promise(validateErrorMsg)
}
}
else {
if (!result)
return _Promise(errorMsg)
}
}
}
return Promise.resolve(true)
}
/**
* 校验
* @param customProp 指定校验,用于用户自定义场景时触发,例如 blur、change 事件
*/
function validate(customProp = ''): Promise<{ valid: boolean, errors: ErrorMessage[] }> {
return new Promise((resolve, reject) => {
try {
const task = findFormItem(internalChildren?.map(child => child.vnode))
const errors = task.map((item) => {
if (customProp && customProp !== item.prop)
return Promise.resolve(true)
return checkRule(item)
})
Promise.all(errors).then((errorRes) => {
errorRes = errorRes.filter(item => item !== true)
const res = { valid: true, errors: [] }
if (errorRes.length) {
res.valid = false
res.errors = errorRes as any
}
resolve(res)
})
}
catch (error) {
reject(error)
}
})
}
function submit() {
validate()
return false
}
</script>
<script lang="ts">
const componentName = `${PREFIX}-form`
export default defineComponent({
name: componentName,
options: {
virtualHost: true,
addGlobalClass: true,
styleIsolation: 'shared',
},
})
</script>
<template>
<form
:class="classes"
:style="customStyle"
action="#"
@submit.prevent="() => false"
>
<NutCellGroup>
<slot />
</NutCellGroup>
</form>
</template>
<style lang="scss">
@import './index';
</style>

View File

@@ -0,0 +1 @@
@import '../cellgroup/index';

View File

@@ -0,0 +1,2 @@
export type * from './form'
export * from './types'

View File

@@ -0,0 +1,33 @@
import type { FormItemLabelPosition, FormItemRule, FormItemStarPosition } from '../formitem/types'
export interface FormRules {
[key: string]: FormItemRule[]
}
export interface FormRule {
prop: string
rules: FormItemRule[]
}
export interface ErrorMessage {
prop: string
message: string
}
export interface FormInst {
/**
* @description 清空校验结果
*/
reset: () => void
/**
* @description 提交表单进行校验的方法
*/
submit: () => void
/**
* @description 用户主动触发校验,用于用户自定义场景时触发,例如 `blur`、`change` 事件 | 同 `FormItem prop` 值,不传值会校验全部 `Rule`
*/
validate: (customProp?: any) => Promise<{ valid: boolean, errors: ErrorMessage[] }>
}
export type FormLabelPosition = FormItemLabelPosition
export type FormStarPosition = FormItemStarPosition