cl-form.uvue 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370
  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, getCurrentInstance, nextTick, ref, watch, type PropType } from "vue";
  17. import { isEmpty, isNull, isString, parsePt, parseToObject } from "@/cool";
  18. import type { ClFormLabelPosition, ClFormRule, ClFormValidateError } from "../../types";
  19. import { $t, t } from "@/locale";
  20. import { usePage } from "../../hooks";
  21. defineOptions({
  22. name: "cl-form"
  23. });
  24. // 组件属性定义
  25. const props = defineProps({
  26. // 透传样式
  27. pt: {
  28. type: Object,
  29. default: () => ({})
  30. },
  31. // 表单数据模型
  32. modelValue: {
  33. type: Object as PropType<any>,
  34. default: () => ({})
  35. },
  36. // 表单规则
  37. rules: {
  38. type: Object as PropType<Map<string, ClFormRule[]>>,
  39. default: () => new Map<string, ClFormRule[]>()
  40. },
  41. // 标签位置
  42. labelPosition: {
  43. type: String as PropType<ClFormLabelPosition>,
  44. default: "top"
  45. },
  46. // 标签宽度
  47. labelWidth: {
  48. type: String,
  49. default: "140rpx"
  50. },
  51. // 是否显示必填星号
  52. showAsterisk: {
  53. type: Boolean,
  54. default: true
  55. },
  56. // 是否显示错误信息
  57. showMessage: {
  58. type: Boolean,
  59. default: true
  60. },
  61. // 是否禁用整个表单
  62. disabled: {
  63. type: Boolean,
  64. default: false
  65. },
  66. // 滚动到第一个错误位置
  67. scrollToError: {
  68. type: Boolean,
  69. default: true
  70. }
  71. });
  72. const { proxy } = getCurrentInstance()!;
  73. // cl-page 上下文
  74. const page = usePage();
  75. // 透传样式类型
  76. type PassThrough = {
  77. className?: string;
  78. };
  79. // 解析透传样式
  80. const pt = computed(() => parsePt<PassThrough>(props.pt));
  81. // 表单数据
  82. const data = ref({} as UTSJSONObject);
  83. // 表单字段错误信息
  84. const errors = ref(new Map<string, string>());
  85. // 表单字段集合
  86. const fields = ref(new Set<string>([]));
  87. // 标签位置
  88. const labelPosition = computed(() => props.labelPosition);
  89. // 标签宽度
  90. const labelWidth = computed(() => props.labelWidth);
  91. // 是否显示必填星号
  92. const showAsterisk = computed(() => props.showAsterisk);
  93. // 是否显示错误信息
  94. const showMessage = computed(() => props.showMessage);
  95. // 是否禁用整个表单
  96. const disabled = computed(() => props.disabled);
  97. // 错误信息锁定
  98. const errorLock = ref(false);
  99. // 设置字段错误信息
  100. function setError(prop: string, error: string) {
  101. if (errorLock.value) {
  102. return;
  103. }
  104. if (prop != "") {
  105. errors.value.set(prop, error);
  106. }
  107. }
  108. // 移除字段错误信息
  109. function removeError(prop: string) {
  110. if (prop != "") {
  111. errors.value.delete(prop);
  112. }
  113. }
  114. // 获取字段错误信息
  115. function getError(prop: string): string {
  116. if (prop != "") {
  117. return errors.value.get(prop) ?? "";
  118. }
  119. return "";
  120. }
  121. // 清除所有错误信息
  122. function clearErrors() {
  123. errors.value.clear();
  124. }
  125. // 获取字段值
  126. function getValue(prop: string): any | null {
  127. if (prop != "") {
  128. return data.value[prop];
  129. }
  130. return null;
  131. }
  132. // 注册表单字段
  133. function addField(prop: string) {
  134. if (prop != "") {
  135. fields.value.add(prop);
  136. }
  137. }
  138. // 注销表单字段
  139. function removeField(prop: string) {
  140. if (prop != "") {
  141. fields.value.delete(prop);
  142. removeError(prop);
  143. }
  144. }
  145. // 获取字段规则
  146. function getRule(prop: string): ClFormRule[] {
  147. return props.rules.get(prop) ?? ([] as ClFormRule[]);
  148. }
  149. // 验证单个规则
  150. function validateRule(value: any | null, rule: ClFormRule): null | string {
  151. // 必填验证
  152. if (rule.required == true) {
  153. if (
  154. value == null ||
  155. (value == "" && isString(value)) ||
  156. (Array.isArray(value) && value.length == 0)
  157. ) {
  158. return rule.message ?? t("此字段为必填项");
  159. }
  160. }
  161. // 如果值为空且不是必填,直接通过
  162. if ((value == null || (value == "" && isString(value))) && rule.required != true) {
  163. return null;
  164. }
  165. // 最小长度验证
  166. if (rule.min != null) {
  167. if (typeof value == "number") {
  168. if ((value as number) < rule.min) {
  169. return rule.message ?? $t(`最小值为{min}`, { min: rule.min });
  170. }
  171. } else {
  172. const len = Array.isArray(value) ? value.length : `${value}`.length;
  173. if (len < rule.min) {
  174. return rule.message ?? $t(`最少需要{min}个字符`, { min: rule.min });
  175. }
  176. }
  177. }
  178. // 最大长度验证
  179. if (rule.max != null) {
  180. if (typeof value == "number") {
  181. if (value > rule.max) {
  182. return rule.message ?? $t(`最大值为{max}`, { max: rule.max });
  183. }
  184. } else {
  185. const len = Array.isArray(value) ? value.length : `${value}`.length;
  186. if (len > rule.max) {
  187. return rule.message ?? $t(`最多允许{max}个字符`, { max: rule.max });
  188. }
  189. }
  190. }
  191. // 正则验证
  192. if (rule.pattern != null) {
  193. if (!rule.pattern.test(`${value}`)) {
  194. return rule.message ?? t("格式不正确");
  195. }
  196. }
  197. // 自定义验证
  198. if (rule.validator != null) {
  199. const result = rule.validator(value);
  200. if (result != true) {
  201. return typeof result == "string" ? result : (rule.message ?? t("验证失败"));
  202. }
  203. }
  204. return null;
  205. }
  206. // 清除所有验证
  207. function clearValidate() {
  208. errorLock.value = true;
  209. nextTick(() => {
  210. clearErrors();
  211. errorLock.value = false;
  212. });
  213. }
  214. // 验证单个字段
  215. function validateField(prop: string): string | null {
  216. let error = null as string | null;
  217. if (prop != "") {
  218. const value = getValue(prop);
  219. const rules = getRule(prop);
  220. if (!isEmpty(rules)) {
  221. // 逐个验证规则
  222. rules.find((rule) => {
  223. const msg = validateRule(value, rule);
  224. if (msg != null) {
  225. error = msg;
  226. return true;
  227. }
  228. return false;
  229. });
  230. }
  231. removeError(prop);
  232. }
  233. if (error != null) {
  234. setError(prop, error!);
  235. }
  236. return error;
  237. }
  238. // 滚动到第一个错误位置
  239. function scrollToError(prop: string) {
  240. if (props.scrollToError == false) {
  241. return;
  242. }
  243. nextTick(() => {
  244. let component = proxy;
  245. // #ifdef MP
  246. component = proxy?.$children.find((e: any) => e.prop == prop);
  247. // #endif
  248. uni.createSelectorQuery()
  249. .in(component)
  250. .select(".cl-form-item--error")
  251. .boundingClientRect((res) => {
  252. if (!isNull(res)) {
  253. page.scrollTo(((res as NodeInfo).top ?? 0) + page.getScrollTop());
  254. }
  255. })
  256. .exec();
  257. });
  258. }
  259. // 验证整个表单
  260. function validate(callback: (valid: boolean, errors: ClFormValidateError[]) => void) {
  261. const errs = [] as ClFormValidateError[];
  262. fields.value.forEach((prop) => {
  263. const result = validateField(prop);
  264. if (result != null) {
  265. errs.push({
  266. field: prop,
  267. message: result
  268. });
  269. }
  270. });
  271. // 滚动到第一个错误位置
  272. if (errs.length > 0) {
  273. scrollToError(errs[0].field);
  274. }
  275. callback(errs.length == 0, errs);
  276. }
  277. watch(
  278. computed(() => parseToObject(props.modelValue)),
  279. (val: UTSJSONObject) => {
  280. data.value = val;
  281. },
  282. {
  283. immediate: true,
  284. deep: true
  285. }
  286. );
  287. defineExpose({
  288. labelPosition,
  289. labelWidth,
  290. showAsterisk,
  291. showMessage,
  292. disabled,
  293. data,
  294. errors,
  295. fields,
  296. addField,
  297. removeField,
  298. getValue,
  299. setError,
  300. getError,
  301. removeError,
  302. clearErrors,
  303. getRule,
  304. validateRule,
  305. clearValidate,
  306. validateField,
  307. validate
  308. });
  309. </script>
  310. <style lang="scss" scoped>
  311. .cl-form {
  312. @apply w-full;
  313. }
  314. </style>