index.vue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837
  1. <template>
  2. <PageWrapper title="表单基础示例" contentFullHeight>
  3. <CollapseContainer title="基础示例">
  4. <BasicForm
  5. autoFocusFirstItem
  6. :labelWidth="200"
  7. :schemas="schemas"
  8. :actionColOptions="{ span: 24 }"
  9. @submit="handleSubmit"
  10. @reset="handleReset"
  11. >
  12. <template #selectA="{ model, field }">
  13. <Select
  14. :options="optionsA"
  15. mode="multiple"
  16. v-model:value="model[field]"
  17. @change="valueSelectA = model[field]"
  18. allowClear
  19. />
  20. </template>
  21. <template #selectB="{ model, field }">
  22. <Select
  23. :options="optionsB"
  24. mode="multiple"
  25. v-model:value="model[field]"
  26. @change="valueSelectB = model[field]"
  27. allowClear
  28. />
  29. </template>
  30. <template #localSearch="{ model, field }">
  31. <ApiSelect
  32. :api="optionsListApi"
  33. showSearch
  34. v-model:value="model[field]"
  35. optionFilterProp="label"
  36. resultField="list"
  37. labelField="name"
  38. valueField="id"
  39. />
  40. </template>
  41. <template #remoteSearch="{ model, field }">
  42. <ApiSelect
  43. :api="optionsListApi"
  44. showSearch
  45. v-model:value="model[field]"
  46. :filterOption="false"
  47. resultField="list"
  48. labelField="name"
  49. valueField="id"
  50. :params="searchParams"
  51. @search="useDebounceFn(onSearch, 300)"
  52. />
  53. </template>
  54. </BasicForm>
  55. </CollapseContainer>
  56. </PageWrapper>
  57. </template>
  58. <script lang="ts" setup>
  59. import { type Recordable } from '@vben/types';
  60. import { computed, unref, ref } from 'vue';
  61. import { BasicForm, FormSchema, ApiSelect } from '@/components/Form';
  62. import { CollapseContainer } from '@/components/Container';
  63. import { useMessage } from '@/hooks/web/useMessage';
  64. import { PageWrapper } from '@/components/Page';
  65. import { optionsListApi } from '@/api/demo/select';
  66. import { useDebounceFn } from '@vueuse/core';
  67. import { treeOptionsListApi } from '@/api/demo/tree';
  68. import { Select, type SelectProps } from 'ant-design-vue';
  69. import { cloneDeep } from 'lodash-es';
  70. import { areaRecord } from '@/api/demo/cascader';
  71. import { uploadApi } from '@/api/sys/upload';
  72. const valueSelectA = ref<string[]>([]);
  73. const valueSelectB = ref<string[]>([]);
  74. const options = ref<Required<SelectProps>['options']>([]);
  75. for (let i = 1; i < 10; i++) options.value.push({ label: '选项' + i, value: `${i}` });
  76. const optionsA = computed(() => {
  77. return cloneDeep(unref(options)).map((op) => {
  78. op.disabled = unref(valueSelectB).indexOf(op.value as string) !== -1;
  79. return op;
  80. });
  81. });
  82. const optionsB = computed(() => {
  83. return cloneDeep(unref(options)).map((op) => {
  84. op.disabled = unref(valueSelectA).indexOf(op.value as string) !== -1;
  85. return op;
  86. });
  87. });
  88. const provincesOptions = [
  89. {
  90. id: 'guangdong',
  91. label: '广东省',
  92. value: '1',
  93. key: '1',
  94. },
  95. {
  96. id: 'jiangsu',
  97. label: '江苏省',
  98. value: '2',
  99. key: '2',
  100. },
  101. ];
  102. const citiesOptionsData = {
  103. guangdong: [
  104. {
  105. label: '珠海市',
  106. value: '1',
  107. key: '1',
  108. },
  109. {
  110. label: '深圳市',
  111. value: '2',
  112. key: '2',
  113. },
  114. {
  115. label: '广州市',
  116. value: '3',
  117. key: '3',
  118. },
  119. ],
  120. jiangsu: [
  121. {
  122. label: '南京市',
  123. value: '1',
  124. key: '1',
  125. },
  126. {
  127. label: '无锡市',
  128. value: '2',
  129. key: '2',
  130. },
  131. {
  132. label: '苏州市',
  133. value: '3',
  134. key: '3',
  135. },
  136. ],
  137. };
  138. const schemas: FormSchema[] = [
  139. {
  140. field: 'divider-basic',
  141. component: 'Divider',
  142. label: '基础字段',
  143. colProps: {
  144. span: 24,
  145. },
  146. },
  147. {
  148. field: 'field1',
  149. component: 'Input',
  150. label: '字段1',
  151. colProps: {
  152. span: 8,
  153. },
  154. // componentProps:{},
  155. // can func
  156. componentProps: ({ schema, formModel }) => {
  157. console.log('form:', schema);
  158. console.log('formModel:', formModel);
  159. return {
  160. placeholder: '自定义placeholder',
  161. onChange: (e: any) => {
  162. console.log(e);
  163. },
  164. };
  165. },
  166. renderComponentContent: () => {
  167. return {
  168. prefix: () => 'pSlot',
  169. suffix: () => 'sSlot',
  170. };
  171. },
  172. },
  173. {
  174. field: 'field2',
  175. component: 'Input',
  176. label: '带后缀',
  177. defaultValue: '111',
  178. colProps: {
  179. span: 8,
  180. },
  181. componentProps: {
  182. onChange: (e: any) => {
  183. console.log(e);
  184. },
  185. },
  186. suffix: '天',
  187. },
  188. {
  189. field: 'fieldsc',
  190. component: 'Upload',
  191. label: '上传',
  192. colProps: {
  193. span: 8,
  194. },
  195. rules: [{ required: true, message: '请选择上传文件' }],
  196. componentProps: {
  197. api: uploadApi,
  198. },
  199. },
  200. {
  201. field: 'field3',
  202. component: 'DatePicker',
  203. label: '字段3',
  204. colProps: {
  205. span: 8,
  206. },
  207. },
  208. {
  209. field: 'field4',
  210. component: 'Select',
  211. label: '字段4',
  212. colProps: {
  213. span: 8,
  214. },
  215. componentProps: {
  216. options: [
  217. {
  218. label: '选项1',
  219. value: '1',
  220. key: '1',
  221. },
  222. {
  223. label: '选项2',
  224. value: '2',
  225. key: '2',
  226. },
  227. ],
  228. },
  229. },
  230. {
  231. field: 'field5',
  232. component: 'CheckboxGroup',
  233. label: '字段5',
  234. colProps: {
  235. span: 8,
  236. },
  237. componentProps: {
  238. options: [
  239. {
  240. label: '选项1',
  241. value: '1',
  242. },
  243. {
  244. label: '选项2',
  245. value: '2',
  246. },
  247. ],
  248. },
  249. },
  250. {
  251. field: 'field7',
  252. component: 'RadioGroup',
  253. label: '字段7',
  254. colProps: {
  255. span: 8,
  256. },
  257. componentProps: {
  258. options: [
  259. {
  260. label: '选项1',
  261. value: '1',
  262. },
  263. {
  264. label: '选项2',
  265. value: '2',
  266. },
  267. ],
  268. },
  269. },
  270. {
  271. field: 'field8',
  272. component: 'Checkbox',
  273. label: '字段8',
  274. colProps: {
  275. span: 8,
  276. },
  277. renderComponentContent: 'Check',
  278. },
  279. {
  280. field: 'field9',
  281. component: 'Switch',
  282. label: '字段9',
  283. colProps: {
  284. span: 8,
  285. },
  286. },
  287. {
  288. field: 'field10',
  289. component: 'RadioButtonGroup',
  290. label: '字段10',
  291. colProps: {
  292. span: 8,
  293. },
  294. componentProps: {
  295. options: [
  296. {
  297. label: '选项1',
  298. value: '1',
  299. },
  300. {
  301. label: '选项2',
  302. value: '2',
  303. },
  304. ],
  305. onChange: (e, v) => {
  306. console.log('RadioButtonGroup====>:', e, v);
  307. },
  308. },
  309. },
  310. {
  311. field: 'field11',
  312. component: 'Cascader',
  313. label: '字段11',
  314. colProps: {
  315. span: 8,
  316. },
  317. componentProps: {
  318. options: [
  319. {
  320. value: 'zhejiang',
  321. label: 'Zhejiang',
  322. children: [
  323. {
  324. value: 'hangzhou',
  325. label: 'Hangzhou',
  326. children: [
  327. {
  328. value: 'xihu',
  329. label: 'West Lake',
  330. },
  331. ],
  332. },
  333. ],
  334. },
  335. {
  336. value: 'jiangsu',
  337. label: 'Jiangsu',
  338. children: [
  339. {
  340. value: 'nanjing',
  341. label: 'Nanjing',
  342. children: [
  343. {
  344. value: 'zhonghuamen',
  345. label: 'Zhong Hua Men',
  346. },
  347. ],
  348. },
  349. ],
  350. },
  351. ],
  352. },
  353. },
  354. {
  355. field: 'field12',
  356. component: 'BasicTitle',
  357. label: '标题区分',
  358. componentProps: {
  359. line: true,
  360. span: true,
  361. },
  362. colProps: {
  363. span: 24,
  364. },
  365. },
  366. {
  367. field: 'field13',
  368. component: 'CropperAvatar',
  369. label: '头像上传',
  370. colProps: {
  371. span: 8,
  372. },
  373. },
  374. {
  375. field: 'field14',
  376. component: 'Transfer',
  377. label: '穿梭框',
  378. colProps: {
  379. span: 8,
  380. },
  381. componentProps: {
  382. render: (item) => item.label,
  383. dataSource: citiesOptionsData.guangdong,
  384. targetKeys: ['1'],
  385. },
  386. },
  387. {
  388. field: 'divider-api-select',
  389. component: 'Divider',
  390. label: '远程下拉演示',
  391. colProps: {
  392. span: 24,
  393. },
  394. },
  395. {
  396. field: 'field30',
  397. component: 'ApiSelect',
  398. label: '懒加载远程下拉',
  399. required: true,
  400. componentProps: {
  401. // more details see /src/components/Form/src/components/ApiSelect.vue
  402. api: optionsListApi,
  403. params: {
  404. id: 1,
  405. },
  406. resultField: 'list',
  407. // use name as label
  408. labelField: 'name',
  409. // use id as value
  410. valueField: 'id',
  411. // not request untill to select
  412. immediate: true,
  413. onChange: (e, v) => {
  414. console.log('ApiSelect====>:', e, v);
  415. },
  416. // atfer request callback
  417. onOptionsChange: (options) => {
  418. console.log('get options', options.length, options);
  419. },
  420. },
  421. colProps: {
  422. span: 8,
  423. },
  424. defaultValue: '0',
  425. },
  426. {
  427. field: 'field8',
  428. component: 'ApiCascader',
  429. label: '联动ApiCascader',
  430. required: true,
  431. colProps: {
  432. span: 8,
  433. },
  434. componentProps: {
  435. api: areaRecord,
  436. apiParamKey: 'parentCode',
  437. dataField: 'data',
  438. labelField: 'name',
  439. valueField: 'code',
  440. initFetchParams: {
  441. parentCode: '',
  442. },
  443. isLeaf: (record) => {
  444. return !(record.levelType < 3);
  445. },
  446. onChange: (e, ...v) => {
  447. console.log('ApiCascader====>:', e, v);
  448. },
  449. },
  450. },
  451. {
  452. field: 'field31',
  453. component: 'Input',
  454. label: '下拉本地搜索',
  455. helpMessage: ['ApiSelect组件', '远程数据源本地搜索', '只发起一次请求获取所有选项'],
  456. required: true,
  457. slot: 'localSearch',
  458. colProps: {
  459. span: 8,
  460. },
  461. defaultValue: '0',
  462. },
  463. {
  464. field: 'field32',
  465. component: 'Input',
  466. label: '下拉远程搜索',
  467. helpMessage: ['ApiSelect组件', '将关键词发送到接口进行远程搜索'],
  468. required: true,
  469. slot: 'remoteSearch',
  470. colProps: {
  471. span: 8,
  472. },
  473. defaultValue: '0',
  474. },
  475. {
  476. field: 'field33',
  477. component: 'ApiTreeSelect',
  478. label: '远程下拉树',
  479. helpMessage: ['ApiTreeSelect组件', '使用接口提供的数据生成选项'],
  480. required: true,
  481. componentProps: {
  482. api: treeOptionsListApi,
  483. resultField: 'list',
  484. onChange: (e, v) => {
  485. console.log('ApiTreeSelect====>:', e, v);
  486. },
  487. },
  488. colProps: {
  489. span: 8,
  490. },
  491. },
  492. {
  493. field: 'field33',
  494. component: 'ApiTreeSelect',
  495. label: '远程懒加载下拉树',
  496. helpMessage: ['ApiTreeSelect组件', '使用接口提供的数据生成选项'],
  497. required: true,
  498. componentProps: {
  499. api: () => {
  500. return new Promise((resolve) => {
  501. resolve([
  502. {
  503. title: 'Parent Node',
  504. value: '0-0',
  505. },
  506. ]);
  507. });
  508. },
  509. async: true,
  510. onChange: (e, v) => {
  511. console.log('ApiTreeSelect====>:', e, v);
  512. },
  513. onLoadData: ({ treeData, resolve, treeNode }) => {
  514. console.log('treeNode====>:', treeNode);
  515. setTimeout(() => {
  516. const children: Recordable[] = [
  517. { title: `Child Node ${treeNode.eventKey}-0`, value: `${treeNode.eventKey}-0` },
  518. { title: `Child Node ${treeNode.eventKey}-1`, value: `${treeNode.eventKey}-1` },
  519. ];
  520. children.forEach((item) => {
  521. item.isLeaf = false;
  522. item.children = [];
  523. });
  524. treeNode.dataRef.children = children;
  525. treeData.value = [...treeData.value];
  526. resolve();
  527. return;
  528. }, 300);
  529. },
  530. },
  531. colProps: {
  532. span: 8,
  533. },
  534. },
  535. {
  536. field: 'field34',
  537. component: 'ApiRadioGroup',
  538. label: '远程Radio',
  539. helpMessage: ['ApiRadioGroup组件', '使用接口提供的数据生成选项'],
  540. required: true,
  541. componentProps: {
  542. api: optionsListApi,
  543. params: {
  544. count: 2,
  545. },
  546. resultField: 'list',
  547. // use name as label
  548. labelField: 'name',
  549. // use id as value
  550. valueField: 'id',
  551. },
  552. defaultValue: '1',
  553. colProps: {
  554. span: 8,
  555. },
  556. },
  557. {
  558. field: 'field35',
  559. component: 'ApiRadioGroup',
  560. label: '远程Radio',
  561. helpMessage: ['ApiRadioGroup组件', '使用接口提供的数据生成选项'],
  562. required: true,
  563. componentProps: {
  564. api: optionsListApi,
  565. params: {
  566. count: 2,
  567. },
  568. resultField: 'list',
  569. // use name as label
  570. labelField: 'name',
  571. // use id as value
  572. valueField: 'id',
  573. isBtn: true,
  574. onChange: (e, v) => {
  575. console.log('ApiRadioGroup====>:', e, v);
  576. },
  577. },
  578. colProps: {
  579. span: 8,
  580. },
  581. },
  582. {
  583. field: 'field36',
  584. component: 'ApiTree',
  585. label: '远程Tree',
  586. helpMessage: ['ApiTree组件', '使用接口提供的数据生成选项'],
  587. required: true,
  588. componentProps: {
  589. api: treeOptionsListApi,
  590. params: {
  591. count: 2,
  592. },
  593. afterFetch: (v) => {
  594. //do something
  595. return v;
  596. },
  597. resultField: 'list',
  598. },
  599. colProps: {
  600. span: 8,
  601. },
  602. },
  603. {
  604. label: '远程穿梭框',
  605. field: 'field37',
  606. component: 'ApiTransfer',
  607. componentProps: {
  608. render: (item) => item.label,
  609. api: async () => {
  610. return Promise.resolve(citiesOptionsData.guangdong);
  611. },
  612. },
  613. defaultValue: ['1'],
  614. required: true,
  615. },
  616. {
  617. field: 'divider-linked',
  618. component: 'Divider',
  619. label: '字段联动',
  620. colProps: {
  621. span: 24,
  622. },
  623. },
  624. {
  625. field: 'province',
  626. component: 'Select',
  627. label: '省份',
  628. colProps: {
  629. span: 8,
  630. },
  631. componentProps: ({ formModel, formActionType }) => {
  632. return {
  633. options: provincesOptions,
  634. placeholder: '省份与城市联动',
  635. onChange: (e: any) => {
  636. // console.log(e)
  637. let citiesOptions =
  638. e == 1
  639. ? citiesOptionsData[provincesOptions[0].id]
  640. : citiesOptionsData[provincesOptions[1].id];
  641. // console.log(citiesOptions)
  642. if (e === undefined) {
  643. citiesOptions = [];
  644. }
  645. formModel.city = undefined; // reset city value
  646. const { updateSchema } = formActionType;
  647. updateSchema({
  648. field: 'city',
  649. componentProps: {
  650. options: citiesOptions,
  651. },
  652. });
  653. },
  654. };
  655. },
  656. },
  657. {
  658. field: 'city',
  659. component: 'Select',
  660. label: '城市',
  661. colProps: {
  662. span: 8,
  663. },
  664. componentProps: {
  665. options: [], // defalut []
  666. placeholder: '省份与城市联动',
  667. },
  668. },
  669. {
  670. field: 'divider-selects',
  671. component: 'Divider',
  672. label: '互斥多选',
  673. helpMessage: ['两个Select共用数据源', '但不可选择对方已选中的项目'],
  674. colProps: {
  675. span: 24,
  676. },
  677. },
  678. {
  679. field: 'selectA',
  680. component: 'Select',
  681. label: '互斥SelectA',
  682. slot: 'selectA',
  683. defaultValue: [],
  684. colProps: {
  685. span: 8,
  686. },
  687. },
  688. {
  689. field: 'selectB',
  690. component: 'Select',
  691. label: '互斥SelectB',
  692. slot: 'selectB',
  693. defaultValue: [],
  694. colProps: {
  695. span: 8,
  696. },
  697. },
  698. {
  699. field: 'divider-deconstruct',
  700. component: 'Divider',
  701. label: '字段解构',
  702. helpMessage: ['如果组件的值是 array 或者 object', '可以根据 ES6 的解构语法分别取值'],
  703. colProps: {
  704. span: 24,
  705. },
  706. },
  707. {
  708. field: '[startTime, endTime]',
  709. label: '时间范围',
  710. component: 'TimeRangePicker',
  711. componentProps: {
  712. format: 'HH:mm:ss',
  713. placeholder: ['开始时间', '结束时间'],
  714. },
  715. },
  716. {
  717. field: '[startDate, endDate]',
  718. label: '日期范围',
  719. component: 'RangePicker',
  720. componentProps: {
  721. format: 'YYYY-MM-DD',
  722. placeholder: ['开始日期', '结束日期'],
  723. },
  724. },
  725. {
  726. field: '[startDateTime, endDateTime]',
  727. label: '日期时间范围',
  728. component: 'RangePicker',
  729. componentProps: {
  730. format: 'YYYY-MM-DD HH:mm:ss',
  731. placeholder: ['开始日期、时间', '结束日期、时间'],
  732. showTime: { format: 'HH:mm:ss' },
  733. },
  734. },
  735. {
  736. field: 'divider-others',
  737. component: 'Divider',
  738. label: '其它',
  739. colProps: {
  740. span: 24,
  741. },
  742. },
  743. {
  744. field: 'field20',
  745. component: 'InputNumber',
  746. label: '字段20',
  747. required: true,
  748. colProps: {
  749. span: 8,
  750. },
  751. },
  752. {
  753. field: 'field21',
  754. component: 'Slider',
  755. label: '字段21',
  756. componentProps: {
  757. min: 0,
  758. max: 100,
  759. range: true,
  760. marks: {
  761. 20: '20°C',
  762. 60: '60°C',
  763. },
  764. },
  765. colProps: {
  766. span: 8,
  767. },
  768. },
  769. {
  770. field: 'field22',
  771. component: 'Rate',
  772. label: '字段22',
  773. defaultValue: 3,
  774. colProps: {
  775. span: 8,
  776. },
  777. componentProps: {
  778. disabled: false,
  779. allowHalf: true,
  780. },
  781. },
  782. {
  783. field: 'field23',
  784. component: 'ImageUpload',
  785. label: '上传图片',
  786. required: true,
  787. defaultValue: [
  788. 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
  789. ],
  790. componentProps: {
  791. api: uploadApi,
  792. accept: ['png', 'jpeg', 'jpg'],
  793. maxSize: 2,
  794. maxNumber: 1,
  795. },
  796. // rules: [
  797. // {
  798. // required: true,
  799. // trigger: 'change',
  800. // validator(_, value) {
  801. // if (isArray(value) && value.length > 0) {
  802. // return Promise.resolve();
  803. // } else {
  804. // return Promise.reject('请选择上传图片');
  805. // }
  806. // },
  807. // },
  808. // ],
  809. },
  810. ];
  811. const { createMessage } = useMessage();
  812. const keyword = ref<string>('');
  813. const searchParams = computed<Recordable<string>>(() => {
  814. return { keyword: unref(keyword) };
  815. });
  816. function onSearch(value: string) {
  817. keyword.value = value;
  818. }
  819. function handleReset() {
  820. keyword.value = '';
  821. }
  822. function handleSubmit(values: any) {
  823. console.log('values', values);
  824. createMessage.success('click search,values:' + JSON.stringify(values));
  825. }
  826. </script>