feat: add VbenForm component (#4352)

* feat: add form component

* fix: build error

* feat: add form adapter

* feat: add some component

* feat: add some component

* feat: add example

* feat: suppoer custom action button

* chore: update

* feat: add example

* feat: add formModel,formDrawer demo

* fix: build error

* fix: typo

* fix: ci error

---------

Co-authored-by: jinmao <jinmao88@qq.com>
Co-authored-by: likui628 <90845831+likui628@users.noreply.github.com>
This commit is contained in:
Vben
2024-09-10 21:48:51 +08:00
committed by GitHub
parent 86ed732ca8
commit 524b9badf2
271 changed files with 5974 additions and 1247 deletions

View File

@@ -0,0 +1,114 @@
import type {
BaseFormComponentType,
VbenFormSchema as FormSchema,
VbenFormProps,
} from '@vben/common-ui';
import { h } from 'vue';
import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui';
import { $t } from '@vben/locales';
import {
AutoComplete,
Button,
Checkbox,
CheckboxGroup,
DatePicker,
Divider,
Input,
InputNumber,
InputPassword,
Mentions,
Radio,
RadioGroup,
RangePicker,
Rate,
Select,
Space,
Switch,
TimePicker,
TreeSelect,
Upload,
} from 'ant-design-vue';
// 业务表单组件适配
export type FormComponentType =
| 'AutoComplete'
| 'Checkbox'
| 'CheckboxGroup'
| 'DatePicker'
| 'Divider'
| 'Input'
| 'InputNumber'
| 'InputPassword'
| 'Mentions'
| 'Radio'
| 'RadioGroup'
| 'RangePicker'
| 'Rate'
| 'Select'
| 'Space'
| 'Switch'
| 'TimePicker'
| 'TreeSelect'
| 'Upload'
| BaseFormComponentType;
// 初始化表单组件并注册到form组件内部
setupVbenForm<FormComponentType>({
components: {
AutoComplete,
Checkbox,
CheckboxGroup,
DatePicker,
// 自定义默认的重置按钮
DefaultResetActionButton: (props, { attrs, slots }) => {
return h(Button, { ...props, attrs, type: 'default' }, slots);
},
// 自定义默认的提交按钮
DefaultSubmitActionButton: (props, { attrs, slots }) => {
return h(Button, { ...props, attrs, type: 'primary' }, slots);
},
Divider,
Input,
InputNumber,
InputPassword,
Mentions,
Radio,
RadioGroup,
RangePicker,
Rate,
Select,
Space,
Switch,
TimePicker,
TreeSelect,
Upload,
},
config: {
baseModelPropName: 'value',
modelPropNameMap: {
Checkbox: 'checked',
Radio: 'checked',
Switch: 'checked',
Upload: 'fileList',
},
},
defineRules: {
required: (value, _params, ctx) => {
if ((!value && value !== 0) || value.length === 0) {
return $t('formRules.required', [ctx.label]);
}
return true;
},
},
});
const useVbenForm = useForm<FormComponentType>;
export { useVbenForm, z };
export type VbenFormSchema = FormSchema<FormComponentType>;
export type { VbenFormProps };

View File

@@ -0,0 +1 @@
export * from './form';

View File

@@ -13,11 +13,12 @@ import {
UserDropdown,
} from '@vben/layouts';
import { preferences } from '@vben/preferences';
import { storeToRefs, useAccessStore, useUserStore } from '@vben/stores';
import { useAccessStore, useUserStore } from '@vben/stores';
import { openWindow } from '@vben/utils';
import { $t } from '#/locales';
import { useAuthStore } from '#/store';
import LoginForm from '#/views/_core/authentication/login.vue';
const notifications = ref<NotificationItem[]>([
{
@@ -87,8 +88,6 @@ const menus = computed(() => [
},
]);
const { loginLoading } = storeToRefs(authStore);
const avatar = computed(() => {
return userStore.userInfo?.avatar ?? preferences.app.defaultAvatar;
});
@@ -130,11 +129,9 @@ function handleMakeAll() {
<AuthenticationLoginExpiredModal
v-model:open="accessStore.loginExpired"
:avatar
:loading="loginLoading"
password-placeholder="123456"
username-placeholder="vben"
@submit="authStore.authLogin"
/>
>
<LoginForm />
</AuthenticationLoginExpiredModal>
</template>
<template #lock-screen>
<LockScreen :avatar @to-login="handleLogout" />

View File

@@ -72,6 +72,14 @@
"ellipsis": {
"title": "EllipsisText"
},
"form": {
"title": "Form",
"basic": "Basic Form",
"query": "Query Form",
"rules": "Form Rules",
"dynamic": "Dynamic Form",
"custom": "Custom Component"
},
"captcha": {
"title": "Captcha"
}

View File

@@ -72,6 +72,14 @@
"ellipsis": {
"title": "文本省略"
},
"form": {
"title": "表单",
"basic": "基础表单",
"query": "查询表单",
"rules": "表单校验",
"dynamic": "动态表单",
"custom": "自定义组件"
},
"captcha": {
"title": "验证码"
}

View File

@@ -47,6 +47,55 @@ const routes: RouteRecordRaw[] = [
title: $t('page.examples.captcha.title'),
},
},
{
name: 'FormExample',
path: '/examples/form',
meta: {
title: $t('page.examples.form.title'),
},
children: [
{
name: 'FormBasicExample',
path: '/examples/form/basic',
component: () => import('#/views/examples/form/basic.vue'),
meta: {
title: $t('page.examples.form.basic'),
},
},
{
name: 'FormQueryExample',
path: '/examples/form/query',
component: () => import('#/views/examples/form/query.vue'),
meta: {
title: $t('page.examples.form.query'),
},
},
{
name: 'FormRulesExample',
path: '/examples/form/rules',
component: () => import('#/views/examples/form/rules.vue'),
meta: {
title: $t('page.examples.form.rules'),
},
},
{
name: 'FormDynamicExample',
path: '/examples/form/dynamic',
component: () => import('#/views/examples/form/dynamic.vue'),
meta: {
title: $t('page.examples.form.dynamic'),
},
},
{
name: 'FormCustomExample',
path: '/examples/form/custom',
component: () => import('#/views/examples/form/custom.vue'),
meta: {
title: $t('page.examples.form.custom'),
},
},
],
},
],
},
];

