wd-form.vue 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. <template>
  2. <view :class="`wd-form ${customClass}`" :style="customStyle">
  3. <slot></slot>
  4. <wd-toast v-if="props.errorType === 'toast'" selector="wd-form-toast" />
  5. </view>
  6. </template>
  7. <script lang="ts">
  8. export default {
  9. name: 'wd-form',
  10. options: {
  11. addGlobalClass: true,
  12. virtualHost: true,
  13. styleIsolation: 'shared'
  14. }
  15. }
  16. </script>
  17. <script lang="ts" setup>
  18. import wdToast from '../wd-toast/wd-toast.vue'
  19. import { reactive, watch } from 'vue'
  20. import { deepClone, getPropByPath, isArray, isDef, isPromise, isString } from '../common/util'
  21. import { useChildren } from '../composables/useChildren'
  22. import { useToast } from '../wd-toast'
  23. import { type FormRules, FORM_KEY, type ErrorMessage, formProps, type FormExpose } from './types'
  24. const { show: showToast } = useToast('wd-form-toast')
  25. const props = defineProps(formProps)
  26. const { children, linkChildren } = useChildren(FORM_KEY)
  27. let errorMessages = reactive<Record<string, string>>({})
  28. linkChildren({ props, errorMessages })
  29. watch(
  30. () => props.model,
  31. () => {
  32. if (props.resetOnChange) {
  33. clearMessage()
  34. }
  35. },
  36. { immediate: true, deep: true }
  37. )
  38. /**
  39. * 表单校验
  40. * @param prop 指定校验字段或字段数组
  41. */
  42. async function validate(prop?: string | string[]): Promise<{ valid: boolean; errors: ErrorMessage[] }> {
  43. const errors: ErrorMessage[] = []
  44. let valid: boolean = true
  45. const promises: Promise<void>[] = []
  46. const formRules: FormRules = getMergeRules()
  47. const propsToValidate = isArray(prop) ? prop : isDef(prop) ? [prop] : []
  48. const rulesToValidate: FormRules =
  49. propsToValidate.length > 0
  50. ? propsToValidate.reduce((acc, key) => {
  51. if (formRules[key]) {
  52. acc[key] = formRules[key]
  53. }
  54. return acc
  55. }, {} as FormRules)
  56. : formRules
  57. for (const propName in rulesToValidate) {
  58. const rules = rulesToValidate[propName]
  59. const value = getPropByPath(props.model, propName)
  60. if (rules && rules.length > 0) {
  61. for (const rule of rules) {
  62. if (rule.required && (!isDef(value) || value === '')) {
  63. errors.push({
  64. prop: propName,
  65. message: rule.message
  66. })
  67. valid = false
  68. break
  69. }
  70. if (rule.pattern && !rule.pattern.test(value)) {
  71. errors.push({
  72. prop: propName,
  73. message: rule.message
  74. })
  75. valid = false
  76. break
  77. }
  78. const { validator, ...ruleWithoutValidator } = rule
  79. if (validator) {
  80. const result = validator(value, ruleWithoutValidator)
  81. if (isPromise(result)) {
  82. promises.push(
  83. result
  84. .then((res) => {
  85. if (typeof res === 'string') {
  86. errors.push({
  87. prop: propName,
  88. message: res
  89. })
  90. valid = false
  91. } else if (typeof res === 'boolean' && !res) {
  92. errors.push({
  93. prop: propName,
  94. message: rule.message
  95. })
  96. valid = false
  97. }
  98. })
  99. .catch((error?: string | Error) => {
  100. const message = isDef(error) ? (isString(error) ? error : error.message || rule.message) : rule.message
  101. errors.push({ prop: propName, message })
  102. valid = false
  103. })
  104. )
  105. } else {
  106. if (!result) {
  107. errors.push({
  108. prop: propName,
  109. message: rule.message
  110. })
  111. valid = false
  112. }
  113. }
  114. }
  115. }
  116. }
  117. }
  118. await Promise.all(promises)
  119. showMessage(errors)
  120. if (valid) {
  121. if (propsToValidate.length) {
  122. propsToValidate.forEach(clearMessage)
  123. } else {
  124. clearMessage()
  125. }
  126. }
  127. return {
  128. valid,
  129. errors
  130. }
  131. }
  132. // 合并子组件的rules到父组件的rules
  133. function getMergeRules() {
  134. const mergedRules: FormRules = deepClone(props.rules)
  135. const childrenProps = children.map((child) => child.prop)
  136. // 过滤掉在 children 中不存在对应子组件的规则
  137. Object.keys(mergedRules).forEach((key) => {
  138. if (!childrenProps.includes(key)) {
  139. delete mergedRules[key]
  140. }
  141. })
  142. children.forEach((item) => {
  143. if (isDef(item.prop) && isDef(item.rules) && item.rules.length) {
  144. if (mergedRules[item.prop]) {
  145. mergedRules[item.prop] = [...mergedRules[item.prop], ...item.rules]
  146. } else {
  147. mergedRules[item.prop] = item.rules
  148. }
  149. }
  150. })
  151. return mergedRules
  152. }
  153. function showMessage(errors: ErrorMessage[]) {
  154. const childrenProps = children.map((e) => e.prop).filter(Boolean)
  155. const messages = errors.filter((error) => error.message && childrenProps.includes(error.prop))
  156. if (messages.length) {
  157. messages.sort((a, b) => {
  158. return childrenProps.indexOf(a.prop) - childrenProps.indexOf(b.prop)
  159. })
  160. if (props.errorType === 'toast') {
  161. showToast(messages[0].message)
  162. } else if (props.errorType === 'message') {
  163. messages.forEach((error) => {
  164. errorMessages[error.prop] = error.message
  165. })
  166. }
  167. }
  168. }
  169. function clearMessage(prop?: string) {
  170. if (prop) {
  171. errorMessages[prop] = ''
  172. } else {
  173. Object.keys(errorMessages).forEach((key) => {
  174. errorMessages[key] = ''
  175. })
  176. }
  177. }
  178. /**
  179. * 重置表单项的验证提示
  180. */
  181. function reset() {
  182. clearMessage()
  183. }
  184. defineExpose<FormExpose>({ validate, reset })
  185. </script>
  186. <style lang="scss" scoped></style>