805 lines
19 KiB
Vue
805 lines
19 KiB
Vue
<template>
|
|
<PageWrapper title="表单基础示例" contentFullHeight>
|
|
<CollapseContainer title="基础示例">
|
|
<BasicForm
|
|
autoFocusFirstItem
|
|
:labelWidth="200"
|
|
:schemas="schemas"
|
|
:actionColOptions="{ span: 24 }"
|
|
@submit="handleSubmit"
|
|
@reset="handleReset"
|
|
>
|
|
<template #selectA="{ model, field }">
|
|
<Select
|
|
:options="optionsA"
|
|
mode="multiple"
|
|
v-model:value="model[field]"
|
|
@change="valueSelectA = model[field]"
|
|
allowClear
|
|
/>
|
|
</template>
|
|
<template #selectB="{ model, field }">
|
|
<Select
|
|
:options="optionsB"
|
|
mode="multiple"
|
|
v-model:value="model[field]"
|
|
@change="valueSelectB = model[field]"
|
|
allowClear
|
|
/>
|
|
</template>
|
|
<template #localSearch="{ model, field }">
|
|
<ApiSelect
|
|
:api="optionsListApi"
|
|
showSearch
|
|
v-model:value="model[field]"
|
|
optionFilterProp="label"
|
|
resultField="list"
|
|
labelField="name"
|
|
valueField="id"
|
|
/>
|
|
</template>
|
|
<template #remoteSearch="{ model, field }">
|
|
<ApiSelect
|
|
:api="optionsListApi"
|
|
showSearch
|
|
v-model:value="model[field]"
|
|
:filterOption="false"
|
|
resultField="list"
|
|
labelField="name"
|
|
valueField="id"
|
|
:params="searchParams"
|
|
@search="useDebounceFn(onSearch, 300)"
|
|
/>
|
|
</template>
|
|
</BasicForm>
|
|
</CollapseContainer>
|
|
</PageWrapper>
|
|
</template>
|
|
<script lang="ts" setup>
|
|
import { type Recordable } from '@vben/types';
|
|
import { computed, unref, ref } from 'vue';
|
|
import { BasicForm, FormSchema, ApiSelect } from '@/components/Form';
|
|
import { CollapseContainer } from '@/components/Container';
|
|
import { useMessage } from '@/hooks/web/useMessage';
|
|
import { PageWrapper } from '@/components/Page';
|
|
|
|
import { optionsListApi } from '@/api/demo/select';
|
|
import { useDebounceFn } from '@vueuse/core';
|
|
import { treeOptionsListApi } from '@/api/demo/tree';
|
|
import { Select, type SelectProps } from 'ant-design-vue';
|
|
import { cloneDeep } from 'lodash-es';
|
|
import { areaRecord } from '@/api/demo/cascader';
|
|
import { uploadApi } from '@/api/sys/upload';
|
|
|
|
const valueSelectA = ref<string[]>([]);
|
|
const valueSelectB = ref<string[]>([]);
|
|
const options = ref<Required<SelectProps>['options']>([]);
|
|
for (let i = 1; i < 10; i++) options.value.push({ label: '选项' + i, value: `${i}` });
|
|
|
|
const optionsA = computed(() => {
|
|
return cloneDeep(unref(options)).map((op) => {
|
|
op.disabled = unref(valueSelectB).indexOf(op.value as string) !== -1;
|
|
return op;
|
|
});
|
|
});
|
|
const optionsB = computed(() => {
|
|
return cloneDeep(unref(options)).map((op) => {
|
|
op.disabled = unref(valueSelectA).indexOf(op.value as string) !== -1;
|
|
return op;
|
|
});
|
|
});
|
|
|
|
const provincesOptions = [
|
|
{
|
|
id: 'guangdong',
|
|
label: '广东省',
|
|
value: '1',
|
|
key: '1',
|
|
},
|
|
{
|
|
id: 'jiangsu',
|
|
label: '江苏省',
|
|
value: '2',
|
|
key: '2',
|
|
},
|
|
];
|
|
const citiesOptionsData = {
|
|
guangdong: [
|
|
{
|
|
label: '珠海市',
|
|
value: '1',
|
|
key: '1',
|
|
},
|
|
{
|
|
label: '深圳市',
|
|
value: '2',
|
|
key: '2',
|
|
},
|
|
{
|
|
label: '广州市',
|
|
value: '3',
|
|
key: '3',
|
|
},
|
|
],
|
|
jiangsu: [
|
|
{
|
|
label: '南京市',
|
|
value: '1',
|
|
key: '1',
|
|
},
|
|
{
|
|
label: '无锡市',
|
|
value: '2',
|
|
key: '2',
|
|
},
|
|
{
|
|
label: '苏州市',
|
|
value: '3',
|
|
key: '3',
|
|
},
|
|
],
|
|
};
|
|
|
|
const schemas: FormSchema[] = [
|
|
{
|
|
field: 'divider-basic',
|
|
component: 'Divider',
|
|
label: '基础字段',
|
|
colProps: {
|
|
span: 24,
|
|
},
|
|
},
|
|
{
|
|
field: 'field1',
|
|
component: 'Input',
|
|
label: '字段1',
|
|
|
|
colProps: {
|
|
span: 8,
|
|
},
|
|
// componentProps:{},
|
|
// can func
|
|
componentProps: ({ schema, formModel }) => {
|
|
console.log('form:', schema);
|
|
console.log('formModel:', formModel);
|
|
return {
|
|
placeholder: '自定义placeholder',
|
|
onChange: (e: any) => {
|
|
console.log(e);
|
|
},
|
|
};
|
|
},
|
|
renderComponentContent: () => {
|
|
return {
|
|
prefix: () => 'pSlot',
|
|
suffix: () => 'sSlot',
|
|
};
|
|
},
|
|
},
|
|
{
|
|
field: 'field2',
|
|
component: 'Input',
|
|
label: '带后缀',
|
|
defaultValue: '111',
|
|
colProps: {
|
|
span: 8,
|
|
},
|
|
componentProps: {
|
|
onChange: (e: any) => {
|
|
console.log(e);
|
|
},
|
|
},
|
|
suffix: '天',
|
|
},
|
|
{
|
|
field: 'fieldsc',
|
|
component: 'Upload',
|
|
label: '上传',
|
|
colProps: {
|
|
span: 8,
|
|
},
|
|
rules: [{ required: true, message: '请选择上传文件' }],
|
|
componentProps: {
|
|
api: uploadApi,
|
|
},
|
|
},
|
|
{
|
|
field: 'field3',
|
|
component: 'DatePicker',
|
|
label: '字段3',
|
|
colProps: {
|
|
span: 8,
|
|
},
|
|
},
|
|
{
|
|
field: 'field4',
|
|
component: 'Select',
|
|
label: '字段4',
|
|
colProps: {
|
|
span: 8,
|
|
},
|
|
componentProps: {
|
|
options: [
|
|
{
|
|
label: '选项1',
|
|
value: '1',
|
|
key: '1',
|
|
},
|
|
{
|
|
label: '选项2',
|
|
value: '2',
|
|
key: '2',
|
|
},
|
|
],
|
|
},
|
|
},
|
|
{
|
|
field: 'field5',
|
|
component: 'CheckboxGroup',
|
|
label: '字段5',
|
|
colProps: {
|
|
span: 8,
|
|
},
|
|
componentProps: {
|
|
options: [
|
|
{
|
|
label: '选项1',
|
|
value: '1',
|
|
},
|
|
{
|
|
label: '选项2',
|
|
value: '2',
|
|
},
|
|
],
|
|
},
|
|
},
|
|
{
|
|
field: 'field7',
|
|
component: 'RadioGroup',
|
|
label: '字段7',
|
|
colProps: {
|
|
span: 8,
|
|
},
|
|
componentProps: {
|
|
options: [
|
|
{
|
|
label: '选项1',
|
|
value: '1',
|
|
},
|
|
{
|
|
label: '选项2',
|
|
value: '2',
|
|
},
|
|
],
|
|
},
|
|
},
|
|
{
|
|
field: 'field8',
|
|
component: 'Checkbox',
|
|
label: '字段8',
|
|
colProps: {
|
|
span: 8,
|
|
},
|
|
renderComponentContent: 'Check',
|
|
},
|
|
{
|
|
field: 'field9',
|
|
component: 'Switch',
|
|
label: '字段9',
|
|
colProps: {
|
|
span: 8,
|
|
},
|
|
},
|
|
{
|
|
field: 'field10',
|
|
component: 'RadioButtonGroup',
|
|
label: '字段10',
|
|
colProps: {
|
|
span: 8,
|
|
},
|
|
componentProps: {
|
|
options: [
|
|
{
|
|
label: '选项1',
|
|
value: '1',
|
|
},
|
|
{
|
|
label: '选项2',
|
|
value: '2',
|
|
},
|
|
],
|
|
onChange: (e, v) => {
|
|
console.log('RadioButtonGroup====>:', e, v);
|
|
},
|
|
},
|
|
},
|
|
{
|
|
field: 'field11',
|
|
component: 'Cascader',
|
|
label: '字段11',
|
|
colProps: {
|
|
span: 8,
|
|
},
|
|
componentProps: {
|
|
options: [
|
|
{
|
|
value: 'zhejiang',
|
|
label: 'Zhejiang',
|
|
children: [
|
|
{
|
|
value: 'hangzhou',
|
|
label: 'Hangzhou',
|
|
children: [
|
|
{
|
|
value: 'xihu',
|
|
label: 'West Lake',
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
value: 'jiangsu',
|
|
label: 'Jiangsu',
|
|
children: [
|
|
{
|
|
value: 'nanjing',
|
|
label: 'Nanjing',
|
|
children: [
|
|
{
|
|
value: 'zhonghuamen',
|
|
label: 'Zhong Hua Men',
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
},
|
|
{
|
|
field: 'divider-api-select',
|
|
component: 'Divider',
|
|
label: '远程下拉演示',
|
|
colProps: {
|
|
span: 24,
|
|
},
|
|
},
|
|
{
|
|
field: 'field30',
|
|
component: 'ApiSelect',
|
|
label: '懒加载远程下拉',
|
|
required: true,
|
|
componentProps: {
|
|
// more details see /src/components/Form/src/components/ApiSelect.vue
|
|
api: optionsListApi,
|
|
params: {
|
|
id: 1,
|
|
},
|
|
|
|
resultField: 'list',
|
|
// use name as label
|
|
labelField: 'name',
|
|
// use id as value
|
|
valueField: 'id',
|
|
// not request untill to select
|
|
immediate: true,
|
|
onChange: (e, v) => {
|
|
console.log('ApiSelect====>:', e, v);
|
|
},
|
|
// atfer request callback
|
|
onOptionsChange: (options) => {
|
|
console.log('get options', options.length, options);
|
|
},
|
|
},
|
|
colProps: {
|
|
span: 8,
|
|
},
|
|
defaultValue: '0',
|
|
},
|
|
{
|
|
field: 'field8',
|
|
component: 'ApiCascader',
|
|
label: '联动ApiCascader',
|
|
required: true,
|
|
colProps: {
|
|
span: 8,
|
|
},
|
|
componentProps: {
|
|
api: areaRecord,
|
|
apiParamKey: 'parentCode',
|
|
dataField: 'data',
|
|
labelField: 'name',
|
|
valueField: 'code',
|
|
initFetchParams: {
|
|
parentCode: '',
|
|
},
|
|
isLeaf: (record) => {
|
|
return !(record.levelType < 3);
|
|
},
|
|
onChange: (e, ...v) => {
|
|
console.log('ApiCascader====>:', e, v);
|
|
},
|
|
},
|
|
},
|
|
{
|
|
field: 'field31',
|
|
component: 'Input',
|
|
label: '下拉本地搜索',
|
|
helpMessage: ['ApiSelect组件', '远程数据源本地搜索', '只发起一次请求获取所有选项'],
|
|
required: true,
|
|
slot: 'localSearch',
|
|
colProps: {
|
|
span: 8,
|
|
},
|
|
defaultValue: '0',
|
|
},
|
|
{
|
|
field: 'field32',
|
|
component: 'Input',
|
|
label: '下拉远程搜索',
|
|
helpMessage: ['ApiSelect组件', '将关键词发送到接口进行远程搜索'],
|
|
required: true,
|
|
slot: 'remoteSearch',
|
|
colProps: {
|
|
span: 8,
|
|
},
|
|
defaultValue: '0',
|
|
},
|
|
{
|
|
field: 'field33',
|
|
component: 'ApiTreeSelect',
|
|
label: '远程下拉树',
|
|
helpMessage: ['ApiTreeSelect组件', '使用接口提供的数据生成选项'],
|
|
required: true,
|
|
componentProps: {
|
|
api: treeOptionsListApi,
|
|
resultField: 'list',
|
|
onChange: (e, v) => {
|
|
console.log('ApiTreeSelect====>:', e, v);
|
|
},
|
|
},
|
|
colProps: {
|
|
span: 8,
|
|
},
|
|
},
|
|
{
|
|
field: 'field33',
|
|
component: 'ApiTreeSelect',
|
|
label: '远程懒加载下拉树',
|
|
helpMessage: ['ApiTreeSelect组件', '使用接口提供的数据生成选项'],
|
|
required: true,
|
|
componentProps: {
|
|
api: () => {
|
|
return new Promise((resolve) => {
|
|
resolve([
|
|
{
|
|
title: 'Parent Node',
|
|
value: '0-0',
|
|
},
|
|
]);
|
|
});
|
|
},
|
|
async: true,
|
|
onChange: (e, v) => {
|
|
console.log('ApiTreeSelect====>:', e, v);
|
|
},
|
|
onLoadData: ({ treeData, resolve, treeNode }) => {
|
|
console.log('treeNode====>:', treeNode);
|
|
setTimeout(() => {
|
|
const children: Recordable[] = [
|
|
{ title: `Child Node ${treeNode.eventKey}-0`, value: `${treeNode.eventKey}-0` },
|
|
{ title: `Child Node ${treeNode.eventKey}-1`, value: `${treeNode.eventKey}-1` },
|
|
];
|
|
children.forEach((item) => {
|
|
item.isLeaf = false;
|
|
item.children = [];
|
|
});
|
|
treeNode.dataRef.children = children;
|
|
treeData.value = [...treeData.value];
|
|
resolve();
|
|
return;
|
|
}, 300);
|
|
},
|
|
},
|
|
colProps: {
|
|
span: 8,
|
|
},
|
|
},
|
|
{
|
|
field: 'field34',
|
|
component: 'ApiRadioGroup',
|
|
label: '远程Radio',
|
|
helpMessage: ['ApiRadioGroup组件', '使用接口提供的数据生成选项'],
|
|
required: true,
|
|
componentProps: {
|
|
api: optionsListApi,
|
|
params: {
|
|
count: 2,
|
|
},
|
|
resultField: 'list',
|
|
// use name as label
|
|
labelField: 'name',
|
|
// use id as value
|
|
valueField: 'id',
|
|
},
|
|
defaultValue: '1',
|
|
colProps: {
|
|
span: 8,
|
|
},
|
|
},
|
|
{
|
|
field: 'field35',
|
|
component: 'ApiRadioGroup',
|
|
label: '远程Radio',
|
|
helpMessage: ['ApiRadioGroup组件', '使用接口提供的数据生成选项'],
|
|
required: true,
|
|
componentProps: {
|
|
api: optionsListApi,
|
|
params: {
|
|
count: 2,
|
|
},
|
|
resultField: 'list',
|
|
// use name as label
|
|
labelField: 'name',
|
|
// use id as value
|
|
valueField: 'id',
|
|
isBtn: true,
|
|
onChange: (e, v) => {
|
|
console.log('ApiRadioGroup====>:', e, v);
|
|
},
|
|
},
|
|
colProps: {
|
|
span: 8,
|
|
},
|
|
},
|
|
{
|
|
field: 'field36',
|
|
component: 'ApiTree',
|
|
label: '远程Tree',
|
|
helpMessage: ['ApiTree组件', '使用接口提供的数据生成选项'],
|
|
required: true,
|
|
componentProps: {
|
|
api: treeOptionsListApi,
|
|
params: {
|
|
count: 2,
|
|
},
|
|
afterFetch: (v) => {
|
|
//do something
|
|
return v;
|
|
},
|
|
resultField: 'list',
|
|
},
|
|
colProps: {
|
|
span: 8,
|
|
},
|
|
},
|
|
{
|
|
label: '远程穿梭框',
|
|
field: 'field37',
|
|
component: 'ApiTransfer',
|
|
componentProps: {
|
|
render: (item) => item.label,
|
|
api: async () => {
|
|
return Promise.resolve(citiesOptionsData.guangdong);
|
|
},
|
|
},
|
|
defaultValue: ['1'],
|
|
required: true,
|
|
},
|
|
{
|
|
field: 'divider-linked',
|
|
component: 'Divider',
|
|
label: '字段联动',
|
|
colProps: {
|
|
span: 24,
|
|
},
|
|
},
|
|
{
|
|
field: 'province',
|
|
component: 'Select',
|
|
label: '省份',
|
|
colProps: {
|
|
span: 8,
|
|
},
|
|
componentProps: ({ formModel, formActionType }) => {
|
|
return {
|
|
options: provincesOptions,
|
|
placeholder: '省份与城市联动',
|
|
onChange: (e: any) => {
|
|
// console.log(e)
|
|
let citiesOptions =
|
|
e == 1
|
|
? citiesOptionsData[provincesOptions[0].id]
|
|
: citiesOptionsData[provincesOptions[1].id];
|
|
// console.log(citiesOptions)
|
|
if (e === undefined) {
|
|
citiesOptions = [];
|
|
}
|
|
formModel.city = undefined; // reset city value
|
|
const { updateSchema } = formActionType;
|
|
updateSchema({
|
|
field: 'city',
|
|
componentProps: {
|
|
options: citiesOptions,
|
|
},
|
|
});
|
|
},
|
|
};
|
|
},
|
|
},
|
|
{
|
|
field: 'city',
|
|
component: 'Select',
|
|
label: '城市',
|
|
colProps: {
|
|
span: 8,
|
|
},
|
|
componentProps: {
|
|
options: [], // defalut []
|
|
placeholder: '省份与城市联动',
|
|
},
|
|
},
|
|
{
|
|
field: 'divider-selects',
|
|
component: 'Divider',
|
|
label: '互斥多选',
|
|
helpMessage: ['两个Select共用数据源', '但不可选择对方已选中的项目'],
|
|
colProps: {
|
|
span: 24,
|
|
},
|
|
},
|
|
{
|
|
field: 'selectA',
|
|
component: 'Select',
|
|
label: '互斥SelectA',
|
|
slot: 'selectA',
|
|
defaultValue: [],
|
|
colProps: {
|
|
span: 8,
|
|
},
|
|
},
|
|
{
|
|
field: 'selectB',
|
|
component: 'Select',
|
|
label: '互斥SelectB',
|
|
slot: 'selectB',
|
|
defaultValue: [],
|
|
colProps: {
|
|
span: 8,
|
|
},
|
|
},
|
|
{
|
|
field: 'divider-deconstruct',
|
|
component: 'Divider',
|
|
label: '字段解构',
|
|
helpMessage: ['如果组件的值是 array 或者 object', '可以根据 ES6 的解构语法分别取值'],
|
|
colProps: {
|
|
span: 24,
|
|
},
|
|
},
|
|
{
|
|
field: '[startTime, endTime]',
|
|
label: '时间范围',
|
|
component: 'TimeRangePicker',
|
|
componentProps: {
|
|
format: 'HH:mm:ss',
|
|
placeholder: ['开始时间', '结束时间'],
|
|
},
|
|
},
|
|
{
|
|
field: '[startDate, endDate]',
|
|
label: '日期范围',
|
|
component: 'RangePicker',
|
|
componentProps: {
|
|
format: 'YYYY-MM-DD',
|
|
placeholder: ['开始日期', '结束日期'],
|
|
},
|
|
},
|
|
{
|
|
field: '[startDateTime, endDateTime]',
|
|
label: '日期时间范围',
|
|
component: 'RangePicker',
|
|
componentProps: {
|
|
format: 'YYYY-MM-DD HH:mm:ss',
|
|
placeholder: ['开始日期、时间', '结束日期、时间'],
|
|
showTime: { format: 'HH:mm:ss' },
|
|
},
|
|
},
|
|
{
|
|
field: 'divider-others',
|
|
component: 'Divider',
|
|
label: '其它',
|
|
colProps: {
|
|
span: 24,
|
|
},
|
|
},
|
|
{
|
|
field: 'field20',
|
|
component: 'InputNumber',
|
|
label: '字段20',
|
|
required: true,
|
|
colProps: {
|
|
span: 8,
|
|
},
|
|
},
|
|
{
|
|
field: 'field21',
|
|
component: 'Slider',
|
|
label: '字段21',
|
|
componentProps: {
|
|
min: 0,
|
|
max: 100,
|
|
range: true,
|
|
marks: {
|
|
20: '20°C',
|
|
60: '60°C',
|
|
},
|
|
},
|
|
colProps: {
|
|
span: 8,
|
|
},
|
|
},
|
|
{
|
|
field: 'field22',
|
|
component: 'Rate',
|
|
label: '字段22',
|
|
defaultValue: 3,
|
|
colProps: {
|
|
span: 8,
|
|
},
|
|
componentProps: {
|
|
disabled: false,
|
|
allowHalf: true,
|
|
},
|
|
},
|
|
{
|
|
field: 'field23',
|
|
component: 'ImageUpload',
|
|
label: '上传图片',
|
|
required: true,
|
|
defaultValue: [
|
|
'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
|
|
],
|
|
componentProps: {
|
|
api: uploadApi,
|
|
accept: ['png', 'jpeg', 'jpg'],
|
|
maxSize: 2,
|
|
maxNumber: 1,
|
|
},
|
|
// rules: [
|
|
// {
|
|
// required: true,
|
|
// trigger: 'change',
|
|
// validator(_, value) {
|
|
// if (isArray(value) && value.length > 0) {
|
|
// return Promise.resolve();
|
|
// } else {
|
|
// return Promise.reject('请选择上传图片');
|
|
// }
|
|
// },
|
|
// },
|
|
// ],
|
|
},
|
|
];
|
|
|
|
const { createMessage } = useMessage();
|
|
const keyword = ref<string>('');
|
|
const searchParams = computed<Recordable<string>>(() => {
|
|
return { keyword: unref(keyword) };
|
|
});
|
|
|
|
function onSearch(value: string) {
|
|
keyword.value = value;
|
|
}
|
|
|
|
function handleReset() {
|
|
keyword.value = '';
|
|
}
|
|
|
|
function handleSubmit(values: any) {
|
|
console.log('values', values);
|
|
createMessage.success('click search,values:' + JSON.stringify(values));
|
|
}
|
|
</script>
|