mirror of
https://github.com/vbenjs/vue-vben-admin.git
synced 2025-01-23 01:30:26 +08:00
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:
parent
86ed732ca8
commit
524b9badf2
114
apps/web-antd/src/adapter/form.ts
Normal file
114
apps/web-antd/src/adapter/form.ts
Normal 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 };
|
1
apps/web-antd/src/adapter/index.ts
Normal file
1
apps/web-antd/src/adapter/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './form';
|
@ -13,11 +13,12 @@ import {
|
|||||||
UserDropdown,
|
UserDropdown,
|
||||||
} from '@vben/layouts';
|
} from '@vben/layouts';
|
||||||
import { preferences } from '@vben/preferences';
|
import { preferences } from '@vben/preferences';
|
||||||
import { storeToRefs, useAccessStore, useUserStore } from '@vben/stores';
|
import { useAccessStore, useUserStore } from '@vben/stores';
|
||||||
import { openWindow } from '@vben/utils';
|
import { openWindow } from '@vben/utils';
|
||||||
|
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
import { useAuthStore } from '#/store';
|
import { useAuthStore } from '#/store';
|
||||||
|
import LoginForm from '#/views/_core/authentication/login.vue';
|
||||||
|
|
||||||
const notifications = ref<NotificationItem[]>([
|
const notifications = ref<NotificationItem[]>([
|
||||||
{
|
{
|
||||||
@ -87,8 +88,6 @@ const menus = computed(() => [
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const { loginLoading } = storeToRefs(authStore);
|
|
||||||
|
|
||||||
const avatar = computed(() => {
|
const avatar = computed(() => {
|
||||||
return userStore.userInfo?.avatar ?? preferences.app.defaultAvatar;
|
return userStore.userInfo?.avatar ?? preferences.app.defaultAvatar;
|
||||||
});
|
});
|
||||||
@ -130,11 +129,9 @@ function handleMakeAll() {
|
|||||||
<AuthenticationLoginExpiredModal
|
<AuthenticationLoginExpiredModal
|
||||||
v-model:open="accessStore.loginExpired"
|
v-model:open="accessStore.loginExpired"
|
||||||
:avatar
|
:avatar
|
||||||
:loading="loginLoading"
|
>
|
||||||
password-placeholder="123456"
|
<LoginForm />
|
||||||
username-placeholder="vben"
|
</AuthenticationLoginExpiredModal>
|
||||||
@submit="authStore.authLogin"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
<template #lock-screen>
|
<template #lock-screen>
|
||||||
<LockScreen :avatar @to-login="handleLogout" />
|
<LockScreen :avatar @to-login="handleLogout" />
|
||||||
|
@ -1,15 +1,49 @@
|
|||||||
<script lang="ts" setup>
|
<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 { AuthenticationCodeLogin, z } from '@vben/common-ui';
|
||||||
import { LOGIN_PATH } from '@vben/constants';
|
import { $t } from '@vben/locales';
|
||||||
|
|
||||||
defineOptions({ name: 'CodeLogin' });
|
defineOptions({ name: 'CodeLogin' });
|
||||||
|
|
||||||
const loading = ref(false);
|
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
|
* Asynchronously handle the login process
|
||||||
@ -23,8 +57,8 @@ async function handleLogin(values: LoginCodeParams) {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<AuthenticationCodeLogin
|
<AuthenticationCodeLogin
|
||||||
|
:form-schema="formSchema"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:login-path="LOGIN_PATH"
|
|
||||||
@submit="handleLogin"
|
@submit="handleLogin"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
@ -1,13 +1,32 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from 'vue';
|
import type { VbenFormSchema } from '@vben/common-ui';
|
||||||
|
|
||||||
import { AuthenticationForgetPassword } from '@vben/common-ui';
|
import { computed, ref } from 'vue';
|
||||||
import { LOGIN_PATH } from '@vben/constants';
|
|
||||||
|
import { AuthenticationForgetPassword, z } from '@vben/common-ui';
|
||||||
|
import { $t } from '@vben/locales';
|
||||||
|
|
||||||
defineOptions({ name: 'ForgetPassword' });
|
defineOptions({ name: 'ForgetPassword' });
|
||||||
|
|
||||||
const loading = ref(false);
|
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) {
|
function handleSubmit(value: string) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log('reset email:', value);
|
console.log('reset email:', value);
|
||||||
@ -16,8 +35,8 @@ function handleSubmit(value: string) {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<AuthenticationForgetPassword
|
<AuthenticationForgetPassword
|
||||||
|
:form-schema="formSchema"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:login-path="LOGIN_PATH"
|
|
||||||
@submit="handleSubmit"
|
@submit="handleSubmit"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
@ -1,18 +1,91 @@
|
|||||||
<script lang="ts" setup>
|
<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';
|
import { useAuthStore } from '#/store';
|
||||||
|
|
||||||
defineOptions({ name: 'Login' });
|
defineOptions({ name: 'Login' });
|
||||||
|
|
||||||
const authStore = useAuthStore();
|
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: {
|
||||||
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<AuthenticationLogin
|
<AuthenticationLogin
|
||||||
|
:form-schema="formSchema"
|
||||||
:loading="authStore.loginLoading"
|
:loading="authStore.loginLoading"
|
||||||
password-placeholder="123456"
|
|
||||||
username-placeholder="vben"
|
|
||||||
@submit="authStore.authLogin"
|
@submit="authStore.authLogin"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
@ -1,15 +1,91 @@
|
|||||||
<script lang="ts" setup>
|
<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 { AuthenticationRegister, z } from '@vben/common-ui';
|
||||||
import { LOGIN_PATH } from '@vben/constants';
|
import { $t } from '@vben/locales';
|
||||||
|
|
||||||
defineOptions({ name: 'Register' });
|
defineOptions({ name: 'Register' });
|
||||||
|
|
||||||
const loading = ref(false);
|
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) {
|
function handleSubmit(value: LoginAndRegisterParams) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log('register submit:', value);
|
console.log('register submit:', value);
|
||||||
@ -18,8 +94,8 @@ function handleSubmit(value: LoginAndRegisterParams) {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<AuthenticationRegister
|
<AuthenticationRegister
|
||||||
|
:form-schema="formSchema"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:login-path="LOGIN_PATH"
|
|
||||||
@submit="handleSubmit"
|
@submit="handleSubmit"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
89
apps/web-ele/src/adapter/form.ts
Normal file
89
apps/web-ele/src/adapter/form.ts
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
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 {
|
||||||
|
ElButton,
|
||||||
|
ElCheckbox,
|
||||||
|
ElCheckboxGroup,
|
||||||
|
ElDivider,
|
||||||
|
ElInput,
|
||||||
|
ElInputNumber,
|
||||||
|
ElRadioGroup,
|
||||||
|
ElSelect,
|
||||||
|
ElSpace,
|
||||||
|
ElSwitch,
|
||||||
|
ElTimePicker,
|
||||||
|
ElTreeSelect,
|
||||||
|
ElUpload,
|
||||||
|
} from 'element-plus';
|
||||||
|
// 业务表单组件适配
|
||||||
|
|
||||||
|
export type FormComponentType =
|
||||||
|
| 'Checkbox'
|
||||||
|
| 'CheckboxGroup'
|
||||||
|
| 'DatePicker'
|
||||||
|
| 'Divider'
|
||||||
|
| 'Input'
|
||||||
|
| 'InputNumber'
|
||||||
|
| 'RadioGroup'
|
||||||
|
| 'Select'
|
||||||
|
| 'Space'
|
||||||
|
| 'Switch'
|
||||||
|
| 'TimePicker'
|
||||||
|
| 'TreeSelect'
|
||||||
|
| 'Upload'
|
||||||
|
| BaseFormComponentType;
|
||||||
|
|
||||||
|
// 初始化表单组件,并注册到form组件内部
|
||||||
|
setupVbenForm<FormComponentType>({
|
||||||
|
components: {
|
||||||
|
Checkbox: ElCheckbox,
|
||||||
|
CheckboxGroup: ElCheckboxGroup,
|
||||||
|
// 自定义默认的重置按钮
|
||||||
|
DefaultResetActionButton: (props, { attrs, slots }) => {
|
||||||
|
return h(ElButton, { ...props, attrs, type: 'info' }, slots);
|
||||||
|
},
|
||||||
|
// 自定义默认的提交按钮
|
||||||
|
DefaultSubmitActionButton: (props, { attrs, slots }) => {
|
||||||
|
return h(ElButton, { ...props, attrs, type: 'primary' }, slots);
|
||||||
|
},
|
||||||
|
Divider: ElDivider,
|
||||||
|
Input: ElInput,
|
||||||
|
InputNumber: ElInputNumber,
|
||||||
|
RadioGroup: ElRadioGroup,
|
||||||
|
Select: ElSelect,
|
||||||
|
Space: ElSpace,
|
||||||
|
Switch: ElSwitch,
|
||||||
|
TimePicker: ElTimePicker,
|
||||||
|
TreeSelect: ElTreeSelect,
|
||||||
|
Upload: ElUpload,
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
modelPropNameMap: {
|
||||||
|
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 };
|
1
apps/web-ele/src/adapter/index.ts
Normal file
1
apps/web-ele/src/adapter/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './form';
|
@ -13,11 +13,12 @@ import {
|
|||||||
UserDropdown,
|
UserDropdown,
|
||||||
} from '@vben/layouts';
|
} from '@vben/layouts';
|
||||||
import { preferences } from '@vben/preferences';
|
import { preferences } from '@vben/preferences';
|
||||||
import { storeToRefs, useAccessStore, useUserStore } from '@vben/stores';
|
import { useAccessStore, useUserStore } from '@vben/stores';
|
||||||
import { openWindow } from '@vben/utils';
|
import { openWindow } from '@vben/utils';
|
||||||
|
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
import { useAuthStore } from '#/store';
|
import { useAuthStore } from '#/store';
|
||||||
|
import LoginForm from '#/views/_core/authentication/login.vue';
|
||||||
|
|
||||||
const notifications = ref<NotificationItem[]>([
|
const notifications = ref<NotificationItem[]>([
|
||||||
{
|
{
|
||||||
@ -87,8 +88,6 @@ const menus = computed(() => [
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const { loginLoading } = storeToRefs(authStore);
|
|
||||||
|
|
||||||
const avatar = computed(() => {
|
const avatar = computed(() => {
|
||||||
return userStore.userInfo?.avatar ?? preferences.app.defaultAvatar;
|
return userStore.userInfo?.avatar ?? preferences.app.defaultAvatar;
|
||||||
});
|
});
|
||||||
@ -130,11 +129,9 @@ function handleMakeAll() {
|
|||||||
<AuthenticationLoginExpiredModal
|
<AuthenticationLoginExpiredModal
|
||||||
v-model:open="accessStore.loginExpired"
|
v-model:open="accessStore.loginExpired"
|
||||||
:avatar
|
:avatar
|
||||||
:loading="loginLoading"
|
>
|
||||||
password-placeholder="123456"
|
<LoginForm />
|
||||||
username-placeholder="vben"
|
</AuthenticationLoginExpiredModal>
|
||||||
@submit="authStore.authLogin"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
<template #lock-screen>
|
<template #lock-screen>
|
||||||
<LockScreen :avatar @to-login="handleLogout" />
|
<LockScreen :avatar @to-login="handleLogout" />
|
||||||
|
@ -1,15 +1,49 @@
|
|||||||
<script lang="ts" setup>
|
<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 { AuthenticationCodeLogin, z } from '@vben/common-ui';
|
||||||
import { LOGIN_PATH } from '@vben/constants';
|
import { $t } from '@vben/locales';
|
||||||
|
|
||||||
defineOptions({ name: 'CodeLogin' });
|
defineOptions({ name: 'CodeLogin' });
|
||||||
|
|
||||||
const loading = ref(false);
|
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
|
* Asynchronously handle the login process
|
||||||
@ -23,8 +57,8 @@ async function handleLogin(values: LoginCodeParams) {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<AuthenticationCodeLogin
|
<AuthenticationCodeLogin
|
||||||
|
:form-schema="formSchema"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:login-path="LOGIN_PATH"
|
|
||||||
@submit="handleLogin"
|
@submit="handleLogin"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
@ -1,13 +1,32 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from 'vue';
|
import type { VbenFormSchema } from '@vben/common-ui';
|
||||||
|
|
||||||
import { AuthenticationForgetPassword } from '@vben/common-ui';
|
import { computed, ref } from 'vue';
|
||||||
import { LOGIN_PATH } from '@vben/constants';
|
|
||||||
|
import { AuthenticationForgetPassword, z } from '@vben/common-ui';
|
||||||
|
import { $t } from '@vben/locales';
|
||||||
|
|
||||||
defineOptions({ name: 'ForgetPassword' });
|
defineOptions({ name: 'ForgetPassword' });
|
||||||
|
|
||||||
const loading = ref(false);
|
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) {
|
function handleSubmit(value: string) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log('reset email:', value);
|
console.log('reset email:', value);
|
||||||
@ -16,8 +35,8 @@ function handleSubmit(value: string) {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<AuthenticationForgetPassword
|
<AuthenticationForgetPassword
|
||||||
|
:form-schema="formSchema"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:login-path="LOGIN_PATH"
|
|
||||||
@submit="handleSubmit"
|
@submit="handleSubmit"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
@ -1,18 +1,91 @@
|
|||||||
<script lang="ts" setup>
|
<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';
|
import { useAuthStore } from '#/store';
|
||||||
|
|
||||||
defineOptions({ name: 'Login' });
|
defineOptions({ name: 'Login' });
|
||||||
|
|
||||||
const authStore = useAuthStore();
|
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: {
|
||||||
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<AuthenticationLogin
|
<AuthenticationLogin
|
||||||
|
:form-schema="formSchema"
|
||||||
:loading="authStore.loginLoading"
|
:loading="authStore.loginLoading"
|
||||||
password-placeholder="123456"
|
|
||||||
username-placeholder="vben"
|
|
||||||
@submit="authStore.authLogin"
|
@submit="authStore.authLogin"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
@ -1,15 +1,91 @@
|
|||||||
<script lang="ts" setup>
|
<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 { AuthenticationRegister, z } from '@vben/common-ui';
|
||||||
import { LOGIN_PATH } from '@vben/constants';
|
import { $t } from '@vben/locales';
|
||||||
|
|
||||||
defineOptions({ name: 'Register' });
|
defineOptions({ name: 'Register' });
|
||||||
|
|
||||||
const loading = ref(false);
|
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) {
|
function handleSubmit(value: LoginAndRegisterParams) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log('register submit:', value);
|
console.log('register submit:', value);
|
||||||
@ -18,8 +94,8 @@ function handleSubmit(value: LoginAndRegisterParams) {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<AuthenticationRegister
|
<AuthenticationRegister
|
||||||
|
:form-schema="formSchema"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:login-path="LOGIN_PATH"
|
|
||||||
@submit="handleSubmit"
|
@submit="handleSubmit"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
98
apps/web-naive/src/adapter/form.ts
Normal file
98
apps/web-naive/src/adapter/form.ts
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
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 {
|
||||||
|
NButton,
|
||||||
|
NCheckbox,
|
||||||
|
NCheckboxGroup,
|
||||||
|
NDatePicker,
|
||||||
|
NDivider,
|
||||||
|
NInput,
|
||||||
|
NInputNumber,
|
||||||
|
NRadioGroup,
|
||||||
|
NSelect,
|
||||||
|
NSpace,
|
||||||
|
NSwitch,
|
||||||
|
NTimePicker,
|
||||||
|
NTreeSelect,
|
||||||
|
NUpload,
|
||||||
|
} from 'naive-ui';
|
||||||
|
// 业务表单组件适配
|
||||||
|
|
||||||
|
export type FormComponentType =
|
||||||
|
| 'Checkbox'
|
||||||
|
| 'CheckboxGroup'
|
||||||
|
| 'DatePicker'
|
||||||
|
| 'Divider'
|
||||||
|
| 'Input'
|
||||||
|
| 'InputNumber'
|
||||||
|
| 'RadioGroup'
|
||||||
|
| 'Select'
|
||||||
|
| 'Space'
|
||||||
|
| 'Switch'
|
||||||
|
| 'TimePicker'
|
||||||
|
| 'TreeSelect'
|
||||||
|
| 'Upload'
|
||||||
|
| BaseFormComponentType;
|
||||||
|
|
||||||
|
// 初始化表单组件,并注册到form组件内部
|
||||||
|
setupVbenForm<FormComponentType>({
|
||||||
|
components: {
|
||||||
|
Checkbox: NCheckbox,
|
||||||
|
CheckboxGroup: NCheckboxGroup,
|
||||||
|
DatePicker: NDatePicker,
|
||||||
|
// 自定义默认的重置按钮
|
||||||
|
DefaultResetActionButton: (props, { attrs, slots }) => {
|
||||||
|
return h(NButton, { ...props, attrs, text: false, type: 'info' }, slots);
|
||||||
|
},
|
||||||
|
// 自定义默认的提交按钮
|
||||||
|
DefaultSubmitActionButton: (props, { attrs, slots }) => {
|
||||||
|
return h(
|
||||||
|
NButton,
|
||||||
|
{ ...props, attrs, text: false, type: 'primary' },
|
||||||
|
slots,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
Divider: NDivider,
|
||||||
|
Input: NInput,
|
||||||
|
InputNumber: NInputNumber,
|
||||||
|
RadioGroup: NRadioGroup,
|
||||||
|
Select: NSelect,
|
||||||
|
Space: NSpace,
|
||||||
|
Switch: NSwitch,
|
||||||
|
TimePicker: NTimePicker,
|
||||||
|
TreeSelect: NTreeSelect,
|
||||||
|
Upload: NUpload,
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
baseModelPropName: 'value',
|
||||||
|
modelPropNameMap: {
|
||||||
|
Checkbox: 'checked',
|
||||||
|
Radio: '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 };
|
1
apps/web-naive/src/adapter/index.ts
Normal file
1
apps/web-naive/src/adapter/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './form';
|
@ -13,11 +13,12 @@ import {
|
|||||||
UserDropdown,
|
UserDropdown,
|
||||||
} from '@vben/layouts';
|
} from '@vben/layouts';
|
||||||
import { preferences } from '@vben/preferences';
|
import { preferences } from '@vben/preferences';
|
||||||
import { storeToRefs, useAccessStore, useUserStore } from '@vben/stores';
|
import { useAccessStore, useUserStore } from '@vben/stores';
|
||||||
import { openWindow } from '@vben/utils';
|
import { openWindow } from '@vben/utils';
|
||||||
|
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
import { useAuthStore } from '#/store';
|
import { useAuthStore } from '#/store';
|
||||||
|
import LoginForm from '#/views/_core/authentication/login.vue';
|
||||||
|
|
||||||
const notifications = ref<NotificationItem[]>([
|
const notifications = ref<NotificationItem[]>([
|
||||||
{
|
{
|
||||||
@ -87,8 +88,6 @@ const menus = computed(() => [
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const { loginLoading } = storeToRefs(authStore);
|
|
||||||
|
|
||||||
const avatar = computed(() => {
|
const avatar = computed(() => {
|
||||||
return userStore.userInfo?.avatar ?? preferences.app.defaultAvatar;
|
return userStore.userInfo?.avatar ?? preferences.app.defaultAvatar;
|
||||||
});
|
});
|
||||||
@ -130,11 +129,9 @@ function handleMakeAll() {
|
|||||||
<AuthenticationLoginExpiredModal
|
<AuthenticationLoginExpiredModal
|
||||||
v-model:open="accessStore.loginExpired"
|
v-model:open="accessStore.loginExpired"
|
||||||
:avatar
|
:avatar
|
||||||
:loading="loginLoading"
|
>
|
||||||
password-placeholder="123456"
|
<LoginForm />
|
||||||
username-placeholder="vben"
|
</AuthenticationLoginExpiredModal>
|
||||||
@submit="authStore.authLogin"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
<template #lock-screen>
|
<template #lock-screen>
|
||||||
<LockScreen :avatar @to-login="handleLogout" />
|
<LockScreen :avatar @to-login="handleLogout" />
|
||||||
|
@ -1,15 +1,49 @@
|
|||||||
<script lang="ts" setup>
|
<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 { AuthenticationCodeLogin, z } from '@vben/common-ui';
|
||||||
import { LOGIN_PATH } from '@vben/constants';
|
import { $t } from '@vben/locales';
|
||||||
|
|
||||||
defineOptions({ name: 'CodeLogin' });
|
defineOptions({ name: 'CodeLogin' });
|
||||||
|
|
||||||
const loading = ref(false);
|
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
|
* Asynchronously handle the login process
|
||||||
@ -23,8 +57,8 @@ async function handleLogin(values: LoginCodeParams) {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<AuthenticationCodeLogin
|
<AuthenticationCodeLogin
|
||||||
|
:form-schema="formSchema"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:login-path="LOGIN_PATH"
|
|
||||||
@submit="handleLogin"
|
@submit="handleLogin"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
@ -1,13 +1,32 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from 'vue';
|
import type { VbenFormSchema } from '@vben/common-ui';
|
||||||
|
|
||||||
import { AuthenticationForgetPassword } from '@vben/common-ui';
|
import { computed, ref } from 'vue';
|
||||||
import { LOGIN_PATH } from '@vben/constants';
|
|
||||||
|
import { AuthenticationForgetPassword, z } from '@vben/common-ui';
|
||||||
|
import { $t } from '@vben/locales';
|
||||||
|
|
||||||
defineOptions({ name: 'ForgetPassword' });
|
defineOptions({ name: 'ForgetPassword' });
|
||||||
|
|
||||||
const loading = ref(false);
|
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) {
|
function handleSubmit(value: string) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log('reset email:', value);
|
console.log('reset email:', value);
|
||||||
@ -16,8 +35,8 @@ function handleSubmit(value: string) {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<AuthenticationForgetPassword
|
<AuthenticationForgetPassword
|
||||||
|
:form-schema="formSchema"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:login-path="LOGIN_PATH"
|
|
||||||
@submit="handleSubmit"
|
@submit="handleSubmit"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
@ -1,18 +1,91 @@
|
|||||||
<script lang="ts" setup>
|
<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';
|
import { useAuthStore } from '#/store';
|
||||||
|
|
||||||
defineOptions({ name: 'Login' });
|
defineOptions({ name: 'Login' });
|
||||||
|
|
||||||
const authStore = useAuthStore();
|
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: {
|
||||||
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<AuthenticationLogin
|
<AuthenticationLogin
|
||||||
|
:form-schema="formSchema"
|
||||||
:loading="authStore.loginLoading"
|
:loading="authStore.loginLoading"
|
||||||
password-placeholder="123456"
|
|
||||||
username-placeholder="vben"
|
|
||||||
@submit="authStore.authLogin"
|
@submit="authStore.authLogin"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
@ -1,15 +1,91 @@
|
|||||||
<script lang="ts" setup>
|
<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 { AuthenticationRegister, z } from '@vben/common-ui';
|
||||||
import { LOGIN_PATH } from '@vben/constants';
|
import { $t } from '@vben/locales';
|
||||||
|
|
||||||
defineOptions({ name: 'Register' });
|
defineOptions({ name: 'Register' });
|
||||||
|
|
||||||
const loading = ref(false);
|
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) {
|
function handleSubmit(value: LoginAndRegisterParams) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log('register submit:', value);
|
console.log('register submit:', value);
|
||||||
@ -18,8 +94,8 @@ function handleSubmit(value: LoginAndRegisterParams) {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<AuthenticationRegister
|
<AuthenticationRegister
|
||||||
|
:form-schema="formSchema"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:login-path="LOGIN_PATH"
|
|
||||||
@submit="handleSubmit"
|
@submit="handleSubmit"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
@ -160,6 +160,10 @@ function sidebarComponents(): DefaultTheme.SidebarItem[] {
|
|||||||
link: 'common-ui/vben-drawer',
|
link: 'common-ui/vben-drawer',
|
||||||
text: 'Vben Drawer 抽屉',
|
text: 'Vben Drawer 抽屉',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
link: 'common-ui/vben-form',
|
||||||
|
text: 'Vben Form 表单',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
11
docs/src/components/common-ui/vben-form.md
Normal file
11
docs/src/components/common-ui/vben-form.md
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
outline: deep
|
||||||
|
---
|
||||||
|
|
||||||
|
# Vben Form 表单
|
||||||
|
|
||||||
|
框架提供的表单组件,可适配 `Element Plus`、`Ant Design Vue`、`Naive`UI 框架。
|
||||||
|
|
||||||
|
# 使用
|
||||||
|
|
||||||
|
TODO
|
@ -61,11 +61,6 @@ If you want to adjust the content of the login form, you can configure the `Auth
|
|||||||
*/
|
*/
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
|
|
||||||
/**
|
|
||||||
* @en Password placeholder
|
|
||||||
*/
|
|
||||||
passwordPlaceholder?: string;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @en QR code login path
|
* @en QR code login path
|
||||||
*/
|
*/
|
||||||
@ -114,11 +109,6 @@ If you want to adjust the content of the login form, you can configure the `Auth
|
|||||||
* @en Login box title
|
* @en Login box title
|
||||||
*/
|
*/
|
||||||
title?: string;
|
title?: string;
|
||||||
|
|
||||||
/**
|
|
||||||
* @en Username placeholder
|
|
||||||
*/
|
|
||||||
usernamePlaceholder?: string;
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -54,11 +54,6 @@
|
|||||||
*/
|
*/
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh_CN 密码占位符
|
|
||||||
*/
|
|
||||||
passwordPlaceholder?: string;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh_CN 二维码登录路径
|
* @zh_CN 二维码登录路径
|
||||||
*/
|
*/
|
||||||
@ -108,10 +103,6 @@
|
|||||||
*/
|
*/
|
||||||
title?: string;
|
title?: string;
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh_CN 用户名占位符
|
|
||||||
*/
|
|
||||||
usernamePlaceholder?: string;
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -15,12 +15,14 @@ export async function unicorn(): Promise<Linter.Config[]> {
|
|||||||
rules: {
|
rules: {
|
||||||
...pluginUnicorn.configs.recommended.rules,
|
...pluginUnicorn.configs.recommended.rules,
|
||||||
|
|
||||||
|
'unicorn/better-regex': 'off',
|
||||||
'unicorn/consistent-destructuring': 'off',
|
'unicorn/consistent-destructuring': 'off',
|
||||||
'unicorn/consistent-function-scoping': 'off',
|
'unicorn/consistent-function-scoping': 'off',
|
||||||
'unicorn/filename-case': 'off',
|
'unicorn/filename-case': 'off',
|
||||||
'unicorn/import-style': 'off',
|
'unicorn/import-style': 'off',
|
||||||
'unicorn/no-array-for-each': 'off',
|
'unicorn/no-array-for-each': 'off',
|
||||||
'unicorn/no-null': 'off',
|
'unicorn/no-null': 'off',
|
||||||
|
'unicorn/no-useless-undefined': 'off',
|
||||||
'unicorn/prefer-at': 'off',
|
'unicorn/prefer-at': 'off',
|
||||||
'unicorn/prefer-dom-node-text-content': 'off',
|
'unicorn/prefer-dom-node-text-content': 'off',
|
||||||
'unicorn/prefer-export-from': ['error', { ignoreUsedVariables: true }],
|
'unicorn/prefer-export-from': ['error', { ignoreUsedVariables: true }],
|
||||||
|
@ -81,7 +81,11 @@ function defineApplicationConfig(userConfigPromise?: DefineApplicationOptions) {
|
|||||||
port,
|
port,
|
||||||
warmup: {
|
warmup: {
|
||||||
// 预热文件
|
// 预热文件
|
||||||
clientFiles: ['./index.html', './src/{views,layouts,router,store}/*'],
|
clientFiles: [
|
||||||
|
'./index.html',
|
||||||
|
'./bootstrap.ts',
|
||||||
|
'./src/{views,layouts,router,store,api}/*',
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -3,6 +3,7 @@ import type { PluginOption } from 'vite';
|
|||||||
import type { ArchiverPluginOptions } from '../typing';
|
import type { ArchiverPluginOptions } from '../typing';
|
||||||
|
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
|
import fsp from 'node:fs/promises';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
|
|
||||||
import archiver from 'archiver';
|
import archiver from 'archiver';
|
||||||
@ -18,7 +19,14 @@ export const viteArchiverPlugin = (
|
|||||||
|
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
const folderToZip = 'dist';
|
const folderToZip = 'dist';
|
||||||
const zipOutputPath = join(process.cwd(), outputDir, `${name}.zip`);
|
|
||||||
|
const zipOutputDir = join(process.cwd(), outputDir);
|
||||||
|
const zipOutputPath = join(zipOutputDir, `${name}.zip`);
|
||||||
|
try {
|
||||||
|
await fsp.mkdir(zipOutputDir, { recursive: true });
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await zipFolder(folderToZip, zipOutputPath);
|
await zipFolder(folderToZip, zipOutputPath);
|
||||||
|
@ -23,12 +23,15 @@
|
|||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
text-rendering: optimizelegibility;
|
text-rendering: optimizelegibility;
|
||||||
-webkit-tap-highlight-color: transparent;
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
|
||||||
|
/* -webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale; */
|
||||||
}
|
}
|
||||||
|
|
||||||
#app,
|
#app,
|
||||||
body,
|
body,
|
||||||
html {
|
html {
|
||||||
@apply !pointer-events-auto size-full overscroll-none;
|
@apply size-full overscroll-none;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
@ -18,7 +18,6 @@ export {
|
|||||||
CircleHelp,
|
CircleHelp,
|
||||||
Copy,
|
Copy,
|
||||||
CornerDownLeft,
|
CornerDownLeft,
|
||||||
Disc as IconDefault,
|
|
||||||
Ellipsis,
|
Ellipsis,
|
||||||
Expand,
|
Expand,
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
@ -35,6 +34,7 @@ export {
|
|||||||
LogOut,
|
LogOut,
|
||||||
MailCheck,
|
MailCheck,
|
||||||
Maximize,
|
Maximize,
|
||||||
|
Menu as IconDefault,
|
||||||
Menu,
|
Menu,
|
||||||
Minimize,
|
Minimize,
|
||||||
Minimize2,
|
Minimize2,
|
||||||
|
@ -4,7 +4,6 @@ export default defineBuildConfig({
|
|||||||
clean: true,
|
clean: true,
|
||||||
declaration: true,
|
declaration: true,
|
||||||
entries: [
|
entries: [
|
||||||
'src/index',
|
|
||||||
'src/store',
|
'src/store',
|
||||||
'src/constants/index',
|
'src/constants/index',
|
||||||
'src/utils/index',
|
'src/utils/index',
|
||||||
|
@ -17,14 +17,7 @@
|
|||||||
"dist"
|
"dist"
|
||||||
],
|
],
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
"main": "./dist/index.mjs",
|
|
||||||
"module": "./dist/index.mjs",
|
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
|
||||||
"types": "./src/index.ts",
|
|
||||||
"development": "./src/index.ts",
|
|
||||||
"default": "./dist/index.mjs"
|
|
||||||
},
|
|
||||||
"./constants": {
|
"./constants": {
|
||||||
"types": "./src/constants/index.ts",
|
"types": "./src/constants/index.ts",
|
||||||
"development": "./src/constants/index.ts",
|
"development": "./src/constants/index.ts",
|
||||||
@ -53,9 +46,25 @@
|
|||||||
},
|
},
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
"./constants": {
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/constants/index.d.ts",
|
||||||
"default": "./dist/index.mjs"
|
"default": "./dist/constants/index.mjs"
|
||||||
|
},
|
||||||
|
"./utils": {
|
||||||
|
"types": "./dist/utils/index.d.ts",
|
||||||
|
"default": "./dist/utils/index.mjs"
|
||||||
|
},
|
||||||
|
"./color": {
|
||||||
|
"types": "./dist/color/index.d.ts",
|
||||||
|
"default": "./dist/color/index.mjs"
|
||||||
|
},
|
||||||
|
"./cache": {
|
||||||
|
"types": "./dist/cache/index.d.ts",
|
||||||
|
"default": "./dist/cache/index.mjs"
|
||||||
|
},
|
||||||
|
"./store": {
|
||||||
|
"types": "./dist/store/index.d.ts",
|
||||||
|
"default": "./dist/store.mjs"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
export * from './cache';
|
|
||||||
export * from './color';
|
|
||||||
export * from './constants';
|
|
||||||
export * from './store';
|
|
||||||
export * from './utils';
|
|
@ -0,0 +1,60 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { StateHandler } from '../state-handler';
|
||||||
|
|
||||||
|
describe('stateHandler', () => {
|
||||||
|
it('should resolve when condition is set to true', async () => {
|
||||||
|
const handler = new StateHandler();
|
||||||
|
|
||||||
|
// 模拟异步设置 condition 为 true
|
||||||
|
setTimeout(() => {
|
||||||
|
handler.setConditionTrue(); // 明确触发 condition 为 true
|
||||||
|
}, 10);
|
||||||
|
|
||||||
|
// 等待条件被设置为 true
|
||||||
|
await handler.waitForCondition();
|
||||||
|
expect(handler.isConditionTrue()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should resolve immediately if condition is already true', async () => {
|
||||||
|
const handler = new StateHandler();
|
||||||
|
handler.setConditionTrue(); // 提前设置为 true
|
||||||
|
|
||||||
|
// 立即 resolve,因为 condition 已经是 true
|
||||||
|
await handler.waitForCondition();
|
||||||
|
expect(handler.isConditionTrue()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject when condition is set to false after waiting', async () => {
|
||||||
|
const handler = new StateHandler();
|
||||||
|
|
||||||
|
// 模拟异步设置 condition 为 false
|
||||||
|
setTimeout(() => {
|
||||||
|
handler.setConditionFalse(); // 明确触发 condition 为 false
|
||||||
|
}, 10);
|
||||||
|
|
||||||
|
// 等待过程中,期望 Promise 被 reject
|
||||||
|
await expect(handler.waitForCondition()).rejects.toThrow();
|
||||||
|
expect(handler.isConditionTrue()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reset condition to false', () => {
|
||||||
|
const handler = new StateHandler();
|
||||||
|
handler.setConditionTrue(); // 设置为 true
|
||||||
|
handler.reset(); // 重置为 false
|
||||||
|
|
||||||
|
expect(handler.isConditionTrue()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should resolve when condition is set to true after reset', async () => {
|
||||||
|
const handler = new StateHandler();
|
||||||
|
handler.reset(); // 确保初始为 false
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
handler.setConditionTrue(); // 重置后设置为 true
|
||||||
|
}, 10);
|
||||||
|
|
||||||
|
await handler.waitForCondition();
|
||||||
|
expect(handler.isConditionTrue()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
80
packages/@core/base/shared/src/utils/__tests__/util.test.ts
Normal file
80
packages/@core/base/shared/src/utils/__tests__/util.test.ts
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { bindMethods } from '../util';
|
||||||
|
|
||||||
|
class TestClass {
|
||||||
|
public value: string;
|
||||||
|
|
||||||
|
constructor(value: string) {
|
||||||
|
this.value = value;
|
||||||
|
bindMethods(this); // 调用通用方法
|
||||||
|
}
|
||||||
|
|
||||||
|
getValue() {
|
||||||
|
return this.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
setValue(newValue: string) {
|
||||||
|
this.value = newValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('bindMethods', () => {
|
||||||
|
it('should bind methods to the instance correctly', () => {
|
||||||
|
const instance = new TestClass('initial');
|
||||||
|
|
||||||
|
// 解构方法
|
||||||
|
const { getValue } = instance;
|
||||||
|
|
||||||
|
// 检查 getValue 是否能正确调用,并且 this 绑定了 instance
|
||||||
|
expect(getValue()).toBe('initial');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should bind multiple methods', () => {
|
||||||
|
const instance = new TestClass('initial');
|
||||||
|
|
||||||
|
const { getValue, setValue } = instance;
|
||||||
|
|
||||||
|
// 检查 getValue 和 setValue 方法是否正确绑定了 this
|
||||||
|
setValue('newValue');
|
||||||
|
expect(getValue()).toBe('newValue');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not bind non-function properties', () => {
|
||||||
|
const instance = new TestClass('initial');
|
||||||
|
|
||||||
|
// 检查普通属性是否保持原样
|
||||||
|
expect(instance.value).toBe('initial');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not bind constructor method', () => {
|
||||||
|
const instance = new TestClass('test');
|
||||||
|
|
||||||
|
// 检查 constructor 是否没有被绑定
|
||||||
|
expect(instance.constructor.name).toBe('TestClass');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not bind getter/setter properties', () => {
|
||||||
|
class TestWithGetterSetter {
|
||||||
|
private _value: string = 'test';
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
bindMethods(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
get value() {
|
||||||
|
return this._value;
|
||||||
|
}
|
||||||
|
|
||||||
|
set value(newValue: string) {
|
||||||
|
this._value = newValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const instance = new TestWithGetterSetter();
|
||||||
|
const { value } = instance;
|
||||||
|
|
||||||
|
// Getter 和 setter 不应被绑定
|
||||||
|
expect(value).toBe('test');
|
||||||
|
});
|
||||||
|
});
|
@ -5,9 +5,11 @@ export * from './inference';
|
|||||||
export * from './letter';
|
export * from './letter';
|
||||||
export * from './merge';
|
export * from './merge';
|
||||||
export * from './nprogress';
|
export * from './nprogress';
|
||||||
|
export * from './state-handler';
|
||||||
export * from './to';
|
export * from './to';
|
||||||
export * from './tree';
|
export * from './tree';
|
||||||
export * from './unique';
|
export * from './unique';
|
||||||
export * from './update-css-variables';
|
export * from './update-css-variables';
|
||||||
|
export * from './util';
|
||||||
export * from './window';
|
export * from './window';
|
||||||
export { default as cloneDeep } from 'lodash.clonedeep';
|
export { default as cloneDeep } from 'lodash.clonedeep';
|
||||||
|
50
packages/@core/base/shared/src/utils/state-handler.ts
Normal file
50
packages/@core/base/shared/src/utils/state-handler.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
export class StateHandler {
|
||||||
|
private condition: boolean = false;
|
||||||
|
private rejectCondition: (() => void) | null = null;
|
||||||
|
private resolveCondition: (() => void) | null = null;
|
||||||
|
|
||||||
|
// 清理 resolve/reject 函数
|
||||||
|
private clearPromises() {
|
||||||
|
this.resolveCondition = null;
|
||||||
|
this.rejectCondition = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
isConditionTrue(): boolean {
|
||||||
|
return this.condition;
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.condition = false;
|
||||||
|
this.clearPromises();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 触发状态为 false 时,reject
|
||||||
|
setConditionFalse() {
|
||||||
|
this.condition = false;
|
||||||
|
if (this.rejectCondition) {
|
||||||
|
this.rejectCondition();
|
||||||
|
this.clearPromises();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 触发状态为 true 时,resolve
|
||||||
|
setConditionTrue() {
|
||||||
|
this.condition = true;
|
||||||
|
if (this.resolveCondition) {
|
||||||
|
this.resolveCondition();
|
||||||
|
this.clearPromises();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回一个 Promise,等待 condition 变为 true
|
||||||
|
waitForCondition(): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (this.condition) {
|
||||||
|
resolve(); // 如果 condition 已经为 true,立即 resolve
|
||||||
|
} else {
|
||||||
|
this.resolveCondition = resolve;
|
||||||
|
this.rejectCondition = reject;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
19
packages/@core/base/shared/src/utils/util.ts
Normal file
19
packages/@core/base/shared/src/utils/util.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
export function bindMethods<T extends object>(instance: T): void {
|
||||||
|
const prototype = Object.getPrototypeOf(instance);
|
||||||
|
const propertyNames = Object.getOwnPropertyNames(prototype);
|
||||||
|
|
||||||
|
propertyNames.forEach((propertyName) => {
|
||||||
|
const descriptor = Object.getOwnPropertyDescriptor(prototype, propertyName);
|
||||||
|
const propertyValue = instance[propertyName as keyof T];
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof propertyValue === 'function' &&
|
||||||
|
propertyName !== 'constructor' &&
|
||||||
|
descriptor &&
|
||||||
|
!descriptor.get &&
|
||||||
|
!descriptor.set
|
||||||
|
) {
|
||||||
|
instance[propertyName as keyof T] = propertyValue.bind(instance);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
@ -4,9 +4,11 @@ import { computed, onMounted, onUnmounted, ref } from 'vue';
|
|||||||
import {
|
import {
|
||||||
CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT,
|
CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT,
|
||||||
CSS_VARIABLE_LAYOUT_CONTENT_WIDTH,
|
CSS_VARIABLE_LAYOUT_CONTENT_WIDTH,
|
||||||
|
} from '@vben-core/shared/constants';
|
||||||
|
import {
|
||||||
getElementVisibleRect,
|
getElementVisibleRect,
|
||||||
type VisibleDomRect,
|
type VisibleDomRect,
|
||||||
} from '@vben-core/shared';
|
} from '@vben-core/shared/utils';
|
||||||
|
|
||||||
import { useCssVar, useDebounceFn } from '@vueuse/core';
|
import { useCssVar, useDebounceFn } from '@vueuse/core';
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { DEFAULT_NAMESPACE } from '@vben-core/shared';
|
import { DEFAULT_NAMESPACE } from '@vben-core/shared/constants';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @see copy https://github.com/element-plus/element-plus/blob/dev/packages/hooks/use-namespace/index.ts
|
* @see copy https://github.com/element-plus/element-plus/blob/dev/packages/hooks/use-namespace/index.ts
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import type { Ref } from 'vue';
|
import type { ComputedRef, Ref } from 'vue';
|
||||||
import { computed, getCurrentInstance, useAttrs, useSlots } from 'vue';
|
import { computed, getCurrentInstance, unref, useAttrs, useSlots } from 'vue';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getFirstNonNullOrUndefined,
|
getFirstNonNullOrUndefined,
|
||||||
kebabToCamelCase,
|
kebabToCamelCase,
|
||||||
} from '@vben-core/shared';
|
} from '@vben-core/shared/utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 依次从插槽、attrs、props、state 中获取值
|
* 依次从插槽、attrs、props、state 中获取值
|
||||||
@ -45,3 +45,49 @@ export function usePriorityValue<
|
|||||||
|
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量获取state中的值(每个值都是ref)
|
||||||
|
* @param props
|
||||||
|
* @param state
|
||||||
|
*/
|
||||||
|
export function usePriorityValues<
|
||||||
|
T extends Record<string, any>,
|
||||||
|
S extends Ref<Record<string, any>> = Readonly<Ref<NoInfer<T>, NoInfer<T>>>,
|
||||||
|
>(props: T, state: S | undefined) {
|
||||||
|
const result: { [K in keyof T]: ComputedRef<T[K]> } = {} as never;
|
||||||
|
|
||||||
|
(Object.keys(props) as (keyof T)[]).forEach((key) => {
|
||||||
|
result[key] = usePriorityValue(key as keyof typeof props, props, state);
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量获取state中的值(集中在一个computed,用于透传)
|
||||||
|
* @param props
|
||||||
|
* @param state
|
||||||
|
*/
|
||||||
|
export function useForwardPriorityValues<
|
||||||
|
T extends Record<string, any>,
|
||||||
|
S extends Ref<Record<string, any>> = Readonly<Ref<NoInfer<T>, NoInfer<T>>>,
|
||||||
|
>(props: T, state: S | undefined) {
|
||||||
|
const computedResult: { [K in keyof T]: ComputedRef<T[K]> } = {} as never;
|
||||||
|
|
||||||
|
(Object.keys(props) as (keyof T)[]).forEach((key) => {
|
||||||
|
computedResult[key] = usePriorityValue(
|
||||||
|
key as keyof typeof props,
|
||||||
|
props,
|
||||||
|
state,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return computed(() => {
|
||||||
|
const unwrapResult: Record<string, any> = {};
|
||||||
|
Object.keys(props).forEach((key) => {
|
||||||
|
unwrapResult[key] = unref(computedResult[key]);
|
||||||
|
});
|
||||||
|
return unwrapResult as { [K in keyof T]: T[K] };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
@ -3,11 +3,19 @@ export type Locale = 'en-US' | 'zh-CN';
|
|||||||
export const messages: Record<Locale, Record<string, string>> = {
|
export const messages: Record<Locale, Record<string, string>> = {
|
||||||
'en-US': {
|
'en-US': {
|
||||||
cancel: 'Cancel',
|
cancel: 'Cancel',
|
||||||
|
collapse: 'Collapse',
|
||||||
confirm: 'Confirm',
|
confirm: 'Confirm',
|
||||||
|
expand: 'Expand',
|
||||||
|
reset: 'Reset',
|
||||||
|
submit: 'Submit',
|
||||||
},
|
},
|
||||||
'zh-CN': {
|
'zh-CN': {
|
||||||
cancel: '取消',
|
cancel: '取消',
|
||||||
|
collapse: '收起',
|
||||||
confirm: '确认',
|
confirm: '确认',
|
||||||
|
expand: '展开',
|
||||||
|
reset: '重置',
|
||||||
|
submit: '提交',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -4,7 +4,8 @@ import type { InitialOptions, Preferences } from './types';
|
|||||||
|
|
||||||
import { markRaw, reactive, readonly, watch } from 'vue';
|
import { markRaw, reactive, readonly, watch } from 'vue';
|
||||||
|
|
||||||
import { isMacOs, merge, StorageManager } from '@vben-core/shared';
|
import { StorageManager } from '@vben-core/shared/cache';
|
||||||
|
import { isMacOs, merge } from '@vben-core/shared/utils';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
breakpointsTailwind,
|
breakpointsTailwind,
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
import type { Preferences } from './types';
|
import type { Preferences } from './types';
|
||||||
|
|
||||||
import {
|
import { generatorColorVariables } from '@vben-core/shared/color';
|
||||||
updateCSSVariables as executeUpdateCSSVariables,
|
import { updateCSSVariables as executeUpdateCSSVariables } from '@vben-core/shared/utils';
|
||||||
generatorColorVariables,
|
|
||||||
} from '@vben-core/shared';
|
|
||||||
|
|
||||||
import { BUILT_IN_THEME_PRESETS } from './constants';
|
import { BUILT_IN_THEME_PRESETS } from './constants';
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
|
||||||
import { diff } from '@vben-core/shared';
|
import { diff } from '@vben-core/shared/utils';
|
||||||
|
|
||||||
import { preferencesManager } from './preferences';
|
import { preferencesManager } from './preferences';
|
||||||
import { isDarkTheme } from './update-css-variables';
|
import { isDarkTheme } from './update-css-variables';
|
||||||
|
21
packages/@core/ui-kit/form-ui/build.config.ts
Normal file
21
packages/@core/ui-kit/form-ui/build.config.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { defineBuildConfig } from 'unbuild';
|
||||||
|
|
||||||
|
export default defineBuildConfig({
|
||||||
|
clean: true,
|
||||||
|
declaration: true,
|
||||||
|
entries: [
|
||||||
|
{
|
||||||
|
builder: 'mkdist',
|
||||||
|
input: './src',
|
||||||
|
loaders: ['vue'],
|
||||||
|
pattern: ['**/*.vue'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
builder: 'mkdist',
|
||||||
|
format: 'esm',
|
||||||
|
input: './src',
|
||||||
|
loaders: ['js'],
|
||||||
|
pattern: ['**/*.ts'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
50
packages/@core/ui-kit/form-ui/package.json
Normal file
50
packages/@core/ui-kit/form-ui/package.json
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"name": "@vben-core/form-ui",
|
||||||
|
"version": "5.2.1",
|
||||||
|
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||||
|
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/vbenjs/vue-vben-admin.git",
|
||||||
|
"directory": "packages/@vben-core/uikit/form-ui"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "pnpm unbuild",
|
||||||
|
"prepublishOnly": "npm run build"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"sideEffects": [
|
||||||
|
"**/*.css"
|
||||||
|
],
|
||||||
|
"main": "./dist/index.mjs",
|
||||||
|
"module": "./dist/index.mjs",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./src/index.ts",
|
||||||
|
"development": "./src/index.ts",
|
||||||
|
"default": "./dist/index.mjs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"default": "./dist/index.mjs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@vben-core/composables": "workspace:*",
|
||||||
|
"@vben-core/shadcn-ui": "workspace:*",
|
||||||
|
"@vben-core/shared": "workspace:*",
|
||||||
|
"@vee-validate/zod": "^4.13.2",
|
||||||
|
"@vueuse/core": "^11.0.3",
|
||||||
|
"vee-validate": "^4.13.2",
|
||||||
|
"vue": "^3.5.3",
|
||||||
|
"zod": "^3.23.8",
|
||||||
|
"zod-defaults": "^0.1.3"
|
||||||
|
}
|
||||||
|
}
|
1
packages/@core/ui-kit/form-ui/postcss.config.mjs
Normal file
1
packages/@core/ui-kit/form-ui/postcss.config.mjs
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default } from '@vben/tailwind-config/postcss';
|
103
packages/@core/ui-kit/form-ui/src/components/form-actions.vue
Normal file
103
packages/@core/ui-kit/form-ui/src/components/form-actions.vue
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, toRaw, unref } from 'vue';
|
||||||
|
|
||||||
|
import { useSimpleLocale } from '@vben-core/composables';
|
||||||
|
import { VbenExpandableArrow } from '@vben-core/shadcn-ui';
|
||||||
|
import { cn, isFunction } from '@vben-core/shared/utils';
|
||||||
|
|
||||||
|
import { COMPONENT_MAP } from '../config';
|
||||||
|
import { injectFormProps } from '../use-form-context';
|
||||||
|
|
||||||
|
const { $t } = useSimpleLocale();
|
||||||
|
|
||||||
|
const [rootProps, form] = injectFormProps();
|
||||||
|
|
||||||
|
const collapsed = defineModel({ default: false });
|
||||||
|
|
||||||
|
const resetButtonOptions = computed(() => {
|
||||||
|
return {
|
||||||
|
show: true,
|
||||||
|
text: `${$t.value('reset')}`,
|
||||||
|
...unref(rootProps).resetButtonOptions,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const submitButtonOptions = computed(() => {
|
||||||
|
return {
|
||||||
|
show: true,
|
||||||
|
text: `${$t.value('submit')}`,
|
||||||
|
...unref(rootProps).submitButtonOptions,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const isQueryForm = computed(() => {
|
||||||
|
return !!unref(rootProps).showCollapseButton;
|
||||||
|
});
|
||||||
|
|
||||||
|
const queryFormStyle = computed(() => {
|
||||||
|
if (isQueryForm.value) {
|
||||||
|
return {
|
||||||
|
'grid-column': `-2 / -1`,
|
||||||
|
marginLeft: 'auto',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
});
|
||||||
|
|
||||||
|
async function handleSubmit(e: Event) {
|
||||||
|
e?.preventDefault();
|
||||||
|
e?.stopPropagation();
|
||||||
|
const { valid } = await form.validate();
|
||||||
|
if (!valid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await unref(rootProps).handleSubmit?.(toRaw(form.values));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleReset(e: Event) {
|
||||||
|
e?.preventDefault();
|
||||||
|
e?.stopPropagation();
|
||||||
|
const props = unref(rootProps);
|
||||||
|
if (isFunction(props.handleReset)) {
|
||||||
|
await props.handleReset?.(form.values);
|
||||||
|
} else {
|
||||||
|
form.resetForm();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
:class="cn('col-span-full w-full text-right', rootProps.actionWrapperClass)"
|
||||||
|
:style="queryFormStyle"
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
:is="COMPONENT_MAP.DefaultResetActionButton"
|
||||||
|
v-if="resetButtonOptions.show"
|
||||||
|
class="mr-3"
|
||||||
|
type="button"
|
||||||
|
@click="handleReset"
|
||||||
|
v-bind="resetButtonOptions"
|
||||||
|
>
|
||||||
|
{{ resetButtonOptions.text }}
|
||||||
|
</component>
|
||||||
|
|
||||||
|
<component
|
||||||
|
:is="COMPONENT_MAP.DefaultSubmitActionButton"
|
||||||
|
v-if="submitButtonOptions.show"
|
||||||
|
type="button"
|
||||||
|
@click="handleSubmit"
|
||||||
|
v-bind="submitButtonOptions"
|
||||||
|
>
|
||||||
|
{{ submitButtonOptions.text }}
|
||||||
|
</component>
|
||||||
|
|
||||||
|
<VbenExpandableArrow
|
||||||
|
v-if="rootProps.showCollapseButton"
|
||||||
|
v-model:model-value="collapsed"
|
||||||
|
class="ml-2"
|
||||||
|
>
|
||||||
|
<span>{{ collapsed ? $t('expand') : $t('collapse') }}</span>
|
||||||
|
</VbenExpandableArrow>
|
||||||
|
</div>
|
||||||
|
</template>
|
65
packages/@core/ui-kit/form-ui/src/config.ts
Normal file
65
packages/@core/ui-kit/form-ui/src/config.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import type { BaseFormComponentType, VbenFormAdapterOptions } from './types';
|
||||||
|
|
||||||
|
import type { Component } from 'vue';
|
||||||
|
import { h } from 'vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
VbenButton,
|
||||||
|
VbenCheckbox,
|
||||||
|
Input as VbenInput,
|
||||||
|
VbenInputPassword,
|
||||||
|
VbenPinInput,
|
||||||
|
VbenSelect,
|
||||||
|
} from '@vben-core/shadcn-ui';
|
||||||
|
|
||||||
|
import { defineRule } from 'vee-validate';
|
||||||
|
|
||||||
|
const DEFAULT_MODEL_PROP_NAME = 'modelValue';
|
||||||
|
|
||||||
|
export const COMPONENT_MAP: Record<BaseFormComponentType, Component> = {
|
||||||
|
DefaultResetActionButton: h(VbenButton, { size: 'sm', variant: 'outline' }),
|
||||||
|
DefaultSubmitActionButton: h(VbenButton, { size: 'sm', variant: 'default' }),
|
||||||
|
VbenCheckbox,
|
||||||
|
VbenInput,
|
||||||
|
VbenInputPassword,
|
||||||
|
VbenPinInput,
|
||||||
|
VbenSelect,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const COMPONENT_BIND_EVENT_MAP: Partial<
|
||||||
|
Record<BaseFormComponentType, string>
|
||||||
|
> = {
|
||||||
|
VbenCheckbox: 'checked',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function setupVbenForm<
|
||||||
|
T extends BaseFormComponentType = BaseFormComponentType,
|
||||||
|
>(options: VbenFormAdapterOptions<T>) {
|
||||||
|
const { components, config, defineRules } = options;
|
||||||
|
|
||||||
|
if (defineRules) {
|
||||||
|
for (const key of Object.keys(defineRules)) {
|
||||||
|
defineRule(key, defineRules[key as never]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseModelPropName =
|
||||||
|
config?.baseModelPropName ?? DEFAULT_MODEL_PROP_NAME;
|
||||||
|
const modelPropNameMap = config?.modelPropNameMap as
|
||||||
|
| Record<BaseFormComponentType, string>
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
for (const component of Object.keys(components)) {
|
||||||
|
const key = component as BaseFormComponentType;
|
||||||
|
COMPONENT_MAP[key] = components[component as never];
|
||||||
|
|
||||||
|
if (baseModelPropName !== DEFAULT_MODEL_PROP_NAME) {
|
||||||
|
COMPONENT_BIND_EVENT_MAP[key] = baseModelPropName;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 覆盖特殊组件的modelPropName
|
||||||
|
if (modelPropNameMap && modelPropNameMap[key]) {
|
||||||
|
COMPONENT_BIND_EVENT_MAP[key] = modelPropNameMap[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
175
packages/@core/ui-kit/form-ui/src/form-api.ts
Normal file
175
packages/@core/ui-kit/form-ui/src/form-api.ts
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
import type {
|
||||||
|
FormState,
|
||||||
|
GenericObject,
|
||||||
|
ResetFormOpts,
|
||||||
|
ValidationOptions,
|
||||||
|
} from 'vee-validate';
|
||||||
|
|
||||||
|
import type { FormActions, VbenFormProps } from './types';
|
||||||
|
|
||||||
|
import { toRaw } from 'vue';
|
||||||
|
|
||||||
|
import { Store } from '@vben-core/shared/store';
|
||||||
|
import { bindMethods, isFunction, StateHandler } from '@vben-core/shared/utils';
|
||||||
|
|
||||||
|
function getDefaultState(): VbenFormProps {
|
||||||
|
return {
|
||||||
|
actionWrapperClass: '',
|
||||||
|
collapsed: false,
|
||||||
|
collapsedRows: 1,
|
||||||
|
commonConfig: {},
|
||||||
|
handleReset: undefined,
|
||||||
|
handleSubmit: undefined,
|
||||||
|
layout: 'horizontal',
|
||||||
|
resetButtonOptions: {},
|
||||||
|
schema: [],
|
||||||
|
showCollapseButton: false,
|
||||||
|
showDefaultActions: true,
|
||||||
|
submitButtonOptions: {},
|
||||||
|
wrapperClass: 'grid-cols-1',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FormApi {
|
||||||
|
// private prevState!: ModalState;
|
||||||
|
private state: null | VbenFormProps = null;
|
||||||
|
// private api: Pick<VbenFormProps, 'handleReset' | 'handleSubmit'>;
|
||||||
|
public form = {} as FormActions;
|
||||||
|
|
||||||
|
isMounted = false;
|
||||||
|
|
||||||
|
stateHandler: StateHandler;
|
||||||
|
|
||||||
|
public store: Store<VbenFormProps>;
|
||||||
|
|
||||||
|
constructor(options: VbenFormProps = {}) {
|
||||||
|
const { ...storeState } = options;
|
||||||
|
|
||||||
|
const defaultState = getDefaultState();
|
||||||
|
|
||||||
|
this.store = new Store<VbenFormProps>(
|
||||||
|
{
|
||||||
|
...defaultState,
|
||||||
|
...storeState,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onUpdate: () => {
|
||||||
|
this.state = this.store.state;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
this.state = this.store.state;
|
||||||
|
this.stateHandler = new StateHandler();
|
||||||
|
bindMethods(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getForm() {
|
||||||
|
if (!this.isMounted) {
|
||||||
|
// 等待form挂载
|
||||||
|
await this.stateHandler.waitForCondition();
|
||||||
|
}
|
||||||
|
if (!this.form?.meta) {
|
||||||
|
throw new Error('<VbenForm /> is not mounted');
|
||||||
|
}
|
||||||
|
return this.form;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果需要多次更新状态,可以使用 batch 方法
|
||||||
|
batchStore(cb: () => void) {
|
||||||
|
this.store.batch(cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getValues() {
|
||||||
|
const form = await this.getForm();
|
||||||
|
return form.values;
|
||||||
|
}
|
||||||
|
|
||||||
|
mount(formActions: FormActions) {
|
||||||
|
if (!this.isMounted) {
|
||||||
|
Object.assign(this.form, formActions);
|
||||||
|
this.stateHandler.setConditionTrue();
|
||||||
|
this.isMounted = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据字段名移除表单项
|
||||||
|
* @param fields
|
||||||
|
*/
|
||||||
|
async removeSchemaByFields(fields: string[]) {
|
||||||
|
const fieldSet = new Set(fields);
|
||||||
|
const schema = this.state?.schema ?? [];
|
||||||
|
|
||||||
|
const filterSchema = schema.filter((item) => fieldSet.has(item.fieldName));
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
schema: filterSchema,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置表单
|
||||||
|
*/
|
||||||
|
async resetForm(
|
||||||
|
state?: Partial<FormState<GenericObject>> | undefined,
|
||||||
|
opts?: Partial<ResetFormOpts>,
|
||||||
|
) {
|
||||||
|
const form = await this.getForm();
|
||||||
|
return form.resetForm(state, opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
async resetValidate() {
|
||||||
|
const form = await this.getForm();
|
||||||
|
const fields = Object.keys(form.errors.value);
|
||||||
|
fields.forEach((field) => {
|
||||||
|
form.setFieldError(field, undefined);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async setFieldValue(field: string, value: any, shouldValidate?: boolean) {
|
||||||
|
const form = await this.getForm();
|
||||||
|
form.setFieldValue(field, value, shouldValidate);
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(
|
||||||
|
stateOrFn:
|
||||||
|
| ((prev: VbenFormProps) => Partial<VbenFormProps>)
|
||||||
|
| Partial<VbenFormProps>,
|
||||||
|
) {
|
||||||
|
if (isFunction(stateOrFn)) {
|
||||||
|
this.store.setState(stateOrFn as (prev: VbenFormProps) => VbenFormProps);
|
||||||
|
} else {
|
||||||
|
this.store.setState((prev) => ({ ...prev, ...stateOrFn }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async setValues(
|
||||||
|
fields: Record<string, any>,
|
||||||
|
shouldValidate: boolean = false,
|
||||||
|
) {
|
||||||
|
const form = await this.getForm();
|
||||||
|
form.setValues(fields, shouldValidate);
|
||||||
|
}
|
||||||
|
|
||||||
|
async submitForm(e?: Event) {
|
||||||
|
e?.preventDefault();
|
||||||
|
e?.stopPropagation();
|
||||||
|
const form = await this.getForm();
|
||||||
|
await form.submitForm();
|
||||||
|
const rawValues = toRaw(form.values || {});
|
||||||
|
await this.state?.handleSubmit?.(rawValues);
|
||||||
|
return rawValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
unmounted() {
|
||||||
|
this.state = null;
|
||||||
|
this.isMounted = false;
|
||||||
|
this.stateHandler.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
async validate(opts?: Partial<ValidationOptions>) {
|
||||||
|
const form = await this.getForm();
|
||||||
|
return await form.validate(opts);
|
||||||
|
}
|
||||||
|
}
|
24
packages/@core/ui-kit/form-ui/src/form-render/context.ts
Normal file
24
packages/@core/ui-kit/form-ui/src/form-render/context.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import type { FormRenderProps } from '../types';
|
||||||
|
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
import { createContext } from '@vben-core/shadcn-ui';
|
||||||
|
|
||||||
|
export const [injectRenderFormProps, provideFormRenderProps] =
|
||||||
|
createContext<FormRenderProps>('FormRenderProps');
|
||||||
|
|
||||||
|
export const useFormContext = () => {
|
||||||
|
const formRenderProps = injectRenderFormProps();
|
||||||
|
|
||||||
|
const isVertical = computed(() => formRenderProps.layout === 'vertical');
|
||||||
|
|
||||||
|
const componentMap = computed(() => formRenderProps.componentMap);
|
||||||
|
const componentBindEventMap = computed(
|
||||||
|
() => formRenderProps.componentBindEventMap,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
componentBindEventMap,
|
||||||
|
componentMap,
|
||||||
|
isVertical,
|
||||||
|
};
|
||||||
|
};
|
116
packages/@core/ui-kit/form-ui/src/form-render/dependencies.ts
Normal file
116
packages/@core/ui-kit/form-ui/src/form-render/dependencies.ts
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import type {
|
||||||
|
FormItemDependencies,
|
||||||
|
FormSchemaRuleType,
|
||||||
|
MaybeComponentProps,
|
||||||
|
} from '../types';
|
||||||
|
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { isFunction } from '@vben-core/shared/utils';
|
||||||
|
|
||||||
|
import { useFormValues } from 'vee-validate';
|
||||||
|
|
||||||
|
import { injectRenderFormProps } from './context';
|
||||||
|
|
||||||
|
export default function useDependencies(
|
||||||
|
getDependencies: () => FormItemDependencies | undefined,
|
||||||
|
) {
|
||||||
|
const values = useFormValues();
|
||||||
|
|
||||||
|
const formRenderProps = injectRenderFormProps();
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
const formApi = formRenderProps.form!;
|
||||||
|
|
||||||
|
if (!values) {
|
||||||
|
throw new Error('useDependencies should be used within <VbenForm>');
|
||||||
|
}
|
||||||
|
|
||||||
|
const isIf = ref(true);
|
||||||
|
const isDisabled = ref(false);
|
||||||
|
const isShow = ref(true);
|
||||||
|
const isRequired = ref(false);
|
||||||
|
const dynamicComponentProps = ref<MaybeComponentProps>({});
|
||||||
|
const dynamicRules = ref<FormSchemaRuleType>();
|
||||||
|
|
||||||
|
const triggerFieldValues = computed(() => {
|
||||||
|
// 该字段可能会被多个字段触发
|
||||||
|
const triggerFields = getDependencies()?.triggerFields ?? [];
|
||||||
|
return triggerFields.map((dep) => {
|
||||||
|
return values.value[dep];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const resetConditionState = () => {
|
||||||
|
isDisabled.value = false;
|
||||||
|
isIf.value = true;
|
||||||
|
isShow.value = true;
|
||||||
|
isRequired.value = false;
|
||||||
|
dynamicRules.value = undefined;
|
||||||
|
dynamicComponentProps.value = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
[triggerFieldValues, getDependencies],
|
||||||
|
async ([_values, dependencies]) => {
|
||||||
|
if (!dependencies || !dependencies?.triggerFields?.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resetConditionState();
|
||||||
|
const {
|
||||||
|
componentProps,
|
||||||
|
disabled,
|
||||||
|
if: whenIf,
|
||||||
|
required,
|
||||||
|
rules,
|
||||||
|
show,
|
||||||
|
trigger,
|
||||||
|
} = dependencies;
|
||||||
|
|
||||||
|
// 1. 优先判断if,如果if为false,则不渲染dom,后续判断也不再执行
|
||||||
|
const formValues = values.value;
|
||||||
|
|
||||||
|
if (isFunction(whenIf)) {
|
||||||
|
isIf.value = !!(await whenIf(formValues, formApi));
|
||||||
|
// 不渲染
|
||||||
|
if (!isIf.value) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 判断show,如果show为false,则隐藏
|
||||||
|
if (isFunction(show)) {
|
||||||
|
isShow.value = !!(await show(formValues, formApi));
|
||||||
|
if (!isShow.value) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFunction(componentProps)) {
|
||||||
|
dynamicComponentProps.value = await componentProps(formValues, formApi);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFunction(rules)) {
|
||||||
|
dynamicRules.value = await rules(formValues, formApi);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFunction(disabled)) {
|
||||||
|
isDisabled.value = !!(await disabled(formValues, formApi));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFunction(required)) {
|
||||||
|
isRequired.value = !!(await required(formValues, formApi));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFunction(trigger)) {
|
||||||
|
await trigger(formValues, formApi);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: true, immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
dynamicComponentProps,
|
||||||
|
dynamicRules,
|
||||||
|
isDisabled,
|
||||||
|
isIf,
|
||||||
|
isRequired,
|
||||||
|
isShow,
|
||||||
|
};
|
||||||
|
}
|
97
packages/@core/ui-kit/form-ui/src/form-render/expandable.ts
Normal file
97
packages/@core/ui-kit/form-ui/src/form-render/expandable.ts
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import type { FormRenderProps } from '../types';
|
||||||
|
|
||||||
|
import { computed, nextTick, onMounted, ref, useTemplateRef, watch } from 'vue';
|
||||||
|
|
||||||
|
import { breakpointsTailwind, useBreakpoints } from '@vueuse/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 动态计算行数
|
||||||
|
*/
|
||||||
|
export function useExpandable(props: FormRenderProps) {
|
||||||
|
const wrapperRef = useTemplateRef<HTMLElement>('wrapperRef');
|
||||||
|
const rowMapping = ref<Record<number, number>>({});
|
||||||
|
// 是否已经计算过一次
|
||||||
|
const isCalculated = ref(false);
|
||||||
|
|
||||||
|
const breakpoints = useBreakpoints(breakpointsTailwind);
|
||||||
|
|
||||||
|
const keepFormItemIndex = computed(() => {
|
||||||
|
const rows = props.collapsedRows ?? 1;
|
||||||
|
const mapping = rowMapping.value;
|
||||||
|
let maxItem = 0;
|
||||||
|
for (let index = 1; index <= rows; index++) {
|
||||||
|
maxItem += mapping?.[index] ?? 0;
|
||||||
|
}
|
||||||
|
return maxItem - 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
[
|
||||||
|
() => props.showCollapseButton,
|
||||||
|
() => breakpoints.active().value,
|
||||||
|
() => props.schema?.length,
|
||||||
|
],
|
||||||
|
async ([val]) => {
|
||||||
|
if (val) {
|
||||||
|
await nextTick();
|
||||||
|
rowMapping.value = {};
|
||||||
|
await calculateRowMapping();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
async function calculateRowMapping() {
|
||||||
|
if (!props.showCollapseButton) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
if (!wrapperRef.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 小屏幕不计算
|
||||||
|
if (breakpoints.smaller('sm').value) {
|
||||||
|
// 保持一行
|
||||||
|
rowMapping.value = { 1: 2 };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formItems = [...wrapperRef.value.children];
|
||||||
|
|
||||||
|
const container = wrapperRef.value;
|
||||||
|
const containerStyles = window.getComputedStyle(container);
|
||||||
|
const rowHeights = containerStyles
|
||||||
|
.getPropertyValue('grid-template-rows')
|
||||||
|
.split(' ');
|
||||||
|
|
||||||
|
const containerRect = container?.getBoundingClientRect();
|
||||||
|
|
||||||
|
formItems.forEach((el) => {
|
||||||
|
const itemRect = el.getBoundingClientRect();
|
||||||
|
|
||||||
|
// 计算元素在第几行
|
||||||
|
const itemTop = itemRect.top - containerRect.top;
|
||||||
|
let rowStart = 0;
|
||||||
|
let cumulativeHeight = 0;
|
||||||
|
|
||||||
|
for (const [i, rowHeight] of rowHeights.entries()) {
|
||||||
|
cumulativeHeight += Number.parseFloat(rowHeight);
|
||||||
|
if (itemTop < cumulativeHeight) {
|
||||||
|
rowStart = i + 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (rowStart > (props?.collapsedRows ?? 1)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
rowMapping.value[rowStart] = (rowMapping.value[rowStart] ?? 0) + 1;
|
||||||
|
isCalculated.value = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
calculateRowMapping();
|
||||||
|
});
|
||||||
|
|
||||||
|
return { isCalculated, keepFormItemIndex, wrapperRef };
|
||||||
|
}
|
283
packages/@core/ui-kit/form-ui/src/form-render/form-field.vue
Normal file
283
packages/@core/ui-kit/form-ui/src/form-render/form-field.vue
Normal file
@ -0,0 +1,283 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { ZodType } from 'zod';
|
||||||
|
|
||||||
|
import type { FormSchema } from '../types';
|
||||||
|
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormMessage,
|
||||||
|
VbenRenderContent,
|
||||||
|
} from '@vben-core/shadcn-ui';
|
||||||
|
import { cn, isFunction, isObject, isString } from '@vben-core/shared/utils';
|
||||||
|
|
||||||
|
import { toTypedSchema } from '@vee-validate/zod';
|
||||||
|
import { useFormValues } from 'vee-validate';
|
||||||
|
|
||||||
|
import { injectRenderFormProps, useFormContext } from './context';
|
||||||
|
import useDependencies from './dependencies';
|
||||||
|
import FormLabel from './form-label.vue';
|
||||||
|
import { isEventObjectLike } from './helper';
|
||||||
|
|
||||||
|
interface Props extends FormSchema {}
|
||||||
|
|
||||||
|
const {
|
||||||
|
component,
|
||||||
|
componentProps,
|
||||||
|
dependencies,
|
||||||
|
description,
|
||||||
|
disabled,
|
||||||
|
fieldName,
|
||||||
|
formFieldProps,
|
||||||
|
label,
|
||||||
|
labelClass,
|
||||||
|
labelWidth,
|
||||||
|
renderComponentContent,
|
||||||
|
rules,
|
||||||
|
} = defineProps<Props>();
|
||||||
|
|
||||||
|
const { componentBindEventMap, componentMap, isVertical } = useFormContext();
|
||||||
|
const formRenderProps = injectRenderFormProps();
|
||||||
|
const values = useFormValues();
|
||||||
|
const formApi = formRenderProps.form;
|
||||||
|
|
||||||
|
const fieldComponent = computed(() => {
|
||||||
|
const finalComponent = isString(component)
|
||||||
|
? componentMap.value[component]
|
||||||
|
: component;
|
||||||
|
if (!finalComponent) {
|
||||||
|
// 组件未注册
|
||||||
|
console.warn(`Component ${component} is not registered`);
|
||||||
|
}
|
||||||
|
return finalComponent;
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
dynamicComponentProps,
|
||||||
|
dynamicRules,
|
||||||
|
isDisabled,
|
||||||
|
isIf,
|
||||||
|
isRequired,
|
||||||
|
isShow,
|
||||||
|
} = useDependencies(() => dependencies);
|
||||||
|
|
||||||
|
const labelStyle = computed(() => {
|
||||||
|
return labelClass?.includes('w-') || isVertical.value
|
||||||
|
? {}
|
||||||
|
: {
|
||||||
|
width: `${labelWidth}px`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentRules = computed(() => {
|
||||||
|
return dynamicRules.value || rules;
|
||||||
|
});
|
||||||
|
|
||||||
|
const shouldRequired = computed(() => {
|
||||||
|
if (!currentRules.value) {
|
||||||
|
return isRequired.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRequired.value) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isString(currentRules.value)) {
|
||||||
|
return currentRules.value === 'required';
|
||||||
|
}
|
||||||
|
|
||||||
|
let isOptional = currentRules?.value?.isOptional?.();
|
||||||
|
|
||||||
|
// 如果有设置默认值,则不是必填,需要特殊处理
|
||||||
|
const typeName = currentRules?.value?._def?.typeName;
|
||||||
|
if (typeName === 'ZodDefault') {
|
||||||
|
const innerType = currentRules?.value?._def.innerType;
|
||||||
|
if (innerType) {
|
||||||
|
isOptional = innerType.isOptional?.();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return !isOptional;
|
||||||
|
});
|
||||||
|
|
||||||
|
const fieldRules = computed(() => {
|
||||||
|
let rules = currentRules.value;
|
||||||
|
if (!rules) {
|
||||||
|
return isRequired.value ? 'required' : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isString(rules)) {
|
||||||
|
return rules;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isOptional = !shouldRequired.value;
|
||||||
|
if (!isOptional) {
|
||||||
|
const unwrappedRules = (rules as any)?.unwrap?.();
|
||||||
|
if (unwrappedRules) {
|
||||||
|
rules = unwrappedRules;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return toTypedSchema(rules as ZodType);
|
||||||
|
});
|
||||||
|
|
||||||
|
const computedProps = computed(() => {
|
||||||
|
const finalComponentProps = isFunction(componentProps)
|
||||||
|
? componentProps(values.value, formApi!)
|
||||||
|
: componentProps;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...finalComponentProps,
|
||||||
|
...dynamicComponentProps.value,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const shouldDisabled = computed(() => {
|
||||||
|
return isDisabled.value || disabled || computedProps.value?.disabled;
|
||||||
|
});
|
||||||
|
|
||||||
|
const customContentRender = computed(() => {
|
||||||
|
if (!isFunction(renderComponentContent)) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return renderComponentContent(values.value, formApi!);
|
||||||
|
});
|
||||||
|
|
||||||
|
const renderContentKey = computed(() => {
|
||||||
|
return Object.keys(customContentRender.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const fieldProps = computed(() => {
|
||||||
|
const rules = fieldRules.value;
|
||||||
|
return {
|
||||||
|
keepValue: true,
|
||||||
|
label,
|
||||||
|
...(rules ? { rules } : {}),
|
||||||
|
...formFieldProps,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
function fieldBindEvent(slotProps: Record<string, any>) {
|
||||||
|
const modelValue = slotProps.componentField.modelValue;
|
||||||
|
const handler = slotProps.componentField['onUpdate:modelValue'];
|
||||||
|
|
||||||
|
const bindEventField = isString(component)
|
||||||
|
? componentBindEventMap.value?.[component]
|
||||||
|
: null;
|
||||||
|
|
||||||
|
let value = modelValue;
|
||||||
|
// antd design 的一些组件会传递一个 event 对象
|
||||||
|
if (modelValue && isObject(modelValue) && bindEventField) {
|
||||||
|
value = isEventObjectLike(modelValue)
|
||||||
|
? modelValue?.target?.[bindEventField]
|
||||||
|
: modelValue;
|
||||||
|
}
|
||||||
|
if (bindEventField) {
|
||||||
|
return {
|
||||||
|
[`onUpdate:${bindEventField}`]: handler,
|
||||||
|
[bindEventField]: value,
|
||||||
|
onChange: (e: Record<string, any>) => {
|
||||||
|
const shouldUnwrap = isEventObjectLike(e);
|
||||||
|
const onChange = slotProps?.componentField?.onChange;
|
||||||
|
if (!shouldUnwrap) {
|
||||||
|
return onChange?.(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return onChange?.(e?.target?.[bindEventField] ?? e);
|
||||||
|
},
|
||||||
|
onInput: () => {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createComponentProps(slotProps: Record<string, any>) {
|
||||||
|
const bindEvents = fieldBindEvent(slotProps);
|
||||||
|
|
||||||
|
const binds = {
|
||||||
|
...slotProps.componentField,
|
||||||
|
...computedProps.value,
|
||||||
|
...bindEvents,
|
||||||
|
};
|
||||||
|
|
||||||
|
return binds;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<FormField
|
||||||
|
v-if="isIf"
|
||||||
|
v-bind="fieldProps"
|
||||||
|
v-slot="slotProps"
|
||||||
|
:name="fieldName"
|
||||||
|
>
|
||||||
|
<FormItem
|
||||||
|
v-show="isShow"
|
||||||
|
:class="{
|
||||||
|
'flex-col': isVertical,
|
||||||
|
'flex-row items-center': !isVertical,
|
||||||
|
}"
|
||||||
|
class="flex pb-6"
|
||||||
|
v-bind="$attrs"
|
||||||
|
>
|
||||||
|
<FormLabel
|
||||||
|
v-if="!hideLabel"
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'flex leading-6',
|
||||||
|
{
|
||||||
|
'mr-2 flex-shrink-0': !isVertical,
|
||||||
|
'flex-row': isVertical,
|
||||||
|
},
|
||||||
|
!isVertical && labelClass,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
:help="help"
|
||||||
|
:required="shouldRequired && !hideRequiredMark"
|
||||||
|
:style="labelStyle"
|
||||||
|
>
|
||||||
|
{{ label }}
|
||||||
|
</FormLabel>
|
||||||
|
<div :class="cn('relative flex w-full items-center', wrapperClass)">
|
||||||
|
<FormControl :class="cn(controlClass)">
|
||||||
|
<slot
|
||||||
|
v-bind="{
|
||||||
|
...slotProps,
|
||||||
|
...createComponentProps(slotProps),
|
||||||
|
disabled: shouldDisabled,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
:is="fieldComponent"
|
||||||
|
v-bind="createComponentProps(slotProps)"
|
||||||
|
:disabled="shouldDisabled"
|
||||||
|
>
|
||||||
|
<template v-for="name in renderContentKey" :key="name" #[name]>
|
||||||
|
<VbenRenderContent
|
||||||
|
:content="customContentRender[name]"
|
||||||
|
v-bind="slotProps"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<!-- <slot></slot> -->
|
||||||
|
</component>
|
||||||
|
</slot>
|
||||||
|
</FormControl>
|
||||||
|
<!-- 自定义后缀 -->
|
||||||
|
<div v-if="suffix" class="ml-1">
|
||||||
|
<VbenRenderContent :content="suffix" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormDescription v-if="description">
|
||||||
|
<VbenRenderContent :content="description" />
|
||||||
|
</FormDescription>
|
||||||
|
|
||||||
|
<Transition name="slide-up">
|
||||||
|
<FormMessage class="absolute -bottom-[22px]" />
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
</template>
|
20
packages/@core/ui-kit/form-ui/src/form-render/form-label.vue
Normal file
20
packages/@core/ui-kit/form-ui/src/form-render/form-label.vue
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { FormLabel, VbenHelpTooltip } from '@vben-core/shadcn-ui';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
help?: string;
|
||||||
|
required?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<Props>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<FormLabel class="flex flex-row-reverse items-center">
|
||||||
|
<VbenHelpTooltip v-if="help" trigger-class="size-3.5 ml-1">
|
||||||
|
{{ help }}
|
||||||
|
</VbenHelpTooltip>
|
||||||
|
<slot></slot>
|
||||||
|
<span v-if="required" class="text-destructive mr-[2px]">*</span>
|
||||||
|
</FormLabel>
|
||||||
|
</template>
|
140
packages/@core/ui-kit/form-ui/src/form-render/form.vue
Normal file
140
packages/@core/ui-kit/form-ui/src/form-render/form.vue
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { GenericObject } from 'vee-validate';
|
||||||
|
import type { ZodTypeAny } from 'zod';
|
||||||
|
|
||||||
|
import type { FormRenderProps, FormSchema, FormShape } from '../types';
|
||||||
|
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
import { Form } from '@vben-core/shadcn-ui';
|
||||||
|
import { cn, isString, merge } from '@vben-core/shared/utils';
|
||||||
|
|
||||||
|
import { provideFormRenderProps } from './context';
|
||||||
|
import { useExpandable } from './expandable';
|
||||||
|
import FormField from './form-field.vue';
|
||||||
|
import { getBaseRules, getDefaultValueInZodStack } from './helper';
|
||||||
|
|
||||||
|
interface Props extends FormRenderProps {}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
collapsedRows: 1,
|
||||||
|
commonConfig: () => ({}),
|
||||||
|
showCollapseButton: false,
|
||||||
|
wrapperClass: 'grid-cols-1 sm:grid-cols-2 md:grid-cols-3',
|
||||||
|
});
|
||||||
|
|
||||||
|
const emits = defineEmits<{
|
||||||
|
submit: [event: any];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
provideFormRenderProps(props);
|
||||||
|
|
||||||
|
const { isCalculated, keepFormItemIndex, wrapperRef } = useExpandable(props);
|
||||||
|
|
||||||
|
const shapes = computed(() => {
|
||||||
|
const resultShapes: FormShape[] = [];
|
||||||
|
props.schema?.forEach((schema) => {
|
||||||
|
const { fieldName } = schema;
|
||||||
|
const rules = schema.rules as ZodTypeAny;
|
||||||
|
|
||||||
|
let typeName = '';
|
||||||
|
if (rules && !isString(rules)) {
|
||||||
|
typeName = rules._def.typeName;
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseRules = getBaseRules(rules) as ZodTypeAny;
|
||||||
|
|
||||||
|
resultShapes.push({
|
||||||
|
default: getDefaultValueInZodStack(rules),
|
||||||
|
fieldName,
|
||||||
|
required: !['ZodNullable', 'ZodOptional'].includes(typeName),
|
||||||
|
rules: baseRules,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return resultShapes;
|
||||||
|
});
|
||||||
|
|
||||||
|
const formComponent = computed(() => (props.form ? 'form' : Form));
|
||||||
|
|
||||||
|
const formComponentProps = computed(() => {
|
||||||
|
return props.form
|
||||||
|
? {
|
||||||
|
onSubmit: props.form.handleSubmit((val) => emits('submit', val)),
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
onSubmit: (val: GenericObject) => emits('submit', val),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const formCollapsed = computed(() => {
|
||||||
|
return props.collapsed && isCalculated.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
const computedSchema = computed((): FormSchema[] => {
|
||||||
|
const {
|
||||||
|
componentProps = {},
|
||||||
|
controlClass = '',
|
||||||
|
disabled,
|
||||||
|
formFieldProps = {},
|
||||||
|
formItemClass = '',
|
||||||
|
hideLabel = false,
|
||||||
|
hideRequiredMark = false,
|
||||||
|
labelClass = '',
|
||||||
|
labelWidth = 100,
|
||||||
|
wrapperClass = '',
|
||||||
|
} = props.commonConfig;
|
||||||
|
return (props.schema || []).map((schema, index): FormSchema => {
|
||||||
|
const keepIndex = keepFormItemIndex.value;
|
||||||
|
|
||||||
|
const hidden =
|
||||||
|
// 折叠状态 & 显示折叠按钮 & 当前索引大于保留索引
|
||||||
|
props.showCollapseButton && !!formCollapsed.value && keepIndex
|
||||||
|
? keepIndex <= index
|
||||||
|
: false;
|
||||||
|
|
||||||
|
return {
|
||||||
|
disabled,
|
||||||
|
hideLabel,
|
||||||
|
hideRequiredMark,
|
||||||
|
labelWidth,
|
||||||
|
wrapperClass,
|
||||||
|
...schema,
|
||||||
|
componentProps: merge({}, schema.componentProps, componentProps),
|
||||||
|
controlClass: cn(controlClass, schema.controlClass),
|
||||||
|
formFieldProps: {
|
||||||
|
...formFieldProps,
|
||||||
|
...schema.formFieldProps,
|
||||||
|
},
|
||||||
|
formItemClass: cn(
|
||||||
|
'flex-shrink-0',
|
||||||
|
{ hidden },
|
||||||
|
formItemClass,
|
||||||
|
schema.formItemClass,
|
||||||
|
),
|
||||||
|
labelClass: cn(labelClass, schema.labelClass),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<component :is="formComponent" v-bind="formComponentProps">
|
||||||
|
<div ref="wrapperRef" :class="wrapperClass" class="grid">
|
||||||
|
<template v-for="cSchema in computedSchema" :key="cSchema.fieldName">
|
||||||
|
<!-- <div v-if="$slots[cSchema.fieldName]" :class="cSchema.formItemClass">
|
||||||
|
<slot :definition="cSchema" :name="cSchema.fieldName"> </slot>
|
||||||
|
</div> -->
|
||||||
|
<FormField
|
||||||
|
v-bind="cSchema"
|
||||||
|
:class="cSchema.formItemClass"
|
||||||
|
:rules="cSchema.rules"
|
||||||
|
>
|
||||||
|
<template #default="slotProps">
|
||||||
|
<slot v-bind="slotProps" :name="cSchema.fieldName"> </slot>
|
||||||
|
</template>
|
||||||
|
</FormField>
|
||||||
|
</template>
|
||||||
|
<slot :shapes="shapes"></slot>
|
||||||
|
</div>
|
||||||
|
</component>
|
||||||
|
</template>
|
60
packages/@core/ui-kit/form-ui/src/form-render/helper.ts
Normal file
60
packages/@core/ui-kit/form-ui/src/form-render/helper.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import type {
|
||||||
|
AnyZodObject,
|
||||||
|
ZodDefault,
|
||||||
|
ZodEffects,
|
||||||
|
ZodNumber,
|
||||||
|
ZodString,
|
||||||
|
ZodTypeAny,
|
||||||
|
} from 'zod';
|
||||||
|
|
||||||
|
import { isObject, isString } from '@vben-core/shared/utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the lowest level Zod type.
|
||||||
|
* This will unpack optionals, refinements, etc.
|
||||||
|
*/
|
||||||
|
export function getBaseRules<
|
||||||
|
ChildType extends AnyZodObject | ZodTypeAny = ZodTypeAny,
|
||||||
|
>(schema: ChildType | ZodEffects<ChildType>): ChildType | null {
|
||||||
|
if (!schema || isString(schema)) return null;
|
||||||
|
if ('innerType' in schema._def)
|
||||||
|
return getBaseRules(schema._def.innerType as ChildType);
|
||||||
|
|
||||||
|
if ('schema' in schema._def)
|
||||||
|
return getBaseRules(schema._def.schema as ChildType);
|
||||||
|
|
||||||
|
return schema as ChildType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search for a "ZodDefault" in the Zod stack and return its value.
|
||||||
|
*/
|
||||||
|
export function getDefaultValueInZodStack(schema: ZodTypeAny): any {
|
||||||
|
if (!schema || isString(schema)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const typedSchema = schema as unknown as ZodDefault<ZodNumber | ZodString>;
|
||||||
|
|
||||||
|
if (typedSchema._def.typeName === 'ZodDefault')
|
||||||
|
return typedSchema._def.defaultValue();
|
||||||
|
|
||||||
|
if ('innerType' in typedSchema._def) {
|
||||||
|
return getDefaultValueInZodStack(
|
||||||
|
typedSchema._def.innerType as unknown as ZodTypeAny,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if ('schema' in typedSchema._def) {
|
||||||
|
return getDefaultValueInZodStack(
|
||||||
|
(typedSchema._def as any).schema as ZodTypeAny,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isEventObjectLike(obj: any) {
|
||||||
|
if (!obj || !isObject(obj)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return Reflect.has(obj, 'target') && Reflect.has(obj, 'stopPropagation');
|
||||||
|
}
|
3
packages/@core/ui-kit/form-ui/src/form-render/index.ts
Normal file
3
packages/@core/ui-kit/form-ui/src/form-render/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export { default as Form } from './form.vue';
|
||||||
|
export { default as FormField } from './form-field.vue';
|
||||||
|
export { default as FormLabel } from './form-label.vue';
|
11
packages/@core/ui-kit/form-ui/src/index.ts
Normal file
11
packages/@core/ui-kit/form-ui/src/index.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
export { setupVbenForm } from './config';
|
||||||
|
export type {
|
||||||
|
BaseFormComponentType,
|
||||||
|
FormSchema as VbenFormSchema,
|
||||||
|
VbenFormProps,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
export * from './use-vben-form';
|
||||||
|
|
||||||
|
// export { default as VbenForm } from './vben-form.vue';
|
||||||
|
export * as z from 'zod';
|
327
packages/@core/ui-kit/form-ui/src/types.ts
Normal file
327
packages/@core/ui-kit/form-ui/src/types.ts
Normal file
@ -0,0 +1,327 @@
|
|||||||
|
import type { VbenButtonProps } from '@vben-core/shadcn-ui';
|
||||||
|
import type { Field, FormContext, GenericObject } from 'vee-validate';
|
||||||
|
import type { ZodTypeAny } from 'zod';
|
||||||
|
|
||||||
|
import type { FormApi } from './form-api';
|
||||||
|
|
||||||
|
import type { Component, HtmlHTMLAttributes, Ref } from 'vue';
|
||||||
|
|
||||||
|
export type FormLayout = 'horizontal' | 'vertical';
|
||||||
|
|
||||||
|
export type BaseFormComponentType =
|
||||||
|
| 'DefaultResetActionButton'
|
||||||
|
| 'DefaultSubmitActionButton'
|
||||||
|
| 'VbenCheckbox'
|
||||||
|
| 'VbenInput'
|
||||||
|
| 'VbenInputPassword'
|
||||||
|
| 'VbenPinInput'
|
||||||
|
| 'VbenSelect'
|
||||||
|
| (Record<never, never> & string);
|
||||||
|
|
||||||
|
type Breakpoints = '' | '2xl:' | '3xl:' | 'lg:' | 'md:' | 'sm:' | 'xl:';
|
||||||
|
|
||||||
|
type GridCols = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13;
|
||||||
|
|
||||||
|
export type WrapperClassType =
|
||||||
|
| `${Breakpoints}grid-cols-${GridCols}`
|
||||||
|
| (Record<never, never> & string);
|
||||||
|
|
||||||
|
export type FormItemClassType =
|
||||||
|
| `${Breakpoints}cols-end-${'auto' | GridCols}`
|
||||||
|
| `${Breakpoints}cols-span-${'auto' | 'full' | GridCols}`
|
||||||
|
| `${Breakpoints}cols-start-${'auto' | GridCols}`
|
||||||
|
| (Record<never, never> & string)
|
||||||
|
| WrapperClassType;
|
||||||
|
|
||||||
|
export interface FormShape {
|
||||||
|
/** 默认值 */
|
||||||
|
default?: any;
|
||||||
|
/** 字段名 */
|
||||||
|
fieldName: string;
|
||||||
|
/** 是否必填 */
|
||||||
|
required?: boolean;
|
||||||
|
rules?: ZodTypeAny;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MaybeComponentPropKey =
|
||||||
|
| 'options'
|
||||||
|
| 'placeholder'
|
||||||
|
| 'title'
|
||||||
|
| keyof HtmlHTMLAttributes
|
||||||
|
| (Record<never, never> & string);
|
||||||
|
|
||||||
|
export type MaybeComponentProps = { [K in MaybeComponentPropKey]?: any };
|
||||||
|
|
||||||
|
export type FormActions = FormContext<GenericObject>;
|
||||||
|
|
||||||
|
export type CustomRenderType = (() => Component | string) | string;
|
||||||
|
|
||||||
|
export type FormSchemaRuleType =
|
||||||
|
| 'required'
|
||||||
|
| null
|
||||||
|
| (Record<never, never> & string)
|
||||||
|
| ZodTypeAny;
|
||||||
|
|
||||||
|
type FormItemDependenciesCondition<T = boolean | PromiseLike<boolean>> = (
|
||||||
|
value: Partial<Record<string, any>>,
|
||||||
|
actions: FormActions,
|
||||||
|
) => T;
|
||||||
|
|
||||||
|
type FormItemDependenciesConditionWithRules = (
|
||||||
|
value: Partial<Record<string, any>>,
|
||||||
|
actions: FormActions,
|
||||||
|
) => FormSchemaRuleType | PromiseLike<FormSchemaRuleType>;
|
||||||
|
|
||||||
|
type FormItemDependenciesConditionWithProps = (
|
||||||
|
value: Partial<Record<string, any>>,
|
||||||
|
actions: FormActions,
|
||||||
|
) => MaybeComponentProps | PromiseLike<MaybeComponentProps>;
|
||||||
|
|
||||||
|
export interface FormItemDependencies {
|
||||||
|
/**
|
||||||
|
* 组件参数
|
||||||
|
* @returns 组件参数
|
||||||
|
*/
|
||||||
|
componentProps?: FormItemDependenciesConditionWithProps;
|
||||||
|
/**
|
||||||
|
* 是否禁用
|
||||||
|
* @returns 是否禁用
|
||||||
|
*/
|
||||||
|
disabled?: FormItemDependenciesCondition;
|
||||||
|
/**
|
||||||
|
* 是否渲染(删除dom)
|
||||||
|
* @returns 是否渲染
|
||||||
|
*/
|
||||||
|
if?: FormItemDependenciesCondition;
|
||||||
|
/**
|
||||||
|
* 是否必填
|
||||||
|
* @returns 是否必填
|
||||||
|
*/
|
||||||
|
required?: FormItemDependenciesCondition;
|
||||||
|
/**
|
||||||
|
* 字段规则
|
||||||
|
*/
|
||||||
|
rules?: FormItemDependenciesConditionWithRules;
|
||||||
|
/**
|
||||||
|
* 是否隐藏(Css)
|
||||||
|
* @returns 是否隐藏
|
||||||
|
*/
|
||||||
|
show?: FormItemDependenciesCondition;
|
||||||
|
/**
|
||||||
|
* 任意触发都会执行
|
||||||
|
*/
|
||||||
|
trigger?: FormItemDependenciesCondition<void>;
|
||||||
|
/**
|
||||||
|
* 触发字段
|
||||||
|
*/
|
||||||
|
triggerFields: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
type ComponentProps =
|
||||||
|
| ((
|
||||||
|
value: Partial<Record<string, any>>,
|
||||||
|
actions: FormActions,
|
||||||
|
) => MaybeComponentProps)
|
||||||
|
| MaybeComponentProps;
|
||||||
|
|
||||||
|
export interface FormCommonConfig {
|
||||||
|
/**
|
||||||
|
* 所有表单项的props
|
||||||
|
*/
|
||||||
|
componentProps?: ComponentProps;
|
||||||
|
/**
|
||||||
|
* 所有表单项的控件样式
|
||||||
|
*/
|
||||||
|
controlClass?: string;
|
||||||
|
/**
|
||||||
|
* 所有表单项的禁用状态
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
disabled?: boolean;
|
||||||
|
/**
|
||||||
|
* 所有表单项的控件样式
|
||||||
|
* @default ""
|
||||||
|
*/
|
||||||
|
formFieldProps?: Partial<typeof Field>;
|
||||||
|
/**
|
||||||
|
* 所有表单项的栅格布局
|
||||||
|
* @default ""
|
||||||
|
*/
|
||||||
|
formItemClass?: string;
|
||||||
|
/**
|
||||||
|
* 隐藏所有表单项label
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
hideLabel?: boolean;
|
||||||
|
/**
|
||||||
|
* 是否隐藏必填标记
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
hideRequiredMark?: boolean;
|
||||||
|
/**
|
||||||
|
* 所有表单项的label样式
|
||||||
|
* @default "w-[100px]"
|
||||||
|
*/
|
||||||
|
labelClass?: string;
|
||||||
|
/**
|
||||||
|
* 所有表单项的label宽度
|
||||||
|
*/
|
||||||
|
labelWidth?: number;
|
||||||
|
/**
|
||||||
|
* 所有表单项的wrapper样式
|
||||||
|
*/
|
||||||
|
wrapperClass?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type RenderComponentContentType = (
|
||||||
|
value: Partial<Record<string, any>>,
|
||||||
|
api: FormActions,
|
||||||
|
) => Record<string, any>;
|
||||||
|
|
||||||
|
export type HandleSubmitFn = (
|
||||||
|
values: Record<string, any>,
|
||||||
|
) => Promise<void> | void;
|
||||||
|
|
||||||
|
export type HandleResetFn = (
|
||||||
|
values: Record<string, any>,
|
||||||
|
) => Promise<void> | void;
|
||||||
|
|
||||||
|
export interface FormSchema<
|
||||||
|
T extends BaseFormComponentType = BaseFormComponentType,
|
||||||
|
> extends FormCommonConfig {
|
||||||
|
/** 组件 */
|
||||||
|
component: Component | T;
|
||||||
|
/** 组件参数 */
|
||||||
|
componentProps?: ComponentProps;
|
||||||
|
/** 默认值 */
|
||||||
|
defaultValue?: any;
|
||||||
|
/** 依赖 */
|
||||||
|
dependencies?: FormItemDependencies;
|
||||||
|
/** 描述 */
|
||||||
|
description?: string;
|
||||||
|
/** 字段名 */
|
||||||
|
fieldName: string;
|
||||||
|
/** 帮助信息 */
|
||||||
|
help?: string;
|
||||||
|
/** 表单项 */
|
||||||
|
label?: string;
|
||||||
|
// 自定义组件内部渲染
|
||||||
|
renderComponentContent?: RenderComponentContentType;
|
||||||
|
/** 字段规则 */
|
||||||
|
rules?: FormSchemaRuleType;
|
||||||
|
/** 后缀 */
|
||||||
|
suffix?: CustomRenderType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FormFieldProps extends FormSchema {
|
||||||
|
required?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FormRenderProps<
|
||||||
|
T extends BaseFormComponentType = BaseFormComponentType,
|
||||||
|
> {
|
||||||
|
/**
|
||||||
|
* 是否展开,在showCollapseButton=true下生效
|
||||||
|
*/
|
||||||
|
collapsed?: boolean;
|
||||||
|
/**
|
||||||
|
* 折叠时保持行数
|
||||||
|
* @default 1
|
||||||
|
*/
|
||||||
|
collapsedRows?: number;
|
||||||
|
/**
|
||||||
|
* 表单项通用后备配置,当子项目没配置时使用这里的配置,子项目配置优先级高于此配置
|
||||||
|
*/
|
||||||
|
commonConfig?: FormCommonConfig;
|
||||||
|
/**
|
||||||
|
* 组件v-model事件绑定
|
||||||
|
*/
|
||||||
|
componentBindEventMap?: Partial<Record<BaseFormComponentType, string>>;
|
||||||
|
/**
|
||||||
|
* 组件集合
|
||||||
|
*/
|
||||||
|
componentMap: Record<BaseFormComponentType, Component>;
|
||||||
|
/**
|
||||||
|
* 表单实例
|
||||||
|
*/
|
||||||
|
form?: FormContext<GenericObject>;
|
||||||
|
/**
|
||||||
|
* 表单项布局
|
||||||
|
*/
|
||||||
|
layout?: FormLayout;
|
||||||
|
/**
|
||||||
|
* 表单定义
|
||||||
|
*/
|
||||||
|
schema?: FormSchema<T>[];
|
||||||
|
/**
|
||||||
|
* 是否显示展开/折叠
|
||||||
|
*/
|
||||||
|
showCollapseButton?: boolean;
|
||||||
|
/**
|
||||||
|
* 表单栅格布局
|
||||||
|
* @default "grid-cols-1"
|
||||||
|
*/
|
||||||
|
wrapperClass?: WrapperClassType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ActionButtonOptions extends VbenButtonProps {
|
||||||
|
show?: boolean;
|
||||||
|
text?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VbenFormProps<
|
||||||
|
T extends BaseFormComponentType = BaseFormComponentType,
|
||||||
|
> extends Omit<
|
||||||
|
FormRenderProps<T>,
|
||||||
|
'componentBindEventMap' | 'componentMap' | 'form'
|
||||||
|
> {
|
||||||
|
/**
|
||||||
|
* 表单操作区域class
|
||||||
|
*/
|
||||||
|
actionWrapperClass?: any;
|
||||||
|
/**
|
||||||
|
* 表单重置回调
|
||||||
|
*/
|
||||||
|
handleReset?: HandleResetFn;
|
||||||
|
/**
|
||||||
|
* 表单提交回调
|
||||||
|
*/
|
||||||
|
handleSubmit?: HandleSubmitFn;
|
||||||
|
/**
|
||||||
|
* 重置按钮参数
|
||||||
|
*/
|
||||||
|
resetButtonOptions?: ActionButtonOptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否显示默认操作按钮
|
||||||
|
*/
|
||||||
|
showDefaultActions?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交按钮参数
|
||||||
|
*/
|
||||||
|
submitButtonOptions?: ActionButtonOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ExtendedFormApi = {
|
||||||
|
useStore: <T = NoInfer<VbenFormProps>>(
|
||||||
|
selector?: (state: NoInfer<VbenFormProps>) => T,
|
||||||
|
) => Readonly<Ref<T>>;
|
||||||
|
} & FormApi;
|
||||||
|
|
||||||
|
export interface VbenFormAdapterOptions<
|
||||||
|
T extends BaseFormComponentType = BaseFormComponentType,
|
||||||
|
> {
|
||||||
|
components: Partial<Record<T, Component>>;
|
||||||
|
config?: {
|
||||||
|
baseModelPropName?: string;
|
||||||
|
modelPropNameMap?: Partial<Record<T, string>>;
|
||||||
|
};
|
||||||
|
defineRules?: {
|
||||||
|
required?: (
|
||||||
|
value: any,
|
||||||
|
params: any,
|
||||||
|
ctx: Record<string, any>,
|
||||||
|
) => boolean | string;
|
||||||
|
};
|
||||||
|
}
|
59
packages/@core/ui-kit/form-ui/src/use-form-context.ts
Normal file
59
packages/@core/ui-kit/form-ui/src/use-form-context.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import type { FormActions, VbenFormProps } from './types';
|
||||||
|
|
||||||
|
import { computed, type ComputedRef, unref, useSlots } from 'vue';
|
||||||
|
|
||||||
|
import { createContext } from '@vben-core/shadcn-ui';
|
||||||
|
import { isString } from '@vben-core/shared/utils';
|
||||||
|
|
||||||
|
import { useForm } from 'vee-validate';
|
||||||
|
import { object, type ZodRawShape } from 'zod';
|
||||||
|
import { getDefaultsForSchema } from 'zod-defaults';
|
||||||
|
|
||||||
|
export const [injectFormProps, provideFormProps] =
|
||||||
|
createContext<[ComputedRef<VbenFormProps> | VbenFormProps, FormActions]>(
|
||||||
|
'VbenFormProps',
|
||||||
|
);
|
||||||
|
|
||||||
|
export function useFormInitial(
|
||||||
|
props: ComputedRef<VbenFormProps> | VbenFormProps,
|
||||||
|
) {
|
||||||
|
const slots = useSlots();
|
||||||
|
const initialValues = generateInitialValues();
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
...(Object.keys(initialValues)?.length ? { initialValues } : {}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const delegatedSlots = computed(() => {
|
||||||
|
const resultSlots: string[] = [];
|
||||||
|
|
||||||
|
for (const key of Object.keys(slots)) {
|
||||||
|
if (key !== 'default') {
|
||||||
|
resultSlots.push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return resultSlots;
|
||||||
|
});
|
||||||
|
|
||||||
|
function generateInitialValues() {
|
||||||
|
const initialValues: Record<string, any> = {};
|
||||||
|
|
||||||
|
const zodObject: ZodRawShape = {};
|
||||||
|
(unref(props).schema || []).forEach((item) => {
|
||||||
|
if (Reflect.has(item, 'defaultValue')) {
|
||||||
|
initialValues[item.fieldName] = item.defaultValue;
|
||||||
|
} else if (item.rules && !isString(item.rules)) {
|
||||||
|
zodObject[item.fieldName] = item.rules;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const schemaInitialValues = getDefaultsForSchema(object(zodObject));
|
||||||
|
|
||||||
|
return { ...initialValues, ...schemaInitialValues };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
delegatedSlots,
|
||||||
|
form,
|
||||||
|
};
|
||||||
|
}
|
49
packages/@core/ui-kit/form-ui/src/use-vben-form.ts
Normal file
49
packages/@core/ui-kit/form-ui/src/use-vben-form.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import type {
|
||||||
|
BaseFormComponentType,
|
||||||
|
ExtendedFormApi,
|
||||||
|
VbenFormProps,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
import { defineComponent, h, isReactive, onBeforeUnmount, watch } from 'vue';
|
||||||
|
|
||||||
|
import { useStore } from '@vben-core/shared/store';
|
||||||
|
|
||||||
|
import { FormApi } from './form-api';
|
||||||
|
import VbenUseForm from './vben-use-form.vue';
|
||||||
|
|
||||||
|
export function useVbenForm<
|
||||||
|
T extends BaseFormComponentType = BaseFormComponentType,
|
||||||
|
>(options: VbenFormProps<T>) {
|
||||||
|
const IS_REACTIVE = isReactive(options);
|
||||||
|
const api = new FormApi(options);
|
||||||
|
const extendedApi: ExtendedFormApi = api as never;
|
||||||
|
extendedApi.useStore = (selector) => {
|
||||||
|
return useStore(api.store, selector);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Form = defineComponent(
|
||||||
|
(props: VbenFormProps, { attrs, slots }) => {
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
api.unmounted();
|
||||||
|
});
|
||||||
|
return () =>
|
||||||
|
h(VbenUseForm, { ...props, ...attrs, formApi: extendedApi }, slots);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
inheritAttrs: false,
|
||||||
|
name: 'VbenUseForm',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
// Add reactivity support
|
||||||
|
if (IS_REACTIVE) {
|
||||||
|
watch(
|
||||||
|
() => options.schema,
|
||||||
|
() => {
|
||||||
|
api.setState({ schema: options.schema });
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [Form, extendedApi] as const;
|
||||||
|
}
|
72
packages/@core/ui-kit/form-ui/src/vben-form.vue
Normal file
72
packages/@core/ui-kit/form-ui/src/vben-form.vue
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { VbenFormProps } from './types';
|
||||||
|
|
||||||
|
import { ref, watchEffect } from 'vue';
|
||||||
|
|
||||||
|
import { useForwardPropsEmits } from '@vben-core/composables';
|
||||||
|
|
||||||
|
import FormActions from './components/form-actions.vue';
|
||||||
|
import { COMPONENT_BIND_EVENT_MAP, COMPONENT_MAP } from './config';
|
||||||
|
import { Form } from './form-render';
|
||||||
|
import { provideFormProps, useFormInitial } from './use-form-context';
|
||||||
|
|
||||||
|
// 通过 extends 会导致热更新卡死
|
||||||
|
interface Props extends VbenFormProps {}
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
actionWrapperClass: '',
|
||||||
|
collapsed: false,
|
||||||
|
collapsedRows: 1,
|
||||||
|
commonConfig: () => ({}),
|
||||||
|
handleReset: undefined,
|
||||||
|
handleSubmit: undefined,
|
||||||
|
layout: 'horizontal',
|
||||||
|
resetButtonOptions: () => ({}),
|
||||||
|
showCollapseButton: false,
|
||||||
|
showDefaultActions: true,
|
||||||
|
submitButtonOptions: () => ({}),
|
||||||
|
wrapperClass: 'grid-cols-1',
|
||||||
|
});
|
||||||
|
|
||||||
|
const forward = useForwardPropsEmits(props);
|
||||||
|
|
||||||
|
const currentCollapsed = ref(false);
|
||||||
|
|
||||||
|
const { delegatedSlots, form } = useFormInitial(props);
|
||||||
|
|
||||||
|
provideFormProps([props, form]);
|
||||||
|
|
||||||
|
const handleUpdateCollapsed = (value: boolean) => {
|
||||||
|
currentCollapsed.value = !!value;
|
||||||
|
};
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
currentCollapsed.value = props.collapsed;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Form
|
||||||
|
v-bind="forward"
|
||||||
|
:collapsed="currentCollapsed"
|
||||||
|
:component-bind-event-map="COMPONENT_BIND_EVENT_MAP"
|
||||||
|
:component-map="COMPONENT_MAP"
|
||||||
|
:form="form"
|
||||||
|
>
|
||||||
|
<template
|
||||||
|
v-for="slotName in delegatedSlots"
|
||||||
|
:key="slotName"
|
||||||
|
#[slotName]="slotProps"
|
||||||
|
>
|
||||||
|
<slot :name="slotName" v-bind="slotProps"></slot>
|
||||||
|
</template>
|
||||||
|
<template #default="slotProps">
|
||||||
|
<slot v-bind="slotProps">
|
||||||
|
<FormActions
|
||||||
|
v-if="showDefaultActions"
|
||||||
|
:model-value="currentCollapsed"
|
||||||
|
@update:model-value="handleUpdateCollapsed"
|
||||||
|
/>
|
||||||
|
</slot>
|
||||||
|
</template>
|
||||||
|
</Form>
|
||||||
|
</template>
|
57
packages/@core/ui-kit/form-ui/src/vben-use-form.vue
Normal file
57
packages/@core/ui-kit/form-ui/src/vben-use-form.vue
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { ExtendedFormApi, VbenFormProps } from './types';
|
||||||
|
|
||||||
|
import { useForwardPriorityValues } from '@vben-core/composables';
|
||||||
|
|
||||||
|
import FormActions from './components/form-actions.vue';
|
||||||
|
import { COMPONENT_BIND_EVENT_MAP, COMPONENT_MAP } from './config';
|
||||||
|
import { Form } from './form-render';
|
||||||
|
import { provideFormProps, useFormInitial } from './use-form-context';
|
||||||
|
|
||||||
|
// 通过 extends 会导致热更新卡死,所以重复写了一遍
|
||||||
|
interface Props extends VbenFormProps {
|
||||||
|
formApi: ExtendedFormApi;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const state = props.formApi?.useStore?.();
|
||||||
|
|
||||||
|
const forward = useForwardPriorityValues(props, state);
|
||||||
|
|
||||||
|
const { delegatedSlots, form } = useFormInitial(forward);
|
||||||
|
|
||||||
|
provideFormProps([forward, form]);
|
||||||
|
|
||||||
|
props.formApi?.mount?.(form);
|
||||||
|
|
||||||
|
const handleUpdateCollapsed = (value: boolean) => {
|
||||||
|
props.formApi?.setState({ collapsed: !!value });
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Form
|
||||||
|
v-bind="forward"
|
||||||
|
:component-bind-event-map="COMPONENT_BIND_EVENT_MAP"
|
||||||
|
:component-map="COMPONENT_MAP"
|
||||||
|
:form="form"
|
||||||
|
>
|
||||||
|
<template
|
||||||
|
v-for="slotName in delegatedSlots"
|
||||||
|
:key="slotName"
|
||||||
|
#[slotName]="slotProps"
|
||||||
|
>
|
||||||
|
<slot :name="slotName" v-bind="slotProps"></slot>
|
||||||
|
</template>
|
||||||
|
<template #default="slotProps">
|
||||||
|
<slot v-bind="slotProps">
|
||||||
|
<FormActions
|
||||||
|
v-if="forward.showDefaultActions"
|
||||||
|
:model-value="state.collapsed"
|
||||||
|
@update:model-value="handleUpdateCollapsed"
|
||||||
|
/>
|
||||||
|
</slot>
|
||||||
|
</template>
|
||||||
|
</Form>
|
||||||
|
</template>
|
1
packages/@core/ui-kit/form-ui/tailwind.config.mjs
Normal file
1
packages/@core/ui-kit/form-ui/tailwind.config.mjs
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default } from '@vben/tailwind-config';
|
6
packages/@core/ui-kit/form-ui/tsconfig.json
Normal file
6
packages/@core/ui-kit/form-ui/tsconfig.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/tsconfig",
|
||||||
|
"extends": "@vben/tsconfig/web.json",
|
||||||
|
"include": ["src"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
@ -22,7 +22,7 @@ import {
|
|||||||
|
|
||||||
import { useNamespace } from '@vben-core/composables';
|
import { useNamespace } from '@vben-core/composables';
|
||||||
import { Ellipsis } from '@vben-core/icons';
|
import { Ellipsis } from '@vben-core/icons';
|
||||||
import { isHttpUrl } from '@vben-core/shared';
|
import { isHttpUrl } from '@vben-core/shared/utils';
|
||||||
|
|
||||||
import { useResizeObserver } from '@vueuse/core';
|
import { useResizeObserver } from '@vueuse/core';
|
||||||
|
|
||||||
@ -430,7 +430,7 @@ $namespace: vben;
|
|||||||
--menu-item-padding-x: 12px;
|
--menu-item-padding-x: 12px;
|
||||||
--menu-item-popup-padding-y: 20px;
|
--menu-item-popup-padding-y: 20px;
|
||||||
--menu-item-popup-padding-x: 12px;
|
--menu-item-popup-padding-x: 12px;
|
||||||
--menu-item-margin-y: 3px;
|
--menu-item-margin-y: 2px;
|
||||||
--menu-item-margin-x: 0px;
|
--menu-item-margin-x: 0px;
|
||||||
--menu-item-collapse-padding-y: 23.5px;
|
--menu-item-collapse-padding-y: 23.5px;
|
||||||
--menu-item-collapse-padding-x: 0px;
|
--menu-item-collapse-padding-x: 0px;
|
||||||
@ -475,7 +475,7 @@ $namespace: vben;
|
|||||||
&.is-rounded {
|
&.is-rounded {
|
||||||
--menu-item-margin-x: 8px;
|
--menu-item-margin-x: 8px;
|
||||||
--menu-item-collapse-margin-x: 6px;
|
--menu-item-collapse-margin-x: 6px;
|
||||||
--menu-item-radius: 10px;
|
--menu-item-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.is-horizontal:not(.is-rounded) {
|
&.is-horizontal:not(.is-rounded) {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@vben-core/popup-ui",
|
"name": "@vben-core/popup-ui",
|
||||||
"version": "5.1.1",
|
"version": "5.2.1",
|
||||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -5,7 +5,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|||||||
import { DrawerApi } from '../drawer-api';
|
import { DrawerApi } from '../drawer-api';
|
||||||
|
|
||||||
// 模拟 Store 类
|
// 模拟 Store 类
|
||||||
vi.mock('@vben-core/shared', () => {
|
vi.mock('@vben-core/shared/store', () => {
|
||||||
return {
|
return {
|
||||||
isFunction: (fn: any) => typeof fn === 'function',
|
isFunction: (fn: any) => typeof fn === 'function',
|
||||||
Store: class {
|
Store: class {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import type { DrawerApiOptions, DrawerState } from './drawer';
|
import type { DrawerApiOptions, DrawerState } from './drawer';
|
||||||
|
|
||||||
import { isFunction, Store } from '@vben-core/shared';
|
import { Store } from '@vben-core/shared/store';
|
||||||
|
import { bindMethods, isFunction } from '@vben-core/shared/utils';
|
||||||
|
|
||||||
export class DrawerApi {
|
export class DrawerApi {
|
||||||
private api: Pick<
|
private api: Pick<
|
||||||
@ -58,13 +59,14 @@ export class DrawerApi {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
this.state = this.store.state;
|
||||||
this.api = {
|
this.api = {
|
||||||
onBeforeClose,
|
onBeforeClose,
|
||||||
onCancel,
|
onCancel,
|
||||||
onConfirm,
|
onConfirm,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
};
|
};
|
||||||
|
bindMethods(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果需要多次更新状态,可以使用 batch 方法
|
// 如果需要多次更新状态,可以使用 batch 方法
|
||||||
|
@ -5,10 +5,10 @@ import { ref, watch } from 'vue';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
useIsMobile,
|
useIsMobile,
|
||||||
usePriorityValue,
|
usePriorityValues,
|
||||||
useSimpleLocale,
|
useSimpleLocale,
|
||||||
} from '@vben-core/composables';
|
} from '@vben-core/composables';
|
||||||
import { Info, X } from '@vben-core/icons';
|
import { X } from '@vben-core/icons';
|
||||||
import {
|
import {
|
||||||
Sheet,
|
Sheet,
|
||||||
SheetClose,
|
SheetClose,
|
||||||
@ -18,12 +18,12 @@ import {
|
|||||||
SheetHeader,
|
SheetHeader,
|
||||||
SheetTitle,
|
SheetTitle,
|
||||||
VbenButton,
|
VbenButton,
|
||||||
|
VbenHelpTooltip,
|
||||||
VbenIconButton,
|
VbenIconButton,
|
||||||
VbenLoading,
|
VbenLoading,
|
||||||
VbenTooltip,
|
|
||||||
VisuallyHidden,
|
VisuallyHidden,
|
||||||
} from '@vben-core/shadcn-ui';
|
} from '@vben-core/shadcn-ui';
|
||||||
import { cn } from '@vben-core/shared';
|
import { cn } from '@vben-core/shared/utils';
|
||||||
|
|
||||||
interface Props extends DrawerProps {
|
interface Props extends DrawerProps {
|
||||||
class?: string;
|
class?: string;
|
||||||
@ -42,20 +42,22 @@ const { $t } = useSimpleLocale();
|
|||||||
const { isMobile } = useIsMobile();
|
const { isMobile } = useIsMobile();
|
||||||
const state = props.drawerApi?.useStore?.();
|
const state = props.drawerApi?.useStore?.();
|
||||||
|
|
||||||
const title = usePriorityValue('title', props, state);
|
const {
|
||||||
const description = usePriorityValue('description', props, state);
|
cancelText,
|
||||||
const titleTooltip = usePriorityValue('titleTooltip', props, state);
|
closable,
|
||||||
const showFooter = usePriorityValue('footer', props, state);
|
closeOnClickModal,
|
||||||
const showLoading = usePriorityValue('loading', props, state);
|
closeOnPressEscape,
|
||||||
const closable = usePriorityValue('closable', props, state);
|
confirmLoading,
|
||||||
const modal = usePriorityValue('modal', props, state);
|
confirmText,
|
||||||
const confirmLoading = usePriorityValue('confirmLoading', props, state);
|
description,
|
||||||
const cancelText = usePriorityValue('cancelText', props, state);
|
footer: showFooter,
|
||||||
const confirmText = usePriorityValue('confirmText', props, state);
|
loading: showLoading,
|
||||||
const closeOnClickModal = usePriorityValue('closeOnClickModal', props, state);
|
modal,
|
||||||
const closeOnPressEscape = usePriorityValue('closeOnPressEscape', props, state);
|
showCancelButton,
|
||||||
const showCancelButton = usePriorityValue('showCancelButton', props, state);
|
showConfirmButton,
|
||||||
const showConfirmButton = usePriorityValue('showConfirmButton', props, state);
|
title,
|
||||||
|
titleTooltip,
|
||||||
|
} = usePriorityValues(props, state);
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => showLoading.value,
|
() => showLoading.value,
|
||||||
@ -116,12 +118,9 @@ function pointerDownOutside(e: Event) {
|
|||||||
<slot name="title">
|
<slot name="title">
|
||||||
{{ title }}
|
{{ title }}
|
||||||
|
|
||||||
<VbenTooltip v-if="titleTooltip" side="right">
|
<VbenHelpTooltip v-if="titleTooltip" trigger-class="pb-1">
|
||||||
<template #trigger>
|
|
||||||
<Info class="inline-flex size-5 cursor-pointer pb-1" />
|
|
||||||
</template>
|
|
||||||
{{ titleTooltip }}
|
{{ titleTooltip }}
|
||||||
</VbenTooltip>
|
</VbenHelpTooltip>
|
||||||
</slot>
|
</slot>
|
||||||
</SheetTitle>
|
</SheetTitle>
|
||||||
<SheetDescription v-if="description" class="mt-1 text-xs">
|
<SheetDescription v-if="description" class="mt-1 text-xs">
|
||||||
|
@ -6,7 +6,7 @@ import type {
|
|||||||
|
|
||||||
import { defineComponent, h, inject, nextTick, provide, reactive } from 'vue';
|
import { defineComponent, h, inject, nextTick, provide, reactive } from 'vue';
|
||||||
|
|
||||||
import { useStore } from '@vben-core/shared';
|
import { useStore } from '@vben-core/shared/store';
|
||||||
|
|
||||||
import VbenDrawer from './drawer.vue';
|
import VbenDrawer from './drawer.vue';
|
||||||
import { DrawerApi } from './drawer-api';
|
import { DrawerApi } from './drawer-api';
|
||||||
|
@ -3,7 +3,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|||||||
import { ModalApi } from '../modal-api'; // 假设 ModalApi 位于同一目录
|
import { ModalApi } from '../modal-api'; // 假设 ModalApi 位于同一目录
|
||||||
import type { ModalState } from '../modal';
|
import type { ModalState } from '../modal';
|
||||||
|
|
||||||
vi.mock('@vben-core/shared', () => {
|
vi.mock('@vben-core/shared/store', () => {
|
||||||
return {
|
return {
|
||||||
isFunction: (fn: any) => typeof fn === 'function',
|
isFunction: (fn: any) => typeof fn === 'function',
|
||||||
Store: class {
|
Store: class {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import type { ModalApiOptions, ModalState } from './modal';
|
import type { ModalApiOptions, ModalState } from './modal';
|
||||||
|
|
||||||
import { isFunction, Store } from '@vben-core/shared';
|
import { Store } from '@vben-core/shared/store';
|
||||||
|
import { bindMethods, isFunction } from '@vben-core/shared/utils';
|
||||||
|
|
||||||
export class ModalApi {
|
export class ModalApi {
|
||||||
private api: Pick<
|
private api: Pick<
|
||||||
@ -65,12 +66,15 @@ export class ModalApi {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.state = this.store.state;
|
||||||
|
|
||||||
this.api = {
|
this.api = {
|
||||||
onBeforeClose,
|
onBeforeClose,
|
||||||
onCancel,
|
onCancel,
|
||||||
onConfirm,
|
onConfirm,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
};
|
};
|
||||||
|
bindMethods(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果需要多次更新状态,可以使用 batch 方法
|
// 如果需要多次更新状态,可以使用 batch 方法
|
||||||
|
@ -5,10 +5,10 @@ import { computed, nextTick, ref, watch } from 'vue';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
useIsMobile,
|
useIsMobile,
|
||||||
usePriorityValue,
|
usePriorityValues,
|
||||||
useSimpleLocale,
|
useSimpleLocale,
|
||||||
} from '@vben-core/composables';
|
} from '@vben-core/composables';
|
||||||
import { Expand, Info, Shrink } from '@vben-core/icons';
|
import { Expand, Shrink } from '@vben-core/icons';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@ -17,12 +17,12 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
VbenButton,
|
VbenButton,
|
||||||
|
VbenHelpTooltip,
|
||||||
VbenIconButton,
|
VbenIconButton,
|
||||||
VbenLoading,
|
VbenLoading,
|
||||||
VbenTooltip,
|
|
||||||
VisuallyHidden,
|
VisuallyHidden,
|
||||||
} from '@vben-core/shadcn-ui';
|
} from '@vben-core/shadcn-ui';
|
||||||
import { cn } from '@vben-core/shared';
|
import { cn } from '@vben-core/shared/utils';
|
||||||
|
|
||||||
import { useModalDraggable } from './use-modal-draggable';
|
import { useModalDraggable } from './use-modal-draggable';
|
||||||
|
|
||||||
@ -52,25 +52,27 @@ const { $t } = useSimpleLocale();
|
|||||||
const { isMobile } = useIsMobile();
|
const { isMobile } = useIsMobile();
|
||||||
const state = props.modalApi?.useStore?.();
|
const state = props.modalApi?.useStore?.();
|
||||||
|
|
||||||
const header = usePriorityValue('header', props, state);
|
const {
|
||||||
const title = usePriorityValue('title', props, state);
|
cancelText,
|
||||||
const fullscreen = usePriorityValue('fullscreen', props, state);
|
centered,
|
||||||
const description = usePriorityValue('description', props, state);
|
closable,
|
||||||
const titleTooltip = usePriorityValue('titleTooltip', props, state);
|
closeOnClickModal,
|
||||||
const showFooter = usePriorityValue('footer', props, state);
|
closeOnPressEscape,
|
||||||
const showLoading = usePriorityValue('loading', props, state);
|
confirmLoading,
|
||||||
const closable = usePriorityValue('closable', props, state);
|
confirmText,
|
||||||
const modal = usePriorityValue('modal', props, state);
|
description,
|
||||||
const centered = usePriorityValue('centered', props, state);
|
draggable,
|
||||||
const confirmLoading = usePriorityValue('confirmLoading', props, state);
|
footer: showFooter,
|
||||||
const cancelText = usePriorityValue('cancelText', props, state);
|
fullscreen,
|
||||||
const confirmText = usePriorityValue('confirmText', props, state);
|
fullscreenButton,
|
||||||
const draggable = usePriorityValue('draggable', props, state);
|
header,
|
||||||
const fullscreenButton = usePriorityValue('fullscreenButton', props, state);
|
loading: showLoading,
|
||||||
const closeOnClickModal = usePriorityValue('closeOnClickModal', props, state);
|
modal,
|
||||||
const closeOnPressEscape = usePriorityValue('closeOnPressEscape', props, state);
|
showCancelButton,
|
||||||
const showCancelButton = usePriorityValue('showCancelButton', props, state);
|
showConfirmButton,
|
||||||
const showConfirmButton = usePriorityValue('showConfirmButton', props, state);
|
title,
|
||||||
|
titleTooltip,
|
||||||
|
} = usePriorityValues(props, state);
|
||||||
|
|
||||||
const shouldFullscreen = computed(
|
const shouldFullscreen = computed(
|
||||||
() => (fullscreen.value && header.value) || isMobile.value,
|
() => (fullscreen.value && header.value) || isMobile.value,
|
||||||
@ -184,12 +186,9 @@ function pointerDownOutside(e: Event) {
|
|||||||
{{ title }}
|
{{ title }}
|
||||||
|
|
||||||
<slot v-if="titleTooltip" name="titleTooltip">
|
<slot v-if="titleTooltip" name="titleTooltip">
|
||||||
<VbenTooltip side="right">
|
<VbenHelpTooltip trigger-class="pb-1">
|
||||||
<template #trigger>
|
|
||||||
<Info class="inline-flex size-5 cursor-pointer pb-1" />
|
|
||||||
</template>
|
|
||||||
{{ titleTooltip }}
|
{{ titleTooltip }}
|
||||||
</VbenTooltip>
|
</VbenHelpTooltip>
|
||||||
</slot>
|
</slot>
|
||||||
</slot>
|
</slot>
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
|
@ -2,7 +2,7 @@ import type { ExtendedModalApi, ModalApiOptions, ModalProps } from './modal';
|
|||||||
|
|
||||||
import { defineComponent, h, inject, nextTick, provide, reactive } from 'vue';
|
import { defineComponent, h, inject, nextTick, provide, reactive } from 'vue';
|
||||||
|
|
||||||
import { useStore } from '@vben-core/shared';
|
import { useStore } from '@vben-core/shared/store';
|
||||||
|
|
||||||
import VbenModal from './modal.vue';
|
import VbenModal from './modal.vue';
|
||||||
import { ModalApi } from './modal-api';
|
import { ModalApi } from './modal-api';
|
||||||
@ -33,7 +33,15 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
|
|||||||
...attrs,
|
...attrs,
|
||||||
...slots,
|
...slots,
|
||||||
});
|
});
|
||||||
return () => h(connectedComponent, { ...props, ...attrs }, slots);
|
return () =>
|
||||||
|
h(
|
||||||
|
connectedComponent,
|
||||||
|
{
|
||||||
|
...props,
|
||||||
|
...attrs,
|
||||||
|
},
|
||||||
|
slots,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
inheritAttrs: false,
|
inheritAttrs: false,
|
||||||
@ -65,7 +73,15 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
|
|||||||
const Modal = defineComponent(
|
const Modal = defineComponent(
|
||||||
(props: ModalProps, { attrs, slots }) => {
|
(props: ModalProps, { attrs, slots }) => {
|
||||||
return () =>
|
return () =>
|
||||||
h(VbenModal, { ...props, ...attrs, modalApi: extendedApi }, slots);
|
h(
|
||||||
|
VbenModal,
|
||||||
|
{
|
||||||
|
...props,
|
||||||
|
...attrs,
|
||||||
|
modalApi: extendedApi,
|
||||||
|
},
|
||||||
|
slots,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
inheritAttrs: false,
|
inheritAttrs: false,
|
||||||
|
@ -11,6 +11,6 @@
|
|||||||
"framework": "vite",
|
"framework": "vite",
|
||||||
"aliases": {
|
"aliases": {
|
||||||
"components": "@vben-core/shadcn-ui/components",
|
"components": "@vben-core/shadcn-ui/components",
|
||||||
"utils": "@vben-core/shared"
|
"utils": "@vben-core/shared/utils"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,8 +11,8 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "pnpm unbuild",
|
"#build": "pnpm unbuild",
|
||||||
"prepublishOnly": "npm run build"
|
"#prepublishOnly": "npm run build"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
@ -20,24 +20,22 @@
|
|||||||
"sideEffects": [
|
"sideEffects": [
|
||||||
"**/*.css"
|
"**/*.css"
|
||||||
],
|
],
|
||||||
"main": "./dist/index.mjs",
|
"#main": "./dist/index.mjs",
|
||||||
"module": "./dist/index.mjs",
|
"main": "./src/index.ts",
|
||||||
|
"#module": "./dist/index.mjs",
|
||||||
|
"module": "./src/index.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"types": "./src/index.ts",
|
"types": "./src/index.ts",
|
||||||
"development": "./src/index.ts",
|
"development": "./src/index.ts",
|
||||||
"default": "./dist/index.mjs"
|
"//default": "./dist/index.mjs",
|
||||||
},
|
"default": "./src/index.ts"
|
||||||
"./*": {
|
|
||||||
"types": "./src/*/index.ts",
|
|
||||||
"development": "./src/*/index.ts",
|
|
||||||
"default": "./dist/*/index.mjs"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"default": "./dist/index.mjs"
|
"default": "./src/index.ts"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -50,6 +48,7 @@
|
|||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"lucide-vue-next": "^0.439.0",
|
"lucide-vue-next": "^0.439.0",
|
||||||
"radix-vue": "^1.9.5",
|
"radix-vue": "^1.9.5",
|
||||||
|
"vee-validate": "^4.13.2",
|
||||||
"vue": "^3.5.3"
|
"vue": "^3.5.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,24 @@
|
|||||||
|
import type { AsTag } from 'radix-vue';
|
||||||
|
|
||||||
|
import type { ButtonVariants, ButtonVariantSize } from '../ui/button';
|
||||||
|
|
||||||
|
import type { Component } from 'vue';
|
||||||
|
|
||||||
|
export interface VbenButtonProps {
|
||||||
|
/**
|
||||||
|
* The element or component this component should render as. Can be overwrite by `asChild`
|
||||||
|
* @defaultValue "div"
|
||||||
|
*/
|
||||||
|
as?: AsTag | Component;
|
||||||
|
/**
|
||||||
|
* Change the default rendered element for the one passed as a child, merging their props and behavior.
|
||||||
|
*
|
||||||
|
* Read our [Composition](https://www.radix-vue.com/guides/composition.html) guide for more details.
|
||||||
|
*/
|
||||||
|
asChild?: boolean;
|
||||||
|
class?: any;
|
||||||
|
disabled?: boolean;
|
||||||
|
loading?: boolean;
|
||||||
|
size?: ButtonVariantSize;
|
||||||
|
variant?: ButtonVariants;
|
||||||
|
}
|
@ -1,20 +1,16 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { VbenButtonProps } from './button';
|
||||||
|
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
|
||||||
import { LoaderCircle } from '@vben-core/icons';
|
import { LoaderCircle } from '@vben-core/icons';
|
||||||
import { cn } from '@vben-core/shared';
|
import { cn } from '@vben-core/shared/utils';
|
||||||
|
|
||||||
import { Primitive, type PrimitiveProps } from 'radix-vue';
|
import { Primitive } from 'radix-vue';
|
||||||
|
|
||||||
import { type ButtonVariants, buttonVariants } from '../ui/button';
|
import { buttonVariants } from '../ui/button';
|
||||||
|
|
||||||
interface Props extends PrimitiveProps {
|
interface Props extends VbenButtonProps {}
|
||||||
class?: any;
|
|
||||||
disabled?: boolean;
|
|
||||||
loading?: boolean;
|
|
||||||
size?: ButtonVariants['size'];
|
|
||||||
variant?: ButtonVariants['variant'];
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
as: 'button',
|
as: 'button',
|
||||||
|
@ -1,22 +1,21 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { ButtonVariants } from '../ui/button';
|
import type { ButtonVariants } from '../ui/button';
|
||||||
|
import type { VbenButtonProps } from './button';
|
||||||
|
|
||||||
import { computed, useSlots } from 'vue';
|
import { computed, useSlots } from 'vue';
|
||||||
|
|
||||||
import { cn } from '@vben-core/shared';
|
import { cn } from '@vben-core/shared/utils';
|
||||||
|
|
||||||
import { type PrimitiveProps } from 'radix-vue';
|
|
||||||
|
|
||||||
import { VbenTooltip } from '../tooltip';
|
import { VbenTooltip } from '../tooltip';
|
||||||
import VbenButton from './button.vue';
|
import VbenButton from './button.vue';
|
||||||
|
|
||||||
interface Props extends PrimitiveProps {
|
interface Props extends VbenButtonProps {
|
||||||
class?: any;
|
class?: any;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
tooltip?: string;
|
tooltip?: string;
|
||||||
tooltipSide?: 'bottom' | 'left' | 'right' | 'top';
|
tooltipSide?: 'bottom' | 'left' | 'right' | 'top';
|
||||||
variant?: ButtonVariants['variant'];
|
variant?: ButtonVariants;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
@ -1,2 +1,3 @@
|
|||||||
|
export type * from './button';
|
||||||
export { default as VbenButton } from './button.vue';
|
export { default as VbenButton } from './button.vue';
|
||||||
export { default as VbenIconButton } from './icon-button.vue';
|
export { default as VbenIconButton } from './icon-button.vue';
|
||||||
|
@ -1,24 +1,26 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { CheckboxRootEmits, CheckboxRootProps } from 'radix-vue';
|
import type { CheckboxRootEmits, CheckboxRootProps } from 'radix-vue';
|
||||||
|
|
||||||
|
import { useId } from 'vue';
|
||||||
|
|
||||||
import { useForwardPropsEmits } from 'radix-vue';
|
import { useForwardPropsEmits } from 'radix-vue';
|
||||||
|
|
||||||
import { Checkbox } from '../ui/checkbox';
|
import { Checkbox } from '../ui/checkbox';
|
||||||
|
|
||||||
const props = defineProps<
|
const props = defineProps<CheckboxRootProps>();
|
||||||
{
|
|
||||||
name: string;
|
|
||||||
} & CheckboxRootProps
|
|
||||||
>();
|
|
||||||
|
|
||||||
const emits = defineEmits<CheckboxRootEmits>();
|
const emits = defineEmits<CheckboxRootEmits>();
|
||||||
|
|
||||||
const checked = defineModel<boolean>('checked');
|
const checked = defineModel<boolean>('checked');
|
||||||
|
|
||||||
const forwarded = useForwardPropsEmits(props, emits);
|
const forwarded = useForwardPropsEmits(props, emits);
|
||||||
|
|
||||||
|
const id = useId();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Checkbox v-bind="forwarded" :id="name" v-model:checked="checked" />
|
<div class="flex items-center">
|
||||||
<label :for="name" class="ml-2 cursor-pointer text-sm"> <slot></slot> </label>
|
<Checkbox v-bind="forwarded" :id="id" v-model:checked="checked" />
|
||||||
|
<label :for="id" class="ml-2 cursor-pointer text-sm"> <slot></slot> </label>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, onMounted, ref, unref, watch, watchEffect } from 'vue';
|
import { computed, onMounted, ref, unref, watch, watchEffect } from 'vue';
|
||||||
|
|
||||||
import { isNumber } from '@vben-core/shared';
|
import { isNumber } from '@vben-core/shared/utils';
|
||||||
|
|
||||||
import { TransitionPresets, useTransition } from '@vueuse/core';
|
import { TransitionPresets, useTransition } from '@vueuse/core';
|
||||||
|
|
||||||
|
@ -0,0 +1,36 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { ChevronDown } from '@vben-core/icons';
|
||||||
|
import { cn } from '@vben-core/shared/utils';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
// 控制箭头展开/收起状态
|
||||||
|
const collapsed = defineModel({ default: false });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'text-primary hover:text-primary-hover inline-flex cursor-pointer items-center',
|
||||||
|
props.class,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
@click="collapsed = !collapsed"
|
||||||
|
>
|
||||||
|
<slot :is-expanded="collapsed">
|
||||||
|
{{ collapsed }}
|
||||||
|
<!-- <span>{{ isExpanded ? '收起' : '展开' }}</span> -->
|
||||||
|
</slot>
|
||||||
|
<div
|
||||||
|
:class="{ 'rotate-180': !collapsed }"
|
||||||
|
class="transition-transform duration-300"
|
||||||
|
>
|
||||||
|
<slot name="icon">
|
||||||
|
<ChevronDown class="size-4" />
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
@ -0,0 +1 @@
|
|||||||
|
export { default as VbenExpandableArrow } from './expandable-arrow.vue';
|
@ -22,7 +22,7 @@ isFullscreen.value = !!(
|
|||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<VbenIconButton @click="toggle">
|
<VbenIconButton @click="toggle">
|
||||||
<Minimize v-if="isFullscreen" class="size-4" />
|
<Minimize v-if="isFullscreen" class="text-foreground size-4" />
|
||||||
<Maximize v-else class="size-4" />
|
<Maximize v-else class="text-foreground size-4" />
|
||||||
</VbenIconButton>
|
</VbenIconButton>
|
||||||
</template>
|
</template>
|
||||||
|
@ -2,7 +2,12 @@
|
|||||||
import { type Component, computed } from 'vue';
|
import { type Component, computed } from 'vue';
|
||||||
|
|
||||||
import { Icon, IconDefault } from '@vben-core/icons';
|
import { Icon, IconDefault } from '@vben-core/icons';
|
||||||
import { isFunction, isHttpUrl, isObject, isString } from '@vben-core/shared';
|
import {
|
||||||
|
isFunction,
|
||||||
|
isHttpUrl,
|
||||||
|
isObject,
|
||||||
|
isString,
|
||||||
|
} from '@vben-core/shared/utils';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
// 没有是否显示默认图标
|
// 没有是否显示默认图标
|
||||||
|
@ -6,10 +6,10 @@ export * from './checkbox';
|
|||||||
export * from './context-menu';
|
export * from './context-menu';
|
||||||
export * from './count-to-animator';
|
export * from './count-to-animator';
|
||||||
export * from './dropdown-menu';
|
export * from './dropdown-menu';
|
||||||
|
export * from './expandable-arrow';
|
||||||
export * from './full-screen';
|
export * from './full-screen';
|
||||||
export * from './hover-card';
|
export * from './hover-card';
|
||||||
export * from './icon';
|
export * from './icon';
|
||||||
export * from './input';
|
|
||||||
export * from './input-password';
|
export * from './input-password';
|
||||||
export * from './link';
|
export * from './link';
|
||||||
export * from './logo';
|
export * from './logo';
|
||||||
@ -19,9 +19,11 @@ export * from './popover';
|
|||||||
export * from './render-content';
|
export * from './render-content';
|
||||||
export * from './scrollbar';
|
export * from './scrollbar';
|
||||||
export * from './segmented';
|
export * from './segmented';
|
||||||
|
export * from './select';
|
||||||
export * from './spinner';
|
export * from './spinner';
|
||||||
export * from './swap';
|
export * from './swap';
|
||||||
export * from './tooltip';
|
export * from './tooltip';
|
||||||
|
export * from './ui/accordion';
|
||||||
export * from './ui/avatar';
|
export * from './ui/avatar';
|
||||||
export * from './ui/badge';
|
export * from './ui/badge';
|
||||||
export * from './ui/breadcrumb';
|
export * from './ui/breadcrumb';
|
||||||
@ -30,16 +32,21 @@ export * from './ui/card';
|
|||||||
export * from './ui/checkbox';
|
export * from './ui/checkbox';
|
||||||
export * from './ui/dialog';
|
export * from './ui/dialog';
|
||||||
export * from './ui/dropdown-menu';
|
export * from './ui/dropdown-menu';
|
||||||
|
export * from './ui/form';
|
||||||
export * from './ui/hover-card';
|
export * from './ui/hover-card';
|
||||||
export * from './ui/input';
|
export * from './ui/input';
|
||||||
|
export * from './ui/label';
|
||||||
export * from './ui/number-field';
|
export * from './ui/number-field';
|
||||||
export * from './ui/pin-input';
|
export * from './ui/pin-input';
|
||||||
export * from './ui/popover';
|
export * from './ui/popover';
|
||||||
|
export * from './ui/radio-group';
|
||||||
export * from './ui/scroll-area';
|
export * from './ui/scroll-area';
|
||||||
export * from './ui/select';
|
export * from './ui/select';
|
||||||
|
export * from './ui/separator';
|
||||||
export * from './ui/sheet';
|
export * from './ui/sheet';
|
||||||
export * from './ui/switch';
|
export * from './ui/switch';
|
||||||
export * from './ui/tabs';
|
export * from './ui/tabs';
|
||||||
|
export * from './ui/textarea';
|
||||||
export * from './ui/toast';
|
export * from './ui/toast';
|
||||||
export * from './ui/toggle';
|
export * from './ui/toggle';
|
||||||
export * from './ui/toggle-group';
|
export * from './ui/toggle-group';
|
||||||
|
@ -2,13 +2,18 @@
|
|||||||
import { ref, useSlots } from 'vue';
|
import { ref, useSlots } from 'vue';
|
||||||
|
|
||||||
import { Eye, EyeOff } from '@vben-core/icons';
|
import { Eye, EyeOff } from '@vben-core/icons';
|
||||||
|
import { cn } from '@vben-core/shared/utils';
|
||||||
|
|
||||||
import { useForwardProps } from 'radix-vue';
|
import { Input } from '../ui/input';
|
||||||
|
|
||||||
import { type InputProps, VbenInput } from '../input';
|
|
||||||
import PasswordStrength from './password-strength.vue';
|
import PasswordStrength from './password-strength.vue';
|
||||||
|
|
||||||
interface Props extends InputProps {}
|
interface Props {
|
||||||
|
class?: any;
|
||||||
|
/**
|
||||||
|
* 是否显示密码强度
|
||||||
|
*/
|
||||||
|
passwordStrength?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
inheritAttrs: false,
|
inheritAttrs: false,
|
||||||
@ -19,30 +24,30 @@ const props = defineProps<Props>();
|
|||||||
const modelValue = defineModel<string>();
|
const modelValue = defineModel<string>();
|
||||||
|
|
||||||
const slots = useSlots();
|
const slots = useSlots();
|
||||||
const forward = useForwardProps(props);
|
|
||||||
|
|
||||||
const show = ref(false);
|
const show = ref(false);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="relative">
|
<div class="relative w-full">
|
||||||
<VbenInput
|
<Input
|
||||||
|
v-bind="$attrs"
|
||||||
v-model="modelValue"
|
v-model="modelValue"
|
||||||
v-bind="{ ...forward, ...$attrs }"
|
:class="cn(props.class)"
|
||||||
:type="show ? 'text' : 'password'"
|
:type="show ? 'text' : 'password'"
|
||||||
>
|
/>
|
||||||
<template v-if="passwordStrength">
|
<template v-if="passwordStrength">
|
||||||
<PasswordStrength :password="modelValue" />
|
<PasswordStrength :password="modelValue" />
|
||||||
<p
|
<p v-if="slots.strengthText" class="text-muted-foreground mt-1.5 text-xs">
|
||||||
v-if="slots.strengthText"
|
<slot name="strengthText"> </slot>
|
||||||
class="text-muted-foreground mt-1.5 text-xs"
|
</p>
|
||||||
>
|
</template>
|
||||||
<slot name="strengthText"> </slot>
|
|
||||||
</p>
|
|
||||||
</template>
|
|
||||||
</VbenInput>
|
|
||||||
<div
|
<div
|
||||||
class="hover:text-foreground text-foreground/60 absolute inset-y-0 right-0 top-3 flex cursor-pointer pr-3 text-lg leading-5"
|
:class="{
|
||||||
|
'top-3': !!passwordStrength,
|
||||||
|
'top-1/2 -translate-y-1/2 items-center': !passwordStrength,
|
||||||
|
}"
|
||||||
|
class="hover:text-foreground text-foreground/60 absolute inset-y-0 right-0 flex cursor-pointer pr-3 text-lg leading-5"
|
||||||
@click="show = !show"
|
@click="show = !show"
|
||||||
>
|
>
|
||||||
<Eye v-if="show" class="size-4" />
|
<Eye v-if="show" class="size-4" />
|
||||||
|
@ -1,2 +0,0 @@
|
|||||||
export { default as VbenInput } from './input.vue';
|
|
||||||
export type * from './types';
|
|
@ -1,53 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import type { InputProps } from './types';
|
|
||||||
|
|
||||||
import { computed } from 'vue';
|
|
||||||
|
|
||||||
defineOptions({
|
|
||||||
inheritAttrs: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const props = defineProps<InputProps>();
|
|
||||||
|
|
||||||
const modelValue = defineModel<number | string>();
|
|
||||||
|
|
||||||
const inputClass = computed(() => {
|
|
||||||
if (props.status === 'error') {
|
|
||||||
return 'border-destructive';
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="relative mb-6">
|
|
||||||
<label
|
|
||||||
v-if="!label"
|
|
||||||
:for="name"
|
|
||||||
class="mb-2 block text-sm font-medium dark:text-white"
|
|
||||||
>
|
|
||||||
{{ label }}
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
:id="name"
|
|
||||||
v-model="modelValue"
|
|
||||||
:class="[props.class, inputClass]"
|
|
||||||
autocomplete="off"
|
|
||||||
class="border-input bg-input-background ring-offset-background placeholder:text-muted-foreground/60 focus-visible:ring-ring focus:border-primary flex h-10 w-full rounded-md border p-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
|
||||||
required
|
|
||||||
type="text"
|
|
||||||
v-bind="$attrs"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<slot></slot>
|
|
||||||
|
|
||||||
<Transition name="slide-up">
|
|
||||||
<p
|
|
||||||
v-if="status === 'error'"
|
|
||||||
class="text-destructive bottom-130 absolute mt-1 text-xs"
|
|
||||||
>
|
|
||||||
{{ errorTip }}
|
|
||||||
</p>
|
|
||||||
</Transition>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
@ -1,25 +0,0 @@
|
|||||||
interface InputProps {
|
|
||||||
class?: any;
|
|
||||||
/**
|
|
||||||
* 错误提示信息
|
|
||||||
*/
|
|
||||||
errorTip?: string;
|
|
||||||
/**
|
|
||||||
* 输入框的 label
|
|
||||||
*/
|
|
||||||
label?: string;
|
|
||||||
/**
|
|
||||||
* 输入框的 name
|
|
||||||
*/
|
|
||||||
name?: string;
|
|
||||||
/**
|
|
||||||
* 是否显示密码强度
|
|
||||||
*/
|
|
||||||
passwordStrength?: boolean;
|
|
||||||
/**
|
|
||||||
* 输入框的校验状态
|
|
||||||
*/
|
|
||||||
status?: 'default' | 'error';
|
|
||||||
}
|
|
||||||
|
|
||||||
export type { InputProps };
|
|
@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { cn } from '@vben-core/shared';
|
import { cn } from '@vben-core/shared/utils';
|
||||||
|
|
||||||
import { Primitive, type PrimitiveProps } from 'radix-vue';
|
import { Primitive, type PrimitiveProps } from 'radix-vue';
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ import type { MenuRecordBadgeRaw } from '@vben-core/typings';
|
|||||||
|
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
|
||||||
import { isValidColor } from '@vben-core/shared';
|
import { isValidColor } from '@vben-core/shared/color';
|
||||||
|
|
||||||
import BadgeDot from './menu-badge-dot.vue';
|
import BadgeDot from './menu-badge-dot.vue';
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { PinInputProps } from './types';
|
import type { PinInputProps } from './types';
|
||||||
|
|
||||||
import { computed, ref, watch } from 'vue';
|
import { computed, onBeforeUnmount, ref, useId, watch } from 'vue';
|
||||||
|
|
||||||
import { VbenButton } from '../button';
|
import { VbenButton } from '../button';
|
||||||
import { PinInput, PinInputGroup, PinInputInput } from '../ui/pin-input';
|
import { PinInput, PinInputGroup, PinInputInput } from '../ui/pin-input';
|
||||||
@ -14,21 +14,27 @@ const props = withDefaults(defineProps<PinInputProps>(), {
|
|||||||
btnLoading: false,
|
btnLoading: false,
|
||||||
codeLength: 6,
|
codeLength: 6,
|
||||||
handleSendCode: async () => {},
|
handleSendCode: async () => {},
|
||||||
|
maxTime: 60,
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
complete: [];
|
complete: [];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const timer = ref<ReturnType<typeof setTimeout>>();
|
||||||
|
|
||||||
const modelValue = defineModel<string>();
|
const modelValue = defineModel<string>();
|
||||||
|
|
||||||
const inputValue = ref<string[]>([]);
|
const inputValue = ref<string[]>([]);
|
||||||
|
const countdown = ref(0);
|
||||||
|
|
||||||
const inputClass = computed(() => {
|
const btnText = computed(() => {
|
||||||
if (props.status === 'error') {
|
const countdownValue = countdown.value;
|
||||||
return 'border-destructive';
|
return props.createText?.(countdownValue);
|
||||||
}
|
});
|
||||||
return '';
|
|
||||||
|
const btnLoading = computed(() => {
|
||||||
|
return props.loading || countdown.value > 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
@ -42,45 +48,58 @@ function handleComplete(e: string[]) {
|
|||||||
modelValue.value = e.join('');
|
modelValue.value = e.join('');
|
||||||
emit('complete');
|
emit('complete');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleSend(e: Event) {
|
||||||
|
e?.preventDefault();
|
||||||
|
await props.handleSendCode();
|
||||||
|
countdown.value = props.maxTime;
|
||||||
|
startCountdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
function startCountdown() {
|
||||||
|
if (countdown.value > 0) {
|
||||||
|
timer.value = setTimeout(() => {
|
||||||
|
countdown.value--;
|
||||||
|
startCountdown();
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
countdown.value = 0;
|
||||||
|
clearTimeout(timer.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const id = useId();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="relative mb-6">
|
<PinInput
|
||||||
<label :for="name" class="mb-2 block text-sm font-medium">
|
:id="id"
|
||||||
{{ label }}
|
v-model="inputValue"
|
||||||
</label>
|
class="flex w-full justify-between"
|
||||||
<PinInput
|
otp
|
||||||
:id="name"
|
placeholder="○"
|
||||||
v-model="inputValue"
|
type="number"
|
||||||
:class="inputClass"
|
@complete="handleComplete"
|
||||||
class="flex justify-between"
|
>
|
||||||
otp
|
<div class="relative flex w-full">
|
||||||
placeholder="○"
|
<PinInputGroup class="mr-2">
|
||||||
type="number"
|
|
||||||
@complete="handleComplete"
|
|
||||||
>
|
|
||||||
<PinInputGroup>
|
|
||||||
<PinInputInput
|
<PinInputInput
|
||||||
v-for="(id, index) in codeLength"
|
v-for="(item, index) in codeLength"
|
||||||
:key="id"
|
:key="item"
|
||||||
:index="index"
|
:index="index"
|
||||||
/>
|
/>
|
||||||
</PinInputGroup>
|
</PinInputGroup>
|
||||||
<VbenButton
|
<VbenButton
|
||||||
:loading="btnLoading"
|
:loading="btnLoading"
|
||||||
class="w-[300px] xl:w-full"
|
class="flex-grow"
|
||||||
size="lg"
|
size="lg"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@click="handleSendCode"
|
@click="handleSend"
|
||||||
>
|
>
|
||||||
{{ btnText }}
|
{{ btnText }}
|
||||||
</VbenButton>
|
</VbenButton>
|
||||||
</PinInput>
|
</div>
|
||||||
<p
|
</PinInput>
|
||||||
v-if="status === 'error'"
|
|
||||||
class="text-destructive bottom-130 absolute mt-1 text-xs"
|
|
||||||
>
|
|
||||||
{{ errorTip }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
@ -1,38 +1,26 @@
|
|||||||
interface PinInputProps {
|
interface PinInputProps {
|
||||||
/**
|
|
||||||
* 发送验证码按钮loading
|
|
||||||
*/
|
|
||||||
btnLoading?: boolean;
|
|
||||||
/**
|
|
||||||
* 发送验证码按钮文本
|
|
||||||
*/
|
|
||||||
btnText?: string;
|
|
||||||
class?: any;
|
class?: any;
|
||||||
/**
|
/**
|
||||||
* 验证码长度
|
* 验证码长度
|
||||||
*/
|
*/
|
||||||
codeLength?: number;
|
codeLength?: number;
|
||||||
/**
|
/**
|
||||||
* 错误提示信息
|
* 发送验证码按钮文本
|
||||||
*/
|
*/
|
||||||
errorTip?: string;
|
createText?: (countdown: number) => string;
|
||||||
/**
|
/**
|
||||||
* 自定义验证码发送逻辑
|
* 自定义验证码发送逻辑
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
handleSendCode?: () => Promise<void>;
|
handleSendCode?: () => Promise<void>;
|
||||||
/**
|
/**
|
||||||
* 输入框的 label
|
* 发送验证码按钮loading
|
||||||
*/
|
*/
|
||||||
label: string;
|
loading?: boolean;
|
||||||
/**
|
/**
|
||||||
* 输入框的 name
|
* 最大重试时间
|
||||||
*/
|
*/
|
||||||
name: string;
|
maxTime?: number;
|
||||||
/**
|
|
||||||
* 输入框的校验状态
|
|
||||||
*/
|
|
||||||
status?: 'default' | 'error';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type { PinInputProps };
|
export type { PinInputProps };
|
||||||
|
@ -1,26 +1,39 @@
|
|||||||
<script setup lang="ts">
|
<script lang="ts">
|
||||||
import type { Component } from 'vue';
|
import type { Component, PropType } from 'vue';
|
||||||
|
import { defineComponent, h } from 'vue';
|
||||||
|
|
||||||
defineOptions({
|
import { isFunction, isObject } from '@vben-core/shared/utils';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
name: 'RenderContent',
|
name: 'RenderContent',
|
||||||
});
|
props: {
|
||||||
|
content: {
|
||||||
const props = withDefaults(
|
default: undefined as
|
||||||
defineProps<{
|
| PropType<(() => any) | Component | string>
|
||||||
content: Component | string | undefined;
|
| undefined,
|
||||||
props?: Record<string, any>;
|
type: [Object, String, Function],
|
||||||
}>(),
|
},
|
||||||
{
|
|
||||||
props: () => ({}),
|
|
||||||
},
|
},
|
||||||
);
|
setup(props, { attrs, slots }) {
|
||||||
|
return () => {
|
||||||
const isComponent = typeof props.content === 'object' && props.content !== null;
|
if (!props.content) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const isComponent =
|
||||||
|
(isObject(props.content) || isFunction(props.content)) &&
|
||||||
|
props.content !== null;
|
||||||
|
if (!isComponent) {
|
||||||
|
return props.content;
|
||||||
|
}
|
||||||
|
return h(props.content as never, {
|
||||||
|
...attrs,
|
||||||
|
props: {
|
||||||
|
...props,
|
||||||
|
...attrs,
|
||||||
|
},
|
||||||
|
slots,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
|
||||||
<component :is="content" v-bind="props" v-if="isComponent" />
|
|
||||||
<template v-else-if="!isComponent">
|
|
||||||
{{ content }}
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
import { cn } from '@vben-core/shared';
|
import { cn } from '@vben-core/shared/utils';
|
||||||
|
|
||||||
import { ScrollArea, ScrollBar } from '../ui/scroll-area';
|
import { ScrollArea, ScrollBar } from '../ui/scroll-area';
|
||||||
|
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user