basic.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473
  1. <script lang="ts" setup>
  2. import { h, ref } from 'vue';
  3. import { Page } from '@vben/common-ui';
  4. import { useDebounceFn } from '@vueuse/core';
  5. import {
  6. Button,
  7. Card,
  8. message,
  9. Spin,
  10. TabPane,
  11. Tabs,
  12. Tag,
  13. } from 'ant-design-vue';
  14. import dayjs from 'dayjs';
  15. import { useVbenForm, z } from '#/adapter/form';
  16. import { getAllMenusApi } from '#/api';
  17. import DocButton from '../doc-button.vue';
  18. const activeTab = ref('basic');
  19. const keyword = ref('');
  20. const fetching = ref(false);
  21. // 模拟远程获取数据
  22. function fetchRemoteOptions({ keyword = '选项' }: Record<string, any>) {
  23. fetching.value = true;
  24. return new Promise((resolve) => {
  25. setTimeout(() => {
  26. const options = Array.from({ length: 10 }).map((_, index) => ({
  27. label: `${keyword}-${index}`,
  28. value: `${keyword}-${index}`,
  29. }));
  30. resolve(options);
  31. fetching.value = false;
  32. }, 1000);
  33. });
  34. }
  35. const [BaseForm, baseFormApi] = useVbenForm({
  36. // 所有表单项共用,可单独在表单内覆盖
  37. commonConfig: {
  38. // 在label后显示一个冒号
  39. colon: true,
  40. // 所有表单项
  41. componentProps: {
  42. class: 'w-full',
  43. },
  44. },
  45. fieldMappingTime: [['rangePicker', ['startTime', 'endTime'], 'YYYY-MM-DD']],
  46. // 提交函数
  47. handleSubmit: onSubmit,
  48. // 垂直布局,label和input在不同行,值为vertical
  49. // 水平布局,label和input在同一行
  50. layout: 'horizontal',
  51. schema: [
  52. {
  53. // 组件需要在 #/adapter.ts内注册,并加上类型
  54. component: 'Input',
  55. // 对应组件的参数
  56. componentProps: {
  57. placeholder: '请输入用户名',
  58. },
  59. // 字段名
  60. fieldName: 'username',
  61. // 界面显示的label
  62. label: '字符串',
  63. rules: 'required',
  64. },
  65. {
  66. // 组件需要在 #/adapter.ts内注册,并加上类型
  67. component: 'ApiSelect',
  68. // 对应组件的参数
  69. componentProps: {
  70. // 菜单接口转options格式
  71. afterFetch: (data: { name: string; path: string }[]) => {
  72. return data.map((item: any) => ({
  73. label: item.name,
  74. value: item.path,
  75. }));
  76. },
  77. // 菜单接口
  78. api: getAllMenusApi,
  79. },
  80. // 字段名
  81. fieldName: 'api',
  82. // 界面显示的label
  83. label: 'ApiSelect',
  84. },
  85. {
  86. component: 'ApiSelect',
  87. // 对应组件的参数
  88. componentProps: () => {
  89. return {
  90. api: fetchRemoteOptions,
  91. // 禁止本地过滤
  92. filterOption: false,
  93. // 如果正在获取数据,使用插槽显示一个loading
  94. notFoundContent: fetching.value ? undefined : null,
  95. // 搜索词变化时记录下来, 使用useDebounceFn防抖。
  96. onSearch: useDebounceFn((value: string) => {
  97. keyword.value = value;
  98. }, 300),
  99. // 远程搜索参数。当搜索词变化时,params也会更新
  100. params: {
  101. keyword: keyword.value || undefined,
  102. },
  103. showSearch: true,
  104. };
  105. },
  106. // 字段名
  107. fieldName: 'remoteSearch',
  108. // 界面显示的label
  109. label: '远程搜索',
  110. renderComponentContent: () => {
  111. return {
  112. notFoundContent: fetching.value ? h(Spin) : undefined,
  113. };
  114. },
  115. rules: 'selectRequired',
  116. },
  117. {
  118. component: 'ApiTreeSelect',
  119. // 对应组件的参数
  120. componentProps: {
  121. // 菜单接口
  122. api: getAllMenusApi,
  123. // 菜单接口转options格式
  124. labelField: 'name',
  125. valueField: 'path',
  126. childrenField: 'children',
  127. },
  128. // 字段名
  129. fieldName: 'apiTree',
  130. // 界面显示的label
  131. label: 'ApiTreeSelect',
  132. },
  133. {
  134. component: 'InputPassword',
  135. componentProps: {
  136. placeholder: '请输入密码',
  137. },
  138. fieldName: 'password',
  139. label: '密码',
  140. },
  141. {
  142. component: 'InputNumber',
  143. componentProps: {
  144. placeholder: '请输入',
  145. },
  146. fieldName: 'number',
  147. label: '数字(带后缀)',
  148. suffix: () => '¥',
  149. },
  150. {
  151. component: 'IconPicker',
  152. fieldName: 'icon',
  153. label: '图标',
  154. },
  155. {
  156. colon: false,
  157. component: 'Select',
  158. componentProps: {
  159. allowClear: true,
  160. filterOption: true,
  161. options: [
  162. {
  163. label: '选项1',
  164. value: '1',
  165. },
  166. {
  167. label: '选项2',
  168. value: '2',
  169. },
  170. ],
  171. placeholder: '请选择',
  172. showSearch: true,
  173. },
  174. fieldName: 'options',
  175. label: () => h(Tag, { color: 'warning' }, () => '😎自定义:'),
  176. },
  177. {
  178. component: 'RadioGroup',
  179. componentProps: {
  180. options: [
  181. {
  182. label: '选项1',
  183. value: '1',
  184. },
  185. {
  186. label: '选项2',
  187. value: '2',
  188. },
  189. ],
  190. },
  191. fieldName: 'radioGroup',
  192. label: '单选组',
  193. },
  194. {
  195. component: 'Radio',
  196. fieldName: 'radio',
  197. label: '',
  198. renderComponentContent: () => {
  199. return {
  200. default: () => ['Radio'],
  201. };
  202. },
  203. },
  204. {
  205. component: 'CheckboxGroup',
  206. componentProps: {
  207. name: 'cname',
  208. options: [
  209. {
  210. label: '选项1',
  211. value: '1',
  212. },
  213. {
  214. label: '选项2',
  215. value: '2',
  216. },
  217. ],
  218. },
  219. fieldName: 'checkboxGroup',
  220. label: '多选组',
  221. },
  222. {
  223. component: 'Checkbox',
  224. fieldName: 'checkbox',
  225. label: '',
  226. renderComponentContent: () => {
  227. return {
  228. default: () => ['我已阅读并同意'],
  229. };
  230. },
  231. rules: z
  232. .boolean()
  233. .refine((v) => v, { message: '为什么不同意?勾上它!' }),
  234. },
  235. {
  236. component: 'Mentions',
  237. componentProps: {
  238. options: [
  239. {
  240. label: 'afc163',
  241. value: 'afc163',
  242. },
  243. {
  244. label: 'zombieJ',
  245. value: 'zombieJ',
  246. },
  247. ],
  248. placeholder: '请输入',
  249. },
  250. fieldName: 'mentions',
  251. label: '提及',
  252. },
  253. {
  254. component: 'Rate',
  255. fieldName: 'rate',
  256. label: '评分',
  257. },
  258. {
  259. component: 'Switch',
  260. componentProps: {
  261. class: 'w-auto',
  262. },
  263. fieldName: 'switch',
  264. help: () =>
  265. ['这是一个多行帮助信息', '第二行', '第三行'].map((v) => h('p', v)),
  266. label: '开关',
  267. },
  268. {
  269. component: 'DatePicker',
  270. fieldName: 'datePicker',
  271. label: '日期选择框',
  272. },
  273. {
  274. component: 'RangePicker',
  275. fieldName: 'rangePicker',
  276. label: '范围选择器',
  277. },
  278. {
  279. component: 'TimePicker',
  280. fieldName: 'timePicker',
  281. label: '时间选择框',
  282. },
  283. {
  284. component: 'TreeSelect',
  285. componentProps: {
  286. allowClear: true,
  287. placeholder: '请选择',
  288. showSearch: true,
  289. treeData: [
  290. {
  291. label: 'root 1',
  292. value: 'root 1',
  293. children: [
  294. {
  295. label: 'parent 1',
  296. value: 'parent 1',
  297. children: [
  298. {
  299. label: 'parent 1-0',
  300. value: 'parent 1-0',
  301. children: [
  302. {
  303. label: 'my leaf',
  304. value: 'leaf1',
  305. },
  306. {
  307. label: 'your leaf',
  308. value: 'leaf2',
  309. },
  310. ],
  311. },
  312. {
  313. label: 'parent 1-1',
  314. value: 'parent 1-1',
  315. },
  316. ],
  317. },
  318. {
  319. label: 'parent 2',
  320. value: 'parent 2',
  321. },
  322. ],
  323. },
  324. ],
  325. treeNodeFilterProp: 'label',
  326. },
  327. fieldName: 'treeSelect',
  328. label: '树选择',
  329. },
  330. ],
  331. // 大屏一行显示3个,中屏一行显示2个,小屏一行显示1个
  332. wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
  333. });
  334. const [CustomLayoutForm] = useVbenForm({
  335. // 所有表单项共用,可单独在表单内覆盖
  336. commonConfig: {
  337. // 所有表单项
  338. componentProps: {
  339. class: 'w-full',
  340. },
  341. },
  342. layout: 'horizontal',
  343. schema: [
  344. {
  345. component: 'Select',
  346. fieldName: 'field1',
  347. label: '字符串',
  348. },
  349. {
  350. component: 'TreeSelect',
  351. fieldName: 'field2',
  352. label: '字符串',
  353. },
  354. {
  355. component: 'Mentions',
  356. fieldName: 'field3',
  357. label: '字符串',
  358. },
  359. {
  360. component: 'Input',
  361. fieldName: 'field4',
  362. label: '字符串',
  363. },
  364. {
  365. component: 'InputNumber',
  366. fieldName: 'field5',
  367. // 从第三列开始 相当于中间空了一列
  368. formItemClass: 'col-start-3',
  369. label: '前面空了一列',
  370. },
  371. {
  372. component: 'Textarea',
  373. fieldName: 'field6',
  374. // 占满三列空间 基线对齐
  375. formItemClass: 'col-span-3 items-baseline',
  376. label: '占满三列',
  377. },
  378. {
  379. component: 'Input',
  380. fieldName: 'field7',
  381. // 占满2列空间 从第二列开始 相当于前面空了一列
  382. formItemClass: 'col-span-2 col-start-2',
  383. label: '占满2列',
  384. },
  385. {
  386. component: 'Input',
  387. fieldName: 'field8',
  388. // 左右留空
  389. formItemClass: 'col-start-2',
  390. label: '左右留空',
  391. },
  392. {
  393. component: 'InputPassword',
  394. fieldName: 'field9',
  395. formItemClass: 'col-start-1',
  396. label: '字符串',
  397. },
  398. ],
  399. // 一共三列
  400. wrapperClass: 'grid-cols-3',
  401. });
  402. function onSubmit(values: Record<string, any>) {
  403. message.success({
  404. content: `form values: ${JSON.stringify(values)}`,
  405. });
  406. }
  407. function handleSetFormValue() {
  408. /**
  409. * 设置表单值(多个)
  410. */
  411. baseFormApi.setValues({
  412. checkboxGroup: ['1'],
  413. datePicker: dayjs('2022-01-01'),
  414. mentions: '@afc163',
  415. number: 3,
  416. options: '1',
  417. password: '2',
  418. radioGroup: '1',
  419. rangePicker: [dayjs('2022-01-01'), dayjs('2022-01-02')],
  420. rate: 3,
  421. switch: true,
  422. timePicker: dayjs('2022-01-01 12:00:00'),
  423. treeSelect: 'leaf1',
  424. username: '1',
  425. });
  426. // 设置单个表单值
  427. baseFormApi.setFieldValue('checkbox', true);
  428. }
  429. </script>
  430. <template>
  431. <Page
  432. content-class="flex flex-col gap-4"
  433. description="表单组件基础示例,请注意,该页面用到的参数代码会添加一些简单注释,方便理解,请仔细查看。"
  434. header-class="pb-0"
  435. title="表单组件"
  436. >
  437. <template #description>
  438. <div class="text-muted-foreground">
  439. <p>
  440. 表单组件基础示例,请注意,该页面用到的参数代码会添加一些简单注释,方便理解,请仔细查看。
  441. </p>
  442. </div>
  443. <Tabs v-model:active-key="activeTab" :tab-bar-style="{ marginBottom: 0 }">
  444. <TabPane key="basic" tab="基础示例" />
  445. <TabPane key="layout" tab="自定义布局" />
  446. </Tabs>
  447. </template>
  448. <template #extra>
  449. <DocButton class="mb-2" path="/components/common-ui/vben-form" />
  450. </template>
  451. <Card v-show="activeTab === 'basic'" title="基础示例">
  452. <template #extra>
  453. <Button type="primary" @click="handleSetFormValue">设置表单值</Button>
  454. </template>
  455. <BaseForm />
  456. </Card>
  457. <Card v-show="activeTab === 'layout'" title="使用tailwind自定义布局">
  458. <CustomLayoutForm />
  459. </Card>
  460. </Page>
  461. </template>