Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

form-builder-vue3.vue 19KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584
  1. <template>
  2. <!-- 表单 -->
  3. <form @submit="formSubmit">
  4. <view v-for="(item, index) in formData" :key="index">
  5. <view v-show="!item.show">
  6. <!-- 是否显示-->
  7. <view
  8. style="min-height: 90rpx"
  9. :class="
  10. item.vertical === 2 ? 'as-layout-vertical' : 'as-layout-horizontal'
  11. "
  12. >
  13. <!-- 标题 -->
  14. <view
  15. class="as-gravity-center-start"
  16. :style="'min-width:' + config.titleWidth + 'rpx'"
  17. style="
  18. margin: 0 20rpx;
  19. flex-direction: row;
  20. display: flex;
  21. flex-direction: row;
  22. "
  23. >
  24. <image
  25. class="img-size"
  26. :src="item.required ? `${$imgUrl}must.png` : ''"
  27. ></image>
  28. <view
  29. :class="item.vertical === 2 ? 'text-left' : 'text-justify'"
  30. class="text-title"
  31. style="width: 100%"
  32. >
  33. {{ item.title }}
  34. </view>
  35. </view>
  36. <!-- 内容 -->
  37. <view class="as-weight as-gravity-center-start">
  38. <!-- 文本内容 -->
  39. <view v-if="item.type === 1" class="text" :style="item.style">{{
  40. item[item.value]
  41. }}</view>
  42. <!-- 输入框 -->
  43. <view v-if="item.type === 2" style="width: 100%">
  44. <input
  45. :disabled="item.disabled"
  46. v-model="item[item.value]"
  47. :type="item.inputType"
  48. :placeholder="item.hint ? item.hint : '请输入' + item.title"
  49. placeholder-class="text-hint"
  50. class="text as-gravity-center-start"
  51. :maxlength="item.maxlength"
  52. :style="item.style"
  53. style="min-height: 90rpx; word-break: break-all"
  54. />
  55. </view>
  56. <!-- 多项选择器 -->
  57. <view v-if="item.type === 3">
  58. <checkbox-group
  59. @change="checkboxChange($event, item)"
  60. :disabled="item.disabled"
  61. >
  62. <view class="as-layout-horizontal">
  63. <label
  64. style="
  65. display: flex;
  66. flex-direction: row;
  67. margin-right: 30rpx;
  68. "
  69. v-for="(itemData, index) in item.itemData"
  70. :key="index"
  71. >
  72. <checkbox
  73. style="transform: scale(0.7)"
  74. :value="itemData.value"
  75. :checked="itemData.checked"
  76. />
  77. <view>{{ itemData.name }}</view>
  78. </label>
  79. </view>
  80. </checkbox-group>
  81. </view>
  82. <!-- 普通选择器 当 range 是一个 Array<Object> 时,通过 range-key 来指定 Object 中 key 的值作为选择器显示内容 :range-key="item[item.value]"-->
  83. <view v-if="item.type === 4" style="width: 100%">
  84. <!-- range[范围] value[初始选择]-->
  85. <picker
  86. :disabled="item.disabled"
  87. :range-key="item.itemKey"
  88. :mode="item.mode ? item.mode : 'selector'"
  89. @change="bindPickerChange($event, item)"
  90. :range="item.itemData"
  91. >
  92. <view class="text">
  93. {{
  94. item[item.value]
  95. ? item.itemKey
  96. ? item[item.value][item.itemKey]
  97. : item[item.value]
  98. : item.hint
  99. ? item.hint
  100. : "请选择" + item.title
  101. }}
  102. </view>
  103. </picker>
  104. </view>
  105. <!-- 富文本框 -->
  106. <view v-if="item.type === 5" style="width: 100%">
  107. <view>
  108. <textarea
  109. :disabled="item.disabled"
  110. class="text"
  111. v-model="item[item.value]"
  112. @input="textareaInput($event, item)"
  113. placeholder-class="text-hint"
  114. style="width: 100%"
  115. :maxlength="item.maxlength"
  116. auto-height
  117. :placeholder="item.hint ? item.hint : '请输入' + item.title"
  118. >
  119. </textarea>
  120. <view
  121. v-if="item.maxlength"
  122. class="as-gravity-center-end"
  123. style="display: flex; flex-direction: row; height: 20%"
  124. >
  125. <view class="text">{{ !item.num ? "0" : item.num }}</view>
  126. <view class="text"
  127. >/{{ item.maxlength ? item.maxlength : 100 }}</view
  128. >
  129. </view>
  130. </view>
  131. </view>
  132. <!-- 图片选择器 :imgWidth="imgWidth ? imgWidth : 0"-->
  133. <view v-if="item.type === 6" style="width: 100%">
  134. <form-image
  135. @backImg="backImg($event, item)"
  136. style="width: 100%"
  137. :retract="40"
  138. ></form-image>
  139. </view>
  140. <!-- 单向选择器 -->
  141. <view v-if="item.type === 7" style="width: 100%">
  142. <radio-group @change="radioChange($event, item)">
  143. <view class="as-layout-horizontal">
  144. <label
  145. style="
  146. display: flex;
  147. flex-direction: row;
  148. margin-right: 30rpx;
  149. "
  150. v-for="(itemData, index) in item.itemData"
  151. :key="index"
  152. >
  153. <radio
  154. style="transform: scale(0.7)"
  155. :value="itemData.value"
  156. :checked="itemData.checked"
  157. />
  158. <view>{{ itemData.name }}</view>
  159. </label>
  160. </view>
  161. </radio-group>
  162. </view>
  163. <!-- 证件照上传 -->
  164. <view v-if="item.type === 8" style="width: 100%">
  165. <view style="margin: 0 40rpx">
  166. <view class="text-error">{{ item.hint }}</view>
  167. <view
  168. class="as-layout-horizontal as-gravity-center"
  169. style="height: 250rpx; margin-top: 20rpx"
  170. >
  171. <view
  172. class="as-layout-vertical"
  173. style="width: 90%; height: 100%"
  174. >
  175. <image
  176. :src="
  177. item.placeholderImg1
  178. ? item.placeholderImg1
  179. : `${$imgUrl}license2.png`
  180. "
  181. style="width: 100%; height: 80%"
  182. @tap="ImageSelection(item, 1)"
  183. ></image>
  184. <view
  185. class="as-gravity-center text-hint"
  186. style="margin-top: 5rpx"
  187. >
  188. {{ item.hint1 }}
  189. </view>
  190. </view>
  191. <view style="width: 40rpx"></view>
  192. <view
  193. class="as-layout-vertical"
  194. style="width: 90%; height: 100%"
  195. >
  196. <image
  197. :src="
  198. item.placeholderImg2
  199. ? item.placeholderImg2
  200. : `${$imgUrl}license2.png`
  201. "
  202. style="width: 100%; height: 80%"
  203. @tap="ImageSelection(item, 2)"
  204. ></image>
  205. <view
  206. class="as-gravity-center text-hint"
  207. style="margin-top: 5rpx"
  208. >
  209. {{ item.hint2 }}
  210. </view>
  211. </view>
  212. </view>
  213. </view>
  214. </view>
  215. <!-- 车牌号输入 -->
  216. <view v-if="item.type === 9" style="width: 100%">
  217. <view style="margin: 20rpx 30rpx 0 30rpx">
  218. <car-num-ber-input
  219. @numberInputResult="numberInputResult($event, item)"
  220. >
  221. </car-num-ber-input>
  222. </view>
  223. </view>
  224. <!-- 车牌号输入 -->
  225. <view v-if="item.type === 10" style="width: 100%">
  226. <pick-regions
  227. :defaultRegion="defaultRegionCode"
  228. @getRegion="handleGetRegion($event, item)"
  229. >
  230. <view class="text">
  231. {{
  232. item[item.value]
  233. ? item.itemKey
  234. ? item[item.value][item.itemKey]
  235. : item[item.value]
  236. : item.hint
  237. ? item.hint
  238. : "请选择" + item.title
  239. }}
  240. </view>
  241. </pick-regions>
  242. </view>
  243. </view>
  244. </view>
  245. <view
  246. v-show="index !== formData.length - 1 && !item.underline"
  247. class="as-line3"
  248. style="margin: 0rpx 20rpx 0rpx 20rpx"
  249. ></view>
  250. </view>
  251. </view>
  252. <button class="btn btn-text as-gravity-center" form-type="submit">
  253. {{ config ? config.submitName : "提交" }}
  254. </button>
  255. </form>
  256. </template>
  257. <script setup lang="ts">
  258. import { TypeData } from "./tools";
  259. import { request } from "@/utils/network/request.js";
  260. import carNumBerInput from "@/components/car-number-input/car-number-input";
  261. import { pathToBase64 } from "@/utils/util/imageTool.js";
  262. import pickRegions from "@/components/pick-regions/pick-regions.vue";
  263. import { fileURL } from "@/datas/fileURL.js";
  264. const defaultRegionCode = "520115";
  265. const props = defineProps({
  266. formData: {
  267. type: Array as () => Array<TypeData>,
  268. default: () => [],
  269. },
  270. config: {
  271. type: Object,
  272. default: function () {
  273. return {
  274. submitName: "提交", //提交按钮名称
  275. titleWidth: 160, //标题宽度
  276. };
  277. },
  278. },
  279. });
  280. //defineEmits
  281. const emit = defineEmits<{
  282. (e: "submit", content: any): void;
  283. (e: "uploadImg", content: any, item: TypeData, index: Number): void;
  284. (e: "radioChange", event: any, item: TypeData): void;
  285. }>();
  286. //defineExpose 可宏来显式指定在 <script setup> 组件中要暴露出去的属性。
  287. //普通选择器
  288. function bindPickerChange(e: any, item: TypeData) {
  289. let select = e.target.value;
  290. // #ifdef H5
  291. select = e.detail.value;
  292. // #endif
  293. if (item.mode === "date" || item.mode === "time") {
  294. /* 日期选择器*/
  295. item[item.value] = select;
  296. } else {
  297. /* 普通选择器*/
  298. item[item.value] = item.itemData[select];
  299. }
  300. }
  301. // 获取选择的地区
  302. function handleGetRegion(e: any, item: TypeData) {
  303. console.log("输出内容", e);
  304. item[item.value] = e[0].name + "-" + e[1].name + "-" + e[2].name;
  305. }
  306. //多项选择器
  307. function checkboxChange(e: any, item: TypeData) {
  308. item[item.value] = e.detail.value; /* 赋值*/
  309. }
  310. //单项选择器
  311. function radioChange(e: any, item: TypeData) {
  312. item[item.value] = e.detail.value; /* 赋值*/
  313. emit("radioChange", e, item);
  314. }
  315. //富文本框输入内容
  316. function textareaInput(e: Event, item: TypeData) {
  317. item.num = item[item.value].length;
  318. }
  319. //点击图片按钮
  320. function backImg(e: Event, item: TypeData) {
  321. item[item.value] = e;
  322. }
  323. //车牌输入
  324. function numberInputResult(e: Event, item: TypeData) {
  325. console.log("输出内容", e);
  326. item[item.value] = e;
  327. }
  328. //选择图片按钮
  329. function ImageSelection(item: TypeData, index: number) {
  330. uni.chooseImage({
  331. count: 1, //最多可以选择的文件个数
  332. sourceType: ["camera"], //album 从相册选视频,camera 使用相机拍摄,默认为:['album', 'camera']
  333. success(res: any) {
  334. if (res.tempFiles[0].size > 2000000) {
  335. uni.showToast({
  336. title: "图片大于2M,请重新上传",
  337. icon: "none",
  338. duration: 1500,
  339. });
  340. return;
  341. }
  342. // #ifdef MP-WEIXIN
  343. uni.getFileSystemManager().readFile({
  344. filePath: res.tempFiles[0].tempFilePath, //要读取的文件的路径
  345. encoding: "base64", //编码格式
  346. success: (res) => {
  347. let imgBase64 = res.data;
  348. let reqData = {
  349. imageBase: imgBase64,
  350. accountNum: "qtzl_xcx",
  351. secretKey: "4DE47302-EDC7-41D4-888F-59A79DF4EA46",
  352. };
  353. //上传给服务器
  354. uni.showLoading({
  355. title: "正在加载中...",
  356. mask: true,
  357. });
  358. //文件上传
  359. request("", {
  360. baseUrl: "https://etcfile.etcjz.cn/v1/file/uploadImageBaseFile",
  361. data: reqData,
  362. }).then((content) => {
  363. uni.hideLoading();
  364. if (content.rc != "00") {
  365. uni.showModal({
  366. title: "请求错误",
  367. content: "图片上传失败",
  368. showCancel: false,
  369. confirmText: "取消",
  370. });
  371. }
  372. //处理后的 Data
  373. let data = {
  374. source: 2, //来源
  375. imageBase64: imgBase64,
  376. imageType: index,
  377. imageUrl: content.data.fileUrl,
  378. };
  379. emit("uploadImg", data, item, index);
  380. });
  381. },
  382. fail: (err) => {
  383. console.log("错误提示", err);
  384. },
  385. });
  386. // #endif
  387. // #ifdef H5
  388. pathToBase64(res.tempFilePaths[0]).then((data) => {
  389. let base64 = data.replaceAll("data:image/jpeg;base64,", "");
  390. base64 = base64.replaceAll("data:image/png;base64,", "");
  391. base64 = base64.replaceAll("data:image/jpg;base64,", "");
  392. let reqData = {
  393. imageBase: base64,
  394. accountNum: "qtzl_xcx",
  395. secretKey: "4DE47302-EDC7-41D4-888F-59A79DF4EA46",
  396. };
  397. //上传给服务器
  398. uni.showLoading({
  399. title: "文件上传中...",
  400. mask: true,
  401. });
  402. //文件上传
  403. request("", {
  404. baseUrl: "https://etcfile.etcjz.cn/v1/file/uploadImageBaseFile",
  405. data: reqData,
  406. })
  407. .then((content) => {
  408. console.log("上传结果", content);
  409. uni.hideLoading();
  410. if (content.rc != "00" && content.rc != 0) {
  411. uni.showModal({
  412. title: "请求错误",
  413. content: "图片上传失败",
  414. showCancel: false,
  415. confirmText: "取消",
  416. });
  417. }
  418. //展示缩略图
  419. if (index % 2 != 0) {
  420. //奇数第一张 偶数第二张
  421. item.placeholderImg1 = content.data.fileUrl;
  422. item[item.value.split(",")[0]] = content.data.fileUrl;
  423. } else {
  424. item.placeholderImg2 = content.data.fileUrl;
  425. item[item.value.split(",")[1]] = content.data.fileUrl;
  426. }
  427. //只展示缩略图
  428. if (!item.inputType) {
  429. emit("uploadImg", data, item, index);
  430. } else {
  431. //if(item.type === 1)
  432. //证件OCR识别
  433. uni.showLoading({
  434. title: "证件识别中...",
  435. mask: true,
  436. });
  437. let reqData2 = {
  438. imageDriveBase: "",
  439. imageBase64: base64,
  440. imageType: index % 2 != 0 ? 1 : 2, //1代表身份证正面,2代表反面
  441. imageUrl: content.data.fileUrl,
  442. };
  443. //默认身份证
  444. let code = "IF010012019070410001";
  445. if (item.inputType == "2") {
  446. //2为行驶证
  447. code = "IF010012019070410002";
  448. reqData2.imageBase64 = "";
  449. reqData2.imageDriveBase = base64;
  450. }
  451. request(code, {
  452. data: reqData2,
  453. }).then((ocr) => {
  454. uni.hideLoading();
  455. if (ocr.rc !== "00") {
  456. uni.showModal({
  457. title: "请求错误",
  458. content: "证件识别失败",
  459. showCancel: false,
  460. confirmText: "取消",
  461. });
  462. }
  463. let data = ocr.rd;
  464. //[身份证正面]姓名(name) 住址(address) 出生日期(birthday) 身份证号(idno) 民族(nation)
  465. //[身份证反面]证件机关(agency) 有效起始日期(begindate) 有效结束日期(enddate)
  466. emit("uploadImg", data, item, index);
  467. });
  468. }
  469. })
  470. .catch((e) => {
  471. console.log("输出内容", e);
  472. });
  473. });
  474. // #endif
  475. },
  476. fail(res: any) {
  477. if (!res.authSetting["scope.album"]) {
  478. uni.showModal({
  479. title: "授权失败",
  480. content: "需要从您的相机或相册获取图片,请在设置界面打开相关权限",
  481. success: (res) => {
  482. if (res.confirm) {
  483. uni.openSetting();
  484. }
  485. },
  486. });
  487. }
  488. },
  489. });
  490. }
  491. function showToast(hint: string) {
  492. uni.showToast({
  493. icon: "none",
  494. title: hint,
  495. });
  496. }
  497. //内容提交
  498. function formSubmit() {
  499. let content = {};
  500. /* 整理数据对象返回内容 */
  501. for (var i = 0; i < props.formData.length; i++) {
  502. let data = props.formData[i];
  503. /* 时间另外判断 */
  504. if (data.required) {
  505. let reg = new RegExp(":", "g"); //g代表全部
  506. let newMsg = data.title.replace(reg, "");
  507. if (data.value.indexOf(",") != -1 && data.type === 8) {
  508. if (!data[data.value.split(",")[0]]) {
  509. showToast(data.hint1 + "不能为空");
  510. return;
  511. } else if (!data[data.value.split(",")[1]]) {
  512. showToast(data.hint2 + "不能为空");
  513. return;
  514. }
  515. } else if (!data[data.value]) {
  516. showToast(newMsg + "不能为空");
  517. return;
  518. }
  519. }
  520. if (data.value.indexOf(",") != -1 && data.type === 8) {
  521. if (data[data.value.split(",")[0]]) {
  522. content[data.value.split(",")[0]] = data[data.value.split(",")[0]];
  523. }
  524. if (data[data.value.split(",")[1]]) {
  525. content[data.value.split(",")[1]] = data[data.value.split(",")[1]];
  526. }
  527. } else if (data.type === 10) {
  528. console.log("输出内容========", data);
  529. for (var i = 0; i < data.value.split(",").length; i++) {
  530. content[data.value.split(",")[i]] = data[data.value].split("-")[i];
  531. }
  532. } else {
  533. if (data.type === 4 && data.name) {
  534. content[data.value] = data[data.value][data.name];
  535. } else {
  536. content[data.value] = data[data.value];
  537. }
  538. }
  539. }
  540. emit("submit", content);
  541. }
  542. </script>
  543. <style lang="scss" scoped>
  544. .img-size {
  545. width: 30rpx;
  546. height: 30rpx;
  547. }
  548. .btn {
  549. height: 80rpx;
  550. background: #307afe;
  551. opacity: 1;
  552. border-radius: 100rpx;
  553. margin: 20rpx 20rpx 20rpx 20rpx;
  554. }
  555. .btn-text {
  556. color: #ffffff;
  557. font-size: 28rpx;
  558. }
  559. </style>