cl-form.uvue 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  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. // 透传样式类型
  67. type PassThrough = {
  68. className?: string;
  69. };
  70. // 解析透传样式
  71. const pt = computed(() => parsePt<PassThrough>(props.pt));
  72. // 表单数据
  73. const data = ref({} as UTSJSONObject);
  74. // 表单字段错误信息
  75. const errors = ref(new Map<string, string>());
  76. // 表单字段集合
  77. const fields = ref(new Set<string>([]));
  78. // 标签位置
  79. const labelPosition = computed(() => props.labelPosition);
  80. // 标签宽度
  81. const labelWidth = computed(() => props.labelWidth);
  82. // 是否显示必填星号
  83. const showAsterisk = computed(() => props.showAsterisk);
  84. // 是否显示错误信息
  85. const showMessage = computed(() => props.showMessage);
  86. // 是否禁用整个表单
  87. const disabled = computed(() => props.disabled);
  88. // 错误信息锁定
  89. const errorLock = ref(false);
  90. // 设置字段错误信息
  91. function setError(prop: string, error: string) {
  92. if (errorLock.value) {
  93. return;
  94. }
  95. if (prop != "") {
  96. errors.value.set(prop, error);
  97. }
  98. }
  99. // 移除字段错误信息
  100. function removeError(prop: string) {
  101. if (prop != "") {
  102. errors.value.delete(prop);
  103. }
  104. }
  105. // 获取字段错误信息
  106. function getError(prop: string): string {
  107. if (prop != "") {
  108. return errors.value.get(prop) ?? "";
  109. }
  110. return "";
  111. }
  112. // 清除所有错误信息
  113. function clearErrors() {
  114. errors.value.clear();
  115. }
  116. // 获取字段值
  117. function getValue(prop: string): any | null {
  118. if (prop != "") {
  119. return data.value[prop];
  120. }
  121. return null;
  122. }
  123. // 注册表单字段
  124. function addField(prop: string) {
  125. if (prop != "") {
  126. fields.value.add(prop);
  127. }
  128. }
  129. // 注销表单字段
  130. function removeField(prop: string) {
  131. if (prop != "") {
  132. fields.value.delete(prop);
  133. removeError(prop);
  134. }
  135. }
  136. // 获取字段规则
  137. function getRule(prop: string): ClFormRule[] {
  138. return props.rules.get(prop) ?? ([] as ClFormRule[]);
  139. }
  140. // 验证单个规则
  141. function validateRule(value: any | null, rule: ClFormRule): null | string {
  142. // 必填验证
  143. if (rule.required == true) {
  144. if (value == null || value == "" || (Array.isArray(value) && value.length == 0)) {
  145. return rule.message ?? t("此字段为必填项");
  146. }
  147. }
  148. // 如果值为空且不是必填,直接通过
  149. if ((value == null || value == "") && rule.required != true) {
  150. return null;
  151. }
  152. // 最小长度验证
  153. if (rule.min != null) {
  154. if (typeof value == "number") {
  155. if ((value as number) < rule.min) {
  156. return rule.message ?? $t(`最小值为{min}`, { min: rule.min });
  157. }
  158. } else {
  159. const len = Array.isArray(value) ? value.length : `${value}`.length;
  160. if (len < rule.min) {
  161. return rule.message ?? $t(`最少需要{min}个字符`, { min: rule.min });
  162. }
  163. }
  164. }
  165. // 最大长度验证
  166. if (rule.max != null) {
  167. if (typeof value == "number") {
  168. if (value > rule.max) {
  169. return rule.message ?? $t(`最大值为{max}`, { max: rule.max });
  170. }
  171. } else {
  172. const len = Array.isArray(value) ? value.length : `${value}`.length;
  173. if (len > rule.max) {
  174. return rule.message ?? $t(`最多允许{max}个字符`, { max: rule.max });
  175. }
  176. }
  177. }
  178. // 正则验证
  179. if (rule.pattern != null) {
  180. if (!rule.pattern.test(`${value}`)) {
  181. return rule.message ?? t("格式不正确");
  182. }
  183. }
  184. // 自定义验证
  185. if (rule.validator != null) {
  186. const result = rule.validator(value);
  187. if (result != true) {
  188. return typeof result == "string" ? result : (rule.message ?? t("验证失败"));
  189. }
  190. }
  191. return null;
  192. }
  193. // 清除所有验证
  194. function clearValidate() {
  195. errorLock.value = true;
  196. nextTick(() => {
  197. clearErrors();
  198. errorLock.value = false;
  199. });
  200. }
  201. // 验证单个字段
  202. function validateField(prop: string): string | null {
  203. let error = null as string | null;
  204. if (prop != "") {
  205. const value = getValue(prop);
  206. const rules = getRule(prop);
  207. if (!isEmpty(rules)) {
  208. // 逐个验证规则
  209. rules.find((rule) => {
  210. const msg = validateRule(value, rule);
  211. if (msg != null) {
  212. error = msg;
  213. return true;
  214. }
  215. return false;
  216. });
  217. }
  218. removeError(prop);
  219. }
  220. if (error != null) {
  221. setError(prop, error!);
  222. }
  223. return error;
  224. }
  225. // 验证整个表单
  226. function validate(callback: (valid: boolean, errors: ClFormValidateError[]) => void) {
  227. const errs = [] as ClFormValidateError[];
  228. fields.value.forEach((prop) => {
  229. const result = validateField(prop);
  230. if (result != null) {
  231. errs.push({
  232. field: prop,
  233. message: result
  234. });
  235. }
  236. });
  237. callback(errs.length == 0, errs);
  238. }
  239. watch(
  240. computed(() => parseToObject(props.modelValue)),
  241. (val: UTSJSONObject) => {
  242. data.value = val;
  243. },
  244. {
  245. immediate: true,
  246. deep: true
  247. }
  248. );
  249. defineExpose({
  250. labelPosition,
  251. labelWidth,
  252. showAsterisk,
  253. showMessage,
  254. disabled,
  255. data,
  256. errors,
  257. fields,
  258. addField,
  259. removeField,
  260. getValue,
  261. setError,
  262. getError,
  263. removeError,
  264. clearErrors,
  265. getRule,
  266. validateRule,
  267. clearValidate,
  268. validateField,
  269. validate
  270. });
  271. </script>
  272. <style lang="scss" scoped>
  273. .cl-form {
  274. @apply w-full;
  275. }
  276. </style>