index.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  1. <template>
  2. <view class="form">
  3. <block v-for="item in formConfig" :key="item.key">
  4. <!-- 文本 -->
  5. <view v-if="item.type === 'input'" v-show="item.visible !== false" class="form_block">
  6. <gInput
  7. :ref="(el) => setComponentRef(el, item.key)"
  8. :label="item.label"
  9. :readOnly="item.readOnly"
  10. :placeholder="item.placeholder"
  11. :required="item.required"
  12. :errorMsg="item.errorMsg"
  13. :value="formData[item.key]"
  14. @inputChange="(e) => onInputChange(e, item.key)"
  15. @validateRequest="(e) => onValidateRequest({ detail: { key: item.key, value: e.value } })"
  16. />
  17. </view>
  18. <!-- 文本域 -->
  19. <view v-if="item.type === 'textArea'" v-show="item.visible !== false" class="form_block">
  20. <gTextArea
  21. :ref="(el) => setComponentRef(el, item.key)"
  22. :label="item.label"
  23. :readOnly="item.readOnly"
  24. :placeholder="item.placeholder"
  25. :required="item.required"
  26. :errorMsg="item.errorMsg"
  27. :value="formData[item.key]"
  28. @inputChange="(e) => onInputChange(e, item.key)"
  29. @validateRequest="(e) => onValidateRequest({ detail: { key: item.key, value: e.value } })"
  30. />
  31. </view>
  32. <!-- 选择 -->
  33. <view v-if="item.type === 'radio'" v-show="item.visible !== false" class="form_block">
  34. <gRadio
  35. :ref="(el) => setComponentRef(el, item.key)"
  36. :label="item.label"
  37. :options="item.options"
  38. :inline="item.inline"
  39. :readOnly="item.readOnly"
  40. :required="item.required"
  41. :errorMsg="item.errorMsg"
  42. :value="formData[item.key]"
  43. @radioChange="(e) => onRadioChange(e, item.key)"
  44. @validateRequest="(e) => onValidateRequest({ detail: { key: item.key, value: e.value } })"
  45. />
  46. </view>
  47. <!-- 短信验证 -->
  48. <view v-if="item.type === 'smsCode'" v-show="item.visible !== false" class="form_block">
  49. <gSmsCode
  50. :ref="(el) => setComponentRef(el, item.key)"
  51. :phoneLabel="item.phoneLabel"
  52. :codeLabel="item.codeLabel"
  53. :sendCodeText="item.sendCodeText"
  54. :countdownText="item.countdownText"
  55. :readOnly="item.readOnly"
  56. :required="item.required"
  57. :errorMsg="item.errorMsg"
  58. :phoneValue="formData[item.key + '_phone']"
  59. :codeValue="formData[item.key + '_code']"
  60. @phoneInputChange="(e) => onSmsCodePhoneChange(e, item.key)"
  61. @codeInputChange="(e) => onSmsCodeCodeChange(e, item.key)"
  62. @sendCodeRequest="(e) => onSmsCodeSendRequest(e, item.key)"
  63. @validationResult="(e) => handleSmsValidationResult(e, item.key)"
  64. />
  65. </view>
  66. <!-- 省市区选择器 -->
  67. <view v-if="item.type === 'regionPicker'" v-show="item.visible !== false" class="form_block">
  68. <gRegionPicker
  69. :ref="(el) => setComponentRef(el, item.key)"
  70. :label="item.label"
  71. :level="item.level"
  72. :readOnly="item.readOnly"
  73. :placeholder="item.placeholder"
  74. :required="item.required"
  75. :value="formData[item.key]"
  76. @regionPickerChange="(e) => onRegionPickerChange(e, item.key)"
  77. />
  78. </view>
  79. <!-- 事件触发器 -->
  80. <view v-if="item.type === 'action'" v-show="item.visible !== false" class="form_block">
  81. <gAction
  82. :ref="(el) => setComponentRef(el, item.key)"
  83. :label="item.label"
  84. :readOnly="item.readOnly"
  85. :placeholder="item.placeholder"
  86. :required="item.required"
  87. :errorMsg="item.errorMsg"
  88. :value="formData[item.key]"
  89. @actionClick="(e) => onActionClick(e, item.key)"
  90. />
  91. </view>
  92. </block>
  93. </view>
  94. </template>
  95. <script setup lang="ts">
  96. import { ref, defineProps, defineEmits, watch, nextTick, defineExpose } from 'vue';
  97. import gInput from './input/index.vue';
  98. import gTextArea from './textArea/index.vue';
  99. import gRadio from './radio/index.vue';
  100. import gSmsCode from './smsCode/index.vue';
  101. import gRegionPicker from './regionPicker/index.vue';
  102. import gAction from './action/index.vue';
  103. const props = defineProps({
  104. formConfig: {
  105. type: Array,
  106. default: () => []
  107. },
  108. formData: {
  109. type: Object,
  110. default: () => ({})
  111. }
  112. });
  113. const emit = defineEmits(['formDataChange', 'smsCodeSendSuccess', 'smsCodeSendFail', 'actionClick', 'errorReport', 'formSubmit']);
  114. const formData = ref({});
  115. const hasError = ref(false);
  116. const componentRefs = ref({});
  117. const setComponentRef = (el, key) => {
  118. if (el) {
  119. componentRefs.value[key] = el;
  120. }
  121. };
  122. watch(() => props.formData, (newVal) => {
  123. formData.value = { ...newVal };
  124. }, { deep: true, immediate: true });
  125. // [组件]输入框
  126. const onInputChange = (e, key) => {
  127. const { value } = e;
  128. const newFormData = { ...formData.value, [key]: value };
  129. formData.value = newFormData;
  130. emit('formDataChange', { formData: newFormData, target: { [key]: value } });
  131. };
  132. // [组件]单选框
  133. const onRadioChange = (e, key) => {
  134. const { value } = e;
  135. const newFormData = { ...formData.value, [key]: value };
  136. formData.value = newFormData;
  137. emit('formDataChange', { formData: newFormData, target: { [key]: value } });
  138. };
  139. // [组件]发送短信验证码
  140. const onSmsCodePhoneChange = (e, key) => {
  141. const { phoneValue } = e;
  142. const newFormData = {
  143. ...formData.value,
  144. [`${key}_phone`]: phoneValue,
  145. [`${key}`]: phoneValue
  146. };
  147. formData.value = newFormData;
  148. emit('formDataChange', {
  149. formData: newFormData,
  150. target: {
  151. [`${key}_phone`]: phoneValue,
  152. [`${key}`]: phoneValue }
  153. }
  154. );
  155. };
  156. const onSmsCodeCodeChange = (e, key) => {
  157. const { codeValue } = e;
  158. const newFormData = { ...formData.value, [`${key}_code`]: codeValue };
  159. formData.value = newFormData;
  160. emit('formDataChange', { formData: newFormData, target: { [`${key}_code`]: codeValue } });
  161. };
  162. const onSmsCodeSendRequest = (e, key) => {
  163. const { phoneValue } = e;
  164. const config = props.formConfig.find(item => item.key === key);
  165. const sendCodeApi = config.sendCodeApi;
  166. if (sendCodeApi) {
  167. const validatePhone = config.validatePhone;
  168. if(validatePhone && !validatePhone(phoneValue)) {
  169. uni.showModal({
  170. title: "温馨提示",
  171. content: config.errorMsg,
  172. showCancel: false,
  173. });
  174. return;
  175. }
  176. sendCodeApi(phoneValue)
  177. .then((res) => {
  178. const component = componentRefs.value[key];
  179. if (component && component.startCountdown) {
  180. component.startCountdown();
  181. }
  182. emit('smsCodeSendSuccess', { key, res });
  183. })
  184. .catch((err) => {
  185. emit('smsCodeSendFail', { key, err });
  186. });
  187. }
  188. };
  189. const handleSmsValidationResult = (e, key) => {
  190. const component = componentRefs.value[key];
  191. if (component && component.handleValidationResult) {
  192. component.handleValidationResult(e);
  193. }
  194. }
  195. // [组件]省市区选择器
  196. const onRegionPickerChange = (e, key) => {
  197. const { code, postcode, value } = e;
  198. const config = props.formConfig.find(item => item.key === key);
  199. const newFormData = {
  200. ...formData.value,
  201. [`${key}`]: e[config.currentBindKey || value],
  202. [`${key}_regionCode`]: code,
  203. [`${key}_regionPostCode`]: postcode,
  204. [`${key}_regionValue`]: value,
  205. };
  206. formData.value = newFormData;
  207. emit('formDataChange', {
  208. formData: newFormData,
  209. target: {
  210. [`${key}`]: e[config.currentBindKey || value],
  211. [`${key}_regionCode`]: code,
  212. [`${key}_regionPostCode`]: postcode,
  213. [`${key}_regionValue`]: value,
  214. }
  215. });
  216. };
  217. // [组件]事件触发器
  218. const onActionClick = (e, key) => {
  219. const { value } = e;
  220. const newFormData = { ...formData.value, [key]: value };
  221. emit('actionClick', { formData: newFormData, target: { [key]: value } })
  222. };
  223. // [辅助]校验
  224. const onValidateRequest = (e) => {
  225. const { key, value } = e.detail;
  226. let config;
  227. let fieldKey;
  228. if (key.includes('_phone') || key.includes('_code')) {
  229. const [baseKey, field] = key.split('_');
  230. config = props.formConfig.find(form => form.key === baseKey);
  231. fieldKey = field;
  232. } else {
  233. config = props.formConfig.find(form => form.key === key);
  234. }
  235. if (!config) return;
  236. let isValid = true;
  237. let validateFunc;
  238. if (fieldKey === 'phone') {
  239. validateFunc = config.validatePhone;
  240. } else if (fieldKey === 'code') {
  241. validateFunc = config.validateCode;
  242. } else {
  243. validateFunc = config.validate;
  244. }
  245. if (config.required && (
  246. value === void(0)
  247. || value === null
  248. || (typeof value === 'string' && value.trim() === '')
  249. || (Array.isArray(value) && value.length === 0)
  250. )) {
  251. isValid = false;
  252. } else if (value && typeof validateFunc === 'function' && !validateFunc(value)) {
  253. isValid = false;
  254. }
  255. const errorMsg = config.errorMsg;
  256. const component = componentRefs.value[config.key];
  257. if (component) {
  258. if (fieldKey) {
  259. // smsCode 组件内部处理
  260. component.handleValidationResult({ key: fieldKey, isValid });
  261. } else {
  262. // 其他组件暂无直接设置 error 的方法,通常通过 props 或事件反馈,这里简化处理,假设组件不需要显式 error 状态
  263. // 如果组件需要显示 error 状态,需要在组件内增加 error prop 并传递
  264. }
  265. }
  266. if (!isValid) {
  267. hasError.value = true;
  268. emit('errorReport', { errorMsg });
  269. }
  270. };
  271. // [动作]提交
  272. const submitForm = () => {
  273. hasError.value = false;
  274. let allValid = true;
  275. props.formConfig.forEach((config) => {
  276. if (config.enabled === false) { // Assuming explicit false check or check logic
  277. return;
  278. }
  279. // 模拟校验逻辑,实际项目中可能需要更完善的校验
  280. // 这里简单重用 onValidateRequest 的逻辑,但要注意它依赖 componentRefs 和事件
  281. // 简化:直接检查数据
  282. let isValid = true;
  283. if (config.type === 'smsCode') {
  284. const phoneVal = formData.value[`${config.key}_phone`];
  285. const codeVal = formData.value[`${config.key}_code`];
  286. // Check Phone
  287. let phoneValid = true;
  288. if(config.required && !phoneVal) phoneValid = false;
  289. if(phoneVal && config.validatePhone && !config.validatePhone(phoneVal)) phoneValid = false;
  290. // Check Code
  291. let codeValid = true;
  292. if(config.required && !codeVal) codeValid = false;
  293. if(codeVal && config.validateCode && !config.validateCode(codeVal)) codeValid = false;
  294. if(!phoneValid || !codeValid) isValid = false;
  295. // Trigger validation visual update if needed
  296. const component = componentRefs.value[config.key];
  297. if(component) {
  298. component.handleValidationResult({ key: 'phone', isValid: phoneValid });
  299. component.handleValidationResult({ key: 'code', isValid: codeValid });
  300. }
  301. } else {
  302. const val = formData.value[config.key];
  303. if (config.required && (
  304. val === void(0) ||
  305. val === null ||
  306. (typeof val === 'string' && val.trim() === '') ||
  307. (Array.isArray(val) && val.length === 0)
  308. )) {
  309. isValid = false;
  310. } else if (val && config.validate && !config.validate(val)) {
  311. isValid = false;
  312. }
  313. }
  314. if (!isValid) {
  315. allValid = false;
  316. // Trigger visual error if needed (omitted for simplicity as component props usually handle this or errorMsg toast is enough)
  317. emit('errorReport', { errorMsg: config.errorMsg });
  318. }
  319. });
  320. if (allValid) {
  321. emit('formSubmit', { formData: formData.value });
  322. }
  323. };
  324. defineExpose({
  325. submitForm
  326. });
  327. </script>
  328. <style>
  329. .form {
  330. width: 100%;
  331. height: fit-content;
  332. background-color: #fff;
  333. padding: 0 20upx;
  334. }
  335. .form .form_block + .form_block {
  336. border-top: 2upx solid #e5e5e5;
  337. }
  338. </style>