Merge remote-tracking branch 'vben/main' into vben-dev
# Conflicts: # .gitignore # .husky/pre-commit # .vscode/extensions.json # .vscode/settings.json # README.ja-JP.md # README.md # README.zh-CN.md # apps/backend-mock/api/auth/codes.ts # apps/backend-mock/api/auth/login.post.ts # apps/backend-mock/api/auth/logout.post.ts # apps/backend-mock/api/auth/refresh.post.ts # apps/backend-mock/api/menu/all.ts # apps/backend-mock/api/status.ts # apps/backend-mock/api/table/list.ts # apps/backend-mock/api/test.get.ts # apps/backend-mock/api/test.post.ts # apps/backend-mock/api/user/info.ts # apps/backend-mock/middleware/1.api.ts # apps/backend-mock/nitro.config.ts # apps/backend-mock/package.json # apps/backend-mock/routes/[...].ts # apps/backend-mock/tsconfig.json # apps/backend-mock/utils/cookie-utils.ts # apps/backend-mock/utils/jwt-utils.ts # apps/backend-mock/utils/mock-data.ts # apps/backend-mock/utils/response.ts # apps/web-antd/.env # apps/web-antd/src/adapter/component/index.ts # apps/web-antd/src/adapter/form.ts # apps/web-antd/src/api/request.ts # apps/web-antd/src/layouts/auth.vue # apps/web-antd/src/layouts/basic.vue # apps/web-antd/src/preferences.ts # apps/web-antd/src/router/guard.ts # apps/web-antd/src/router/routes/modules/demos.ts # apps/web-antd/src/store/auth.ts # apps/web-antd/src/views/demos/antd/index.vue # apps/web-antdv-next/vite.config.ts # apps/web-ele/.env # apps/web-ele/index.html # apps/web-ele/package.json # apps/web-ele/src/adapter/component/index.ts # apps/web-ele/src/adapter/form.ts # apps/web-ele/src/adapter/vxe-table.ts # apps/web-ele/src/api/request.ts # apps/web-ele/src/bootstrap.ts # apps/web-ele/src/layouts/auth.vue # apps/web-ele/src/layouts/basic.vue # apps/web-ele/src/locales/index.ts # apps/web-ele/src/locales/langs/en-US/demos.json # apps/web-ele/src/locales/langs/en-US/page.json # apps/web-ele/src/locales/langs/zh-CN/demos.json # apps/web-ele/src/locales/langs/zh-CN/page.json # apps/web-ele/src/preferences.ts # apps/web-ele/src/router/guard.ts # apps/web-ele/src/router/routes/core.ts # apps/web-ele/src/router/routes/modules/dashboard.ts # apps/web-ele/src/router/routes/modules/demos.ts # apps/web-ele/src/router/routes/modules/vben.ts # apps/web-ele/src/store/auth.ts # apps/web-ele/src/views/_core/authentication/code-login.vue # apps/web-ele/src/views/_core/authentication/forget-password.vue # apps/web-ele/src/views/_core/authentication/register.vue # apps/web-ele/src/views/dashboard/analytics/analytics-trends.vue # apps/web-ele/src/views/dashboard/analytics/analytics-visits-data.vue # apps/web-ele/src/views/dashboard/analytics/analytics-visits-sales.vue # apps/web-ele/src/views/dashboard/analytics/analytics-visits-source.vue # apps/web-ele/src/views/dashboard/analytics/analytics-visits.vue # apps/web-ele/src/views/dashboard/analytics/index.vue # apps/web-ele/src/views/demos/element/index.vue # apps/web-ele/src/views/demos/form/basic.vue # apps/web-ele/tsconfig.json # apps/web-ele/tsconfig.node.json # apps/web-naive/.env # apps/web-naive/index.html # apps/web-naive/package.json # apps/web-naive/src/adapter/component/index.ts # apps/web-naive/src/adapter/form.ts # apps/web-naive/src/adapter/vxe-table.ts # apps/web-naive/src/api/request.ts # apps/web-naive/src/bootstrap.ts # apps/web-naive/src/layouts/auth.vue # apps/web-naive/src/layouts/basic.vue # apps/web-naive/src/locales/index.ts # apps/web-naive/src/locales/langs/en-US/demos.json # apps/web-naive/src/locales/langs/en-US/page.json # apps/web-naive/src/locales/langs/zh-CN/demos.json # apps/web-naive/src/locales/langs/zh-CN/page.json # apps/web-naive/src/preferences.ts # apps/web-naive/src/router/guard.ts # apps/web-naive/src/router/routes/core.ts # apps/web-naive/src/router/routes/modules/dashboard.ts # apps/web-naive/src/router/routes/modules/demos.ts # apps/web-naive/src/router/routes/modules/vben.ts # apps/web-naive/src/store/auth.ts # apps/web-naive/src/views/_core/authentication/code-login.vue # apps/web-naive/src/views/_core/authentication/forget-password.vue # apps/web-naive/src/views/_core/authentication/register.vue # apps/web-naive/src/views/dashboard/analytics/analytics-trends.vue # apps/web-naive/src/views/dashboard/analytics/analytics-visits-data.vue # apps/web-naive/src/views/dashboard/analytics/analytics-visits-sales.vue # apps/web-naive/src/views/dashboard/analytics/analytics-visits-source.vue # apps/web-naive/src/views/dashboard/analytics/analytics-visits.vue # apps/web-naive/src/views/dashboard/analytics/index.vue # apps/web-naive/src/views/demos/form/basic.vue # apps/web-naive/src/views/demos/naive/index.vue # apps/web-naive/tsconfig.json # apps/web-naive/tsconfig.node.json # apps/web-naive/vite.config.ts # docs/.vitepress/components/demo-preview.vue # docs/.vitepress/components/preview-group.vue # docs/.vitepress/config/en.mts # docs/.vitepress/config/index.mts # docs/.vitepress/config/plugins/demo-preview.ts # docs/.vitepress/config/shared.mts # docs/.vitepress/config/zh.mts # docs/.vitepress/theme/components/site-layout.vue # docs/.vitepress/theme/components/vben-contributors.vue # docs/.vitepress/theme/styles/variables.css # docs/package.json # docs/src/_env/adapter/component.ts # docs/src/_env/adapter/form.ts # docs/src/_env/adapter/vxe-table.ts # docs/src/commercial/community.md # docs/src/components/common-ui/vben-count-to-animator.md # docs/src/components/common-ui/vben-drawer.md # docs/src/components/common-ui/vben-form.md # docs/src/components/common-ui/vben-modal.md # docs/src/components/common-ui/vben-vxe-table.md # docs/src/components/layout-ui/page.md # docs/src/demos/vben-drawer/auto-height/drawer.vue # docs/src/demos/vben-drawer/basic/index.vue # docs/src/demos/vben-drawer/dynamic/index.vue # docs/src/demos/vben-drawer/shared-data/drawer.vue # docs/src/demos/vben-drawer/shared-data/index.vue # docs/src/demos/vben-form/api/index.vue # docs/src/demos/vben-form/basic/index.vue # docs/src/demos/vben-form/custom/index.vue # docs/src/demos/vben-form/dynamic/index.vue # docs/src/demos/vben-form/query/index.vue # docs/src/demos/vben-form/rules/index.vue # docs/src/demos/vben-modal/auto-height/modal.vue # docs/src/demos/vben-modal/basic/index.vue # docs/src/demos/vben-modal/dynamic/index.vue # docs/src/demos/vben-modal/shared-data/index.vue # docs/src/demos/vben-modal/shared-data/modal.vue # docs/src/demos/vben-vxe-table/basic/index.vue # docs/src/demos/vben-vxe-table/custom-cell/index.vue # docs/src/demos/vben-vxe-table/edit-row/index.vue # docs/src/demos/vben-vxe-table/fixed/index.vue # docs/src/demos/vben-vxe-table/form/index.vue # docs/src/demos/vben-vxe-table/remote/index.vue # docs/src/demos/vben-vxe-table/tree/index.vue # docs/src/demos/vben-vxe-table/virtual/index.vue # docs/src/en/guide/essentials/development.md # docs/src/en/guide/essentials/external-module.md # docs/src/en/guide/essentials/route.md # docs/src/en/guide/essentials/server.md # docs/src/en/guide/essentials/settings.md # docs/src/en/guide/in-depth/access.md # docs/src/en/guide/in-depth/loading.md # docs/src/en/guide/in-depth/locale.md # docs/src/en/guide/in-depth/theme.md # docs/src/en/guide/introduction/quick-start.md # docs/src/en/guide/introduction/thin.md # docs/src/en/guide/introduction/vben.md # docs/src/en/guide/other/faq.md # docs/src/en/guide/project/dir.md # docs/src/en/guide/project/standard.md # docs/src/en/guide/project/tailwindcss.md # docs/src/en/index.md # docs/src/friend-links/index.md # docs/src/guide/essentials/build.md # docs/src/guide/essentials/development.md # docs/src/guide/essentials/external-module.md # docs/src/guide/essentials/route.md # docs/src/guide/essentials/server.md # docs/src/guide/essentials/settings.md # docs/src/guide/in-depth/access.md # docs/src/guide/in-depth/check-updates.md # docs/src/guide/in-depth/loading.md # docs/src/guide/in-depth/locale.md # docs/src/guide/in-depth/theme.md # docs/src/guide/introduction/quick-start.md # docs/src/guide/introduction/thin.md # docs/src/guide/introduction/vben.md # docs/src/guide/introduction/why.md # docs/src/guide/other/faq.md # docs/src/guide/project/dir.md # docs/src/guide/project/standard.md # docs/src/guide/project/tailwindcss.md # docs/src/index.md # docs/tsconfig.json # package.json # packages/@core/ui-kit/shadcn-ui/src/components/logo/logo.vue # packages/constants/src/core.ts # packages/effects/common-ui/src/components/index.ts # packages/effects/common-ui/src/ui/dashboard/analysis/analysis-overview.vue # packages/effects/common-ui/src/ui/dashboard/workbench/workbench-header.vue # packages/effects/common-ui/src/ui/dashboard/workbench/workbench-project.vue # packages/effects/layouts/src/authentication/authentication.vue # packages/effects/layouts/src/basic/layout.vue # packages/effects/layouts/src/widgets/notification/notification.vue # packages/effects/layouts/src/widgets/notification/types.ts # packages/icons/src/iconify/index.ts # packages/icons/src/svg/index.ts # packages/stores/src/modules/user.ts # playground/src/adapter/component/index.ts # playground/src/store/auth.ts # pnpm-lock.yaml # vben-admin.code-workspace
This commit is contained in:
229
playground/src/views/examples/button-group/index.vue
Normal file
229
playground/src/views/examples/button-group/index.vue
Normal file
@@ -0,0 +1,229 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import { reactive, ref } from 'vue';
|
||||
|
||||
import {
|
||||
Page,
|
||||
VbenButton,
|
||||
VbenButtonGroup,
|
||||
VbenCheckButtonGroup,
|
||||
} from '@vben/common-ui';
|
||||
import { LoaderCircle, Square, SquareCheckBig } from '@vben/icons';
|
||||
|
||||
import { Button, Card, message } from 'antdv-next';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
|
||||
const radioValue = ref<string | undefined>('a');
|
||||
const checkValue = ref(['a', 'b']);
|
||||
|
||||
const options = [
|
||||
{ label: '选项1', value: 'a' },
|
||||
{ label: '选项2', value: 'b', num: 999 },
|
||||
{ label: '选项3', value: 'c' },
|
||||
{ label: '选项4', value: 'd' },
|
||||
{ label: '选项5', value: 'e' },
|
||||
{ label: '选项6', value: 'f' },
|
||||
];
|
||||
|
||||
function resetValues() {
|
||||
radioValue.value = undefined;
|
||||
checkValue.value = [];
|
||||
}
|
||||
|
||||
function beforeChange(v: any, isChecked: boolean) {
|
||||
return new Promise((resolve) => {
|
||||
message.loading({
|
||||
content: `正在设置${v}为${isChecked ? '选中' : '未选中'}...`,
|
||||
duration: 0,
|
||||
key: 'beforeChange',
|
||||
});
|
||||
setTimeout(() => {
|
||||
message.success({ content: `${v} 已设置成功`, key: 'beforeChange' });
|
||||
resolve(true);
|
||||
}, 2000);
|
||||
});
|
||||
}
|
||||
|
||||
const compProps = reactive({
|
||||
beforeChange: undefined,
|
||||
disabled: false,
|
||||
gap: 0,
|
||||
showIcon: true,
|
||||
size: 'middle',
|
||||
allowClear: false,
|
||||
} as Recordable<any>);
|
||||
|
||||
const [Form] = useVbenForm({
|
||||
handleValuesChange(values) {
|
||||
Object.keys(values).forEach((k) => {
|
||||
if (k === 'beforeChange') {
|
||||
compProps[k] = values[k] ? beforeChange : undefined;
|
||||
} else {
|
||||
compProps[k] = values[k];
|
||||
}
|
||||
});
|
||||
},
|
||||
commonConfig: {
|
||||
labelWidth: 150,
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '大', value: 'large' },
|
||||
{ label: '中', value: 'middle' },
|
||||
{ label: '小', value: 'small' },
|
||||
],
|
||||
},
|
||||
defaultValue: compProps.size,
|
||||
fieldName: 'size',
|
||||
label: '尺寸',
|
||||
},
|
||||
{
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '无', value: 0 },
|
||||
{ label: '小', value: 5 },
|
||||
{ label: '中', value: 15 },
|
||||
{ label: '大', value: 30 },
|
||||
],
|
||||
},
|
||||
defaultValue: compProps.gap,
|
||||
fieldName: 'gap',
|
||||
label: '间距',
|
||||
},
|
||||
{
|
||||
component: 'Switch',
|
||||
defaultValue: compProps.showIcon,
|
||||
fieldName: 'showIcon',
|
||||
label: '显示图标',
|
||||
},
|
||||
{
|
||||
component: 'Switch',
|
||||
defaultValue: compProps.disabled,
|
||||
fieldName: 'disabled',
|
||||
label: '禁用',
|
||||
},
|
||||
{
|
||||
component: 'Switch',
|
||||
defaultValue: false,
|
||||
fieldName: 'beforeChange',
|
||||
label: '前置回调',
|
||||
},
|
||||
{
|
||||
component: 'Switch',
|
||||
defaultValue: false,
|
||||
fieldName: 'allowClear',
|
||||
label: '允许清除',
|
||||
help: '单选时是否允许取消选中(值为undefined)',
|
||||
},
|
||||
{
|
||||
component: 'InputNumber',
|
||||
defaultValue: 0,
|
||||
fieldName: 'maxCount',
|
||||
label: '最大选中数量',
|
||||
help: '多选时有效,0表示不限制',
|
||||
},
|
||||
],
|
||||
showDefaultActions: false,
|
||||
submitOnChange: true,
|
||||
});
|
||||
|
||||
function onBtnClick(value: any) {
|
||||
const opt = options.find((o) => o.value === value);
|
||||
if (opt) {
|
||||
message.success(`点击了按钮${opt.label},value = ${value}`);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<Page
|
||||
title="VbenButtonGroup 按钮组"
|
||||
description="VbenButtonGroup是一个按钮容器,用于包裹一组按钮,协调整体样式。VbenCheckButtonGroup则可以作为一个表单组件,提供单选或多选功能"
|
||||
>
|
||||
<Card title="基本用法">
|
||||
<template #extra>
|
||||
<Button type="primary" @click="resetValues">清空值</Button>
|
||||
</template>
|
||||
<p class="mt-4">按钮组:</p>
|
||||
<div class="mt-2 flex flex-col gap-2">
|
||||
<VbenButtonGroup v-bind="compProps" border>
|
||||
<VbenButton
|
||||
v-for="btn in options"
|
||||
:key="btn.value"
|
||||
variant="link"
|
||||
@click="onBtnClick(btn.value)"
|
||||
>
|
||||
{{ btn.label }}
|
||||
</VbenButton>
|
||||
</VbenButtonGroup>
|
||||
<VbenButtonGroup v-bind="compProps" border>
|
||||
<VbenButton
|
||||
v-for="btn in options"
|
||||
:key="btn.value"
|
||||
variant="outline"
|
||||
@click="onBtnClick(btn.value)"
|
||||
>
|
||||
{{ btn.label }}
|
||||
</VbenButton>
|
||||
</VbenButtonGroup>
|
||||
</div>
|
||||
<p class="mt-4">单选:{{ radioValue }}</p>
|
||||
<div class="mt-2 flex flex-col gap-2">
|
||||
<VbenCheckButtonGroup
|
||||
v-model="radioValue"
|
||||
:options="options"
|
||||
v-bind="compProps"
|
||||
/>
|
||||
</div>
|
||||
<p class="mt-4">单选插槽:{{ radioValue }}</p>
|
||||
<div class="mt-2 flex flex-col gap-2">
|
||||
<VbenCheckButtonGroup
|
||||
v-model="radioValue"
|
||||
:options="options"
|
||||
v-bind="compProps"
|
||||
>
|
||||
<template #option="{ label, value, data }">
|
||||
<div class="flex items-center">
|
||||
<span>{{ label }}</span>
|
||||
<span class="ml-2 text-gray-400">{{ value }}</span>
|
||||
<span v-if="data.num" class="white ml-2">{{ data.num }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</VbenCheckButtonGroup>
|
||||
</div>
|
||||
<p class="mt-4">多选{{ checkValue }}</p>
|
||||
<div class="mt-2 flex flex-col gap-2">
|
||||
<VbenCheckButtonGroup
|
||||
v-model="checkValue"
|
||||
multiple
|
||||
:options="options"
|
||||
v-bind="compProps"
|
||||
/>
|
||||
</div>
|
||||
<p class="mt-4">自定义图标{{ checkValue }}</p>
|
||||
<div class="mt-2 flex flex-col gap-2">
|
||||
<VbenCheckButtonGroup
|
||||
v-model="checkValue"
|
||||
multiple
|
||||
:options="options"
|
||||
v-bind="compProps"
|
||||
>
|
||||
<template #icon="{ loading, checked }">
|
||||
<LoaderCircle class="animate-spin" v-if="loading" />
|
||||
<SquareCheckBig v-else-if="checked" />
|
||||
<Square v-else />
|
||||
</template>
|
||||
</VbenCheckButtonGroup>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card title="设置" class="mt-4">
|
||||
<Form />
|
||||
</Card>
|
||||
</Page>
|
||||
</template>
|
||||
@@ -5,7 +5,7 @@ import { reactive, ref } from 'vue';
|
||||
|
||||
import { Page, PointSelectionCaptcha } from '@vben/common-ui';
|
||||
|
||||
import { Card, Input, InputNumber, message, Switch } from 'ant-design-vue';
|
||||
import { Card, Input, InputNumber, message, Switch } from 'antdv-next';
|
||||
|
||||
import { $t } from '#/locales';
|
||||
|
||||
@@ -64,7 +64,7 @@ const handleClick = (point: CaptchaPoint) => {
|
||||
/>
|
||||
<div class="ml-8 flex w-96 items-center">
|
||||
<Switch
|
||||
v-model:checked="params.showHintImage"
|
||||
v-model="params.showHintImage"
|
||||
:checked-children="$t('examples.captcha.hintImage')"
|
||||
:un-checked-children="$t('examples.captcha.hintText')"
|
||||
class="mr-4 w-40"
|
||||
@@ -82,7 +82,7 @@ const handleClick = (point: CaptchaPoint) => {
|
||||
</div>
|
||||
|
||||
<Switch
|
||||
v-model:checked="params.showConfirm"
|
||||
v-model="params.showConfirm"
|
||||
:checked-children="$t('examples.captcha.showConfirm')"
|
||||
:un-checked-children="$t('examples.captcha.hideConfirm')"
|
||||
class="ml-8 w-28"
|
||||
|
||||
@@ -9,7 +9,7 @@ import { ref } from 'vue';
|
||||
import { Page, SliderCaptcha } from '@vben/common-ui';
|
||||
import { Bell, Sun } from '@vben/icons';
|
||||
|
||||
import { Button, Card, message } from 'ant-design-vue';
|
||||
import { Button, Card, message } from 'antdv-next';
|
||||
|
||||
function handleSuccess(data: CaptchaVerifyPassingData) {
|
||||
const { time } = data;
|
||||
@@ -33,7 +33,7 @@ const el6 = ref<SliderCaptchaActionType>();
|
||||
<template>
|
||||
<Page description="用于前端简单的拖动校验场景" title="滑块校验">
|
||||
<Card class="mb-5" title="基础示例">
|
||||
<div class="flex items-center justify-center p-4 px-[30%]">
|
||||
<div class="flex-center p-4 px-[30%]">
|
||||
<SliderCaptcha ref="el1" @success="handleSuccess" />
|
||||
<Button class="ml-2" type="primary" @click="handleBtnClick(el1)">
|
||||
还原
|
||||
@@ -41,7 +41,7 @@ const el6 = ref<SliderCaptchaActionType>();
|
||||
</div>
|
||||
</Card>
|
||||
<Card class="mb-5" title="自定义圆角">
|
||||
<div class="flex items-center justify-center p-4 px-[30%]">
|
||||
<div class="flex-center p-4 px-[30%]">
|
||||
<SliderCaptcha
|
||||
ref="el2"
|
||||
class="rounded-full"
|
||||
@@ -53,7 +53,7 @@ const el6 = ref<SliderCaptchaActionType>();
|
||||
</div>
|
||||
</Card>
|
||||
<Card class="mb-5" title="自定义背景色">
|
||||
<div class="flex items-center justify-center p-4 px-[30%]">
|
||||
<div class="flex-center p-4 px-[30%]">
|
||||
<SliderCaptcha
|
||||
ref="el3"
|
||||
:bar-style="{
|
||||
@@ -69,7 +69,7 @@ const el6 = ref<SliderCaptchaActionType>();
|
||||
</div>
|
||||
</Card>
|
||||
<Card class="mb-5" title="自定义拖拽图标">
|
||||
<div class="flex items-center justify-center p-4 px-[30%]">
|
||||
<div class="flex-center p-4 px-[30%]">
|
||||
<SliderCaptcha ref="el4" @success="handleSuccess">
|
||||
<template #actionIcon="{ isPassing }">
|
||||
<Bell v-if="isPassing" />
|
||||
@@ -82,7 +82,7 @@ const el6 = ref<SliderCaptchaActionType>();
|
||||
</div>
|
||||
</Card>
|
||||
<Card class="mb-5" title="自定义文本">
|
||||
<div class="flex items-center justify-center p-4 px-[30%]">
|
||||
<div class="flex-center p-4 px-[30%]">
|
||||
<SliderCaptcha
|
||||
ref="el5"
|
||||
success-text="成功"
|
||||
@@ -95,7 +95,7 @@ const el6 = ref<SliderCaptchaActionType>();
|
||||
</div>
|
||||
</Card>
|
||||
<Card class="mb-5" title="自定义内容(slot)">
|
||||
<div class="flex items-center justify-center p-4 px-[30%]">
|
||||
<div class="flex-center p-4 px-[30%]">
|
||||
<SliderCaptcha ref="el6" @success="handleSuccess">
|
||||
<template #text="{ isPassing }">
|
||||
<template v-if="isPassing">
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Page, SliderRotateCaptcha } from '@vben/common-ui';
|
||||
import { preferences } from '@vben/preferences';
|
||||
import { useUserStore } from '@vben/stores';
|
||||
|
||||
import { Card, message } from 'ant-design-vue';
|
||||
import { Card, message } from 'antdv-next';
|
||||
|
||||
const userStore = useUserStore();
|
||||
function handleSuccess() {
|
||||
@@ -20,7 +20,7 @@ const avatar = computed(() => {
|
||||
<template>
|
||||
<Page description="用于前端简单的拖动校验场景" title="滑块旋转校验">
|
||||
<Card class="mb-5" title="基本示例">
|
||||
<div class="flex items-center justify-center p-4">
|
||||
<div class="flex-center p-4">
|
||||
<SliderRotateCaptcha :src="avatar" @success="handleSuccess" />
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
import { Page, SliderTranslateCaptcha } from '@vben/common-ui';
|
||||
|
||||
import { Card, message } from 'antdv-next';
|
||||
|
||||
function handleSuccess() {
|
||||
message.success('success!');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page
|
||||
description="用于前端简单的拼图滑块水平拖动校验场景"
|
||||
title="拼图滑块校验"
|
||||
>
|
||||
<Card class="mb-5" title="基本示例">
|
||||
<div class="flex-center p-4">
|
||||
<SliderTranslateCaptcha
|
||||
src="https://unpkg.com/@vbenjs/static-source@0.1.7/source/pro-avatar.webp"
|
||||
:canvas-width="420"
|
||||
:canvas-height="420"
|
||||
@success="handleSuccess"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</Page>
|
||||
</template>
|
||||
57
playground/src/views/examples/context-menu/index.vue
Normal file
57
playground/src/views/examples/context-menu/index.vue
Normal file
@@ -0,0 +1,57 @@
|
||||
<script setup lang="ts">
|
||||
import { Page, VbenContextMenu } from '@vben/common-ui';
|
||||
|
||||
import { Button, Card, message } from 'antdv-next';
|
||||
|
||||
const needHidden = (role: string) => {
|
||||
return role === 'user';
|
||||
};
|
||||
|
||||
const contextMenus = () => {
|
||||
return [
|
||||
{
|
||||
text: '刷新',
|
||||
key: 'refresh',
|
||||
handler: (data: any) => {
|
||||
message.success('刷新成功', data);
|
||||
},
|
||||
hidden: needHidden('admin'),
|
||||
},
|
||||
{
|
||||
text: '关闭当前',
|
||||
key: 'close-current',
|
||||
handler: (data: any) => {
|
||||
message.success('关闭当前', data);
|
||||
},
|
||||
hidden: needHidden('user'),
|
||||
},
|
||||
{
|
||||
text: '关闭其他',
|
||||
key: 'close-other',
|
||||
handler: (data: any) => {
|
||||
message.success('关闭其他', data);
|
||||
},
|
||||
},
|
||||
{
|
||||
text: '关闭所有',
|
||||
key: 'close-all',
|
||||
handler: (data: any) => {
|
||||
message.success('关闭所有', data);
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page title="Context Menu 上下文菜单">
|
||||
<Card title="基本使用">
|
||||
<div>一共四个菜单(刷新、关闭当前、关闭其他、关闭所有)</div>
|
||||
<br />
|
||||
<br />
|
||||
<VbenContextMenu :menus="contextMenus" :modal="true" item-class="pr-6">
|
||||
<Button> 右键点击我打开上下文菜单(有隐藏项) </Button>
|
||||
</VbenContextMenu>
|
||||
</Card>
|
||||
</Page>
|
||||
</template>
|
||||
175
playground/src/views/examples/count-to/index.vue
Normal file
175
playground/src/views/examples/count-to/index.vue
Normal file
@@ -0,0 +1,175 @@
|
||||
<script lang="ts" setup>
|
||||
import type { CountToProps, TransitionPresets } from '@vben/common-ui';
|
||||
|
||||
import { reactive } from 'vue';
|
||||
|
||||
import { CountTo, Page, TransitionPresetsKeys } from '@vben/common-ui';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Form,
|
||||
FormItem,
|
||||
Input,
|
||||
InputNumber,
|
||||
message,
|
||||
Row,
|
||||
Select,
|
||||
Switch,
|
||||
} from 'antdv-next';
|
||||
|
||||
const props = reactive<CountToProps & { transition: TransitionPresets }>({
|
||||
decimal: '.',
|
||||
decimals: 2,
|
||||
decimalStyle: {
|
||||
fontSize: 'small',
|
||||
fontStyle: 'italic',
|
||||
},
|
||||
delay: 0,
|
||||
disabled: false,
|
||||
duration: 2000,
|
||||
endVal: 100_000,
|
||||
mainStyle: {
|
||||
color: 'hsl(var(--primary))',
|
||||
fontSize: 'xx-large',
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
prefix: '¥',
|
||||
prefixStyle: {
|
||||
paddingRight: '0.5rem',
|
||||
},
|
||||
separator: ',',
|
||||
startVal: 0,
|
||||
suffix: '元',
|
||||
suffixStyle: {
|
||||
paddingLeft: '0.5rem',
|
||||
},
|
||||
transition: 'easeOutQuart',
|
||||
});
|
||||
|
||||
function changeNumber() {
|
||||
props.endVal =
|
||||
Math.floor(Math.random() * 100_000_000) / 10 ** (props.decimals || 0);
|
||||
}
|
||||
|
||||
function openDocumentation() {
|
||||
window.open('https://vueuse.org/core/useTransition/', '_blank');
|
||||
}
|
||||
|
||||
function onStarted() {
|
||||
message.loading({
|
||||
content: '动画已开始',
|
||||
duration: 0,
|
||||
key: 'animator-info',
|
||||
});
|
||||
}
|
||||
|
||||
function onFinished() {
|
||||
message.success({
|
||||
content: '动画已结束',
|
||||
duration: 2,
|
||||
key: 'animator-info',
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<Page title="CountTo" description="数字滚动动画组件。使用">
|
||||
<template #description>
|
||||
<span>
|
||||
使用useTransition封装的数字滚动动画组件,每次改变当前值都会产生过渡动画。
|
||||
</span>
|
||||
<Button type="link" @click="openDocumentation">
|
||||
查看useTransition文档
|
||||
</Button>
|
||||
</template>
|
||||
<Card title="基本用法">
|
||||
<div class="flex-center w-full pb-4">
|
||||
<CountTo v-bind="props" @started="onStarted" @finished="onFinished" />
|
||||
</div>
|
||||
<Form :model="props">
|
||||
<Row :gutter="20">
|
||||
<Col :span="8">
|
||||
<FormItem label="初始值" name="startVal">
|
||||
<InputNumber v-model:value="props.startVal" />
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col :span="8">
|
||||
<FormItem label="当前值" name="endVal">
|
||||
<InputNumber
|
||||
v-model:value="props.endVal"
|
||||
class="w-full"
|
||||
:precision="props.decimals"
|
||||
>
|
||||
<template #addonAfter>
|
||||
<IconifyIcon
|
||||
v-tippy="`设置一个随机值`"
|
||||
class="size-5 cursor-pointer outline-hidden"
|
||||
icon="ix:random-filled"
|
||||
@click="changeNumber"
|
||||
/>
|
||||
</template>
|
||||
</InputNumber>
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col :span="8">
|
||||
<FormItem label="禁用动画" name="disabled">
|
||||
<Switch v-model="props.disabled" />
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col :span="8">
|
||||
<FormItem label="延迟动画" name="delay">
|
||||
<InputNumber v-model:value="props.delay" :min="0" />
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col :span="8">
|
||||
<FormItem label="持续时间" name="duration">
|
||||
<InputNumber v-model:value="props.duration" :min="0" />
|
||||
</FormItem>
|
||||
</Col>
|
||||
|
||||
<Col :span="8">
|
||||
<FormItem label="小数位数" name="decimals">
|
||||
<InputNumber
|
||||
v-model:value="props.decimals"
|
||||
:min="0"
|
||||
:precision="0"
|
||||
/>
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col :span="8">
|
||||
<FormItem label="分隔符" name="separator">
|
||||
<Input v-model:value="props.separator" />
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col :span="8">
|
||||
<FormItem label="小数点" name="decimal">
|
||||
<Input v-model:value="props.decimal" />
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col :span="8">
|
||||
<FormItem label="动画" name="transition">
|
||||
<Select
|
||||
v-model:value="props.transition"
|
||||
:options="
|
||||
TransitionPresetsKeys.map((p) => ({ label: p, value: p }))
|
||||
"
|
||||
/>
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col :span="8">
|
||||
<FormItem label="前缀" name="prefix">
|
||||
<Input v-model:value="props.prefix" />
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col :span="8">
|
||||
<FormItem label="后缀" name="suffix">
|
||||
<Input v-model:value="props.suffix" />
|
||||
</FormItem>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
</Card>
|
||||
</Page>
|
||||
</template>
|
||||
144
playground/src/views/examples/cropper/index.vue
Normal file
144
playground/src/views/examples/cropper/index.vue
Normal file
@@ -0,0 +1,144 @@
|
||||
<script lang="ts" setup>
|
||||
import type { UploadChangeParam } from 'antdv-next';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Page, VCropper } from '@vben/common-ui';
|
||||
|
||||
import { Button, Card, Select, Upload } from 'antdv-next';
|
||||
|
||||
const options = [
|
||||
{ label: '1:1', value: '1:1' },
|
||||
{ label: '16:9', value: '16:9' },
|
||||
{ label: '不限制', value: '' },
|
||||
];
|
||||
|
||||
const cropperRef = ref<InstanceType<typeof VCropper>>();
|
||||
|
||||
const cropLoading = ref(false);
|
||||
const validAspectRatio = ref<string | undefined>('1:1');
|
||||
const imgUrl = ref('');
|
||||
const cropperImg = ref();
|
||||
|
||||
const selectImgFile = (event: UploadChangeParam) => {
|
||||
const file = event.fileList[0]?.originFileObj;
|
||||
if (!file) return;
|
||||
|
||||
if (!file.type.startsWith('image/')) {
|
||||
console.error('请上传图片文件');
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.addEventListener('load', (e) => {
|
||||
imgUrl.value = e.target?.result as string;
|
||||
});
|
||||
reader.addEventListener('error', () => {
|
||||
console.error('Failed to read file');
|
||||
});
|
||||
|
||||
reader.readAsDataURL(file);
|
||||
};
|
||||
|
||||
const cropImage = async () => {
|
||||
if (!cropperRef.value) return;
|
||||
cropLoading.value = true;
|
||||
try {
|
||||
cropperImg.value = await cropperRef.value.getCropImage(
|
||||
'image/jpeg',
|
||||
0.92,
|
||||
'base64',
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('图片裁剪失败:', error);
|
||||
} finally {
|
||||
cropLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 下载图片
|
||||
*/
|
||||
const downloadImage = () => {
|
||||
if (!cropperImg.value) return;
|
||||
|
||||
const link = document.createElement('a');
|
||||
link.download = `cropped-image-${Date.now()}.png`;
|
||||
link.href = cropperImg.value;
|
||||
link.click();
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<Page
|
||||
title="VCropper 图片裁剪"
|
||||
description="VCropper是一个图片裁剪组件,提供基础的图片裁剪功能。"
|
||||
>
|
||||
<Card>
|
||||
<div class="image-cropper-container">
|
||||
<div class="cropper-ratio-display">
|
||||
<label class="ratio-label">当前裁剪比例:</label>
|
||||
<Select
|
||||
class="w-24"
|
||||
v-model:value="validAspectRatio"
|
||||
:options="options"
|
||||
/>
|
||||
<Upload
|
||||
:max-count="1"
|
||||
:show-upload-list="false"
|
||||
:before-upload="() => false"
|
||||
@change="selectImgFile"
|
||||
>
|
||||
<Button>上传图片</Button>
|
||||
</Upload>
|
||||
</div>
|
||||
|
||||
<div v-if="imgUrl" class="cropper-main-wrapper">
|
||||
<VCropper
|
||||
ref="cropperRef"
|
||||
:img="imgUrl"
|
||||
:aspect-ratio="validAspectRatio"
|
||||
:width="600"
|
||||
:height="600"
|
||||
/>
|
||||
|
||||
<!-- 操作按钮组 -->
|
||||
<div class="cropper-btn-group">
|
||||
<Button :loading="cropLoading" @click="cropImage" type="primary">
|
||||
裁剪
|
||||
</Button>
|
||||
<Button v-if="cropperImg" @click="downloadImage" danger>
|
||||
下载图片
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- 裁剪预览 -->
|
||||
<img
|
||||
v-if="cropperImg"
|
||||
class="h-full w-80"
|
||||
:src="cropperImg"
|
||||
alt="裁剪预览"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Page>
|
||||
</template>
|
||||
<style scoped>
|
||||
/* 比例展示区域 */
|
||||
.cropper-ratio-display {
|
||||
@apply my-2.5 flex items-center justify-start gap-4;
|
||||
}
|
||||
|
||||
.ratio-label {
|
||||
@apply text-sm font-medium;
|
||||
}
|
||||
|
||||
/* 主裁剪区域 */
|
||||
.cropper-main-wrapper {
|
||||
@apply flex items-center gap-4;
|
||||
}
|
||||
|
||||
.cropper-btn-group {
|
||||
@apply flex flex-col gap-2;
|
||||
}
|
||||
</style>
|
||||
@@ -2,7 +2,7 @@
|
||||
import { VBEN_DOC_URL } from '@vben/constants';
|
||||
import { openWindow } from '@vben/utils';
|
||||
|
||||
import { Button } from 'ant-design-vue';
|
||||
import { Button } from 'antdv-next';
|
||||
|
||||
const props = defineProps<{ path: string }>();
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ref } from 'vue';
|
||||
|
||||
import { useVbenDrawer } from '@vben/common-ui';
|
||||
|
||||
import { Button, message } from 'ant-design-vue';
|
||||
import { Button, message } from 'antdv-next';
|
||||
|
||||
const list = ref<number[]>([]);
|
||||
|
||||
@@ -35,7 +35,7 @@ function handleUpdate(len: number) {
|
||||
<div
|
||||
v-for="item in list"
|
||||
:key="item"
|
||||
class="even:bg-heavy bg-muted flex-center h-[220px] w-full"
|
||||
class="flex-center h-55 w-full bg-muted even:bg-heavy"
|
||||
>
|
||||
{{ item }}
|
||||
</div>
|
||||
|
||||
@@ -1,24 +1,35 @@
|
||||
<script lang="ts" setup>
|
||||
import { useVbenDrawer } from '@vben/common-ui';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
import { Button, message } from 'antdv-next';
|
||||
|
||||
const [Drawer, drawerApi] = useVbenDrawer({
|
||||
onCancel() {
|
||||
drawerApi.close();
|
||||
},
|
||||
onClosed() {
|
||||
drawerApi.setState({ overlayBlur: 0, placement: 'right' });
|
||||
},
|
||||
onConfirm() {
|
||||
message.info('onConfirm');
|
||||
// drawerApi.close();
|
||||
},
|
||||
});
|
||||
|
||||
function lockDrawer() {
|
||||
drawerApi.lock();
|
||||
setTimeout(() => {
|
||||
drawerApi.unlock();
|
||||
}, 3000);
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<Drawer title="基础抽屉示例" title-tooltip="标题提示内容">
|
||||
<template #extra> extra </template>
|
||||
base demo
|
||||
|
||||
<Button type="primary" @click="lockDrawer">锁定抽屉状态</Button>
|
||||
<!-- <template #prepend-footer> slot </template> -->
|
||||
<!-- <template #append-footer> prepend slot </template> -->
|
||||
<!-- <template #center-footer> center slot </template> -->
|
||||
</Drawer>
|
||||
</template>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { useVbenDrawer } from '@vben/common-ui';
|
||||
|
||||
import { Button, message } from 'ant-design-vue';
|
||||
import { Button, message } from 'antdv-next';
|
||||
|
||||
const [Drawer, drawerApi] = useVbenDrawer({
|
||||
onCancel() {
|
||||
|
||||
@@ -7,6 +7,10 @@ defineOptions({
|
||||
name: 'FormDrawerDemo',
|
||||
});
|
||||
|
||||
interface FormDrawerData {
|
||||
values?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
schema: [
|
||||
{
|
||||
@@ -30,24 +34,28 @@ const [Form, formApi] = useVbenForm({
|
||||
],
|
||||
showDefaultActions: false,
|
||||
});
|
||||
const [Drawer, drawerApi] = useVbenDrawer({
|
||||
const [Drawer, drawerApi] = useVbenDrawer<FormDrawerData>({
|
||||
onCancel() {
|
||||
drawerApi.close();
|
||||
},
|
||||
onConfirm: async () => {
|
||||
await formApi.submitForm();
|
||||
await formApi.submit();
|
||||
drawerApi.close();
|
||||
},
|
||||
onOpenChange(isOpen: boolean) {
|
||||
if (isOpen) {
|
||||
const { values } = drawerApi.getData<Record<string, any>>();
|
||||
if (values) {
|
||||
formApi.setValues(values);
|
||||
const data = drawerApi.getData();
|
||||
if (data?.values) {
|
||||
formApi.setValues(data.values);
|
||||
} else {
|
||||
formApi.reset();
|
||||
}
|
||||
}
|
||||
},
|
||||
title: '内嵌表单示例',
|
||||
});
|
||||
|
||||
defineExpose({ drawerApi });
|
||||
</script>
|
||||
<template>
|
||||
<Drawer>
|
||||
|
||||
@@ -1,9 +1,31 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenDrawer } from '@vben/common-ui';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
import { Input, message } from 'antdv-next';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
|
||||
const value = ref('');
|
||||
|
||||
const [Form] = useVbenForm({
|
||||
schema: [
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: 'KeepAlive测试:内部组件',
|
||||
},
|
||||
fieldName: 'field1',
|
||||
hideLabel: true,
|
||||
label: '字段1',
|
||||
},
|
||||
],
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const [Drawer, drawerApi] = useVbenDrawer({
|
||||
destroyOnClose: false,
|
||||
onCancel() {
|
||||
drawerApi.close();
|
||||
},
|
||||
@@ -16,6 +38,11 @@ const [Drawer, drawerApi] = useVbenDrawer({
|
||||
<template>
|
||||
<Drawer append-to-main title="基础抽屉示例" title-tooltip="标题提示内容">
|
||||
<template #extra> extra </template>
|
||||
本抽屉指定在内容区域打开
|
||||
此弹窗指定在内容区域打开,并且在关闭之后弹窗内容不会被销毁
|
||||
<Input
|
||||
v-model:value="value"
|
||||
placeholder="KeepAlive测试:connectedComponent"
|
||||
/>
|
||||
<Form />
|
||||
</Drawer>
|
||||
</template>
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
import { type DrawerPlacement, Page, useVbenDrawer } from '@vben/common-ui';
|
||||
import type { DrawerPlacement, DrawerState } from '@vben/common-ui';
|
||||
|
||||
import { Button, Card } from 'ant-design-vue';
|
||||
import type { ExplicitDrawerData } from './typed-data-contract';
|
||||
|
||||
import { Page, useVbenDrawer } from '@vben/common-ui';
|
||||
|
||||
import { Button, Card } from 'antdv-next';
|
||||
|
||||
import DocButton from '../doc-button.vue';
|
||||
import AutoHeightDemo from './auto-height-demo.vue';
|
||||
@@ -10,7 +14,12 @@ import DynamicDemo from './dynamic-demo.vue';
|
||||
import FormDrawerDemo from './form-drawer-demo.vue';
|
||||
import inContentDemo from './in-content-demo.vue';
|
||||
import SharedDataDemo from './shared-data-demo.vue';
|
||||
import TypedDataAutoDemo from './typed-data-auto-demo.vue';
|
||||
import { useFactoryDrawer } from './typed-data-contract';
|
||||
import TypedDataExplicitDemo from './typed-data-explicit-demo.vue';
|
||||
import TypedDataFactoryDemo from './typed-data-factory-demo.vue';
|
||||
|
||||
defineOptions({ name: 'DrawerExample' });
|
||||
const [BaseDrawer, baseDrawerApi] = useVbenDrawer({
|
||||
// 连接抽离的组件
|
||||
connectedComponent: BaseDemo,
|
||||
@@ -39,26 +48,39 @@ const [FormDrawer, formDrawerApi] = useVbenDrawer({
|
||||
connectedComponent: FormDrawerDemo,
|
||||
});
|
||||
|
||||
const [TypedDataAutoDrawer, typedDataAutoDrawerApi] = useVbenDrawer({
|
||||
connectedComponent: TypedDataAutoDemo,
|
||||
});
|
||||
|
||||
const [TypedDataExplicitDrawer, typedDataExplicitDrawerApi] =
|
||||
useVbenDrawer<ExplicitDrawerData>({
|
||||
connectedComponent: TypedDataExplicitDemo,
|
||||
});
|
||||
|
||||
const [TypedDataFactoryDrawer, typedDataFactoryDrawerApi] = useFactoryDrawer({
|
||||
connectedComponent: TypedDataFactoryDemo,
|
||||
});
|
||||
|
||||
function openBaseDrawer(placement: DrawerPlacement = 'right') {
|
||||
baseDrawerApi.setState({ placement });
|
||||
baseDrawerApi.open();
|
||||
baseDrawerApi.setState({ placement }).open();
|
||||
}
|
||||
|
||||
function openBlurDrawer() {
|
||||
baseDrawerApi.setState({ overlayBlur: 5 }).open();
|
||||
}
|
||||
|
||||
function openInContentDrawer(placement: DrawerPlacement = 'right') {
|
||||
inContentDrawerApi.setState({ class: '', placement });
|
||||
const state: Partial<DrawerState> = { class: '', placement };
|
||||
if (placement === 'top') {
|
||||
// 页面顶部区域的层级只有200,所以设置一个低于200的值,抽屉从顶部滑出来的时候才比较合适
|
||||
inContentDrawerApi.setState({ zIndex: 199 });
|
||||
} else {
|
||||
inContentDrawerApi.setState({ zIndex: undefined });
|
||||
state.zIndex = 199;
|
||||
}
|
||||
inContentDrawerApi.open();
|
||||
inContentDrawerApi.setState(state).open();
|
||||
}
|
||||
|
||||
function openMaxContentDrawer() {
|
||||
// 这里只是用来演示方便。实际上自己使用的时候可以直接将这些配置卸载Drawer的属性里
|
||||
inContentDrawerApi.setState({ class: 'w-full', placement: 'right' });
|
||||
inContentDrawerApi.open();
|
||||
// 这里只是用来演示方便。实际上自己使用的时候可以直接将这些配置写在Drawer的属性里
|
||||
inContentDrawerApi.setState({ class: 'w-full', placement: 'right' }).open();
|
||||
}
|
||||
|
||||
function openAutoHeightDrawer() {
|
||||
@@ -70,24 +92,52 @@ function openDynamicDrawer() {
|
||||
}
|
||||
|
||||
function handleUpdateTitle() {
|
||||
dynamicDrawerApi.setState({ title: '外部动态标题' });
|
||||
dynamicDrawerApi.open();
|
||||
dynamicDrawerApi.setState({ title: '外部动态标题' }).open();
|
||||
}
|
||||
|
||||
function openSharedDrawer() {
|
||||
sharedDrawerApi.setData({
|
||||
content: '外部传递的数据 content',
|
||||
payload: '外部传递的数据 payload',
|
||||
});
|
||||
sharedDrawerApi.open();
|
||||
sharedDrawerApi
|
||||
.setData({
|
||||
content: '外部传递的数据 content',
|
||||
payload: '外部传递的数据 payload',
|
||||
})
|
||||
.open();
|
||||
}
|
||||
|
||||
function openFormDrawer() {
|
||||
formDrawerApi.setData({
|
||||
// 表单值
|
||||
values: { field1: 'abc', field2: '123' },
|
||||
});
|
||||
formDrawerApi.open();
|
||||
formDrawerApi
|
||||
.setData({
|
||||
// 表单值
|
||||
values: { field1: 'abc', field2: '123' },
|
||||
})
|
||||
.open();
|
||||
}
|
||||
|
||||
function openTypedDataAutoDrawer() {
|
||||
typedDataAutoDrawerApi
|
||||
.setData({
|
||||
message: '外部无需声明泛型,由 connected component 自动推导。',
|
||||
method: '自动推导',
|
||||
})
|
||||
.open();
|
||||
}
|
||||
|
||||
function openTypedDataExplicitDrawer() {
|
||||
typedDataExplicitDrawerApi
|
||||
.setData({
|
||||
message: '父子组件显式引用同一个数据类型。',
|
||||
method: '显式泛型',
|
||||
})
|
||||
.open();
|
||||
}
|
||||
|
||||
function openTypedDataFactoryDrawer() {
|
||||
typedDataFactoryDrawerApi
|
||||
.setData({
|
||||
message: '父子组件复用预绑定的 typed composable。',
|
||||
method: '契约工厂',
|
||||
})
|
||||
.open();
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -106,6 +156,9 @@ function openFormDrawer() {
|
||||
<DynamicDrawer />
|
||||
<SharedDataDrawer />
|
||||
<FormDrawer />
|
||||
<TypedDataAutoDrawer />
|
||||
<TypedDataExplicitDrawer />
|
||||
<TypedDataFactoryDrawer />
|
||||
|
||||
<Card class="mb-4" title="基本使用">
|
||||
<p class="mb-3">一个基础的抽屉示例</p>
|
||||
@@ -125,6 +178,9 @@ function openFormDrawer() {
|
||||
<Button class="mb-2 ml-2" type="primary" @click="openBaseDrawer('top')">
|
||||
顶部打开
|
||||
</Button>
|
||||
<Button class="mb-2 ml-2" type="primary" @click="openBlurDrawer">
|
||||
遮罩层模糊效果
|
||||
</Button>
|
||||
</Card>
|
||||
|
||||
<Card class="mb-4" title="在内容区域打开">
|
||||
@@ -184,5 +240,30 @@ function openFormDrawer() {
|
||||
打开抽屉并设置表单schema以及数据
|
||||
</Button>
|
||||
</Card>
|
||||
|
||||
<Card class="mb-4" title="共享数据:自动推导">
|
||||
<p class="mb-3">
|
||||
子组件 expose API,父组件从 connected component 推导类型
|
||||
</p>
|
||||
<Button type="primary" @click="openTypedDataAutoDrawer">
|
||||
打开自动推导示例
|
||||
</Button>
|
||||
</Card>
|
||||
|
||||
<Card class="mb-4" title="共享数据:显式泛型">
|
||||
<p class="mb-3">无法自动推导时,父子组件显式引用同一个数据类型</p>
|
||||
<Button type="primary" @click="openTypedDataExplicitDrawer">
|
||||
打开显式泛型示例
|
||||
</Button>
|
||||
</Card>
|
||||
|
||||
<Card class="mb-4" title="共享数据:契约工厂">
|
||||
<p class="mb-3">
|
||||
通过 createVbenDrawer 预绑定类型并复用 typed composable
|
||||
</p>
|
||||
<Button type="primary" @click="openTypedDataFactoryDrawer">
|
||||
打开契约工厂示例
|
||||
</Button>
|
||||
</Card>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
@@ -3,11 +3,16 @@ import { ref } from 'vue';
|
||||
|
||||
import { useVbenDrawer } from '@vben/common-ui';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
import { message } from 'antdv-next';
|
||||
|
||||
const data = ref();
|
||||
interface SharedData {
|
||||
content: string;
|
||||
payload: string;
|
||||
}
|
||||
|
||||
const [Drawer, drawerApi] = useVbenDrawer({
|
||||
const data = ref<SharedData>();
|
||||
|
||||
const [Drawer, drawerApi] = useVbenDrawer<SharedData>({
|
||||
onCancel() {
|
||||
drawerApi.close();
|
||||
},
|
||||
@@ -17,10 +22,12 @@ const [Drawer, drawerApi] = useVbenDrawer({
|
||||
},
|
||||
onOpenChange(isOpen: boolean) {
|
||||
if (isOpen) {
|
||||
data.value = drawerApi.getData<Record<string, any>>();
|
||||
data.value = drawerApi.getData();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
defineExpose({ drawerApi });
|
||||
</script>
|
||||
<template>
|
||||
<Drawer title="数据共享示例">
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenDrawer } from '@vben/common-ui';
|
||||
|
||||
interface AutoDrawerData {
|
||||
message: string;
|
||||
method: '自动推导';
|
||||
}
|
||||
|
||||
const data = ref<AutoDrawerData>();
|
||||
|
||||
const [Drawer, drawerApi] = useVbenDrawer<AutoDrawerData>({
|
||||
onOpenChange(isOpen) {
|
||||
data.value = isOpen ? drawerApi.getData() : undefined;
|
||||
},
|
||||
});
|
||||
|
||||
defineExpose({ drawerApi });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Drawer title="自动推导数据类型">
|
||||
<div class="space-y-2 px-2">
|
||||
<p>类型来源:{{ data?.method }}</p>
|
||||
<p>接收内容:{{ data?.message }}</p>
|
||||
</div>
|
||||
</Drawer>
|
||||
</template>
|
||||
13
playground/src/views/examples/drawer/typed-data-contract.ts
Normal file
13
playground/src/views/examples/drawer/typed-data-contract.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { createVbenDrawer } from '@vben/common-ui';
|
||||
|
||||
export interface ExplicitDrawerData {
|
||||
message: string;
|
||||
method: '显式泛型';
|
||||
}
|
||||
|
||||
export interface FactoryDrawerData {
|
||||
message: string;
|
||||
method: '契约工厂';
|
||||
}
|
||||
|
||||
export const useFactoryDrawer = createVbenDrawer<FactoryDrawerData>();
|
||||
@@ -0,0 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import type { ExplicitDrawerData } from './typed-data-contract';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenDrawer } from '@vben/common-ui';
|
||||
|
||||
const data = ref<ExplicitDrawerData>();
|
||||
|
||||
const [Drawer, drawerApi] = useVbenDrawer<ExplicitDrawerData>({
|
||||
onOpenChange(isOpen) {
|
||||
data.value = isOpen ? drawerApi.getData() : undefined;
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Drawer title="显式泛型数据类型">
|
||||
<div class="space-y-2 px-2">
|
||||
<p>类型来源:{{ data?.method }}</p>
|
||||
<p>接收内容:{{ data?.message }}</p>
|
||||
</div>
|
||||
</Drawer>
|
||||
</template>
|
||||
@@ -0,0 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import type { FactoryDrawerData } from './typed-data-contract';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useFactoryDrawer } from './typed-data-contract';
|
||||
|
||||
const data = ref<FactoryDrawerData>();
|
||||
|
||||
const [Drawer, drawerApi] = useFactoryDrawer({
|
||||
onOpenChange(isOpen) {
|
||||
data.value = isOpen ? drawerApi.getData() : undefined;
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Drawer title="契约工厂数据类型">
|
||||
<div class="space-y-2 px-2">
|
||||
<p>类型来源:{{ data?.method }}</p>
|
||||
<p>接收内容:{{ data?.message }}</p>
|
||||
</div>
|
||||
</Drawer>
|
||||
</template>
|
||||
@@ -3,7 +3,7 @@ import { ref } from 'vue';
|
||||
|
||||
import { EllipsisText, Page } from '@vben/common-ui';
|
||||
|
||||
import { Card } from 'ant-design-vue';
|
||||
import { Card } from 'antdv-next';
|
||||
|
||||
import DocButton from '../doc-button.vue';
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { Button, Card, message, Space } from 'ant-design-vue';
|
||||
import { Button, Card, message, Space } from 'antdv-next';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
|
||||
@@ -82,6 +82,7 @@ function handleClick(
|
||||
action:
|
||||
| 'batchAddSchema'
|
||||
| 'batchDeleteSchema'
|
||||
| 'componentRef'
|
||||
| 'disabled'
|
||||
| 'hiddenAction'
|
||||
| 'hiddenResetButton'
|
||||
@@ -129,6 +130,13 @@ function handleClick(
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'componentRef': {
|
||||
// 获取下拉组件的实例,并调用它的focus方法
|
||||
formApi
|
||||
.getFieldComponentRef<{ focus?: () => void }>('fieldOptions')
|
||||
?.focus?.();
|
||||
break;
|
||||
}
|
||||
case 'disabled': {
|
||||
formApi.setState({ commonConfig: { disabled: true } });
|
||||
break;
|
||||
@@ -182,6 +190,7 @@ function handleClick(
|
||||
formApi.setState({ submitButtonOptions: { show: true } });
|
||||
break;
|
||||
}
|
||||
|
||||
case 'updateActionAlign': {
|
||||
formApi.setState({
|
||||
// 可以自行调整class
|
||||
@@ -189,7 +198,6 @@ function handleClick(
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'updateResetButton': {
|
||||
formApi.setState({
|
||||
resetButtonOptions: { disabled: true },
|
||||
@@ -257,6 +265,7 @@ function handleClick(
|
||||
<Button @click="handleClick('batchDeleteSchema')">
|
||||
批量删除表单项
|
||||
</Button>
|
||||
<Button @click="handleClick('componentRef')">下拉组件获取焦点</Button>
|
||||
</Space>
|
||||
<Card title="操作示例">
|
||||
<BaseForm />
|
||||
|
||||
@@ -1,19 +1,74 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import type { UploadFile } from 'antdv-next';
|
||||
import type { Dayjs } from 'dayjs';
|
||||
|
||||
import { h, ref, toRaw } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { Button, Card, message, TabPane, Tabs } from 'ant-design-vue';
|
||||
import { useDebounceFn } from '@vueuse/core';
|
||||
import { Button, Card, message, Spin, Tag } from 'antdv-next';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { useVbenForm, z } from '#/adapter/form';
|
||||
import { getAllMenusApi } from '#/api';
|
||||
import { upload_file } from '#/api/examples/upload';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import DocButton from '../doc-button.vue';
|
||||
|
||||
const activeTab = ref('basic');
|
||||
const keyword = ref('');
|
||||
const fetching = ref(false);
|
||||
|
||||
interface BasicFormValues extends Record<string, any> {
|
||||
cropImage?: UploadFile[];
|
||||
files?: UploadFile[];
|
||||
rangePicker?: [Dayjs, Dayjs];
|
||||
}
|
||||
|
||||
function encodeBasicFormValues(values: Readonly<BasicFormValues>) {
|
||||
const { rangePicker, ...formValues } = values;
|
||||
return {
|
||||
...formValues,
|
||||
endTime: rangePicker?.[1]?.format('YYYY-MM-DD'),
|
||||
startTime: rangePicker?.[0]?.format('YYYY-MM-DD'),
|
||||
};
|
||||
}
|
||||
|
||||
type BasicSubmitValues = ReturnType<typeof encodeBasicFormValues>;
|
||||
|
||||
function decodeBasicFormValues(
|
||||
values: Readonly<BasicSubmitValues>,
|
||||
): BasicFormValues {
|
||||
const { endTime, startTime, ...formValues } = values;
|
||||
return {
|
||||
...formValues,
|
||||
...(startTime && endTime
|
||||
? { rangePicker: [dayjs(startTime), dayjs(endTime)] }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
// 模拟远程获取数据
|
||||
function fetchRemoteOptions({ keyword = '选项' }: Record<string, any>) {
|
||||
fetching.value = true;
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
const options = Array.from({ length: 10 }).map((_, index) => ({
|
||||
label: `${keyword}-${index}`,
|
||||
value: `${keyword}-${index}`,
|
||||
}));
|
||||
resolve(options);
|
||||
fetching.value = false;
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
const [BaseForm, baseFormApi] = useVbenForm({
|
||||
codec: {
|
||||
decode: decodeBasicFormValues,
|
||||
encode: encodeBasicFormValues,
|
||||
},
|
||||
// 所有表单项共用,可单独在表单内覆盖
|
||||
commonConfig: {
|
||||
// 在label后显示一个冒号
|
||||
@@ -23,9 +78,11 @@ const [BaseForm, baseFormApi] = useVbenForm({
|
||||
class: 'w-full',
|
||||
},
|
||||
},
|
||||
fieldMappingTime: [['rangePicker', ['startTime', 'endTime'], 'YYYY-MM-DD']],
|
||||
// 提交函数
|
||||
handleSubmit: onSubmit,
|
||||
handleValuesChange(_values, fieldsChanged) {
|
||||
message.info(`表单以下字段发生变化:${fieldsChanged.join(',')}`);
|
||||
},
|
||||
|
||||
// 垂直布局,label和input在不同行,值为vertical
|
||||
// 水平布局,label和input在同一行
|
||||
@@ -44,6 +101,13 @@ const [BaseForm, baseFormApi] = useVbenForm({
|
||||
label: '字符串',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'desc',
|
||||
// 界面显示的description
|
||||
description: '这是表单描述',
|
||||
label: '字符串(带描述)',
|
||||
},
|
||||
{
|
||||
// 组件需要在 #/adapter.ts内注册,并加上类型
|
||||
component: 'ApiSelect',
|
||||
@@ -58,22 +122,60 @@ const [BaseForm, baseFormApi] = useVbenForm({
|
||||
},
|
||||
// 菜单接口
|
||||
api: getAllMenusApi,
|
||||
autoSelect: 'first',
|
||||
},
|
||||
// 字段名
|
||||
fieldName: 'api',
|
||||
// 界面显示的label
|
||||
label: 'ApiSelect',
|
||||
},
|
||||
{
|
||||
component: 'ApiSelect',
|
||||
// 对应组件的参数
|
||||
componentProps: () => {
|
||||
return {
|
||||
api: fetchRemoteOptions,
|
||||
// 禁止本地过滤
|
||||
filterOption: false,
|
||||
// 如果正在获取数据,使用插槽显示一个loading
|
||||
notFoundContent: fetching.value ? undefined : null,
|
||||
// 搜索词变化时记录下来, 使用useDebounceFn防抖。
|
||||
onSearch: useDebounceFn((value: string) => {
|
||||
keyword.value = value;
|
||||
}, 300),
|
||||
// 远程搜索参数。当搜索词变化时,params也会更新
|
||||
params: {
|
||||
keyword: keyword.value || undefined,
|
||||
},
|
||||
// 远程搜索判断。当为true时,才允许调用api
|
||||
shouldFetch: (params: any) => {
|
||||
return !!params?.keyword;
|
||||
},
|
||||
showSearch: true,
|
||||
};
|
||||
},
|
||||
// 字段名
|
||||
fieldName: 'remoteSearch',
|
||||
// 界面显示的label
|
||||
label: '远程搜索',
|
||||
help: '远程查询,仅有输入时方进行查询',
|
||||
renderComponentContent: () => {
|
||||
return {
|
||||
notFoundContent: fetching.value ? h(Spin) : undefined,
|
||||
};
|
||||
},
|
||||
rules: 'selectRequired',
|
||||
},
|
||||
{
|
||||
component: 'ApiTreeSelect',
|
||||
// 对应组件的参数
|
||||
componentProps: {
|
||||
// 菜单接口
|
||||
api: getAllMenusApi,
|
||||
childrenField: 'children',
|
||||
// 菜单接口转options格式
|
||||
labelField: 'name',
|
||||
valueField: 'path',
|
||||
childrenField: 'children',
|
||||
},
|
||||
// 字段名
|
||||
fieldName: 'apiTree',
|
||||
@@ -103,6 +205,7 @@ const [BaseForm, baseFormApi] = useVbenForm({
|
||||
label: '图标',
|
||||
},
|
||||
{
|
||||
colon: false,
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
@@ -121,7 +224,7 @@ const [BaseForm, baseFormApi] = useVbenForm({
|
||||
showSearch: true,
|
||||
},
|
||||
fieldName: 'options',
|
||||
label: '下拉选',
|
||||
label: () => h(Tag, { color: 'warning' }, () => '😎自定义:'),
|
||||
},
|
||||
{
|
||||
component: 'RadioGroup',
|
||||
@@ -177,6 +280,9 @@ const [BaseForm, baseFormApi] = useVbenForm({
|
||||
default: () => ['我已阅读并同意'],
|
||||
};
|
||||
},
|
||||
rules: z
|
||||
.boolean()
|
||||
.refine((v) => v, { message: '为什么不同意?勾上它!' }),
|
||||
},
|
||||
{
|
||||
component: 'Mentions',
|
||||
@@ -207,10 +313,21 @@ const [BaseForm, baseFormApi] = useVbenForm({
|
||||
class: 'w-auto',
|
||||
},
|
||||
fieldName: 'switch',
|
||||
help: () =>
|
||||
['这是一个多行帮助信息', '第二行', '第三行'].map((v) => h('p', v)),
|
||||
label: '开关',
|
||||
},
|
||||
{
|
||||
component: 'DatePicker',
|
||||
dependencies: {
|
||||
resolve: ({ values }) => ({
|
||||
help: () =>
|
||||
[`这是一个可输出其他字段值的帮助信息${values.rate}`].map((value) =>
|
||||
h('p', value),
|
||||
),
|
||||
}),
|
||||
triggerFields: ['rate'],
|
||||
},
|
||||
fieldName: 'datePicker',
|
||||
label: '日期选择框',
|
||||
},
|
||||
@@ -271,81 +388,117 @@ const [BaseForm, baseFormApi] = useVbenForm({
|
||||
fieldName: 'treeSelect',
|
||||
label: '树选择',
|
||||
},
|
||||
{
|
||||
component: 'Upload',
|
||||
componentProps: {
|
||||
// 更多属性见:https://ant.design/components/upload-cn
|
||||
accept: '.png,.jpg,.jpeg',
|
||||
// 自动携带认证信息
|
||||
customRequest: upload_file,
|
||||
disabled: false,
|
||||
maxCount: 3,
|
||||
// 单位:MB
|
||||
maxSize: 2,
|
||||
multiple: false,
|
||||
showUploadList: true,
|
||||
// 上传列表的内建样式,支持四种基本样式 text, picture, picture-card 和 picture-circle
|
||||
listType: 'picture-card',
|
||||
draggable: true, // 启用拖拽排序
|
||||
// onChange事件已被重写,如需自定义请在此基础上扩展
|
||||
handleChange: ({ file }: { file: UploadFile }) => {
|
||||
const { name, status } = file;
|
||||
if (status === 'done') {
|
||||
message.success(`${name} ${$t('examples.form.upload-success')}`);
|
||||
} else if (status === 'error') {
|
||||
message.error(`${name} ${$t('examples.form.upload-fail')}`);
|
||||
}
|
||||
},
|
||||
onDragSort: (oldIndex: number, newIndex: number) => {
|
||||
console.warn(`图片从 ${oldIndex} 移动到 ${newIndex}`);
|
||||
},
|
||||
},
|
||||
fieldName: 'files',
|
||||
label: $t('examples.form.file'),
|
||||
renderComponentContent: () => {
|
||||
return {
|
||||
default: () => $t('examples.form.upload-image'),
|
||||
};
|
||||
},
|
||||
rules: 'selectRequired',
|
||||
},
|
||||
{
|
||||
component: 'Upload',
|
||||
componentProps: {
|
||||
accept: '.png,.jpg,.jpeg',
|
||||
customRequest: upload_file,
|
||||
maxCount: 1,
|
||||
maxSize: 2,
|
||||
listType: 'picture-card',
|
||||
// 是否启用图片裁剪(多选或者非图片不唤起裁剪框)
|
||||
crop: true,
|
||||
// 裁剪比例
|
||||
aspectRatio: '1:1',
|
||||
},
|
||||
fieldName: 'cropImage',
|
||||
label: $t('examples.form.crop-image'),
|
||||
renderComponentContent: () => {
|
||||
return {
|
||||
default: () => $t('examples.form.upload-image'),
|
||||
};
|
||||
},
|
||||
rules: 'selectRequired',
|
||||
},
|
||||
{
|
||||
component: 'RichEditor',
|
||||
fieldName: 'richEditor',
|
||||
label: '富文本',
|
||||
formItemClass: 'col-span-3 items-baseline',
|
||||
},
|
||||
],
|
||||
// 大屏一行显示3个,中屏一行显示2个,小屏一行显示1个
|
||||
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
|
||||
});
|
||||
|
||||
const [CustomLayoutForm] = useVbenForm({
|
||||
// 所有表单项共用,可单独在表单内覆盖
|
||||
commonConfig: {
|
||||
// 所有表单项
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: [
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'field1',
|
||||
label: '字符串',
|
||||
},
|
||||
{
|
||||
component: 'TreeSelect',
|
||||
fieldName: 'field2',
|
||||
label: '字符串',
|
||||
},
|
||||
{
|
||||
component: 'Mentions',
|
||||
fieldName: 'field3',
|
||||
label: '字符串',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'field4',
|
||||
label: '字符串',
|
||||
},
|
||||
{
|
||||
component: 'InputNumber',
|
||||
fieldName: 'field5',
|
||||
// 从第三列开始 相当于中间空了一列
|
||||
formItemClass: 'col-start-3',
|
||||
label: '前面空了一列',
|
||||
},
|
||||
{
|
||||
component: 'Textarea',
|
||||
fieldName: 'field6',
|
||||
// 占满三列空间 基线对齐
|
||||
formItemClass: 'col-span-3 items-baseline',
|
||||
label: '占满三列',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'field7',
|
||||
// 占满2列空间 从第二列开始 相当于前面空了一列
|
||||
formItemClass: 'col-span-2 col-start-2',
|
||||
label: '占满2列',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'field8',
|
||||
// 左右留空
|
||||
formItemClass: 'col-start-2',
|
||||
label: '左右留空',
|
||||
},
|
||||
{
|
||||
component: 'InputPassword',
|
||||
fieldName: 'field9',
|
||||
formItemClass: 'col-start-1',
|
||||
label: '字符串',
|
||||
},
|
||||
],
|
||||
// 一共三列
|
||||
wrapperClass: 'grid-cols-3',
|
||||
});
|
||||
function onSubmit(values: BasicSubmitValues) {
|
||||
const files = (toRaw(values.files) ?? []) as UploadFile[];
|
||||
const cropImage = (toRaw(values.cropImage) ?? []) as UploadFile[];
|
||||
const doneFiles = files.filter((file) => file.status === 'done');
|
||||
const failedFiles = files.filter((file) => file.status !== 'done');
|
||||
const doneCrop = cropImage.filter((file) => file.status === 'done');
|
||||
const failedCrop = cropImage.filter((file) => file.status !== 'done');
|
||||
|
||||
function onSubmit(values: Record<string, any>) {
|
||||
const msg = [
|
||||
...doneFiles.map((file) => file.response?.url || file.url),
|
||||
...failedFiles.map((file) => file.name),
|
||||
].join(', ');
|
||||
const msgCrop = [
|
||||
...doneCrop.map((file) => file.response?.url || file.url),
|
||||
...failedCrop.map((file) => file.name),
|
||||
].join(', ');
|
||||
|
||||
if (failedFiles.length === 0) {
|
||||
message.success({
|
||||
content: `${$t('examples.form.upload-urls')}: ${msg}`,
|
||||
});
|
||||
} else {
|
||||
message.error({
|
||||
content: `${$t('examples.form.upload-error')}: ${msg}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (doneCrop.length > 0 && failedCrop.length === 0) {
|
||||
message.success({
|
||||
content: `${$t('examples.form.upload-urls')}: ${msgCrop}`,
|
||||
});
|
||||
} else if (failedCrop.length > 0) {
|
||||
message.error({
|
||||
content: `${$t('examples.form.upload-error')}: ${msgCrop}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
// 如果需要可提交前替换为需要的urls
|
||||
values.files = doneFiles.map((file) => file.response?.url || file.url);
|
||||
values.cropImage = doneCrop.map((file) => file.response?.url || file.url);
|
||||
message.success({
|
||||
content: `form values: ${JSON.stringify(values)}`,
|
||||
});
|
||||
@@ -358,6 +511,14 @@ function handleSetFormValue() {
|
||||
baseFormApi.setValues({
|
||||
checkboxGroup: ['1'],
|
||||
datePicker: dayjs('2022-01-01'),
|
||||
files: [
|
||||
{
|
||||
name: 'example.png',
|
||||
status: 'done',
|
||||
uid: '-1',
|
||||
url: 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp',
|
||||
},
|
||||
],
|
||||
mentions: '@afc163',
|
||||
number: 3,
|
||||
options: '1',
|
||||
@@ -369,6 +530,12 @@ function handleSetFormValue() {
|
||||
timePicker: dayjs('2022-01-01 12:00:00'),
|
||||
treeSelect: 'leaf1',
|
||||
username: '1',
|
||||
richEditor: `
|
||||
<h1>Vben Tiptap</h1>
|
||||
<p>这个编辑器已经被封装在 <code>packages/effects/plugins/src/tiptap</code> 中。</p>
|
||||
<p>你可以直接在各个 app 里通过 <code>@vben/plugins/tiptap</code> 引入。</p>
|
||||
<blockquote>默认内置 StarterKit、Underline、TextAlign、Placeholder。</blockquote>
|
||||
`,
|
||||
});
|
||||
|
||||
// 设置单个表单值
|
||||
@@ -380,7 +547,6 @@ function handleSetFormValue() {
|
||||
<Page
|
||||
content-class="flex flex-col gap-4"
|
||||
description="表单组件基础示例,请注意,该页面用到的参数代码会添加一些简单注释,方便理解,请仔细查看。"
|
||||
header-class="pb-0"
|
||||
title="表单组件"
|
||||
>
|
||||
<template #description>
|
||||
@@ -389,22 +555,15 @@ function handleSetFormValue() {
|
||||
表单组件基础示例,请注意,该页面用到的参数代码会添加一些简单注释,方便理解,请仔细查看。
|
||||
</p>
|
||||
</div>
|
||||
<Tabs v-model:active-key="activeTab" :tab-bar-style="{ marginBottom: 0 }">
|
||||
<TabPane key="basic" tab="基础示例" />
|
||||
<TabPane key="layout" tab="自定义布局" />
|
||||
</Tabs>
|
||||
</template>
|
||||
<template #extra>
|
||||
<DocButton class="mb-2" path="/components/common-ui/vben-form" />
|
||||
</template>
|
||||
<Card v-show="activeTab === 'basic'" title="基础示例">
|
||||
<Card title="基础示例">
|
||||
<template #extra>
|
||||
<Button type="primary" @click="handleSetFormValue">设置表单值</Button>
|
||||
</template>
|
||||
<BaseForm />
|
||||
</Card>
|
||||
<Card v-show="activeTab === 'layout'" title="使用tailwind自定义布局">
|
||||
<CustomLayoutForm />
|
||||
</Card>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
311
playground/src/views/examples/form/collapsible.vue
Normal file
311
playground/src/views/examples/form/collapsible.vue
Normal file
@@ -0,0 +1,311 @@
|
||||
<script lang="ts" setup>
|
||||
import type { RadioGroupProps } from 'antdv-next';
|
||||
|
||||
import type { CollapsibleParamSchema, FormLayout } from '@vben/common-ui';
|
||||
|
||||
import { h, ref } from 'vue';
|
||||
|
||||
import { Page, VbenCollapsibleParams } from '@vben/common-ui';
|
||||
|
||||
import { Button, Card, message, RadioGroup } from 'antdv-next';
|
||||
|
||||
import { useVbenForm, z } from '#/adapter/form';
|
||||
|
||||
import DocButton from '../doc-button.vue';
|
||||
|
||||
const layouts: RadioGroupProps['options'] = [
|
||||
{ label: 'Vertical', value: 'vertical' },
|
||||
{ label: 'Horizontal', value: 'horizontal' },
|
||||
];
|
||||
|
||||
const layout = ref<FormLayout>('vertical');
|
||||
|
||||
function getNumberValidator(key: string, limit?: [number?, number?]) {
|
||||
let validator = z.number({
|
||||
error: (issue) =>
|
||||
issue.input === undefined ? `${key} 值不能为空` : `${key} 值只能为数字`,
|
||||
});
|
||||
|
||||
if (limit) {
|
||||
if (limit[0] !== undefined) {
|
||||
validator = validator.min(limit[0], {
|
||||
message: `${key} 值不能小于${limit[0]}`,
|
||||
});
|
||||
}
|
||||
if (limit[1] !== undefined) {
|
||||
validator = validator.max(limit[1], {
|
||||
message: `${key} 值不能大于${limit[1]}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return validator.optional();
|
||||
}
|
||||
|
||||
const paramsSchema: CollapsibleParamSchema[] = [
|
||||
{
|
||||
key: 'micro_batch_size',
|
||||
description: `批次大小,代表模型训练过程中,模型更新模型参数的数据步长,可理解为模型每看多少数据即更新一次模型参数,
|
||||
一般建议的批次大小为16/32,表示模型每看16或32条数据即更新一次参数`,
|
||||
option: {
|
||||
min: 8,
|
||||
max: 1024,
|
||||
step: 8,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'learning_rate',
|
||||
description:
|
||||
'学习率,代表每次更新数据的增量参数权重,学习率数值越大参数变化越大,对模型影响越大',
|
||||
option: {
|
||||
step: 1e-4,
|
||||
type: 'exponential',
|
||||
min: 0,
|
||||
max: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'eval_steps',
|
||||
description:
|
||||
'验证步数,训练阶段针模型的验证间隔步长,用于阶段性评估模型训练准确率、训练损失',
|
||||
option: {
|
||||
min: 1,
|
||||
max: 2_147_483_647,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'num_train_epochs',
|
||||
description:
|
||||
'循环次数,代表模型训练过程中模型学习数据集的次数,可理解为看几遍数据,一般建议的范围是1-3遍即可,可依据需求进行调整',
|
||||
option: {
|
||||
min: 1,
|
||||
max: 200,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'max_length',
|
||||
description: `序列长度,单个训练数据样本的最大长度,超出配置长度将丢弃`,
|
||||
option: {
|
||||
min: 500,
|
||||
max: 131_072,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'warmup_ratio',
|
||||
description: '学习率预热比例,学习率预热阶段占总训练步数的比例',
|
||||
option: {
|
||||
min: 0,
|
||||
max: 1,
|
||||
precision: 2,
|
||||
step: 0.01,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'save_steps',
|
||||
description: 'Checkpoint保存间隔',
|
||||
option: {
|
||||
min: 1,
|
||||
max: 2_147_483_647,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const paramsValidator = z
|
||||
.object({
|
||||
micro_batch_size: getNumberValidator('micro_batch_size', [8, 1024]),
|
||||
learning_rate: getNumberValidator('learning_rate'),
|
||||
eval_steps: getNumberValidator('eval_steps', [1, 2_147_483_647]),
|
||||
num_train_epochs: getNumberValidator('num_train_epochs', [1, 200]),
|
||||
max_length: getNumberValidator('max_length', [500, 131_072]),
|
||||
warmup_ratio: getNumberValidator('warmup_ratio', [0, 1]),
|
||||
save_steps: getNumberValidator('save_steps', [1, 2_147_483_647]),
|
||||
})
|
||||
.required();
|
||||
|
||||
const [BaseForm, baseFormApi] = useVbenForm({
|
||||
showDefaultActions: false,
|
||||
// 所有表单项共用,可单独在表单内覆盖
|
||||
commonConfig: {
|
||||
colon: true,
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
},
|
||||
handleSubmit: onSubmit,
|
||||
layout: 'vertical',
|
||||
schema: [
|
||||
{
|
||||
component: 'Switch',
|
||||
fieldName: 'qat',
|
||||
componentProps: {
|
||||
checkedChildren: '开',
|
||||
unCheckedChildren: '关',
|
||||
class: 'w-auto',
|
||||
},
|
||||
label: 'QAT',
|
||||
formItemClass: 'col-span-2',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
// component:'CollapsibleParams',
|
||||
component: h(VbenCollapsibleParams),
|
||||
componentProps: {
|
||||
params: paramsSchema,
|
||||
// maxHeight: 200, //限制最大高度,展开后可滚动
|
||||
// defaultOpen: true, // 默认false折叠
|
||||
// visibleCount: 0 // 默认3,最小可见为1,小于1取1
|
||||
},
|
||||
modelPropName: 'value',
|
||||
fieldName: 'params',
|
||||
label: '参数配置',
|
||||
formItemClass: 'col-span-8 items-baseline col-start-1',
|
||||
dependencies: {
|
||||
triggerFields: ['qat'],
|
||||
componentProps(values) {
|
||||
return {
|
||||
params: values.qat
|
||||
? [
|
||||
{
|
||||
key: 'calib_steps',
|
||||
description: `校准步数;校准的数据集大小 = 校准步数 * 训练的batch_size`,
|
||||
option: {
|
||||
min: 1,
|
||||
},
|
||||
},
|
||||
...paramsSchema,
|
||||
]
|
||||
: paramsSchema,
|
||||
};
|
||||
},
|
||||
trigger(values, __, controller) {
|
||||
// 访问 form 内 VbenCollapsibleParams 的实例
|
||||
const paramsRef =
|
||||
controller.getFieldComponentRef<typeof VbenCollapsibleParams>(
|
||||
'params',
|
||||
);
|
||||
if (values.qat) {
|
||||
paramsRef?.updateValues?.({
|
||||
calib_steps: 10,
|
||||
micro_batch_size: 32,
|
||||
learning_rate: 4e-5,
|
||||
eval_steps: 80,
|
||||
num_train_epochs: 3,
|
||||
max_length: 32_768,
|
||||
warmup_ratio: 0.1,
|
||||
save_steps: 80,
|
||||
});
|
||||
} else {
|
||||
paramsRef?.updateValues?.({
|
||||
calib_steps: null,
|
||||
});
|
||||
}
|
||||
},
|
||||
rules(values) {
|
||||
if (values.qat) {
|
||||
return paramsValidator.extend({
|
||||
calib_steps: getNumberValidator('calib_steps', [1]),
|
||||
});
|
||||
}
|
||||
return paramsValidator;
|
||||
},
|
||||
},
|
||||
rules: paramsValidator,
|
||||
defaultValue: {
|
||||
micro_batch_size: 8,
|
||||
learning_rate: 1e-5,
|
||||
eval_steps: 50,
|
||||
num_train_epochs: 3,
|
||||
max_length: 32_768,
|
||||
warmup_ratio: 0.05,
|
||||
save_steps: 50,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'RichEditor',
|
||||
fieldName: 'richEditor',
|
||||
label: '富文本',
|
||||
formItemClass: 'col-span-12 items-baseline',
|
||||
collapsible: true,
|
||||
defaultCollapsed: false, // 默认false
|
||||
},
|
||||
],
|
||||
wrapperClass: 'grid-cols-12',
|
||||
});
|
||||
|
||||
function onSubmit(values: Record<string, any>) {
|
||||
message.info({
|
||||
content: `form values: ${JSON.stringify(values)}`,
|
||||
});
|
||||
}
|
||||
|
||||
function onLayoutChange() {
|
||||
baseFormApi.setState({
|
||||
layout: layout.value,
|
||||
});
|
||||
}
|
||||
|
||||
function handleSetFormValue() {
|
||||
baseFormApi.setFieldValue('params', {
|
||||
micro_batch_size: 1024,
|
||||
learning_rate: 1e-5,
|
||||
eval_steps: 150,
|
||||
num_train_epochs: 13,
|
||||
max_length: 131_072,
|
||||
warmup_ratio: 0.05,
|
||||
save_steps: 150,
|
||||
});
|
||||
}
|
||||
|
||||
function handleResetFormValue() {
|
||||
baseFormApi.reset(undefined, { force: true });
|
||||
}
|
||||
|
||||
async function handleSubmitFormValue() {
|
||||
const { valid } = await baseFormApi.validate();
|
||||
|
||||
if (valid) {
|
||||
baseFormApi.submit();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page
|
||||
auto-content-height
|
||||
content-class="flex flex-col gap-4"
|
||||
title="可折叠表单项"
|
||||
>
|
||||
<template #description>
|
||||
<div class="text-muted-foreground">
|
||||
<p>可折叠表单项、以及可折叠参数配置组件示例</p>
|
||||
</div>
|
||||
</template>
|
||||
<template #extra>
|
||||
<DocButton class="mb-2" path="/components/common-ui/vben-form" />
|
||||
</template>
|
||||
<Card title="基础示例">
|
||||
<template #extra>
|
||||
<div class="inline-flex items-center gap-4!">
|
||||
<RadioGroup
|
||||
:options="layouts"
|
||||
option-type="button"
|
||||
v-model:value="layout"
|
||||
@change="onLayoutChange"
|
||||
/>
|
||||
<Button type="primary" @click="handleSetFormValue">
|
||||
设置表单值
|
||||
</Button>
|
||||
<Button type="primary" @click="handleSubmitFormValue">
|
||||
提交表单
|
||||
</Button>
|
||||
<Button type="primary" @click="handleResetFormValue">
|
||||
重置表单
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
<div class="w-full overflow-hidden">
|
||||
<BaseForm />
|
||||
</div>
|
||||
</Card>
|
||||
</Page>
|
||||
</template>
|
||||
111
playground/src/views/examples/form/custom-layout.vue
Normal file
111
playground/src/views/examples/form/custom-layout.vue
Normal file
@@ -0,0 +1,111 @@
|
||||
<script lang="ts" setup>
|
||||
import { h } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { Card } from 'antdv-next';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
|
||||
import DocButton from '../doc-button.vue';
|
||||
|
||||
const [CustomLayoutForm] = useVbenForm({
|
||||
// 所有表单项共用,可单独在表单内覆盖
|
||||
commonConfig: {
|
||||
// 所有表单项
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: [
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'field1',
|
||||
label: '字符串',
|
||||
},
|
||||
{
|
||||
component: 'TreeSelect',
|
||||
fieldName: 'field2',
|
||||
label: '字符串',
|
||||
},
|
||||
{
|
||||
component: 'Mentions',
|
||||
fieldName: 'field3',
|
||||
label: '字符串',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'field4',
|
||||
label: '字符串',
|
||||
},
|
||||
{
|
||||
component: 'InputNumber',
|
||||
fieldName: 'field5',
|
||||
// 从第三列开始 相当于中间空了一列
|
||||
formItemClass: 'col-start-3',
|
||||
label: '前面空了一列',
|
||||
},
|
||||
{
|
||||
component: 'Divider',
|
||||
fieldName: '_divider',
|
||||
formItemClass: 'col-span-3',
|
||||
hideLabel: true,
|
||||
renderComponentContent: () => {
|
||||
return {
|
||||
default: () => h('div', '分割线'),
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Textarea',
|
||||
fieldName: 'field6',
|
||||
// 占满三列空间 基线对齐
|
||||
formItemClass: 'col-span-3 items-baseline',
|
||||
label: '占满三列',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'field7',
|
||||
// 占满2列空间 从第二列开始 相当于前面空了一列
|
||||
formItemClass: 'col-span-2 col-start-2',
|
||||
label: '占满2列',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'field8',
|
||||
// 左右留空
|
||||
formItemClass: 'col-start-2',
|
||||
label: '左右留空',
|
||||
},
|
||||
{
|
||||
component: 'InputPassword',
|
||||
fieldName: 'field9',
|
||||
formItemClass: 'col-start-1',
|
||||
label: '字符串',
|
||||
},
|
||||
],
|
||||
// 一共三列
|
||||
wrapperClass: 'grid-cols-3',
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page
|
||||
content-class="flex flex-col gap-4"
|
||||
description="使用tailwind自定义表单项的布局"
|
||||
title="表单自定义布局"
|
||||
>
|
||||
<template #description>
|
||||
<div class="text-muted-foreground">
|
||||
<p>使用tailwind自定义表单项的布局,使用Divider分割表单。</p>
|
||||
</div>
|
||||
</template>
|
||||
<template #extra>
|
||||
<DocButton class="mb-2" path="/components/common-ui/vben-form" />
|
||||
</template>
|
||||
<Card title="使用tailwind自定义布局">
|
||||
<CustomLayoutForm />
|
||||
</Card>
|
||||
</Page>
|
||||
</template>
|
||||
@@ -1,13 +1,51 @@
|
||||
<script lang="ts" setup>
|
||||
import { h } from 'vue';
|
||||
import { h, markRaw, ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { Card, Input, message } from 'ant-design-vue';
|
||||
import { Button, Card, Input, message, Select } from 'antdv-next';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { useVbenForm, z } from '#/adapter/form';
|
||||
|
||||
const [Form] = useVbenForm({
|
||||
import TwoFields from './modules/two-fields.vue';
|
||||
|
||||
interface CustomFormValues extends Record<string, unknown> {
|
||||
field?: string;
|
||||
field1?: string;
|
||||
field2?: string;
|
||||
field3?: string;
|
||||
field4?: [string | undefined, string | undefined];
|
||||
field5?: string;
|
||||
}
|
||||
|
||||
function encodeCustomFormValues(values: Readonly<CustomFormValues>) {
|
||||
const { field4, ...formValues } = values;
|
||||
return {
|
||||
...formValues,
|
||||
phoneNumber: field4?.[1],
|
||||
phoneType: field4?.[0],
|
||||
};
|
||||
}
|
||||
|
||||
type CustomSubmitValues = ReturnType<typeof encodeCustomFormValues>;
|
||||
|
||||
const dynamicComponentType = ref<'input' | 'select'>('input');
|
||||
|
||||
function decodeCustomFormValues(
|
||||
values: Readonly<CustomSubmitValues>,
|
||||
): CustomFormValues {
|
||||
const { phoneNumber, phoneType, ...formValues } = values;
|
||||
return {
|
||||
...formValues,
|
||||
field4: [phoneType, phoneNumber ?? ''],
|
||||
};
|
||||
}
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
codec: {
|
||||
decode: decodeCustomFormValues,
|
||||
encode: encodeCustomFormValues,
|
||||
},
|
||||
// 所有表单项共用,可单独在表单内覆盖
|
||||
commonConfig: {
|
||||
// 所有表单项
|
||||
@@ -39,9 +77,10 @@ const [Form] = useVbenForm({
|
||||
}),
|
||||
},
|
||||
{
|
||||
component: h(Input, { placeholder: '请输入' }),
|
||||
component: h(Input, { placeholder: '请输入Field2' }),
|
||||
fieldName: 'field2',
|
||||
label: '自定义组件',
|
||||
modelPropName: 'value',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
@@ -50,12 +89,76 @@ const [Form] = useVbenForm({
|
||||
label: '自定义组件(slot)',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: markRaw(TwoFields),
|
||||
defaultValue: [undefined, ''],
|
||||
fieldName: 'field4',
|
||||
formItemClass: 'col-span-1',
|
||||
label: '组合字段',
|
||||
rules: z
|
||||
.array(z.string().optional())
|
||||
.length(2, '请选择类型并输入手机号码')
|
||||
.refine((v) => !!v[0], {
|
||||
message: '请选择类型',
|
||||
})
|
||||
.refine((v) => !!v[1] && v[1] !== '', {
|
||||
message: ' 输入手机号码',
|
||||
})
|
||||
.refine((v) => v[1]?.match(/^1[3-9]\d{9}$/), {
|
||||
// 使用全角空格占位,将错误提示文字挤到手机号码输入框的下面
|
||||
message: ' 号码格式不正确',
|
||||
}),
|
||||
},
|
||||
{
|
||||
component: markRaw(Input),
|
||||
componentProps: {
|
||||
placeholder: '请输入动态组件值',
|
||||
},
|
||||
fieldName: 'field5',
|
||||
label: '动态组件',
|
||||
modelPropName: 'value',
|
||||
},
|
||||
],
|
||||
// 中屏一行显示2个,小屏一行显示1个
|
||||
wrapperClass: 'grid-cols-1 md:grid-cols-2',
|
||||
});
|
||||
|
||||
function onSubmit(values: Record<string, any>) {
|
||||
function handleToggleDynamicComponent() {
|
||||
const nextType = dynamicComponentType.value === 'input' ? 'select' : 'input';
|
||||
dynamicComponentType.value = nextType;
|
||||
|
||||
if (nextType === 'select') {
|
||||
formApi.updateSchema([
|
||||
{
|
||||
component: markRaw(Select),
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: [
|
||||
{ label: '选项一', value: 'option-1' },
|
||||
{ label: '选项二', value: 'option-2' },
|
||||
],
|
||||
placeholder: '请选择动态组件值',
|
||||
},
|
||||
fieldName: 'field5',
|
||||
modelPropName: 'value',
|
||||
},
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
formApi.updateSchema([
|
||||
{
|
||||
component: markRaw(Input),
|
||||
componentProps: {
|
||||
placeholder: '请输入动态组件值',
|
||||
},
|
||||
fieldName: 'field5',
|
||||
modelPropName: 'value',
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
function onSubmit(values: CustomSubmitValues) {
|
||||
message.success({
|
||||
content: `form values: ${JSON.stringify(values)}`,
|
||||
});
|
||||
@@ -65,9 +168,16 @@ function onSubmit(values: Record<string, any>) {
|
||||
<template>
|
||||
<Page description="表单组件自定义示例" title="表单组件">
|
||||
<Card title="基础示例">
|
||||
<template #extra>
|
||||
<Button @click="handleToggleDynamicComponent">
|
||||
{{
|
||||
dynamicComponentType === 'input' ? '切换为下拉框' : '切换为输入框'
|
||||
}}
|
||||
</Button>
|
||||
</template>
|
||||
<Form>
|
||||
<template #field3="slotProps">
|
||||
<Input placeholder="请输入" v-bind="slotProps" />
|
||||
<Input placeholder="请输入" v-bind="slotProps.componentProps" />
|
||||
</template>
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { Button, Card, message } from 'ant-design-vue';
|
||||
import { Button, Card, message } from 'antdv-next';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
import type { StepItem } from 'antdv-next';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { Button, Card, message, Step, Steps, Switch } from 'ant-design-vue';
|
||||
import { Button, Card, message, Steps, Switch } from 'antdv-next';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
|
||||
@@ -75,6 +77,7 @@ const [SecondForm, secondFormApi] = useVbenForm({
|
||||
],
|
||||
wrapperClass: 'grid-cols-1 md:grid-cols-1 lg:grid-cols-1',
|
||||
});
|
||||
const stepsItems: StepItem[] = [{ title: '表单1' }, { title: '表单2' }];
|
||||
const needMerge = ref(true);
|
||||
async function handleMergeSubmit() {
|
||||
const values = await firstFormApi
|
||||
@@ -102,10 +105,7 @@ async function handleMergeSubmit() {
|
||||
<Button type="primary" @click="handleMergeSubmit">合并提交</Button>
|
||||
</template>
|
||||
<div class="mx-auto max-w-lg">
|
||||
<Steps :current="currentTab" class="steps">
|
||||
<Step title="表单1" />
|
||||
<Step title="表单2" />
|
||||
</Steps>
|
||||
<Steps :current="currentTab" :items="stepsItems" class="steps" />
|
||||
<div class="p-20">
|
||||
<FirstForm v-show="currentTab === 0" />
|
||||
<SecondForm v-show="currentTab === 1" />
|
||||
|
||||
51
playground/src/views/examples/form/modules/two-fields.vue
Normal file
51
playground/src/views/examples/form/modules/two-fields.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<script lang="ts" setup>
|
||||
import type { SelectValue } from 'antdv-next';
|
||||
|
||||
import { Input, Select } from 'antdv-next';
|
||||
|
||||
const emit = defineEmits(['blur']);
|
||||
|
||||
const modelValue = defineModel<[string | undefined, string | undefined]>({
|
||||
default: () => [undefined, undefined],
|
||||
});
|
||||
|
||||
function handlePhoneChange(value: string | undefined) {
|
||||
modelValue.value = [modelValue.value[0], value];
|
||||
}
|
||||
|
||||
function handleTypeChange(value: SelectValue) {
|
||||
modelValue.value = [
|
||||
typeof value === 'string' ? value : undefined,
|
||||
modelValue.value[1],
|
||||
];
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div class="flex w-full gap-1">
|
||||
<Select
|
||||
:value="modelValue[0]"
|
||||
class="w-20"
|
||||
placeholder="类型"
|
||||
allow-clear
|
||||
:class="{ 'valid-success': !!modelValue[0] }"
|
||||
:options="[
|
||||
{ label: '个人', value: 'personal' },
|
||||
{ label: '工作', value: 'work' },
|
||||
{ label: '私密', value: 'private' },
|
||||
]"
|
||||
@blur="emit('blur')"
|
||||
@update:value="handleTypeChange"
|
||||
/>
|
||||
<Input
|
||||
placeholder="请输入11位手机号码"
|
||||
class="flex-1"
|
||||
allow-clear
|
||||
:class="{ 'valid-success': modelValue[1]?.match(/^1[3-9]\d{9}$/) }"
|
||||
:value="modelValue[1]"
|
||||
:maxlength="11"
|
||||
type="tel"
|
||||
@blur="emit('blur')"
|
||||
@update:value="handlePhoneChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { Card, message } from 'ant-design-vue';
|
||||
import { Card, message } from 'antdv-next';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
|
||||
@@ -86,6 +86,62 @@ const [QueryForm] = useVbenForm({
|
||||
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
|
||||
});
|
||||
|
||||
const [InlineForm] = useVbenForm({
|
||||
layout: 'inline',
|
||||
schema: [
|
||||
{
|
||||
// 组件需要在 #/adapter.ts内注册,并加上类型
|
||||
component: 'Input',
|
||||
// 对应组件的参数
|
||||
componentProps: {
|
||||
placeholder: '请输入用户名',
|
||||
},
|
||||
// 字段名
|
||||
fieldName: 'username',
|
||||
// 界面显示的label
|
||||
label: '字符串',
|
||||
},
|
||||
{
|
||||
component: 'InputPassword',
|
||||
componentProps: {
|
||||
placeholder: '请输入密码',
|
||||
},
|
||||
fieldName: 'password',
|
||||
label: '密码',
|
||||
},
|
||||
{
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
placeholder: '请输入',
|
||||
},
|
||||
fieldName: 'number',
|
||||
label: '数字(带后缀)',
|
||||
suffix: () => '¥',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
filterOption: true,
|
||||
options: [
|
||||
{
|
||||
label: '选项1',
|
||||
value: '1',
|
||||
},
|
||||
{
|
||||
label: '选项2',
|
||||
value: '2',
|
||||
},
|
||||
],
|
||||
placeholder: '请选择',
|
||||
showSearch: true,
|
||||
},
|
||||
fieldName: 'options',
|
||||
label: '下拉选',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const [QueryForm1] = useVbenForm({
|
||||
// 默认展开
|
||||
collapsed: true,
|
||||
@@ -125,6 +181,70 @@ const [QueryForm1] = useVbenForm({
|
||||
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
|
||||
});
|
||||
|
||||
const [QueryForm2] = useVbenForm({
|
||||
// 操作按钮组 newLine: 在新行显示。rowEnd: 在行内显示,靠右对齐(默认)。inline: 使用grid默认样式
|
||||
actionLayout: 'newLine',
|
||||
actionPosition: 'left', // 操作按钮组在左侧显示
|
||||
// 默认折叠
|
||||
collapsed: true,
|
||||
collapsedRows: 3,
|
||||
// 所有表单项共用,可单独在表单内覆盖
|
||||
commonConfig: {
|
||||
// 所有表单项
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
},
|
||||
// 提交函数
|
||||
handleSubmit: onSubmit,
|
||||
// 垂直布局,label和input在不同行,值为vertical
|
||||
// 水平布局,label和input在同一行
|
||||
layout: 'vertical',
|
||||
schema: [
|
||||
{
|
||||
// 组件需要在 #/adapter.ts内注册,并加上类型
|
||||
component: 'Input',
|
||||
// 对应组件的参数
|
||||
componentProps: {
|
||||
placeholder: '请输入用户名',
|
||||
},
|
||||
// 字段名
|
||||
fieldName: 'username',
|
||||
// 界面显示的label
|
||||
label: '字符串',
|
||||
},
|
||||
{
|
||||
component: 'InputPassword',
|
||||
componentProps: {
|
||||
placeholder: '请输入密码',
|
||||
},
|
||||
fieldName: 'password',
|
||||
label: '密码',
|
||||
},
|
||||
{
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
placeholder: '请输入',
|
||||
},
|
||||
fieldName: 'number',
|
||||
label: '数字(带后缀)',
|
||||
suffix: () => '¥',
|
||||
},
|
||||
{
|
||||
component: 'DatePicker',
|
||||
fieldName: 'datePicker',
|
||||
label: '日期选择框',
|
||||
},
|
||||
],
|
||||
// 是否可展开
|
||||
showCollapseButton: true,
|
||||
submitButtonOptions: {
|
||||
content: '查询',
|
||||
},
|
||||
// 大屏一行显示3个,中屏一行显示2个,小屏一行显示1个
|
||||
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
|
||||
});
|
||||
|
||||
function onSubmit(values: Record<string, any>) {
|
||||
message.success({
|
||||
content: `form values: ${JSON.stringify(values)}`,
|
||||
@@ -140,6 +260,15 @@ function onSubmit(values: Record<string, any>) {
|
||||
<Card class="mb-5" title="查询表单,默认展开">
|
||||
<QueryForm />
|
||||
</Card>
|
||||
|
||||
<Card class="mb-5" title="查询表单,单行表单">
|
||||
<InlineForm />
|
||||
</Card>
|
||||
|
||||
<Card class="mb-5" title="查询表单,默认展开,垂直布局">
|
||||
<QueryForm2 />
|
||||
</Card>
|
||||
|
||||
<Card title="查询表单,默认折叠,折叠时保留2行">
|
||||
<QueryForm1 />
|
||||
</Card>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { Button, Card, message } from 'ant-design-vue';
|
||||
import { Button, Card, message } from 'antdv-next';
|
||||
|
||||
import { useVbenForm, z } from '#/adapter/form';
|
||||
|
||||
@@ -70,7 +70,7 @@ const [Form, formApi] = useVbenForm({
|
||||
fieldName: 'field4',
|
||||
// 界面显示的label
|
||||
label: '邮箱',
|
||||
rules: z.string().email('请输入正确的邮箱'),
|
||||
rules: z.email('请输入正确的邮箱'),
|
||||
},
|
||||
{
|
||||
component: 'InputNumber',
|
||||
@@ -150,7 +150,9 @@ const [Form, formApi] = useVbenForm({
|
||||
default: () => ['我已阅读并同意'],
|
||||
};
|
||||
},
|
||||
rules: 'selectRequired',
|
||||
rules: z.boolean().refine((value) => value, {
|
||||
message: '请勾选',
|
||||
}),
|
||||
},
|
||||
{
|
||||
component: 'DatePicker',
|
||||
@@ -182,8 +184,7 @@ const [Form, formApi] = useVbenForm({
|
||||
},
|
||||
fieldName: 'input-blur',
|
||||
formFieldProps: {
|
||||
validateOnChange: false,
|
||||
validateOnModelUpdate: false,
|
||||
validateOn: ['blur'],
|
||||
},
|
||||
help: 'blur时才会触发校验',
|
||||
label: 'blur触发',
|
||||
@@ -233,7 +234,7 @@ function onSubmit(values: Record<string, any>) {
|
||||
<Card title="基础组件校验示例">
|
||||
<template #extra>
|
||||
<Button @click="() => formApi.validate()">校验表单</Button>
|
||||
<Button class="mx-2" @click="() => formApi.resetValidate()">
|
||||
<Button class="mx-2" @click="() => formApi.clearValidation()">
|
||||
清空校验信息
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
183
playground/src/views/examples/form/scroll-to-error-test.vue
Normal file
183
playground/src/views/examples/form/scroll-to-error-test.vue
Normal file
@@ -0,0 +1,183 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { Button, Card, Switch } from 'antdv-next';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
|
||||
defineOptions({
|
||||
name: 'ScrollToErrorTest',
|
||||
});
|
||||
|
||||
const scrollEnabled = ref(true);
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
scrollToFirstError: scrollEnabled.value,
|
||||
schema: [
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入用户名',
|
||||
},
|
||||
fieldName: 'username',
|
||||
label: '用户名',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入邮箱',
|
||||
},
|
||||
fieldName: 'email',
|
||||
label: '邮箱',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入手机号',
|
||||
},
|
||||
fieldName: 'phone',
|
||||
label: '手机号',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入地址',
|
||||
},
|
||||
fieldName: 'address',
|
||||
label: '地址',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入备注',
|
||||
},
|
||||
fieldName: 'remark',
|
||||
label: '备注',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入公司名称',
|
||||
},
|
||||
fieldName: 'company',
|
||||
label: '公司名称',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入职位',
|
||||
},
|
||||
fieldName: 'position',
|
||||
label: '职位',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '男', value: 'male' },
|
||||
{ label: '女', value: 'female' },
|
||||
],
|
||||
placeholder: '请选择性别',
|
||||
},
|
||||
fieldName: 'gender',
|
||||
label: '性别',
|
||||
rules: 'selectRequired',
|
||||
},
|
||||
],
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
// 测试 validateAndSubmitForm(验证并提交)
|
||||
async function testValidateAndSubmit() {
|
||||
await formApi.validateAndSubmit();
|
||||
}
|
||||
|
||||
// 测试 validate(手动验证整个表单)
|
||||
async function testValidate() {
|
||||
await formApi.validate();
|
||||
}
|
||||
|
||||
// 测试 validateField(验证单个字段)
|
||||
async function testValidateField() {
|
||||
await formApi.validateField('username');
|
||||
}
|
||||
|
||||
// 切换滚动功能
|
||||
function toggleScrollToError(checked: boolean) {
|
||||
formApi.setState({ scrollToFirstError: checked });
|
||||
}
|
||||
|
||||
// 填充部分数据测试
|
||||
async function fillPartialData() {
|
||||
await formApi.reset();
|
||||
await formApi.setFieldValue('username', '测试用户');
|
||||
await formApi.setFieldValue('email', 'test@example.com');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page
|
||||
description="测试表单验证失败时自动滚动到错误字段的功能"
|
||||
title="滚动到错误字段测试"
|
||||
>
|
||||
<Card title="功能测试">
|
||||
<template #extra>
|
||||
<div class="flex items-center gap-2">
|
||||
<Switch
|
||||
v-model:checked="scrollEnabled"
|
||||
@change="toggleScrollToError"
|
||||
/>
|
||||
<span>启用滚动到错误字段</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="rounded-sm bg-blue-50 dark:bg-blue-900 p-4">
|
||||
<h3 class="mb-2 font-medium">测试说明:</h3>
|
||||
<ul class="list-inside list-disc space-y-1 text-sm">
|
||||
<li>所有验证方法在验证失败时都会自动滚动到第一个错误字段</li>
|
||||
<li>可以通过右上角的开关控制是否启用自动滚动功能</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="rounded-sm border p-4">
|
||||
<h4 class="mb-3 font-medium">验证方法测试:</h4>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<Button type="primary" @click="testValidateAndSubmit">
|
||||
测试 validateAndSubmit()
|
||||
</Button>
|
||||
<Button @click="testValidate"> 测试 validate() </Button>
|
||||
<Button @click="testValidateField"> 测试 validateField() </Button>
|
||||
</div>
|
||||
<div class="mt-2 text-xs text-gray-500">
|
||||
<p>• validateAndSubmit(): 验证表单并提交</p>
|
||||
<p>• validate(): 手动验证整个表单</p>
|
||||
<p>• validateField(): 验证单个字段(这里测试用户名字段)</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-sm border p-4">
|
||||
<h4 class="mb-3 font-medium">数据填充测试:</h4>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<Button @click="fillPartialData"> 填充部分数据 </Button>
|
||||
<Button @click="() => formApi.reset()"> 清空表单 </Button>
|
||||
</div>
|
||||
<div class="mt-2 text-xs text-gray-500">
|
||||
<p>• 填充部分数据后验证,会滚动到第一个错误字段</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Form />
|
||||
</div>
|
||||
</Card>
|
||||
</Page>
|
||||
</template>
|
||||
195
playground/src/views/examples/form/value-format.vue
Normal file
195
playground/src/views/examples/form/value-format.vue
Normal file
@@ -0,0 +1,195 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Dayjs } from 'dayjs';
|
||||
|
||||
import { computed, nextTick, onMounted, ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { Button, Card, message, Space, Tag } from 'antdv-next';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
|
||||
import DocButton from '../doc-button.vue';
|
||||
|
||||
interface ValueFormatFormValues {
|
||||
deadline?: Dayjs;
|
||||
keyword?: string;
|
||||
reportRange?: [Dayjs, Dayjs];
|
||||
}
|
||||
|
||||
function encodeValueFormatValues(values: Readonly<ValueFormatFormValues>) {
|
||||
return {
|
||||
deadline: values.deadline?.valueOf(),
|
||||
endTime: values.reportRange?.[1].valueOf(),
|
||||
keyword: values.keyword,
|
||||
startTime: values.reportRange?.[0].valueOf(),
|
||||
};
|
||||
}
|
||||
|
||||
type ValueFormatSubmitValues = ReturnType<typeof encodeValueFormatValues>;
|
||||
|
||||
function decodeValueFormatValues(
|
||||
values: Readonly<ValueFormatSubmitValues>,
|
||||
): ValueFormatFormValues {
|
||||
let reportRange: [Dayjs, Dayjs] | undefined;
|
||||
if (values.startTime !== undefined && values.endTime !== undefined) {
|
||||
reportRange = [dayjs(values.startTime), dayjs(values.endTime)];
|
||||
}
|
||||
return {
|
||||
deadline:
|
||||
values.deadline === undefined ? undefined : dayjs(values.deadline),
|
||||
keyword: values.keyword,
|
||||
reportRange,
|
||||
};
|
||||
}
|
||||
|
||||
const transformedValues = ref<Partial<ValueFormatSubmitValues>>({});
|
||||
const liveValues = ref<Partial<ValueFormatFormValues>>({});
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
codec: {
|
||||
decode: decodeValueFormatValues,
|
||||
encode: encodeValueFormatValues,
|
||||
},
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
},
|
||||
handleSubmit,
|
||||
handleValuesChange,
|
||||
schema: [
|
||||
{
|
||||
component: 'RangePicker',
|
||||
fieldName: 'reportRange',
|
||||
help: '由表单 codec 拆分为 startTime / endTime',
|
||||
label: '统计时间范围',
|
||||
},
|
||||
{
|
||||
component: 'DatePicker',
|
||||
fieldName: 'deadline',
|
||||
help: '由表单 codec 编码为时间戳',
|
||||
label: '截止时间',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入关键字',
|
||||
},
|
||||
fieldName: 'keyword',
|
||||
label: '关键字',
|
||||
},
|
||||
],
|
||||
wrapperClass: 'grid-cols-1 md:grid-cols-2',
|
||||
});
|
||||
|
||||
const liveValuesPreview = computed(() => formatJsonPreview(liveValues.value));
|
||||
|
||||
const transformedValuesPreview = computed(() => {
|
||||
return formatJsonPreview(transformedValues.value);
|
||||
});
|
||||
|
||||
function formatJsonPreview(value: unknown) {
|
||||
return JSON.stringify(
|
||||
value,
|
||||
(_key, currentValue) => {
|
||||
return dayjs.isDayjs(currentValue)
|
||||
? currentValue.format('YYYY-MM-DD HH:mm:ss')
|
||||
: currentValue;
|
||||
},
|
||||
2,
|
||||
);
|
||||
}
|
||||
|
||||
async function handleInspectValues() {
|
||||
await syncPreviewValues();
|
||||
message.success('已刷新 getValues 输出');
|
||||
}
|
||||
|
||||
function handleSetExampleValue() {
|
||||
formApi.setValues({
|
||||
deadline: dayjs('2026-04-12 18:30:00'),
|
||||
keyword: 'invoice',
|
||||
reportRange: [dayjs('2026-04-01 00:00:00'), dayjs('2026-04-12 23:59:59')],
|
||||
});
|
||||
}
|
||||
|
||||
function handleSubmit(values: ValueFormatSubmitValues) {
|
||||
transformedValues.value = values;
|
||||
message.success({
|
||||
content: `getValues output: ${JSON.stringify(values)}`,
|
||||
});
|
||||
}
|
||||
|
||||
function handleValuesChange(
|
||||
values: Readonly<ValueFormatFormValues>,
|
||||
_fieldsChanged: string[],
|
||||
getFormattedValues: () => ValueFormatSubmitValues,
|
||||
) {
|
||||
liveValues.value = { ...values };
|
||||
transformedValues.value = getFormattedValues();
|
||||
}
|
||||
|
||||
async function syncPreviewValues(values?: Readonly<ValueFormatFormValues>) {
|
||||
const rawValues = values ?? (await formApi.getRawValues());
|
||||
liveValues.value = { ...rawValues };
|
||||
transformedValues.value = await formApi.getValues();
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await nextTick();
|
||||
await syncPreviewValues();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page
|
||||
content-class="flex flex-col gap-4"
|
||||
description="演示表单级 codec 如何双向转换组件值和提交 payload。"
|
||||
title="表单 Codec"
|
||||
>
|
||||
<template #description>
|
||||
<div class="text-muted-foreground space-y-2">
|
||||
<p>
|
||||
<code>getRawValues()</code> 返回组件原始值,<code>getValues()</code> /
|
||||
提交时会按 <code>codec.encode</code> 输出 payload,回填时通过
|
||||
<code>codec.decode</code> 恢复组件值。
|
||||
</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<Tag color="processing">encode:生成完整提交值</Tag>
|
||||
<Tag color="success">decode:恢复完整表单值</Tag>
|
||||
<Tag color="warning">多字段转换原子执行</Tag>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #extra>
|
||||
<DocButton path="/components/common-ui/vben-form" />
|
||||
</template>
|
||||
|
||||
<Card title="Codec 示例">
|
||||
<template #extra>
|
||||
<Space wrap>
|
||||
<Button @click="handleSetExampleValue">填充示例数据</Button>
|
||||
<Button type="primary" @click="handleInspectValues">
|
||||
查看 getValues 输出
|
||||
</Button>
|
||||
</Space>
|
||||
</template>
|
||||
<Form />
|
||||
</Card>
|
||||
|
||||
<div class="grid gap-4 lg:grid-cols-2">
|
||||
<Card title="getRawValues() 输出(组件值)">
|
||||
<pre class="bg-muted overflow-auto rounded-md p-4 text-sm">{{
|
||||
liveValuesPreview
|
||||
}}</pre>
|
||||
</Card>
|
||||
<Card title="getValues / submit 输出(codec.encode 后)">
|
||||
<pre class="bg-muted overflow-auto rounded-md p-4 text-sm">{{
|
||||
transformedValuesPreview
|
||||
}}</pre>
|
||||
</Card>
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
||||
66
playground/src/views/examples/json-viewer/data.ts
Normal file
66
playground/src/views/examples/json-viewer/data.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
export const json1 = {
|
||||
additionalInfo: {
|
||||
author: 'Your Name',
|
||||
debug: true,
|
||||
version: '1.3.10',
|
||||
versionCode: 132,
|
||||
},
|
||||
additionalNotes: 'This JSON is used for demonstration purposes',
|
||||
tools: [
|
||||
{
|
||||
description: 'Description of Tool 1',
|
||||
name: 'Tool 1',
|
||||
},
|
||||
{
|
||||
description: 'Description of Tool 2',
|
||||
name: 'Tool 2',
|
||||
},
|
||||
{
|
||||
description: 'Description of Tool 3',
|
||||
name: 'Tool 3',
|
||||
},
|
||||
{
|
||||
description: 'Description of Tool 4',
|
||||
name: 'Tool 4',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const json2 = JSON.parse(`
|
||||
{
|
||||
"id": "chatgpt-123",
|
||||
"object": "chat.completion",
|
||||
"created": 1677652288,
|
||||
"model": "gpt-3.5-turbo-0613",
|
||||
"system_fingerprint": "fp_44709d6fcb",
|
||||
"choices": [{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "Hello there, how may I assist you today?"
|
||||
},
|
||||
"finish_reason": "stop"
|
||||
}],
|
||||
"usage": {
|
||||
"prompt_tokens": 9,
|
||||
"completion_tokens": 12,
|
||||
"total_tokens": 21,
|
||||
"debug_mode": true
|
||||
},
|
||||
"debug": {
|
||||
"startAt": "2021-08-01T00:00:00Z",
|
||||
"logs": [
|
||||
{
|
||||
"timestamp": "2021-08-01T00:00:00Z",
|
||||
"message": "This is a debug message",
|
||||
"extra":[ "extra1", "extra2" ]
|
||||
},
|
||||
{
|
||||
"timestamp": "2021-08-01T00:00:01Z",
|
||||
"message": "This is another debug message",
|
||||
"extra":[ "extra3", "extra4" ]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
`);
|
||||
51
playground/src/views/examples/json-viewer/index.vue
Normal file
51
playground/src/views/examples/json-viewer/index.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<script lang="ts" setup>
|
||||
import type { JsonViewerAction, JsonViewerValue } from '@vben/common-ui';
|
||||
|
||||
import { JsonViewer, Page } from '@vben/common-ui';
|
||||
|
||||
import { Card, message } from 'antdv-next';
|
||||
|
||||
import { json1, json2 } from './data';
|
||||
|
||||
function handleKeyClick(key: string) {
|
||||
message.info(`点击了Key ${key}`);
|
||||
}
|
||||
|
||||
function handleValueClick(value: JsonViewerValue) {
|
||||
message.info(`点击了Value ${JSON.stringify(value)}`);
|
||||
}
|
||||
|
||||
function handleCopied(_event: JsonViewerAction) {
|
||||
message.success('已复制JSON');
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<Page
|
||||
title="Json Viewer"
|
||||
description="一个渲染 JSON 结构数据的组件,支持复制、展开等,简单易用"
|
||||
>
|
||||
<Card title="默认配置">
|
||||
<JsonViewer :value="json1" />
|
||||
</Card>
|
||||
<Card title="可复制、默认展开3层、显示边框、事件处理" class="mt-4">
|
||||
<JsonViewer
|
||||
:value="json2"
|
||||
:expand-depth="3"
|
||||
copyable
|
||||
:sort="false"
|
||||
@key-click="handleKeyClick"
|
||||
@value-click="handleValueClick"
|
||||
@copied="handleCopied"
|
||||
boxed
|
||||
/>
|
||||
</Card>
|
||||
<Card title="预览模式" class="mt-4">
|
||||
<JsonViewer
|
||||
:value="json2"
|
||||
copyable
|
||||
preview-mode
|
||||
:show-array-index="false"
|
||||
/>
|
||||
</Card>
|
||||
</Page>
|
||||
</template>
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
Slider,
|
||||
Tag,
|
||||
Tooltip,
|
||||
} from 'ant-design-vue';
|
||||
} from 'antdv-next';
|
||||
|
||||
const props = reactive({
|
||||
leftCollapsedWidth: 5,
|
||||
@@ -22,8 +22,8 @@ const props = reactive({
|
||||
leftWidth: 30,
|
||||
resizable: true,
|
||||
rightWidth: 70,
|
||||
splitHandle: false,
|
||||
splitLine: false,
|
||||
splitHandle: true,
|
||||
splitLine: true,
|
||||
});
|
||||
const leftMinWidth = ref(props.leftMinWidth || 1);
|
||||
const leftMaxWidth = ref(props.leftMaxWidth || 100);
|
||||
@@ -42,7 +42,7 @@ const leftMaxWidth = ref(props.leftMaxWidth || 100);
|
||||
<template #left="{ isCollapsed, expand }">
|
||||
<div v-if="isCollapsed" @click="expand">
|
||||
<Tooltip title="点击展开左侧">
|
||||
<Button shape="circle" type="primary">
|
||||
<Button shape="circle" type="primary" class="flex-center">
|
||||
<template #icon>
|
||||
<IconifyIcon class="text-2xl" icon="bi:arrow-right" />
|
||||
</template>
|
||||
@@ -52,7 +52,7 @@ const leftMaxWidth = ref(props.leftMaxWidth || 100);
|
||||
<div
|
||||
v-else
|
||||
:style="{ minWidth: '200px' }"
|
||||
class="border-border bg-card mr-2 rounded-[var(--radius)] border p-2"
|
||||
class="mr-2 rounded-(--radius) border border-border bg-card p-2"
|
||||
>
|
||||
<p>这里是左侧内容</p>
|
||||
<p>这里是左侧内容</p>
|
||||
@@ -77,7 +77,7 @@ const leftMaxWidth = ref(props.leftMaxWidth || 100);
|
||||
v-model:value="leftMinWidth"
|
||||
:max="props.leftMaxWidth - 1"
|
||||
:min="1"
|
||||
style="width: 100px"
|
||||
class="w-25"
|
||||
@after-change="(value) => (props.leftMinWidth = value as number)"
|
||||
/>
|
||||
<span>左侧最大宽度百分比:</span>
|
||||
@@ -85,11 +85,11 @@ const leftMaxWidth = ref(props.leftMaxWidth || 100);
|
||||
v-model:value="props.leftMaxWidth"
|
||||
:max="100"
|
||||
:min="leftMaxWidth + 1"
|
||||
style="width: 100px"
|
||||
class="w-25"
|
||||
@after-change="(value) => (props.leftMaxWidth = value as number)"
|
||||
/>
|
||||
</div>
|
||||
<Alert message="实验性的组件" show-icon type="warning">
|
||||
<Alert title="实验性的组件" show-icon type="warning">
|
||||
<template #description>
|
||||
<p>
|
||||
双列布局组件是一个在Page组件上扩展的相对基础的布局组件,支持左侧折叠(当拖拽导致左侧宽度比最小宽度还要小时,还可以进入折叠状态)、拖拽调整宽度等功能。
|
||||
|
||||
94
playground/src/views/examples/loading/index.vue
Normal file
94
playground/src/views/examples/loading/index.vue
Normal file
@@ -0,0 +1,94 @@
|
||||
<script lang="ts" setup>
|
||||
import { Loading, Page, Spinner } from '@vben/common-ui';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import { refAutoReset } from '@vueuse/core';
|
||||
import { Button, Card, Spin } from 'antdv-next';
|
||||
|
||||
const spinning = refAutoReset(false, 3000);
|
||||
const loading = refAutoReset(false, 3000);
|
||||
|
||||
const spinningV = refAutoReset(false, 3000);
|
||||
const loadingV = refAutoReset(false, 3000);
|
||||
</script>
|
||||
<template>
|
||||
<Page
|
||||
title="Vben Loading"
|
||||
description="加载中状态组件。这个组件可以为其它作为容器的组件添加一个加载中的遮罩层。使用它们时,容器需要relative定位。"
|
||||
>
|
||||
<Card title="Antd Spin">
|
||||
<template #actions>这是Antd 组件库自带的Spin组件演示</template>
|
||||
<Spin :spinning="spinning" description="加载中...">
|
||||
<Button type="primary" @click="spinning = true">显示Spin</Button>
|
||||
</Spin>
|
||||
</Card>
|
||||
|
||||
<Card title="Vben Loading" v-loading="loadingV" class="mt-4">
|
||||
<template #extra>
|
||||
<Button type="primary" @click="loadingV = true">
|
||||
v-loading 指令
|
||||
</Button>
|
||||
</template>
|
||||
<template #actions>
|
||||
Loading组件可以设置文字,并且也提供了icon插槽用于替换加载图标。
|
||||
</template>
|
||||
<div class="flex gap-4">
|
||||
<div class="size-40">
|
||||
<Loading
|
||||
:spinning="loading"
|
||||
text="正在加载..."
|
||||
class="flex-center size-full"
|
||||
>
|
||||
<Button type="primary" @click="loading = true">默认动画</Button>
|
||||
</Loading>
|
||||
</div>
|
||||
<div class="size-40">
|
||||
<Loading :spinning="loading" class="flex-center size-full">
|
||||
<Button type="primary" @click="loading = true">自定义动画1</Button>
|
||||
<template #icon>
|
||||
<IconifyIcon
|
||||
icon="svg-spinners:ring-resize"
|
||||
class="size-10 text-primary"
|
||||
/>
|
||||
</template>
|
||||
</Loading>
|
||||
</div>
|
||||
<div class="size-40">
|
||||
<Loading :spinning="loading" class="flex-center size-full">
|
||||
<Button type="primary" @click="loading = true">自定义动画2</Button>
|
||||
<template #icon>
|
||||
<IconifyIcon
|
||||
icon="svg-spinners:bars-scale"
|
||||
class="size-10 text-primary"
|
||||
/>
|
||||
</template>
|
||||
</Loading>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title="Vben Spinner"
|
||||
v-spinning="spinningV"
|
||||
class="mt-4 overflow-hidden"
|
||||
:styles="{
|
||||
body: {
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
}"
|
||||
>
|
||||
<template #extra>
|
||||
<Button type="primary" @click="spinningV = true">
|
||||
v-spinning 指令
|
||||
</Button>
|
||||
</template>
|
||||
<template #actions>
|
||||
Spinner组件是Loading组件的一个特例,只有一个固定的统一样式。
|
||||
</template>
|
||||
<Spinner :spinning="spinning" class="flex-center size-40">
|
||||
<Button type="primary" @click="spinning = true">显示Spinner</Button>
|
||||
</Spinner>
|
||||
</Card>
|
||||
</Page>
|
||||
</template>
|
||||
@@ -3,7 +3,7 @@ import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { Button, message } from 'ant-design-vue';
|
||||
import { Button, message } from 'antdv-next';
|
||||
|
||||
const list = ref<number[]>([]);
|
||||
|
||||
@@ -16,15 +16,18 @@ const [Modal, modalApi] = useVbenModal({
|
||||
},
|
||||
onOpenChange(isOpen) {
|
||||
if (isOpen) {
|
||||
handleUpdate(10);
|
||||
handleUpdate();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
function handleUpdate(len: number) {
|
||||
function handleUpdate(len?: number) {
|
||||
modalApi.setState({ confirmDisabled: true, loading: true });
|
||||
setTimeout(() => {
|
||||
list.value = Array.from({ length: len }, (_v, k) => k + 1);
|
||||
list.value = Array.from(
|
||||
{ length: len ?? Math.floor(Math.random() * 10) + 1 },
|
||||
(_v, k) => k + 1,
|
||||
);
|
||||
modalApi.setState({ confirmDisabled: false, loading: false });
|
||||
}, 2000);
|
||||
}
|
||||
@@ -35,12 +38,12 @@ function handleUpdate(len: number) {
|
||||
<div
|
||||
v-for="item in list"
|
||||
:key="item"
|
||||
class="even:bg-heavy bg-muted flex-center h-[220px] w-full"
|
||||
class="flex-center h-55 w-full bg-muted even:bg-heavy"
|
||||
>
|
||||
{{ item }}
|
||||
</div>
|
||||
<template #prepend-footer>
|
||||
<Button type="link" @click="handleUpdate(6)">点击更新数据</Button>
|
||||
<Button type="link" @click="handleUpdate()">点击更新数据</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
import { Button, message } from 'antdv-next';
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
onCancel() {
|
||||
@@ -18,9 +18,17 @@ const [Modal, modalApi] = useVbenModal({
|
||||
message.info('onOpened:打开动画结束');
|
||||
},
|
||||
});
|
||||
|
||||
function lockModal() {
|
||||
modalApi.lock();
|
||||
setTimeout(() => {
|
||||
modalApi.unlock();
|
||||
}, 3000);
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<Modal class="w-[600px]" title="基础弹窗示例" title-tooltip="标题提示内容">
|
||||
<Modal class="w-150" title="基础弹窗示例" title-tooltip="标题提示内容">
|
||||
base demo
|
||||
<Button type="primary" @click="lockModal">锁定弹窗</Button>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
23
playground/src/views/examples/modal/blur-demo.vue
Normal file
23
playground/src/views/examples/modal/blur-demo.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { Slider } from 'antdv-next';
|
||||
|
||||
const blur = ref(5);
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
overlayBlur: blur.value,
|
||||
});
|
||||
watch(blur, (val) => {
|
||||
modalApi.setState({
|
||||
overlayBlur: val,
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<Modal title="遮罩层模糊">
|
||||
<p>调整滑块来改变遮罩层模糊程度:{{ blur }}</p>
|
||||
<Slider v-model:value="blur" :max="30" :min="0" />
|
||||
</Modal>
|
||||
</template>
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
import { message } from 'antdv-next';
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
draggable: true,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { Button, message } from 'ant-design-vue';
|
||||
import { Button, message } from 'antdv-next';
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
draggable: true,
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<script lang="ts" setup>
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
import { message } from 'antdv-next';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
|
||||
|
||||
|
||||
function onSubmit(values: Record<string, any>) {
|
||||
message.info(JSON.stringify(values)); // 只会执行一次
|
||||
interface FormModalData {
|
||||
values?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
@@ -35,6 +35,7 @@ const [Form, formApi] = useVbenForm({
|
||||
{
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
options: [
|
||||
{ label: '选项1', value: '1' },
|
||||
{ label: '选项2', value: '2' },
|
||||
@@ -49,25 +50,44 @@ const [Form, formApi] = useVbenForm({
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
const [Modal, modalApi] = useVbenModal<FormModalData>({
|
||||
fullscreenButton: false,
|
||||
onCancel() {
|
||||
modalApi.close();
|
||||
},
|
||||
onConfirm: async () => {
|
||||
await formApi.validateAndSubmitForm();
|
||||
await formApi.validateAndSubmit();
|
||||
// modalApi.close();
|
||||
},
|
||||
onOpenChange(isOpen: boolean) {
|
||||
if (isOpen) {
|
||||
const { values } = modalApi.getData<Record<string, any>>();
|
||||
if (values) {
|
||||
formApi.setValues(values);
|
||||
const data = modalApi.getData();
|
||||
if (data?.values) {
|
||||
formApi.setValues(data.values);
|
||||
}
|
||||
}
|
||||
},
|
||||
title: '内嵌表单示例',
|
||||
});
|
||||
|
||||
defineExpose({ modalApi });
|
||||
|
||||
function onSubmit(values: Record<string, any>) {
|
||||
message.loading({
|
||||
content: '正在提交中...',
|
||||
duration: 0,
|
||||
key: 'is-form-submitting',
|
||||
});
|
||||
modalApi.lock();
|
||||
setTimeout(() => {
|
||||
modalApi.close();
|
||||
message.success({
|
||||
content: `提交成功:${JSON.stringify(values)}`,
|
||||
duration: 2,
|
||||
key: 'is-form-submitting',
|
||||
});
|
||||
}, 3000);
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<Modal>
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
import { Input, message } from 'antdv-next';
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
destroyOnClose: false,
|
||||
onCancel() {
|
||||
modalApi.close();
|
||||
},
|
||||
@@ -12,14 +15,16 @@ const [Modal, modalApi] = useVbenModal({
|
||||
// modalApi.close();
|
||||
},
|
||||
});
|
||||
const value = ref();
|
||||
</script>
|
||||
<template>
|
||||
<Modal
|
||||
append-to-main
|
||||
class="w-[600px]"
|
||||
class="w-150"
|
||||
title="基础弹窗示例"
|
||||
title-tooltip="标题提示内容"
|
||||
>
|
||||
此弹窗指定在内容区域打开
|
||||
此弹窗指定在内容区域打开,并且在关闭之后弹窗内容不会被销毁
|
||||
<Input v-model:value="value" placeholder="KeepAlive测试" />
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
@@ -1,16 +1,35 @@
|
||||
<script lang="ts" setup>
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import type { ExplicitModalData } from './typed-data-contract';
|
||||
|
||||
import { Button, Card } from 'ant-design-vue';
|
||||
import { onBeforeUnmount } from 'vue';
|
||||
|
||||
import {
|
||||
alert,
|
||||
clearAllAlerts,
|
||||
confirm,
|
||||
Page,
|
||||
prompt,
|
||||
useVbenModal,
|
||||
} from '@vben/common-ui';
|
||||
|
||||
import { Button, Card, Flex, message } from 'antdv-next';
|
||||
|
||||
import DocButton from '../doc-button.vue';
|
||||
import AutoHeightDemo from './auto-height-demo.vue';
|
||||
import BaseDemo from './base-demo.vue';
|
||||
import BlurDemo from './blur-demo.vue';
|
||||
import DragDemo from './drag-demo.vue';
|
||||
import DynamicDemo from './dynamic-demo.vue';
|
||||
import FormModalDemo from './form-modal-demo.vue';
|
||||
import InContentModalDemo from './in-content-demo.vue';
|
||||
import NestedDemo from './nested-demo.vue';
|
||||
import SharedDataDemo from './shared-data-demo.vue';
|
||||
import TypedDataAutoDemo from './typed-data-auto-demo.vue';
|
||||
import { useFactoryModal } from './typed-data-contract';
|
||||
import TypedDataExplicitDemo from './typed-data-explicit-demo.vue';
|
||||
import TypedDataFactoryDemo from './typed-data-factory-demo.vue';
|
||||
|
||||
defineOptions({ name: 'ModalExample' });
|
||||
|
||||
const [BaseModal, baseModalApi] = useVbenModal({
|
||||
// 连接抽离的组件
|
||||
@@ -42,6 +61,27 @@ const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: FormModalDemo,
|
||||
});
|
||||
|
||||
const [TypedDataAutoModal, typedDataAutoModalApi] = useVbenModal({
|
||||
connectedComponent: TypedDataAutoDemo,
|
||||
});
|
||||
|
||||
const [TypedDataExplicitModal, typedDataExplicitModalApi] =
|
||||
useVbenModal<ExplicitModalData>({
|
||||
connectedComponent: TypedDataExplicitDemo,
|
||||
});
|
||||
|
||||
const [TypedDataFactoryModal, typedDataFactoryModalApi] = useFactoryModal({
|
||||
connectedComponent: TypedDataFactoryDemo,
|
||||
});
|
||||
|
||||
const [NestedModal, nestedModalApi] = useVbenModal({
|
||||
connectedComponent: NestedDemo,
|
||||
});
|
||||
|
||||
const [BlurModal, blurModalApi] = useVbenModal({
|
||||
connectedComponent: BlurDemo,
|
||||
});
|
||||
|
||||
function openBaseModal() {
|
||||
baseModalApi.open();
|
||||
}
|
||||
@@ -63,24 +103,118 @@ function openDynamicModal() {
|
||||
}
|
||||
|
||||
function openSharedModal() {
|
||||
sharedModalApi.setData({
|
||||
content: '外部传递的数据 content',
|
||||
payload: '外部传递的数据 payload',
|
||||
});
|
||||
sharedModalApi.open();
|
||||
sharedModalApi
|
||||
.setData({
|
||||
content: '外部传递的数据 content',
|
||||
payload: '外部传递的数据 payload',
|
||||
})
|
||||
.open();
|
||||
}
|
||||
|
||||
function openNestedModal() {
|
||||
nestedModalApi.open();
|
||||
}
|
||||
|
||||
function openBlurModal() {
|
||||
blurModalApi.open();
|
||||
}
|
||||
|
||||
function handleUpdateTitle() {
|
||||
dynamicModalApi.setState({ title: '外部动态标题' });
|
||||
dynamicModalApi.open();
|
||||
dynamicModalApi.setState({ title: '外部动态标题' }).open();
|
||||
}
|
||||
|
||||
function openFormModal() {
|
||||
formModalApi.setData({
|
||||
// 表单值
|
||||
values: { field1: 'abc' },
|
||||
formModalApi
|
||||
.setData({
|
||||
// 表单值
|
||||
values: { field1: 'abc', field2: '123', field3: '1' },
|
||||
})
|
||||
.open();
|
||||
}
|
||||
|
||||
function openTypedDataAutoModal() {
|
||||
typedDataAutoModalApi
|
||||
.setData({
|
||||
message: '外部无需声明泛型,由 connected component 自动推导。',
|
||||
method: '自动推导',
|
||||
})
|
||||
.open();
|
||||
}
|
||||
|
||||
function openTypedDataExplicitModal() {
|
||||
typedDataExplicitModalApi
|
||||
.setData({
|
||||
message: '父子组件显式引用同一个数据类型。',
|
||||
method: '显式泛型',
|
||||
})
|
||||
.open();
|
||||
}
|
||||
|
||||
function openTypedDataFactoryModal() {
|
||||
typedDataFactoryModalApi
|
||||
.setData({
|
||||
message: '父子组件复用预绑定的 typed composable。',
|
||||
method: '契约工厂',
|
||||
})
|
||||
.open();
|
||||
}
|
||||
|
||||
function openAlert() {
|
||||
alert({
|
||||
content: '这是一个弹窗',
|
||||
icon: 'success',
|
||||
}).then(() => {
|
||||
message.info('用户关闭了弹窗');
|
||||
});
|
||||
formModalApi.open();
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
// 清除所有弹窗
|
||||
clearAllAlerts();
|
||||
});
|
||||
|
||||
function openConfirm() {
|
||||
confirm({
|
||||
beforeClose({ isConfirm }) {
|
||||
if (!isConfirm) return;
|
||||
// 这里可以做一些异步操作
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve(true);
|
||||
}, 1000);
|
||||
});
|
||||
},
|
||||
centered: false,
|
||||
content: '这是一个确认弹窗',
|
||||
icon: 'question',
|
||||
})
|
||||
.then(() => {
|
||||
message.success('用户确认了操作');
|
||||
})
|
||||
.catch(() => {
|
||||
message.error('用户取消了操作');
|
||||
});
|
||||
}
|
||||
|
||||
async function openPrompt() {
|
||||
prompt<string>({
|
||||
async beforeClose({ isConfirm, value }) {
|
||||
if (isConfirm && value === '芝士') {
|
||||
message.error('不能吃芝士');
|
||||
return false;
|
||||
}
|
||||
},
|
||||
componentProps: { placeholder: '不能吃芝士...' },
|
||||
content: '中午吃了什么?',
|
||||
icon: 'question',
|
||||
overlayBlur: 3,
|
||||
})
|
||||
.then((res) => {
|
||||
message.success(`用户输入了:${res}`);
|
||||
})
|
||||
.catch(() => {
|
||||
message.error('用户取消了输入');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -100,44 +234,121 @@ function openFormModal() {
|
||||
<DynamicModal />
|
||||
<SharedDataModal />
|
||||
<FormModal />
|
||||
<Card class="mb-4" title="基本使用">
|
||||
<p class="mb-3">一个基础的弹窗示例</p>
|
||||
<Button type="primary" @click="openBaseModal">打开弹窗</Button>
|
||||
</Card>
|
||||
<TypedDataAutoModal />
|
||||
<TypedDataExplicitModal />
|
||||
<TypedDataFactoryModal />
|
||||
<NestedModal />
|
||||
<BlurModal />
|
||||
<Flex wrap="wrap" class="w-full" gap="10">
|
||||
<Card class="w-75" title="基本使用">
|
||||
<p>一个基础的弹窗示例</p>
|
||||
<template #actions>
|
||||
<Button type="primary" @click="openBaseModal">打开弹窗</Button>
|
||||
</template>
|
||||
</Card>
|
||||
|
||||
<Card class="mb-4" title="指定容器">
|
||||
<p class="mb-3">在内容区域打开弹窗的示例</p>
|
||||
<Button type="primary" @click="openInContentModal">打开弹窗</Button>
|
||||
</Card>
|
||||
<Card class="w-75" title="指定容器+关闭后不销毁">
|
||||
<p>在内容区域打开弹窗的示例</p>
|
||||
<template #actions>
|
||||
<Button type="primary" @click="openInContentModal">打开弹窗</Button>
|
||||
</template>
|
||||
</Card>
|
||||
|
||||
<Card class="mb-4" title="内容高度自适应">
|
||||
<p class="mb-3">可根据内容并自动调整高度</p>
|
||||
<Button type="primary" @click="openAutoHeightModal">打开弹窗</Button>
|
||||
</Card>
|
||||
<Card class="w-75" title="内容高度自适应">
|
||||
<p>可根据内容并自动调整高度</p>
|
||||
<template #actions>
|
||||
<Button type="primary" @click="openAutoHeightModal">
|
||||
打开弹窗
|
||||
</Button>
|
||||
</template>
|
||||
</Card>
|
||||
|
||||
<Card class="mb-4" title="可拖拽示例">
|
||||
<p class="mb-3">配置 draggable 可开启拖拽功能</p>
|
||||
<Button type="primary" @click="openDragModal">打开弹窗</Button>
|
||||
</Card>
|
||||
<Card class="w-75" title="可拖拽示例">
|
||||
<p>配置 draggable 可开启拖拽功能</p>
|
||||
<template #actions>
|
||||
<Button type="primary" @click="openDragModal"> 打开弹窗 </Button>
|
||||
</template>
|
||||
</Card>
|
||||
|
||||
<Card class="mb-4" title="动态配置示例">
|
||||
<p class="mb-3">通过 setState 动态调整弹窗数据</p>
|
||||
<Button type="primary" @click="openDynamicModal">打开弹窗</Button>
|
||||
<Button class="ml-2" type="primary" @click="handleUpdateTitle">
|
||||
从外部修改标题并打开
|
||||
</Button>
|
||||
</Card>
|
||||
<Card class="w-75" title="动态配置示例">
|
||||
<p>通过 setState 动态调整弹窗数据</p>
|
||||
<template #extra>
|
||||
<Button type="link" @click="openDynamicModal">打开弹窗</Button>
|
||||
</template>
|
||||
<template #actions>
|
||||
<Button type="primary" @click="handleUpdateTitle">
|
||||
外部修改标题并打开
|
||||
</Button>
|
||||
</template>
|
||||
</Card>
|
||||
|
||||
<Card class="mb-4" title="内外数据共享示例">
|
||||
<p class="mb-3">通过共享 sharedData 来进行数据交互</p>
|
||||
<Button type="primary" @click="openSharedModal">
|
||||
打开弹窗并传递数据
|
||||
</Button>
|
||||
</Card>
|
||||
<Card class="w-75" title="内外数据共享示例">
|
||||
<p>通过共享 sharedData 来进行数据交互</p>
|
||||
<template #actions>
|
||||
<Button type="primary" @click="openSharedModal">
|
||||
打开弹窗并传递数据
|
||||
</Button>
|
||||
</template>
|
||||
</Card>
|
||||
|
||||
<Card class="mb-4" title="表单弹窗示例">
|
||||
<p class="mb-3">弹窗与表单结合</p>
|
||||
<Button type="primary" @click="openFormModal"> 打开弹窗 </Button>
|
||||
</Card>
|
||||
<Card class="w-75" title="表单弹窗示例">
|
||||
<p>弹窗与表单结合</p>
|
||||
<template #actions>
|
||||
<Button type="primary" @click="openFormModal"> 打开表单弹窗 </Button>
|
||||
</template>
|
||||
</Card>
|
||||
|
||||
<Card class="w-75" title="共享数据:自动推导">
|
||||
<p>子组件 expose API,父组件从 connected component 推导类型</p>
|
||||
<template #actions>
|
||||
<Button type="primary" @click="openTypedDataAutoModal">
|
||||
打开自动推导示例
|
||||
</Button>
|
||||
</template>
|
||||
</Card>
|
||||
|
||||
<Card class="w-75" title="共享数据:显式泛型">
|
||||
<p>无法自动推导时,父子组件显式引用同一个数据类型</p>
|
||||
<template #actions>
|
||||
<Button type="primary" @click="openTypedDataExplicitModal">
|
||||
打开显式泛型示例
|
||||
</Button>
|
||||
</template>
|
||||
</Card>
|
||||
|
||||
<Card class="w-75" title="共享数据:契约工厂">
|
||||
<p>通过 createVbenModal 预绑定类型并复用 typed composable</p>
|
||||
<template #actions>
|
||||
<Button type="primary" @click="openTypedDataFactoryModal">
|
||||
打开契约工厂示例
|
||||
</Button>
|
||||
</template>
|
||||
</Card>
|
||||
|
||||
<Card class="w-75" title="嵌套弹窗示例">
|
||||
<p>在已经打开的弹窗中再次打开弹窗</p>
|
||||
<template #actions>
|
||||
<Button type="primary" @click="openNestedModal">打开嵌套弹窗</Button>
|
||||
</template>
|
||||
</Card>
|
||||
|
||||
<Card class="w-75" title="遮罩模糊示例">
|
||||
<p>遮罩层应用类似毛玻璃的模糊效果</p>
|
||||
<template #actions>
|
||||
<Button type="primary" @click="openBlurModal">打开弹窗</Button>
|
||||
</template>
|
||||
</Card>
|
||||
<Card class="w-75" title="轻量提示弹窗">
|
||||
<template #extra>
|
||||
<DocButton path="/components/common-ui/vben-alert" />
|
||||
</template>
|
||||
<p>通过快捷方法创建动态提示弹窗,适合一些轻量的提示和确认、输入等</p>
|
||||
<template #actions>
|
||||
<Button type="primary" @click="openAlert">Alert</Button>
|
||||
<Button type="primary" @click="openConfirm">Confirm</Button>
|
||||
<Button type="primary" @click="openPrompt">Prompt</Button>
|
||||
</template>
|
||||
</Card>
|
||||
</Flex>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
24
playground/src/views/examples/modal/nested-demo.vue
Normal file
24
playground/src/views/examples/modal/nested-demo.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<script lang="ts" setup>
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { Button } from 'antdv-next';
|
||||
|
||||
import DragDemo from './drag-demo.vue';
|
||||
|
||||
const [Modal] = useVbenModal({
|
||||
destroyOnClose: true,
|
||||
});
|
||||
const [BaseModal, baseModalApi] = useVbenModal({
|
||||
connectedComponent: DragDemo,
|
||||
});
|
||||
|
||||
function openNestedModal() {
|
||||
baseModalApi.open();
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<Modal title="嵌套弹窗示例">
|
||||
<Button @click="openNestedModal" type="primary">打开子弹窗</Button>
|
||||
<BaseModal />
|
||||
</Modal>
|
||||
</template>
|
||||
@@ -3,11 +3,16 @@ import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
import { message } from 'antdv-next';
|
||||
|
||||
const data = ref();
|
||||
interface SharedData {
|
||||
content: string;
|
||||
payload: string;
|
||||
}
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
const data = ref<SharedData>();
|
||||
|
||||
const [Modal, modalApi] = useVbenModal<SharedData>({
|
||||
onCancel() {
|
||||
modalApi.close();
|
||||
},
|
||||
@@ -17,10 +22,12 @@ const [Modal, modalApi] = useVbenModal({
|
||||
},
|
||||
onOpenChange(isOpen: boolean) {
|
||||
if (isOpen) {
|
||||
data.value = modalApi.getData<Record<string, any>>();
|
||||
data.value = modalApi.getData();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
defineExpose({ modalApi });
|
||||
</script>
|
||||
<template>
|
||||
<Modal title="数据共享示例">
|
||||
|
||||
29
playground/src/views/examples/modal/typed-data-auto-demo.vue
Normal file
29
playground/src/views/examples/modal/typed-data-auto-demo.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
interface AutoModalData {
|
||||
message: string;
|
||||
method: '自动推导';
|
||||
}
|
||||
|
||||
const data = ref<AutoModalData>();
|
||||
|
||||
const [Modal, modalApi] = useVbenModal<AutoModalData>({
|
||||
onOpenChange(isOpen) {
|
||||
data.value = isOpen ? modalApi.getData() : undefined;
|
||||
},
|
||||
});
|
||||
|
||||
defineExpose({ modalApi });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal title="自动推导数据类型">
|
||||
<div class="space-y-2 px-2">
|
||||
<p>类型来源:{{ data?.method }}</p>
|
||||
<p>接收内容:{{ data?.message }}</p>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
13
playground/src/views/examples/modal/typed-data-contract.ts
Normal file
13
playground/src/views/examples/modal/typed-data-contract.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { createVbenModal } from '@vben/common-ui';
|
||||
|
||||
export interface ExplicitModalData {
|
||||
message: string;
|
||||
method: '显式泛型';
|
||||
}
|
||||
|
||||
export interface FactoryModalData {
|
||||
message: string;
|
||||
method: '契约工厂';
|
||||
}
|
||||
|
||||
export const useFactoryModal = createVbenModal<FactoryModalData>();
|
||||
@@ -0,0 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import type { ExplicitModalData } from './typed-data-contract';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
const data = ref<ExplicitModalData>();
|
||||
|
||||
const [Modal, modalApi] = useVbenModal<ExplicitModalData>({
|
||||
onOpenChange(isOpen) {
|
||||
data.value = isOpen ? modalApi.getData() : undefined;
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal title="显式泛型数据类型">
|
||||
<div class="space-y-2 px-2">
|
||||
<p>类型来源:{{ data?.method }}</p>
|
||||
<p>接收内容:{{ data?.message }}</p>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
@@ -0,0 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import type { FactoryModalData } from './typed-data-contract';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useFactoryModal } from './typed-data-contract';
|
||||
|
||||
const data = ref<FactoryModalData>();
|
||||
|
||||
const [Modal, modalApi] = useFactoryModal({
|
||||
onOpenChange(isOpen) {
|
||||
data.value = isOpen ? modalApi.getData() : undefined;
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal title="契约工厂数据类型">
|
||||
<div class="space-y-2 px-2">
|
||||
<p>类型来源:{{ data?.method }}</p>
|
||||
<p>接收内容:{{ data?.message }}</p>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
197
playground/src/views/examples/motion/index.vue
Normal file
197
playground/src/views/examples/motion/index.vue
Normal file
@@ -0,0 +1,197 @@
|
||||
<script lang="ts" setup>
|
||||
import { reactive } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { Motion, MotionGroup, MotionPresets } from '@vben/plugins/motion';
|
||||
|
||||
import { refAutoReset, watchDebounced } from '@vueuse/core';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Form,
|
||||
FormItem,
|
||||
InputNumber,
|
||||
Row,
|
||||
Select,
|
||||
} from 'antdv-next';
|
||||
// 本例子用不到visible类型的动画。带有VisibleOnce和Visible的类型会在组件进入视口被显示时执行动画,
|
||||
const presets = MotionPresets.filter((v) => !v.includes('Visible'));
|
||||
const showCard1 = refAutoReset(true, 100);
|
||||
const showCard2 = refAutoReset(true, 100);
|
||||
const showCard3 = refAutoReset(true, 100);
|
||||
const motionProps = reactive({
|
||||
delay: 0,
|
||||
duration: 300,
|
||||
enter: { scale: 1 },
|
||||
hovered: { scale: 1.1, transition: { delay: 0, duration: 50 } },
|
||||
preset: 'fade',
|
||||
tapped: { scale: 0.9, transition: { delay: 0, duration: 50 } },
|
||||
});
|
||||
|
||||
const motionGroupProps = reactive({
|
||||
delay: 0,
|
||||
duration: 300,
|
||||
enter: { scale: 1 },
|
||||
hovered: { scale: 1.1, transition: { delay: 0, duration: 50 } },
|
||||
preset: 'fade',
|
||||
tapped: { scale: 0.9, transition: { delay: 0, duration: 50 } },
|
||||
});
|
||||
|
||||
watchDebounced(
|
||||
motionProps,
|
||||
() => {
|
||||
showCard2.value = false;
|
||||
},
|
||||
{ debounce: 200, deep: true },
|
||||
);
|
||||
|
||||
watchDebounced(
|
||||
motionGroupProps,
|
||||
() => {
|
||||
showCard3.value = false;
|
||||
},
|
||||
{ debounce: 200, deep: true },
|
||||
);
|
||||
|
||||
function openDocPage() {
|
||||
window.open('https://motion.vueuse.org/', '_blank');
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<Page title="Motion">
|
||||
<template #description>
|
||||
<span>一个易于使用的为其它组件赋予动画效果的组件。</span>
|
||||
<Button type="link" @click="openDocPage">查看文档</Button>
|
||||
</template>
|
||||
<Card title="使用指令" :styles="{ body: { minHeight: '5rem' } }">
|
||||
<template #extra>
|
||||
<Button type="primary" @click="showCard1 = false">重载</Button>
|
||||
</template>
|
||||
<div>
|
||||
<div class="relative flex gap-2 overflow-hidden" v-if="showCard1">
|
||||
<Button v-motion-fade-visible>fade</Button>
|
||||
<Button v-motion-pop-visible :duration="500">pop</Button>
|
||||
<Button v-motion-slide-left>slide-left</Button>
|
||||
<Button v-motion-slide-right>slide-right</Button>
|
||||
<Button v-motion-slide-bottom>slide-bottom</Button>
|
||||
<Button v-motion-slide-top>slide-top</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<Card
|
||||
class="mt-2"
|
||||
title="使用组件(将内部作为一个整体添加动画)"
|
||||
:styles="{ body: { padding: 0 } }"
|
||||
>
|
||||
<div class="relative flex-center min-h-32 gap-2 overflow-hidden">
|
||||
<Motion
|
||||
v-bind="motionProps"
|
||||
v-if="showCard2"
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
<Button size="large">这个按钮在显示时会有动画效果</Button>
|
||||
<span>附属组件,会作为整体处理动画</span>
|
||||
</Motion>
|
||||
</div>
|
||||
<div class="relative flex-center min-h-32 gap-2 overflow-hidden">
|
||||
<div v-if="showCard2" class="flex items-center gap-2">
|
||||
<span>顺序延迟</span>
|
||||
<Motion
|
||||
v-bind="{
|
||||
...motionProps,
|
||||
delay: motionProps.delay + 100 * i,
|
||||
}"
|
||||
v-for="i in 5"
|
||||
:key="i"
|
||||
>
|
||||
<Button size="large">按钮{{ i }}</Button>
|
||||
</Motion>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Form :model="motionProps" :label-col="{ span: 10 }">
|
||||
<Row>
|
||||
<Col :span="8">
|
||||
<FormItem prop="preset" label="动画效果">
|
||||
<Select
|
||||
v-model:value="motionProps.preset"
|
||||
:options="presets.map((p) => ({ label: p, value: p }))"
|
||||
/>
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col :span="8">
|
||||
<FormItem prop="duration" label="持续时间">
|
||||
<InputNumber v-model:value="motionProps.duration" />
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col :span="8">
|
||||
<FormItem prop="delay" label="延迟动画">
|
||||
<InputNumber v-model:value="motionProps.delay" />
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col :span="8">
|
||||
<FormItem prop="hovered.scale" label="Hover缩放">
|
||||
<InputNumber v-model:value="motionProps.hovered.scale" />
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col :span="8">
|
||||
<FormItem prop="hovered.tapped" label="按下时缩放">
|
||||
<InputNumber v-model:value="motionProps.tapped.scale" />
|
||||
</FormItem>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
</div>
|
||||
</Card>
|
||||
<Card
|
||||
class="mt-2"
|
||||
title="分组动画(每个子元素都会应用相同的独立动画)"
|
||||
:styles="{ body: { padding: 0 } }"
|
||||
>
|
||||
<div class="relative flex-center min-h-32 gap-2 overflow-hidden">
|
||||
<MotionGroup v-bind="motionGroupProps" v-if="showCard3">
|
||||
<Button size="large">按钮1</Button>
|
||||
<Button size="large">按钮2</Button>
|
||||
<Button size="large">按钮3</Button>
|
||||
<Button size="large">按钮4</Button>
|
||||
<Button size="large">按钮5</Button>
|
||||
</MotionGroup>
|
||||
</div>
|
||||
<div>
|
||||
<Form :model="motionGroupProps" :label-col="{ span: 10 }">
|
||||
<Row>
|
||||
<Col :span="8">
|
||||
<FormItem prop="preset" label="动画效果">
|
||||
<Select
|
||||
v-model:value="motionGroupProps.preset"
|
||||
:options="presets.map((p) => ({ label: p, value: p }))"
|
||||
/>
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col :span="8">
|
||||
<FormItem prop="duration" label="持续时间">
|
||||
<InputNumber v-model:value="motionGroupProps.duration" />
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col :span="8">
|
||||
<FormItem prop="delay" label="延迟动画">
|
||||
<InputNumber v-model:value="motionGroupProps.delay" />
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col :span="8">
|
||||
<FormItem prop="hovered.scale" label="Hover缩放">
|
||||
<InputNumber v-model:value="motionGroupProps.hovered.scale" />
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col :span="8">
|
||||
<FormItem prop="hovered.tapped" label="按下时缩放">
|
||||
<InputNumber v-model:value="motionGroupProps.tapped.scale" />
|
||||
</FormItem>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
</div>
|
||||
</Card>
|
||||
</Page>
|
||||
</template>
|
||||
@@ -50,7 +50,7 @@ const resize = (size?: TSize, rect?: TSize) => {
|
||||
>
|
||||
<div
|
||||
:style="{ backgroundColor: colorMap[idx] }"
|
||||
class="h-full w-full"
|
||||
class="size-full"
|
||||
></div>
|
||||
</VResize>
|
||||
</template>
|
||||
|
||||
303
playground/src/views/examples/tippy/index.vue
Normal file
303
playground/src/views/examples/tippy/index.vue
Normal file
@@ -0,0 +1,303 @@
|
||||
<script lang="ts" setup>
|
||||
import type { TippyProps } from '@vben/common-ui';
|
||||
|
||||
import { reactive } from 'vue';
|
||||
|
||||
import { Page, Tippy } from '@vben/common-ui';
|
||||
|
||||
import { Button, Card, Flex } from 'antdv-next';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
|
||||
const tippyProps = reactive<TippyProps>({
|
||||
animation: 'shift-away',
|
||||
arrow: true,
|
||||
content: '这是一个提示',
|
||||
delay: [200, 200],
|
||||
duration: 200,
|
||||
followCursor: false,
|
||||
hideOnClick: false,
|
||||
inertia: true,
|
||||
maxWidth: 'none',
|
||||
placement: 'top',
|
||||
theme: 'dark',
|
||||
trigger: 'mouseenter focusin',
|
||||
});
|
||||
|
||||
function parseBoolean(value: string) {
|
||||
switch (value) {
|
||||
case 'false': {
|
||||
return false;
|
||||
}
|
||||
case 'true': {
|
||||
return true;
|
||||
}
|
||||
default: {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const [Form] = useVbenForm({
|
||||
handleValuesChange(values) {
|
||||
Object.assign(tippyProps, {
|
||||
...values,
|
||||
delay: [values.delay1, values.delay2],
|
||||
followCursor: parseBoolean(values.followCursor),
|
||||
hideOnClick: parseBoolean(values.hideOnClick),
|
||||
trigger: values.trigger.join(' '),
|
||||
});
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
buttonStyle: 'solid',
|
||||
class: 'w-full',
|
||||
options: [
|
||||
{ label: '自动', value: 'auto' },
|
||||
{ label: '暗色', value: 'dark' },
|
||||
{ label: '亮色', value: 'light' },
|
||||
],
|
||||
optionType: 'button',
|
||||
},
|
||||
defaultValue: tippyProps.theme,
|
||||
fieldName: 'theme',
|
||||
label: '主题',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
options: [
|
||||
{ label: '向上滑入', value: 'shift-away' },
|
||||
{ label: '向下滑入', value: 'shift-toward' },
|
||||
{ label: '缩放', value: 'scale' },
|
||||
{ label: '透视', value: 'perspective' },
|
||||
{ label: '淡入', value: 'fade' },
|
||||
],
|
||||
},
|
||||
defaultValue: tippyProps.animation,
|
||||
fieldName: 'animation',
|
||||
label: '动画类型',
|
||||
},
|
||||
{
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
buttonStyle: 'solid',
|
||||
options: [
|
||||
{ label: '是', value: true },
|
||||
{ label: '否', value: false },
|
||||
],
|
||||
optionType: 'button',
|
||||
},
|
||||
defaultValue: tippyProps.inertia,
|
||||
fieldName: 'inertia',
|
||||
label: '动画惯性',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
options: [
|
||||
{ label: '顶部', value: 'top' },
|
||||
{ label: '顶左', value: 'top-start' },
|
||||
{ label: '顶右', value: 'top-end' },
|
||||
{ label: '底部', value: 'bottom' },
|
||||
{ label: '底左', value: 'bottom-start' },
|
||||
{ label: '底右', value: 'bottom-end' },
|
||||
{ label: '左侧', value: 'left' },
|
||||
{ label: '左上', value: 'left-start' },
|
||||
{ label: '左下', value: 'left-end' },
|
||||
{ label: '右侧', value: 'right' },
|
||||
{ label: '右上', value: 'right-start' },
|
||||
{ label: '右下', value: 'right-end' },
|
||||
],
|
||||
},
|
||||
defaultValue: tippyProps.placement,
|
||||
fieldName: 'placement',
|
||||
label: '位置',
|
||||
},
|
||||
{
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
addonAfter: '毫秒',
|
||||
},
|
||||
defaultValue: tippyProps.duration,
|
||||
fieldName: 'duration',
|
||||
label: '动画时长',
|
||||
},
|
||||
{
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
addonAfter: '毫秒',
|
||||
},
|
||||
defaultValue: 100,
|
||||
fieldName: 'delay1',
|
||||
label: '显示延时',
|
||||
},
|
||||
{
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
addonAfter: '毫秒',
|
||||
},
|
||||
defaultValue: 100,
|
||||
fieldName: 'delay2',
|
||||
label: '隐藏延时',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
defaultValue: tippyProps.content,
|
||||
fieldName: 'content',
|
||||
label: '内容',
|
||||
},
|
||||
{
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
buttonStyle: 'solid',
|
||||
options: [
|
||||
{ label: '是', value: true },
|
||||
{ label: '否', value: false },
|
||||
],
|
||||
optionType: 'button',
|
||||
},
|
||||
defaultValue: tippyProps.arrow,
|
||||
fieldName: 'arrow',
|
||||
label: '指示箭头',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
options: [
|
||||
{ label: '不跟随', value: 'false' },
|
||||
{ label: '完全跟随', value: 'true' },
|
||||
{ label: '仅横向', value: 'horizontal' },
|
||||
{ label: '仅纵向', value: 'vertical' },
|
||||
{ label: '仅初始', value: 'initial' },
|
||||
],
|
||||
},
|
||||
defaultValue: tippyProps.followCursor?.toString(),
|
||||
fieldName: 'followCursor',
|
||||
label: '跟随指针',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
mode: 'multiple',
|
||||
options: [
|
||||
{ label: '鼠标移入', value: 'mouseenter' },
|
||||
{ label: '被点击', value: 'click' },
|
||||
{ label: '获得焦点', value: 'focusin' },
|
||||
{ label: '无触发,仅手动', value: 'manual' },
|
||||
],
|
||||
},
|
||||
defaultValue: tippyProps.trigger?.split(' '),
|
||||
fieldName: 'trigger',
|
||||
label: '触发方式',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
options: [
|
||||
{ label: '否', value: 'false' },
|
||||
{ label: '是', value: 'true' },
|
||||
{ label: '仅内部', value: 'toggle' },
|
||||
],
|
||||
},
|
||||
defaultValue: tippyProps.hideOnClick?.toString(),
|
||||
dependencies: {
|
||||
componentProps(_, formAction) {
|
||||
return {
|
||||
disabled: !formAction.values.trigger.includes('click'),
|
||||
};
|
||||
},
|
||||
triggerFields: ['trigger'],
|
||||
},
|
||||
fieldName: 'hideOnClick',
|
||||
help: '只有在触发方式为`click`时才有效',
|
||||
label: '点击后隐藏',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: 'none、200px',
|
||||
},
|
||||
defaultValue: tippyProps.maxWidth,
|
||||
fieldName: 'maxWidth',
|
||||
label: '最大宽度',
|
||||
},
|
||||
],
|
||||
showDefaultActions: false,
|
||||
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
|
||||
});
|
||||
|
||||
function goDoc() {
|
||||
window.open('https://atomiks.github.io/tippyjs/v6/all-props/');
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<Page title="Tippy">
|
||||
<template #description>
|
||||
<div class="flex items-center">
|
||||
<p>
|
||||
Tippy
|
||||
是一个轻量级的提示工具库,它可以用来创建各种交互式提示,如工具提示、引导提示等。
|
||||
</p>
|
||||
<Button type="link" size="small" @click="goDoc">查看文档</Button>
|
||||
</div>
|
||||
</template>
|
||||
<Card title="指令形式使用">
|
||||
<p class="mb-4">
|
||||
指令形式使用比较简洁,直接在需要展示tooltip的组件上用v-tippy传递配置,适用于固定内容的工具提示。
|
||||
</p>
|
||||
<Flex warp="warp" gap="20" align="center">
|
||||
<Button v-tippy="'这是一个提示,使用了默认的配置'">默认配置</Button>
|
||||
|
||||
<Button
|
||||
v-tippy="{ theme: 'light', content: '这是一个提示,总是light主题' }"
|
||||
>
|
||||
指定主题
|
||||
</Button>
|
||||
<Button
|
||||
v-tippy="{
|
||||
theme: 'light',
|
||||
content: '这个提示将在点燃组件100毫秒后激活',
|
||||
delay: 100,
|
||||
}"
|
||||
>
|
||||
指定延时
|
||||
</Button>
|
||||
<Button
|
||||
v-tippy="{
|
||||
content: '本提示的动画为`scale`',
|
||||
animation: 'scale',
|
||||
}"
|
||||
>
|
||||
指定动画
|
||||
</Button>
|
||||
</Flex>
|
||||
</Card>
|
||||
<Card title="组件形式使用" class="mt-4">
|
||||
<div class="flex w-full justify-center">
|
||||
<Tippy v-bind="tippyProps">
|
||||
<Button>鼠标移到这个组件上来体验效果</Button>
|
||||
</Tippy>
|
||||
</div>
|
||||
|
||||
<Form class="mt-4" />
|
||||
<template #actions>
|
||||
<p
|
||||
class="cursor-default text-secondary-foreground hover:text-secondary-foreground"
|
||||
>
|
||||
更多配置请
|
||||
<Button type="link" size="small" @click="goDoc">查看文档</Button>
|
||||
,这里只列出了一些常用的配置
|
||||
</p>
|
||||
</template>
|
||||
</Card>
|
||||
</Page>
|
||||
</template>
|
||||
77
playground/src/views/examples/tiptap/index.vue
Normal file
77
playground/src/views/examples/tiptap/index.vue
Normal file
@@ -0,0 +1,77 @@
|
||||
<script lang="ts" setup>
|
||||
import type { ImageUploadOptions } from '@vben/plugins/tiptap';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { VbenTiptap, VbenTiptapPreview } from '@vben/plugins/tiptap';
|
||||
|
||||
import { Card, Switch } from 'antdv-next';
|
||||
const content = ref(`
|
||||
<h1>Vben Tiptap</h1>
|
||||
<p>这个编辑器已经被封装在 <code>packages/effects/plugins/src/tiptap</code> 中。</p>
|
||||
<p>你可以直接在各个 app 里通过 <code>@vben/plugins/tiptap</code> 引入。</p>
|
||||
<blockquote>默认内置 StarterKit、Underline、TextAlign、Placeholder。</blockquote>
|
||||
`);
|
||||
const previewContent = computed(() => content.value);
|
||||
|
||||
const enableUpload = ref(true);
|
||||
|
||||
// Mock upload: 模拟上传延迟,支持进度回调
|
||||
const imageUpload: ImageUploadOptions = {
|
||||
accept: 'image/*',
|
||||
maxSize: 5 * 1024 * 1024, // 5MB
|
||||
upload: (file, onProgress) =>
|
||||
new Promise((resolve) => {
|
||||
let progress = 0;
|
||||
const interval = setInterval(() => {
|
||||
progress += Math.random() * 30;
|
||||
if (progress >= 100) {
|
||||
progress = 100;
|
||||
clearInterval(interval);
|
||||
}
|
||||
onProgress?.(Math.round(progress));
|
||||
if (progress >= 100) {
|
||||
// 上传完成后返回 mock URL
|
||||
resolve(
|
||||
`https://picsum.photos/seed/${Date.now()}/640/${Math.round((640 * ((file.size % 3) + 2)) / 4)}`,
|
||||
);
|
||||
}
|
||||
}, 300);
|
||||
}),
|
||||
onUploadError: (error) => {
|
||||
console.error('Image upload failed:', error);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page title="Tiptap 富文本">
|
||||
<template #description>
|
||||
<div class="mt-2 text-foreground/80">
|
||||
统一封装后的富文本编辑器,适合在各个 app 中直接复用。
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<Card class="mb-5" title="编辑器">
|
||||
<div class="mb-3 flex items-center gap-3">
|
||||
<span class="text-sm">启用图片上传:</span>
|
||||
<Switch v-model:checked="enableUpload" />
|
||||
</div>
|
||||
<VbenTiptap
|
||||
v-model="content"
|
||||
:image-upload="enableUpload ? imageUpload : undefined"
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Card class="mb-5" title="富文本预览">
|
||||
<VbenTiptapPreview :content="previewContent" />
|
||||
</Card>
|
||||
|
||||
<Card title="HTML 输出">
|
||||
<pre class="overflow-auto rounded-xl border border-border bg-muted p-4">
|
||||
{{ previewContent }}
|
||||
</pre>
|
||||
</Card>
|
||||
</Page>
|
||||
</template>
|
||||
@@ -3,7 +3,7 @@ import type { VxeGridListeners, VxeGridProps } from '#/adapter/vxe-table';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { Button, message } from 'ant-design-vue';
|
||||
import { Button, message } from 'antdv-next';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
|
||||
@@ -43,7 +43,22 @@ const gridEvents: VxeGridListeners<RowType> = {
|
||||
},
|
||||
};
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({ gridEvents, gridOptions });
|
||||
const [Grid, gridApi] = useVbenVxeGrid<RowType>({
|
||||
// 放开注释查看表单组件的类型
|
||||
// formOptions: {
|
||||
// schema: [
|
||||
// {
|
||||
// component: 'Switch',
|
||||
// fieldName: 'name',
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
gridEvents,
|
||||
gridOptions,
|
||||
});
|
||||
|
||||
// 放开注释查看当前表格实例的类型
|
||||
// gridApi.grid
|
||||
|
||||
const showBorder = gridApi.useStore((state) => state.gridOptions?.border);
|
||||
const showStripe = gridApi.useStore((state) => state.gridOptions?.stripe);
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { Button, Image, Switch, Tag } from 'ant-design-vue';
|
||||
import { Button, Image, Switch, Tag } from 'antdv-next';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getExampleTableApi } from '#/api';
|
||||
@@ -95,7 +95,7 @@ const [Grid] = useVbenVxeGrid({ gridOptions });
|
||||
<Image :src="row.imageUrl" height="30" width="30" />
|
||||
</template>
|
||||
<template #open="{ row }">
|
||||
<Switch v-model:checked="row.open" />
|
||||
<Switch v-model="row.open" />
|
||||
</template>
|
||||
<template #status="{ row }">
|
||||
<Tag :color="row.color">{{ row.status }}</Tag>
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { Button, message } from 'ant-design-vue';
|
||||
import { Button, message } from 'antdv-next';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getExampleTableApi } from '#/api';
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { Button } from 'ant-design-vue';
|
||||
import { Button } from 'antdv-next';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getExampleTableApi } from '#/api';
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Dayjs } from 'dayjs';
|
||||
|
||||
import type { VbenFormProps } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
import { message } from 'antdv-next';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getExampleTableApi } from '#/api';
|
||||
import { createDateRangeCodec } from '#/utils/date-range-codec';
|
||||
|
||||
interface RowType {
|
||||
category: string;
|
||||
@@ -18,7 +22,24 @@ interface RowType {
|
||||
releaseDate: string;
|
||||
}
|
||||
|
||||
const formOptions: VbenFormProps = {
|
||||
interface SearchFormValues extends Record<string, unknown> {
|
||||
category?: string;
|
||||
color?: string;
|
||||
date?: [Dayjs, Dayjs];
|
||||
price?: string;
|
||||
productName?: string;
|
||||
}
|
||||
|
||||
const searchCodec = createDateRangeCodec<SearchFormValues>()({
|
||||
endField: 'end',
|
||||
rangeField: 'date',
|
||||
startField: 'start',
|
||||
});
|
||||
|
||||
type SearchSubmitValues = ReturnType<typeof searchCodec.encode>;
|
||||
|
||||
const formOptions: VbenFormProps<SearchFormValues, SearchSubmitValues> = {
|
||||
codec: searchCodec,
|
||||
// 默认展开
|
||||
collapsed: false,
|
||||
schema: [
|
||||
@@ -58,8 +79,9 @@ const formOptions: VbenFormProps = {
|
||||
label: 'Color',
|
||||
},
|
||||
{
|
||||
component: 'DatePicker',
|
||||
fieldName: 'datePicker',
|
||||
component: 'RangePicker',
|
||||
defaultValue: [dayjs().subtract(7, 'days'), dayjs()],
|
||||
fieldName: 'date',
|
||||
label: 'Date',
|
||||
},
|
||||
],
|
||||
@@ -91,7 +113,7 @@ const gridOptions: VxeTableGridOptions<RowType> = {
|
||||
pagerConfig: {},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
query: async ({ page }, formValues: SearchSubmitValues) => {
|
||||
message.success(`Query params: ${JSON.stringify(formValues)}`);
|
||||
return await getExampleTableApi({
|
||||
page: page.currentPage,
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { Button } from 'ant-design-vue';
|
||||
import { Button } from 'antdv-next';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getExampleTableApi } from '#/api';
|
||||
@@ -55,7 +55,7 @@ const gridOptions: VxeGridProps<RowType> = {
|
||||
custom: true,
|
||||
export: true,
|
||||
// import: true,
|
||||
refresh: { code: 'query' },
|
||||
refresh: true,
|
||||
zoom: true,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { Button } from 'ant-design-vue';
|
||||
import { Button } from 'antdv-next';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
|
||||
|
||||
209
playground/src/views/examples/vxe-table/viewed.vue
Normal file
209
playground/src/views/examples/vxe-table/viewed.vue
Normal file
@@ -0,0 +1,209 @@
|
||||
<script lang="ts" setup>
|
||||
import type { OnActionClickParams, VxeGridProps } from '#/adapter/vxe-table';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { Button, message } from 'antdv-next';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getExampleTableApi } from '#/api';
|
||||
|
||||
interface RowType {
|
||||
category: string;
|
||||
color: string;
|
||||
id: string;
|
||||
price: string;
|
||||
productName: string;
|
||||
releaseDate: string;
|
||||
}
|
||||
|
||||
const gridOptions: VxeGridProps<RowType> = {
|
||||
checkboxConfig: {
|
||||
highlight: true,
|
||||
labelField: 'category',
|
||||
},
|
||||
columns: [
|
||||
{ title: '序号', type: 'seq', width: 50 },
|
||||
{ field: 'category', sortable: true, title: 'Category' },
|
||||
{ field: 'color', sortable: true, title: 'Color' },
|
||||
{ field: 'productName', sortable: true, title: 'Product Name' },
|
||||
{ field: 'price', sortable: true, title: 'Price' },
|
||||
{ field: 'releaseDate', formatter: 'formatDateTime', title: 'DateTime' },
|
||||
{
|
||||
align: 'center',
|
||||
cellRender: {
|
||||
attrs: {
|
||||
nameField: 'category',
|
||||
onClick: onActionClick,
|
||||
},
|
||||
name: 'CellOperation',
|
||||
options: [
|
||||
{
|
||||
code: 'view',
|
||||
text: '查看',
|
||||
},
|
||||
'edit',
|
||||
],
|
||||
},
|
||||
field: 'operation',
|
||||
fixed: 'right',
|
||||
headerAlign: 'center',
|
||||
showOverflow: false,
|
||||
title: $t('system.menu.operation'),
|
||||
width: 200,
|
||||
},
|
||||
],
|
||||
exportConfig: {},
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page, sort }) => {
|
||||
return await getExampleTableApi({
|
||||
page: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
sortBy: sort.field,
|
||||
sortOrder: sort.order,
|
||||
});
|
||||
},
|
||||
},
|
||||
sort: true,
|
||||
},
|
||||
sortConfig: {
|
||||
defaultSort: { field: 'category', order: 'desc' },
|
||||
remote: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
custom: true,
|
||||
export: true,
|
||||
// import: true,
|
||||
refresh: true,
|
||||
zoom: true,
|
||||
},
|
||||
};
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
gridOptions,
|
||||
viewedRowOptions: {
|
||||
// 触发已读的操作码
|
||||
actionCodes: ['view'],
|
||||
// 行数据中的唯一标识字段
|
||||
keyField: 'id',
|
||||
// 持久化配置(简写模式,使用内置 localStorage)
|
||||
// persist: 'viewed_rows',
|
||||
persist: {
|
||||
key: 'viewed-rows',
|
||||
type: 'indexedDB',
|
||||
ttl: 7 * 24 * 60 * 60 * 1000, // 7天过期
|
||||
maxSize: 200,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
function onActionClick({ code, row }: OnActionClickParams<RowType>) {
|
||||
switch (code) {
|
||||
case 'edit': {
|
||||
onEdit(row);
|
||||
break;
|
||||
}
|
||||
case 'view': {
|
||||
onView(row);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const editRow = ref<RowType>();
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
draggable: true,
|
||||
onConfirm: () => {
|
||||
modalApi.setState({ loading: true });
|
||||
editRow.value && gridApi.markRowAsViewed(editRow.value);
|
||||
modalApi.setState({ loading: false });
|
||||
modalApi.close();
|
||||
},
|
||||
});
|
||||
|
||||
function onEdit(row: RowType) {
|
||||
editRow.value = row;
|
||||
modalApi.open();
|
||||
}
|
||||
|
||||
function onView(row: RowType) {
|
||||
message.success({
|
||||
content: `查看${row.category}`,
|
||||
key: 'action_process_msg_id',
|
||||
});
|
||||
}
|
||||
|
||||
const isStyle = ref(false);
|
||||
|
||||
function onStyleSet() {
|
||||
isStyle.value = !isStyle.value;
|
||||
gridApi.setState({
|
||||
viewedRowOptions: {
|
||||
rowStyle: () => {
|
||||
return isStyle.value ? { backgroundColor: 'gray' } : '';
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const isClassName = ref(false);
|
||||
|
||||
function onClassNameSet() {
|
||||
isClassName.value = !isClassName.value;
|
||||
gridApi.setState({
|
||||
viewedRowOptions: {
|
||||
rowClassName: () => {
|
||||
return isClassName.value
|
||||
? 'bg-red-100 vxe-row--viewed'
|
||||
: 'vxe-row--viewed';
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function onCustomSet() {
|
||||
const tableData = gridApi.grid.getData();
|
||||
const keys = tableData.slice(0, 2).map((row) => row.id);
|
||||
gridApi.markKeysAsViewed(keys);
|
||||
}
|
||||
|
||||
function onClearViewed() {
|
||||
gridApi.clearViewedRows();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page
|
||||
auto-content-height
|
||||
description="表格行标记支持存储类型 custom | indexedDB | localStorage | memory | sessionStorage 。
|
||||
默认使用memory存储,当设置custom时需要自己实现getKeys()/setKeys()/removeKeys()。
|
||||
具体属性查看packages/effects/plugins/src/vxe-table/types.ts。可通过gridApi调用
|
||||
clearViewedRows()/getViewedKeys()/isRowViewed()/markKeysAsViewed()/markRowAsViewed()/removeViewedKeys()"
|
||||
title="表格行标记示例"
|
||||
>
|
||||
<Modal class="w-150" title="数据修改"> 数据修改完成后设置行标记</Modal>
|
||||
<Grid table-title="已查看行标记" table-title-help="提示">
|
||||
<template #toolbar-tools>
|
||||
<Button class="mr-2" type="primary" @click="onCustomSet">
|
||||
手动标记
|
||||
</Button>
|
||||
<Button class="mr-2" type="primary" @click="onStyleSet">
|
||||
设置Style
|
||||
</Button>
|
||||
<Button class="mr-2" type="primary" @click="onClassNameSet">
|
||||
设置ClassName
|
||||
</Button>
|
||||
<Button type="primary" @click="onClearViewed"> 清空缓存</Button>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
Reference in New Issue
Block a user