View File

@@ -1,15 +1,49 @@
<script lang="ts" setup>
import type { LoginCodeParams } from '@vben/common-ui';
import type { LoginCodeParams, VbenFormSchema } from '@vben/common-ui';
import { ref } from 'vue';
import { computed, ref } from 'vue';
import { AuthenticationCodeLogin } from '@vben/common-ui';
import { LOGIN_PATH } from '@vben/constants';
import { AuthenticationCodeLogin, z } from '@vben/common-ui';
import { $t } from '@vben/locales';
defineOptions({ name: 'CodeLogin' });
const loading = ref(false);
const formSchema = computed((): VbenFormSchema[] => {
return [
{
component: 'VbenInput',
componentProps: {
placeholder: $t('authentication.mobile'),
},
fieldName: 'phoneNumber',
label: $t('authentication.mobile'),
rules: z
.string()
.min(1, { message: $t('authentication.mobileTip') })
.refine((v) => /^\d{11}$/.test(v), {
message: $t('authentication.mobileErrortip'),
}),
},
{
component: 'VbenPinInput',
componentProps: {
createText: (countdown: number) => {
const text =
countdown > 0
? $t('authentication.sendText', [countdown])
: $t('authentication.sendCode');
return text;
},
placeholder: $t('authentication.code'),
},
fieldName: 'code',
label: $t('authentication.code'),
rules: z.string().min(1, { message: $t('authentication.codeTip') }),
},
];
});
/**
* 异步处理登录操作
* Asynchronously handle the login process
@@ -23,8 +57,8 @@ async function handleLogin(values: LoginCodeParams) {
<template>
<AuthenticationCodeLogin
:form-schema="formSchema"
:loading="loading"
:login-path="LOGIN_PATH"
@submit="handleLogin"
/>
</template>

View File

@@ -1,13 +1,32 @@
<script lang="ts" setup>
import { ref } from 'vue';
import type { VbenFormSchema } from '@vben/common-ui';
import { AuthenticationForgetPassword } from '@vben/common-ui';
import { LOGIN_PATH } from '@vben/constants';
import { computed, ref } from 'vue';
import { AuthenticationForgetPassword, z } from '@vben/common-ui';
import { $t } from '@vben/locales';
defineOptions({ name: 'ForgetPassword' });
const loading = ref(false);
const formSchema = computed((): VbenFormSchema[] => {
return [
{
component: 'VbenInput',
componentProps: {
placeholder: 'example@example.com',
},
fieldName: 'email',
label: $t('authentication.email'),
rules: z
.string()
.min(1, { message: $t('authentication.emailTip') })
.email($t('authentication.emailValidErrorTip')),
},
];
});
function handleSubmit(value: string) {
// eslint-disable-next-line no-console
console.log('reset email:', value);
@@ -16,8 +35,8 @@ function handleSubmit(value: string) {
<template>
<AuthenticationForgetPassword
:form-schema="formSchema"
:loading="loading"
:login-path="LOGIN_PATH"
@submit="handleSubmit"
/>
</template>

View File

@@ -1,18 +1,108 @@
<script lang="ts" setup>
import { AuthenticationLogin } from '@vben/common-ui';
import type { VbenFormSchema } from '@vben/common-ui';
import type { BasicOption } from '@vben/types';
import { computed } from 'vue';
import { AuthenticationLogin, z } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { useAuthStore } from '#/store';
defineOptions({ name: 'Login' });
const authStore = useAuthStore();
const MOCK_USER_OPTIONS: BasicOption[] = [
{
label: '超级管理员',
value: 'vben',
},
{
label: '管理员',
value: 'admin',
},
{
label: '用户',
value: 'jack',
},
];
const formSchema = computed((): VbenFormSchema[] => {
return [
{
component: 'VbenSelect',
// componentProps(_values, form) {
// return {
// 'onUpdate:modelValue': (value: string) => {
// const findItem = MOCK_USER_OPTIONS.find(
// (item) => item.value === value,
// );
// if (findItem) {
// form.setValues({
// password: '123456',
// username: findItem.label,
// });
// }
// },
// options: MOCK_USER_OPTIONS,
// placeholder: $t('authentication.selectAccount'),
// };
// },
componentProps: {
options: MOCK_USER_OPTIONS,
placeholder: $t('authentication.selectAccount'),
},
fieldName: 'selectAccount',
label: $t('authentication.selectAccount'),
rules: z
.string()
.min(1, { message: $t('authentication.selectAccount') })
.optional()
.default('vben'),
},
{
component: 'VbenInput',
componentProps: {
placeholder: $t('authentication.usernameTip'),
},
dependencies: {
trigger(values, form) {
if (values.selectAccount) {
const findUser = MOCK_USER_OPTIONS.find(
(item) => item.value === values.selectAccount,
);
if (findUser) {
form.setValues({
password: '123456',
username: findUser.value,
});
}
}
},
triggerFields: ['selectAccount'],
},
fieldName: 'username',
label: $t('authentication.username'),
rules: z.string().min(1, { message: $t('authentication.usernameTip') }),
},
{
component: 'VbenInputPassword',
componentProps: {
placeholder: $t('authentication.password'),
},
fieldName: 'password',
label: $t('authentication.password'),
rules: z.string().min(1, { message: $t('authentication.passwordTip') }),
},
];
});
</script>
<template>
<AuthenticationLogin
:form-schema="formSchema"
:loading="authStore.loginLoading"
password-placeholder="123456"
username-placeholder="vben"
@submit="authStore.authLogin"
/>
</template>

View File

@@ -1,15 +1,91 @@
<script lang="ts" setup>
import type { LoginAndRegisterParams } from '@vben/common-ui';
import type { LoginAndRegisterParams, VbenFormSchema } from '@vben/common-ui';
import { ref } from 'vue';
import { computed, h, ref } from 'vue';
import { AuthenticationRegister } from '@vben/common-ui';
import { LOGIN_PATH } from '@vben/constants';
import { AuthenticationRegister, z } from '@vben/common-ui';
import { $t } from '@vben/locales';
defineOptions({ name: 'Register' });
const loading = ref(false);
const formSchema = computed((): VbenFormSchema[] => {
return [
{
component: 'VbenInput',
componentProps: {
placeholder: $t('authentication.usernameTip'),
},
fieldName: 'username',
label: $t('authentication.username'),
rules: z.string().min(1, { message: $t('authentication.usernameTip') }),
},
{
component: 'VbenInputPassword',
componentProps: {
passwordStrength: true,
placeholder: $t('authentication.password'),
},
fieldName: 'password',
label: $t('authentication.password'),
renderComponentContent() {
return {
strengthText: () => $t('authentication.passwordStrength'),
};
},
rules: z.string().min(1, { message: $t('authentication.passwordTip') }),
},
{
component: 'VbenInputPassword',
componentProps: {
placeholder: $t('authentication.confirmPassword'),
},
dependencies: {
rules(values) {
const { password } = values;
return z
.string()
.min(1, { message: $t('authentication.passwordTip') })
.refine((value) => value === password, {
message: $t('authentication.confirmPasswordTip'),
});
},
triggerFields: ['password'],
},
fieldName: 'confirmPassword',
label: $t('authentication.confirmPassword'),
rules: z.string().min(1, { message: $t('authentication.passwordTip') }),
},
{
component: 'VbenCheckbox',
fieldName: 'agreePolicy',
renderComponentContent: () => ({
default: () =>
h('span', [
$t('authentication.agree'),
h(
'a',
{
class:
'cursor-pointer text-primary ml-1 hover:text-primary-hover',
href: '',
},
[
$t('authentication.privacyPolicy'),
'&',
$t('authentication.terms'),
],
),
]),
}),
rules: z.boolean().refine((value) => !!value, {
message: $t('authentication.agreeTip'),
}),
},
];
});
function handleSubmit(value: LoginAndRegisterParams) {
// eslint-disable-next-line no-console
console.log('register submit:', value);
@@ -18,8 +94,8 @@ function handleSubmit(value: LoginAndRegisterParams) {
<template>
<AuthenticationRegister
:form-schema="formSchema"
:loading="loading"
:login-path="LOGIN_PATH"
@submit="handleSubmit"
/>
</template>

View File

@@ -7,7 +7,7 @@ import { useClipboard } from '@vueuse/core';
import { Button, Card, Input } from 'ant-design-vue';
const source = ref('Hello');
const { copy, text } = useClipboard({ source });
const { copy, text } = useClipboard({ legacy: true, source });
</script>
<template>

View File

@@ -1,14 +1,16 @@
<script lang="ts" setup>
import type { CaptchaPoint } from '@vben/common-ui';
import { ref } from 'vue';
import { Page, type Point, PointSelectionCaptcha } from '@vben/common-ui';
import { Page, PointSelectionCaptcha } from '@vben/common-ui';
import { Card } from 'ant-design-vue';
import { captchaImage, hintImage } from './base64';
const selectedPoints = ref<Point[]>([]);
const handleConfirm = (points: Point[], clear: () => void) => {
const selectedPoints = ref<CaptchaPoint[]>([]);
const handleConfirm = (points: CaptchaPoint[], clear: () => void) => {
selectedPoints.value = points;
clear();
};

View File

@@ -0,0 +1,56 @@
<script lang="ts" setup>
import { useVbenDrawer } from '@vben/common-ui';
import { useVbenForm } from '#/adapter';
defineOptions({
name: 'FormDrawerDemo',
});
const [Form, formApi] = useVbenForm({
schema: [
{
component: 'Input',
componentProps: {
placeholder: '请输入',
},
fieldName: 'field1',
label: '字段1',
rules: 'required',
},
{
component: 'Input',
componentProps: {
placeholder: '请输入',
},
fieldName: 'field2',
label: '字段2',
rules: 'required',
},
],
showDefaultActions: false,
});
const [Drawer, drawerApi] = useVbenDrawer({
onCancel() {
drawerApi.close();
},
onConfirm: async () => {
await formApi.submitForm();
drawerApi.close();
},
onOpenChange(isOpen: boolean) {
if (isOpen) {
const { values } = drawerApi.getData<Record<string, any>>();
if (values) {
formApi.setValues(values);
}
}
},
title: '内嵌表单示例',
});
</script>
<template>
<Drawer>
<Form />
</Drawer>
</template>

View File

@@ -7,6 +7,7 @@ import DocButton from '../doc-button.vue';
import AutoHeightDemo from './auto-height-demo.vue';
import BaseDemo from './base-demo.vue';
import DynamicDemo from './dynamic-demo.vue';
import FormDrawerDemo from './form-drawer-demo.vue';
import SharedDataDemo from './shared-data-demo.vue';
const [BaseDrawer, baseDrawerApi] = useVbenDrawer({
@@ -26,6 +27,10 @@ const [SharedDataDrawer, sharedDrawerApi] = useVbenDrawer({
connectedComponent: SharedDataDemo,
});
const [FormDrawer, formDrawerApi] = useVbenDrawer({
connectedComponent: FormDrawerDemo,
});
function openBaseDrawer() {
baseDrawerApi.open();
}
@@ -50,6 +55,14 @@ function openSharedDrawer() {
});
sharedDrawerApi.open();
}
function openFormDrawer() {
formDrawerApi.setData({
// 表单值
values: { field1: 'abc', field2: '123' },
});
formDrawerApi.open();
}
</script>
<template>
@@ -64,6 +77,7 @@ function openSharedDrawer() {
<AutoHeightDrawer />
<DynamicDrawer />
<SharedDataDrawer />
<FormDrawer />
<Card class="mb-4" title="基本使用">
<p class="mb-3">一个基础的抽屉示例</p>
@@ -89,5 +103,12 @@ function openSharedDrawer() {
打开抽屉并传递数据
</Button>
</Card>
<Card class="mb-4" title="表单抽屉示例">
<p class="mb-3">打开抽屉并设置表单schema以及数据</p>
<Button type="primary" @click="openFormDrawer">
打开抽屉并设置表单schema以及数据
</Button>
</Card>
</Page>
</template>

View File

@@ -0,0 +1,270 @@
<script lang="ts" setup>
import { Page } from '@vben/common-ui';
import { Button, Card, message } from 'ant-design-vue';
import dayjs from 'dayjs';
import { useVbenForm } from '#/adapter';
const [BaseForm, baseFormApi] = useVbenForm({
// 所有表单项共用,可单独在表单内覆盖
commonConfig: {
// 所有表单项
componentProps: {
class: 'w-full',
},
},
// 使用 tailwindcss grid布局
// 提交函数
handleSubmit: onSubmit,
// 垂直布局label和input在不同行值为vertical
layout: 'horizontal',
// 水平布局label和input在同一行
schema: [
{
// 组件需要在 #/adapter.ts内注册并加上类型
component: 'Input',
// 对应组件的参数
componentProps: {
placeholder: '请输入用户名',
},
// 字段名
fieldName: 'username',
// 界面显示的label
label: '字符串',
},
{
component: 'InputPassword',
componentProps: {
placeholder: '请输入密码',
},
fieldName: 'password',
label: '密码',
},
{
component: 'InputNumber',
componentProps: {
placeholder: '请输入',
},
fieldName: 'number',
label: '数字(带后缀)',
suffix: () => '¥',
},
{
component: 'Select',
componentProps: {
allowClear: true,
filterOption: true,
options: [
{
label: '选项1',
value: '1',
},
{
label: '选项2',
value: '2',
},
],
placeholder: '请选择',
showSearch: true,
},
fieldName: 'options',
label: '下拉选',
},
{
component: 'RadioGroup',
componentProps: {
options: [
{
label: '选项1',
value: '1',
},
{
label: '选项2',
value: '2',
},
],
},
fieldName: 'radioGroup',
label: '单选组',
},
{
component: 'Radio',
fieldName: 'radio',
label: '',
renderComponentContent: () => {
return {
default: () => ['Radio'],
};
},
},
{
component: 'CheckboxGroup',
componentProps: {
name: 'cname',
options: [
{
label: '选项1',
value: '1',
},
{
label: '选项2',
value: '2',
},
],
},
fieldName: 'checkboxGroup',
label: '多选组',
},
{
component: 'Checkbox',
fieldName: 'checkbox',
label: '',
renderComponentContent: () => {
return {
default: () => ['我已阅读并同意'],
};
},
},
{
component: 'Mentions',
componentProps: {
options: [
{
label: 'afc163',
value: 'afc163',
},
{
label: 'zombieJ',
value: 'zombieJ',
},
],
placeholder: '请输入',
},
fieldName: 'mentions',
label: '提及',
},
{
component: 'Rate',
fieldName: 'rate',
label: '评分',
},
{
component: 'Switch',
componentProps: {
class: 'w-auto',
},
fieldName: 'switch',
label: '开关',
},
{
component: 'DatePicker',
fieldName: 'datePicker',
label: '日期选择框',
},
{
component: 'RangePicker',
fieldName: 'rangePicker',
label: '范围选择器',
},
{
component: 'TimePicker',
fieldName: 'timePicker',
label: '时间选择框',
},
{
component: 'TreeSelect',
componentProps: {
allowClear: true,
placeholder: '请选择',
showSearch: true,
treeData: [
{
label: 'root 1',
value: 'root 1',
children: [
{
label: 'parent 1',
value: 'parent 1',
children: [
{
label: 'parent 1-0',
value: 'parent 1-0',
children: [
{
label: 'my leaf',
value: 'leaf1',
},
{
label: 'your leaf',
value: 'leaf2',
},
],
},
{
label: 'parent 1-1',
value: 'parent 1-1',
},
],
},
{
label: 'parent 2',
value: 'parent 2',
},
],
},
],
treeNodeFilterProp: 'label',
},
fieldName: 'treeSelect',
label: '树选择',
},
],
// 大屏一行显示3个中屏一行显示2个小屏一行显示1个
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
});
function onSubmit(values: Record<string, any>) {
message.success({
content: `form values: ${JSON.stringify(values)}`,
});
}
function handleSetFormValue() {
/**
* 设置表单值(多个)
*/
baseFormApi.setValues({
checkboxGroup: ['1'],
datePicker: dayjs('2022-01-01'),
mentions: '@afc163',
number: 3,
options: '1',
password: '2',
radioGroup: '1',
rangePicker: [dayjs('2022-01-01'), dayjs('2022-01-02')],
rate: 3,
switch: true,
timePicker: dayjs('2022-01-01 12:00:00'),
treeSelect: 'leaf1',
username: '1',
});
// 设置单个表单值
baseFormApi.setFieldValue('checkbox', true);
}
</script>
<template>
<Page
description="表单组件基础示例,请注意,该页面用到的参数代码会添加一些简单注释,方便理解,请仔细查看。"
title="表单组件"
>
<Card title="基础示例">
<template #extra>
<Button type="primary" @click="handleSetFormValue">设置表单值</Button>
</template>
<BaseForm />
</Card>
</Page>
</template>

