cl-form.uvue 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. <template>
  2. <view
  3. class="cl-form"
  4. :class="[
  5. `cl-form--label-${labelPosition}`,
  6. {
  7. 'cl-form--disabled': disabled
  8. },
  9. pt.className
  10. ]"
  11. >
  12. <slot></slot>
  13. </view>
  14. </template>
  15. <script setup lang="ts">
  16. import { computed, nextTick, ref, watch, type PropType } from "vue";
  17. import { isEmpty, parsePt, parseToObject } from "@/cool";
  18. import type { ClFormLabelPosition, ClFormRule, ClFormValidateError } from "../../types";
  19. import { $t, t } from "@/locale";
  20. defineOptions({
  21. name: "cl-form"
  22. });
  23. // 组件属性定义
  24. const props = defineProps({
  25. // 透传样式
  26. pt: {
  27. type: Object,
  28. default: () => ({})
  29. },
  30. // 表单数据模型
  31. modelValue: {
  32. type: Object as PropType<any>,
  33. default: () => ({})
  34. },
  35. // 表单规则
  36. rules: {
  37. type: Object as PropType<Map<string, ClFormRule[]>>,
  38. default: () => new Map<string, ClFormRule[]>()
  39. },
  40. // 标签位置
  41. labelPosition: {
  42. type: String as PropType<ClFormLabelPosition>,
  43. default: "top"
  44. },
  45. // 标签宽度
  46. labelWidth: {
  47. type: String,
  48. default: "140rpx"
  49. },
  50. // 是否显示必填星号
  51. showAsterisk: {
  52. type: Boolean,
  53. default: true
  54. },
  55. // 是否显示错误信息
  56. showMessage: {
  57. type: Boolean,
  58. default: true
  59. },
  60. // 是否禁用整个表单
  61. disabled: {
  62. type: Boolean,
  63. default: false
  64. }
  65. });
  66. type PassThrough = {
  67. className?: string;
  68. };
  69. // 解析透传样式
  70. const pt = computed(() => parsePt<PassThrough>(props.pt));
  71. // 表单数据
  72. const data = ref({} as UTSJSONObject);
  73. // 表单字段错误信息
  74. const errors = ref(new Map<string, string>());
  75. // 表单字段集合
  76. const fields = ref(new Set<string>([]));
  77. // 标签位置
  78. const labelPosition = computed(() => props.labelPosition);
  79. // 标签宽度
  80. const labelWidth = computed(() => props.labelWidth);
  81. // 是否显示必填星号
  82. const showAsterisk = computed(() => props.showAsterisk);
  83. // 是否显示错误信息
  84. const showMessage = computed(() => props.showMessage);
  85. // 是否禁用整个表单
  86. const disabled = computed(() => props.disabled);
  87. // 设置字段错误信息
  88. function setError(prop: string, error: string) {
  89. if (prop != "") {
  90. errors.value.set(prop, error);
  91. }
  92. }
  93. // 移除字段错误信息
  94. function removeError(prop: string) {
  95. if (prop != "") {
  96. errors.value.delete(prop);
  97. }
  98. }
  99. // 获取字段错误信息
  100. function getError(prop: string): string {
  101. if (prop != "") {
  102. return errors.value.get(prop) ?? "";
  103. }
  104. return "";
  105. }
  106. // 清除所有错误信息
  107. function clearErrors() {
  108. errors.value.clear();
  109. }
  110. // 获取字段值
  111. function getValue(prop: string): any | null {
  112. if (prop != "") {
  113. return data.value[prop];
  114. }
  115. return null;
  116. }
  117. // 注册表单字段
  118. function addField(prop: string) {
  119. if (prop != "") {
  120. fields.value.add(prop);
  121. }
  122. }
  123. // 注销表单字段
  124. function removeField(prop: string) {
  125. if (prop != "") {
  126. fields.value.delete(prop);
  127. removeError(prop);
  128. }
  129. }
  130. // 获取字段规则
  131. function getRule(prop: string): ClFormRule[] {
  132. return props.rules.get(prop) ?? ([] as ClFormRule[]);
  133. }
  134. // 验证单个规则
  135. function validateRule(value: any | null, rule: ClFormRule): null | string {
  136. // 必填验证
  137. if (rule.required == true) {
  138. if (value == null || value == "" || (Array.isArray(value) && value.length == 0)) {
  139. return rule.message ?? t("此字段为必填项");
  140. }
  141. }
  142. // 如果值为空且不是必填,直接通过
  143. if ((value == null || value == "") && rule.required != true) {
  144. return null;
  145. }
  146. // 最小长度验证
  147. if (rule.min != null) {
  148. const len = Array.isArray(value) ? value.length : `${value}`.length;
  149. if (len < rule.min) {
  150. return rule.message ?? $t(`最少需要{min}个字符`, { min: rule.min });
  151. }
  152. }
  153. // 最大长度验证
  154. if (rule.max != null) {
  155. const len = Array.isArray(value) ? value.length : `${value}`.length;
  156. if (len > rule.max) {
  157. return rule.message ?? $t(`最多允许{max}个字符`, { max: rule.max });
  158. }
  159. }
  160. // 正则验证
  161. if (rule.pattern != null) {
  162. if (!rule.pattern.test(`${value}`)) {
  163. return rule.message ?? t("格式不正确");
  164. }
  165. }
  166. // 自定义验证
  167. if (rule.validator != null) {
  168. const result = rule.validator(value);
  169. if (result != true) {
  170. return typeof result == "string" ? result : (rule.message ?? t("验证失败"));
  171. }
  172. }
  173. return null;
  174. }
  175. // 清除所有验证
  176. function clearValidate() {
  177. nextTick(() => {
  178. clearErrors();
  179. });
  180. }
  181. // 验证单个字段
  182. function validateField(prop: string): string | null {
  183. let error = null as string | null;
  184. if (prop != "") {
  185. const value = getValue(prop);
  186. const rules = getRule(prop);
  187. if (!isEmpty(rules)) {
  188. // 逐个验证规则
  189. rules.find((rule) => {
  190. const msg = validateRule(value, rule);
  191. if (msg != null) {
  192. error = msg;
  193. return true;
  194. }
  195. return false;
  196. });
  197. }
  198. removeError(prop);
  199. }
  200. if (error != null) {
  201. setError(prop, error!);
  202. }
  203. return error;
  204. }
  205. // 验证整个表单
  206. function validate(callback: (valid: boolean, errors: ClFormValidateError[]) => void) {
  207. const errs = [] as ClFormValidateError[];
  208. fields.value.forEach((prop) => {
  209. const result = validateField(prop);
  210. if (result != null) {
  211. errs.push({
  212. field: prop,
  213. message: result
  214. });
  215. }
  216. });
  217. callback(errs.length == 0, errs);
  218. }
  219. watch(
  220. computed(() => parseToObject(props.modelValue)),
  221. (val: UTSJSONObject) => {
  222. data.value = val;
  223. },
  224. {
  225. immediate: true,
  226. deep: true
  227. }
  228. );
  229. defineExpose({
  230. labelPosition,
  231. labelWidth,
  232. showAsterisk,
  233. showMessage,
  234. disabled,
  235. data,
  236. errors,
  237. fields,
  238. addField,
  239. removeField,
  240. getValue,
  241. setError,
  242. getError,
  243. removeError,
  244. clearErrors,
  245. getRule,
  246. validateRule,
  247. clearValidate,
  248. validateField,
  249. validate
  250. });
  251. </script>
  252. <style lang="scss" scoped>
  253. .cl-form {
  254. @apply w-full;
  255. }
  256. </style>