View File

@@ -0,0 +1,79 @@
<script lang="ts" setup>
import { h } from 'vue';
import { Page } from '@vben/common-ui';
import { Card, Input, message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter';
const [BaseForm] = useVbenForm({
// 所有表单项共用,可单独在表单内覆盖
commonConfig: {
// 所有表单项
componentProps: {
class: 'w-full',
},
labelWidth: 200,
},
// 使用 tailwindcss grid布局
// 提交函数
handleSubmit: onSubmit,
// 垂直布局label和input在不同行值为vertical
layout: 'horizontal',
// 水平布局label和input在同一行
schema: [
{
// 组件需要在 #/adapter.ts内注册并加上类型
component: 'Input',
fieldName: 'field',
label: '自定义后缀',
suffix: () => h('span', { class: 'text-red-600' }, '元'),
},
{
// 组件需要在 #/adapter.ts内注册并加上类型
component: 'Input',
fieldName: 'field1',
label: '自定义组件slot',
renderComponentContent: () => ({
prefix: () => 'prefix',
suffix: () => 'suffix',
}),
},
{
// 组件需要在 #/adapter.ts内注册并加上类型
component: h(Input, { placeholder: '请输入' }),
fieldName: 'field2',
label: '自定义组件',
rules: 'required',
},
{
// 组件需要在 #/adapter.ts内注册并加上类型
component: 'Input',
fieldName: 'field3',
label: '自定义组件(slot)',
rules: 'required',
},
],
// 中屏一行显示2个小屏一行显示1个
wrapperClass: 'grid-cols-1 md:grid-cols-2',
});
function onSubmit(values: Record<string, any>) {
message.success({
content: `form values: ${JSON.stringify(values)}`,
});
}
</script>
<template>
<Page description="表单组件自定义示例" title="表单组件">
<Card title="基础示例">
<BaseForm>
<template #field3="slotProps">
<Input placeholder="请输入" v-bind="slotProps" />
</template>
</BaseForm>
</Card>
</Page>
</template>

View File

@@ -0,0 +1,271 @@
<script lang="ts" setup>
import { Page } from '@vben/common-ui';
import { Button, Card, message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter';
const [Form, formApi] = useVbenForm({
// 使用 tailwindcss grid布局
// 提交函数
handleSubmit: onSubmit,
// 水平布局label和input在同一行
schema: [
{
component: 'Switch',
defaultValue: true,
fieldName: 'field1Switch',
help: '通过Dom控制销毁',
label: '显示字段1',
},
{
component: 'Switch',
defaultValue: true,
fieldName: 'field2Switch',
help: '通过css控制隐藏',
label: '显示字段2',
},
{
component: 'Switch',
fieldName: 'field3Switch',
label: '禁用字段3',
},
{
component: 'Switch',
fieldName: 'field4Switch',
label: '字段4必填',
},
{
component: 'Input',
dependencies: {
if(values) {
return !!values.field1Switch;
},
// 只有指定的字段改变时,才会触发
triggerFields: ['field1Switch'],
},
// 字段名
fieldName: 'field1',
// 界面显示的label
label: '字段1',
},
{
component: 'Input',
dependencies: {
show(values) {
return !!values.field2Switch;
},
// 只有指定的字段改变时,才会触发
triggerFields: ['field2Switch'],
},
// 字段名
fieldName: 'field2',
// 界面显示的label
label: '字段2',
},
{
component: 'Input',
dependencies: {
disabled(values) {
return !!values.field3Switch;
},
// 只有指定的字段改变时,才会触发
triggerFields: ['field3Switch'],
},
// 字段名
fieldName: 'field3',
// 界面显示的label
label: '字段3',
},
{
component: 'Input',
dependencies: {
required(values) {
return !!values.field4Switch;
},
// 只有指定的字段改变时,才会触发
triggerFields: ['field4Switch'],
},
// 字段名
fieldName: 'field4',
// 界面显示的label
label: '字段4',
},
{
component: 'Input',
dependencies: {
rules(values) {
if (values.field1 === '123') {
return 'required';
}
return null;
},
// 只有指定的字段改变时,才会触发
triggerFields: ['field1'],
},
// 字段名
fieldName: 'field5',
help: '当字段1的值为`123`时,必填',
// 界面显示的label
label: '动态rules',
},
{
component: 'Select',
componentProps: {
allowClear: true,
class: 'w-full',
filterOption: true,
options: [
{
label: '选项1',
value: '1',
},
{
label: '选项2',
value: '2',
},
],
placeholder: '请选择',
showSearch: true,
},
dependencies: {
componentProps(values) {
if (values.field2 === '123') {
return {
options: [
{
label: '选项1',
value: '1',
},
{
label: '选项2',
value: '2',
},
{
label: '选项3',
value: '3',
},
],
};
}
return {};
},
// 只有指定的字段改变时,才会触发
triggerFields: ['field2'],
},
// 字段名
fieldName: 'field6',
help: '当字段2的值为`123`时,更改下拉选项',
// 界面显示的label
label: '动态配置',
},
{
component: 'Input',
fieldName: 'field7',
label: '字段7',
},
],
// 大屏一行显示3个中屏一行显示2个小屏一行显示1个
wrapperClass: 'grid-cols-1 md:grid-cols-3 lg:grid-cols-4',
});
const [SyncForm] = useVbenForm({
handleSubmit: onSubmit,
schema: [
{
component: 'Input',
// 字段名
fieldName: 'field1',
// 界面显示的label
label: '字段1',
},
{
component: 'Input',
componentProps: {
disabled: true,
},
dependencies: {
trigger(values, form) {
form.setFieldValue('field2', values.field1);
},
// 只有指定的字段改变时,才会触发
triggerFields: ['field1'],
},
// 字段名
fieldName: 'field2',
// 界面显示的label
label: '字段2',
},
],
// 大屏一行显示3个中屏一行显示2个小屏一行显示1个
wrapperClass: 'grid-cols-1 md:grid-cols-3 lg:grid-cols-4',
});
function onSubmit(values: Record<string, any>) {
message.success({
content: `form values: ${JSON.stringify(values)}`,
});
}
function handleDelete() {
formApi.setState((prev) => {
return {
...prev,
schema: prev.schema?.filter((item) => item.fieldName !== 'field7'),
};
});
}
function handleAdd() {
formApi.setState((prev) => {
return {
...prev,
schema: [
...(prev?.schema ?? []),
{
component: 'Input',
fieldName: `field${Date.now()}`,
label: '字段+',
},
],
};
});
}
function handleUpdate() {
formApi.setState((prev) => {
return {
...prev,
schema: prev.schema?.map((item) => {
if (item.fieldName === 'field3') {
return {
...item,
label: '字段3-修改',
};
}
return item;
}),
};
});
}
</script>
<template>
<Page
description="表单组件动态联动示例包含了常用的场景。增删改本质上是修改schema你也可以通过 `setState` 动态修改schema。"
title="表单组件"
>
<Card title="表单动态联动示例">
<template #extra>
<Button class="mr-2" @click="handleUpdate">修改字段3</Button>
<Button class="mr-2" @click="handleDelete">删除字段7</Button>
<Button @click="handleAdd">添加字段</Button>
</template>
<Form />
</Card>
<Card class="mt-5" title="字段同步字段1数据与字段2数据同步">
<SyncForm />
</Card>
</Page>
</template>

View File

@@ -0,0 +1,149 @@
<script lang="ts" setup>
import { Page } from '@vben/common-ui';
import { Card, message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter';
const [QueryForm] = useVbenForm({
// 默认展开
collapsed: false,
// 所有表单项共用,可单独在表单内覆盖
commonConfig: {
// 所有表单项
componentProps: {
class: 'w-full',
},
},
// 提交函数
handleSubmit: onSubmit,
// 垂直布局label和input在不同行值为vertical
layout: 'horizontal',
// 使用 tailwindcss grid布局
// 水平布局label和input在同一行
schema: [
{
// 组件需要在 #/adapter.ts内注册并加上类型
component: 'Input',
// 对应组件的参数
componentProps: {
placeholder: '请输入用户名',
},
// 字段名
fieldName: 'username',
// 界面显示的label
label: '字符串',
},
{
component: 'InputPassword',
componentProps: {
placeholder: '请输入密码',
},
fieldName: 'password',
label: '密码',
},
{
component: 'InputNumber',
componentProps: {
placeholder: '请输入',
},
fieldName: 'number',
label: '数字(带后缀)',
suffix: () => '¥',
},
{
component: 'Select',
componentProps: {
allowClear: true,
filterOption: true,
options: [
{
label: '选项1',
value: '1',
},
{
label: '选项2',
value: '2',
},
],
placeholder: '请选择',
showSearch: true,
},
fieldName: 'options',
label: '下拉选',
},
{
component: 'DatePicker',
fieldName: 'datePicker',
label: '日期选择框',
},
],
// 是否可展开
showCollapseButton: true,
submitButtonOptions: {
text: '查询',
},
// 大屏一行显示3个中屏一行显示2个小屏一行显示1个
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
});
const [QueryForm1] = useVbenForm({
// 默认展开
collapsed: true,
collapsedRows: 2,
// 所有表单项共用,可单独在表单内覆盖
commonConfig: {
// 所有表单项
componentProps: {
class: 'w-full',
},
},
// 提交函数
handleSubmit: onSubmit,
// 垂直布局label和input在不同行值为vertical
layout: 'horizontal',
// 使用 tailwindcss grid布局
// 水平布局label和input在同一行
schema: (() => {
const schema = [];
for (let index = 0; index < 14; index++) {
schema.push({
// 组件需要在 #/adapter.ts内注册并加上类型
component: 'Input',
// 字段名
fieldName: `field${index}`,
// 界面显示的label
label: `字段${index}`,
});
}
return schema;
})(),
// 是否可展开
showCollapseButton: true,
submitButtonOptions: {
text: '查询',
},
// 大屏一行显示3个中屏一行显示2个小屏一行显示1个
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
});
function onSubmit(values: Record<string, any>) {
message.success({
content: `form values: ${JSON.stringify(values)}`,
});
}
</script>
<template>
<Page
description="查询表单,常用语和表格组合使用,可进行收缩展开。"
title="表单组件"
>
<Card class="mb-5" title="查询表单,默认展开">
<QueryForm />
</Card>
<Card title="查询表单默认折叠折叠时保留2行">
<QueryForm1 />
</Card>
</Page>
</template>

View File

@@ -0,0 +1,188 @@
<script lang="ts" setup>
import { Page } from '@vben/common-ui';
import { Button, Card, message } from 'ant-design-vue';
import { useVbenForm, z } from '#/adapter';
const [Form, formApi] = useVbenForm({
// 所有表单项共用,可单独在表单内覆盖
commonConfig: {
// 所有表单项
componentProps: {
class: 'w-full',
},
},
// 使用 tailwindcss grid布局
// 提交函数
handleSubmit: onSubmit,
// 垂直布局label和input在不同行值为vertical
layout: 'horizontal',
// 水平布局label和input在同一行
schema: [
{
// 组件需要在 #/adapter.ts内注册并加上类型
component: 'Input',
// 对应组件的参数
componentProps: {
placeholder: '请输入',
},
// 字段名
fieldName: 'field1',
// 界面显示的label
label: '字段1',
rules: 'required',
},
{
component: 'Input',
componentProps: {
placeholder: '请输入',
},
defaultValue: '默认值',
fieldName: 'field2',
label: '默认值(必填)',
rules: 'required',
},
{
component: 'Input',
componentProps: {
placeholder: '请输入',
},
fieldName: 'field3',
label: '默认值(非必填)',
rules: z.string().default('默认值').optional(),
},
{
component: 'Input',
componentProps: {
placeholder: '请输入',
},
fieldName: 'field31',
label: '自定义信息',
rules: z.string().min(1, { message: '最少输入1个字符' }),
},
{
component: 'Input',
// 对应组件的参数
componentProps: {
placeholder: '请输入',
},
// 字段名
fieldName: 'field4',
// 界面显示的label
label: '邮箱',
rules: z.string().email('请输入正确的邮箱'),
},
{
component: 'InputNumber',
componentProps: {
placeholder: '请输入',
},
fieldName: 'number',
label: '数字',
// 预处理函数将空字符串或null转换为undefined
rules: 'required',
},
{
component: 'Select',
componentProps: {
allowClear: true,
filterOption: true,
options: [
{
label: '选项1',
value: '1',
},
{
label: '选项2',
value: '2',
},
],
placeholder: '请选择',
showSearch: true,
},
defaultValue: undefined,
fieldName: 'options',
label: '下拉选',
rules: 'required',
},
{
component: 'RadioGroup',
componentProps: {
options: [
{
label: '选项1',
value: '1',
},
{
label: '选项2',
value: '2',
},
],
},
fieldName: 'radioGroup',
label: '单选组',
rules: 'required',
},
{
component: 'CheckboxGroup',
componentProps: {
name: 'cname',
options: [
{
label: '选项1',
value: '1',
},
{
label: '选项2',
value: '2',
},
],
},
fieldName: 'checkboxGroup',
label: '多选组',
rules: 'required',
},
{
component: 'Checkbox',
fieldName: 'checkbox',
label: '',
renderComponentContent: () => {
return {
default: () => ['我已阅读并同意'],
};
},
rules: 'required',
},
{
component: 'DatePicker',
defaultValue: undefined,
fieldName: 'datePicker',
label: '日期选择框',
rules: 'required',
},
],
// 大屏一行显示3个中屏一行显示2个小屏一行显示1个
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
});
function onSubmit(values: Record<string, any>) {
message.success({
content: `form values: ${JSON.stringify(values)}`,
});
}
</script>
<template>
<Page description="表单校验示例" title="表单组件">
<Card title="基础组件校验示例">
<template #extra>
<Button @click="() => formApi.validate()">校验表单</Button>
<Button class="mx-2" @click="() => formApi.resetValidate()">
清空校验信息
</Button>
</template>
<Form />
</Card>
</Page>
</template>

View File

@@ -0,0 +1,58 @@
<script lang="ts" setup>
import { useVbenModal } from '@vben/common-ui';
import { useVbenForm } from '#/adapter';
defineOptions({
name: 'FormModelDemo',
});
const [Form, formApi] = useVbenForm({
schema: [
{
component: 'Input',
componentProps: {
placeholder: '请输入',
},
fieldName: 'field1',
label: '字段1',
rules: 'required',
},
{
component: 'Input',
componentProps: {
placeholder: '请输入',
},
fieldName: 'field2',
label: '字段2',
rules: 'required',
},
],
showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
fullscreenButton: false,
onCancel() {
modalApi.close();
},
onConfirm: async () => {
await formApi.submitForm();
modalApi.close();
},
onOpenChange(isOpen: boolean) {
if (isOpen) {
const { values } = modalApi.getData<Record<string, any>>();
if (values) {
formApi.setValues(values);
}
}
},
title: '内嵌表单示例',
});
</script>
<template>
<Modal>
<Form />
</Modal>
</template>

View File

@@ -8,6 +8,7 @@ import AutoHeightDemo from './auto-height-demo.vue';
import BaseDemo from './base-demo.vue';
import DragDemo from './drag-demo.vue';
import DynamicDemo from './dynamic-demo.vue';
import FormModalDemo from './form-model-demo.vue';
import SharedDataDemo from './shared-data-demo.vue';
const [BaseModal, baseModalApi] = useVbenModal({
@@ -31,6 +32,10 @@ const [SharedDataModal, sharedModalApi] = useVbenModal({
connectedComponent: SharedDataDemo,
});
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: FormModalDemo,
});
function openBaseModal() {
baseModalApi.open();
}
@@ -59,6 +64,14 @@ function handleUpdateTitle() {
dynamicModalApi.setState({ title: '外部动态标题' });
dynamicModalApi.open();
}
function openFormModal() {
formModalApi.setData({
// 表单值
values: { field1: 'abc' },
});
formModalApi.open();
}
</script>
<template>
@@ -74,7 +87,7 @@ function handleUpdateTitle() {
<DragModal />
<DynamicModal />
<SharedDataModal />
<FormModal />
<Card class="mb-4" title="基本使用">
<p class="mb-3">一个基础的弹窗示例</p>
<Button type="primary" @click="openBaseModal">打开弹窗</Button>
@@ -104,5 +117,10 @@ function handleUpdateTitle() {
打开弹窗并传递数据
</Button>
</Card>
<Card class="mb-4" title="表单弹窗示例">
<p class="mb-3">弹窗与表单结合</p>
<Button type="primary" @click="openFormModal"> 打开弹窗 </Button>
</Card>
</Page>
</template>