物业代码生成

This commit is contained in:
2025-06-18 11:03:42 +08:00
commit 1262d4c745
1881 changed files with 249599 additions and 0 deletions

View File

@@ -0,0 +1,195 @@
<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 { Button, Card, message } from 'ant-design-vue';
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',
} 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];
}
});
},
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: '前置回调',
},
],
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>
</Card>
<Card title="设置" class="mt-4">
<Form />
</Card>
</Page>
</template>

View File

@@ -0,0 +1,181 @@
<script lang="ts" setup>
import type { CaptchaPoint } from '@vben/common-ui';
import { reactive, ref } from 'vue';
import { Page, PointSelectionCaptcha } from '@vben/common-ui';
import { Card, Input, InputNumber, message, Switch } from 'ant-design-vue';
import { $t } from '#/locales';
const DEFAULT_CAPTCHA_IMAGE =
'https://unpkg.com/@vbenjs/static-source@0.1.7/source/default-captcha-image.jpeg';
const DEFAULT_HINT_IMAGE =
'https://unpkg.com/@vbenjs/static-source@0.1.7/source/default-hint-image.png';
const selectedPoints = ref<CaptchaPoint[]>([]);
const params = reactive({
captchaImage: '',
captchaImageUrl: DEFAULT_CAPTCHA_IMAGE,
height: undefined,
hintImage: '',
hintImageUrl: DEFAULT_HINT_IMAGE,
hintText: '唇,燕,碴,找',
paddingX: undefined,
paddingY: undefined,
showConfirm: true,
showHintImage: false,
title: '',
width: undefined,
});
const handleConfirm = (points: CaptchaPoint[], clear: () => void) => {
message.success({
content: `captcha points: ${JSON.stringify(points)}`,
});
clear();
selectedPoints.value = [];
};
const handleRefresh = () => {
selectedPoints.value = [];
};
const handleClick = (point: CaptchaPoint) => {
selectedPoints.value.push(point);
};
</script>
<template>
<Page
:description="$t('examples.captcha.pageDescription')"
:title="$t('examples.captcha.pageTitle')"
>
<Card :title="$t('examples.captcha.basic')" class="mb-4 overflow-x-auto">
<div class="mb-3 flex items-center justify-start">
<Input
v-model:value="params.title"
:placeholder="$t('examples.captcha.titlePlaceholder')"
class="w-64"
/>
<Input
v-model:value="params.captchaImageUrl"
:placeholder="$t('examples.captcha.captchaImageUrlPlaceholder')"
class="ml-8 w-64"
/>
<div class="ml-8 flex w-96 items-center">
<Switch
v-model:checked="params.showHintImage"
:checked-children="$t('examples.captcha.hintImage')"
:un-checked-children="$t('examples.captcha.hintText')"
class="mr-4 w-40"
/>
<Input
v-show="params.showHintImage"
v-model:value="params.hintImageUrl"
:placeholder="$t('examples.captcha.hintImagePlaceholder')"
/>
<Input
v-show="!params.showHintImage"
v-model:value="params.hintText"
:placeholder="$t('examples.captcha.hintTextPlaceholder')"
/>
</div>
<Switch
v-model:checked="params.showConfirm"
:checked-children="$t('examples.captcha.showConfirm')"
:un-checked-children="$t('examples.captcha.hideConfirm')"
class="ml-8 w-28"
/>
</div>
<div class="mb-3 flex items-center justify-start">
<div>
<InputNumber
v-model:value="params.width"
:min="1"
:placeholder="$t('examples.captcha.widthPlaceholder')"
:precision="0"
:step="1"
class="w-64"
>
<template #addonAfter>px</template>
</InputNumber>
</div>
<div class="ml-8">
<InputNumber
v-model:value="params.height"
:min="1"
:placeholder="$t('examples.captcha.heightPlaceholder')"
:precision="0"
:step="1"
class="w-64"
>
<template #addonAfter>px</template>
</InputNumber>
</div>
<div class="ml-8">
<InputNumber
v-model:value="params.paddingX"
:min="1"
:placeholder="$t('examples.captcha.paddingXPlaceholder')"
:precision="0"
:step="1"
class="w-64"
>
<template #addonAfter>px</template>
</InputNumber>
</div>
<div class="ml-8">
<InputNumber
v-model:value="params.paddingY"
:min="1"
:placeholder="$t('examples.captcha.paddingYPlaceholder')"
:precision="0"
:step="1"
class="w-64"
>
<template #addonAfter>px</template>
</InputNumber>
</div>
</div>
<PointSelectionCaptcha
:captcha-image="params.captchaImageUrl || params.captchaImage"
:height="params.height || 220"
:hint-image="
params.showHintImage ? params.hintImageUrl || params.hintImage : ''
"
:hint-text="params.hintText"
:padding-x="params.paddingX"
:padding-y="params.paddingY"
:show-confirm="params.showConfirm"
:width="params.width || 300"
class="float-left"
@click="handleClick"
@confirm="handleConfirm"
@refresh="handleRefresh"
>
<template #title>
{{ params.title || $t('examples.captcha.captchaCardTitle') }}
</template>
</PointSelectionCaptcha>
<ol class="float-left p-5">
<li v-for="point in selectedPoints" :key="point.i" class="flex">
<span class="mr-3 w-16">{{
$t('examples.captcha.index') + point.i
}}</span>
<span class="mr-3 w-52">{{
$t('examples.captcha.timestamp') + point.t
}}</span>
<span class="mr-3 w-16">{{
$t('examples.captcha.x') + point.x
}}</span>
<span class="mr-3 w-16">{{
$t('examples.captcha.y') + point.y
}}</span>
</li>
</ol>
</Card>
</Page>
</template>

View File

@@ -0,0 +1,117 @@
<script lang="ts" setup>
import type {
CaptchaVerifyPassingData,
SliderCaptchaActionType,
} from '@vben/common-ui';
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';
function handleSuccess(data: CaptchaVerifyPassingData) {
const { time } = data;
message.success(`校验成功,耗时${time}`);
}
function handleBtnClick(elRef?: SliderCaptchaActionType) {
if (!elRef) {
return;
}
elRef.resume();
}
const el1 = ref<SliderCaptchaActionType>();
const el2 = ref<SliderCaptchaActionType>();
const el3 = ref<SliderCaptchaActionType>();
const el4 = ref<SliderCaptchaActionType>();
const el5 = ref<SliderCaptchaActionType>();
const el6 = ref<SliderCaptchaActionType>();
</script>
<template>
<Page description="用于前端简单的拖动校验场景" title="滑块校验">
<Card class="mb-5" title="基础示例">
<div class="flex items-center justify-center p-4 px-[30%]">
<SliderCaptcha ref="el1" @success="handleSuccess" />
<Button class="ml-2" type="primary" @click="handleBtnClick(el1)">
还原
</Button>
</div>
</Card>
<Card class="mb-5" title="自定义圆角">
<div class="flex items-center justify-center p-4 px-[30%]">
<SliderCaptcha
ref="el2"
class="rounded-full"
@success="handleSuccess"
/>
<Button class="ml-2" type="primary" @click="handleBtnClick(el2)">
还原
</Button>
</div>
</Card>
<Card class="mb-5" title="自定义背景色">
<div class="flex items-center justify-center p-4 px-[30%]">
<SliderCaptcha
ref="el3"
:bar-style="{
backgroundColor: '#018ffb',
}"
success-text="校验成功"
text="拖动以进行校验"
@success="handleSuccess"
/>
<Button class="ml-2" type="primary" @click="handleBtnClick(el3)">
还原
</Button>
</div>
</Card>
<Card class="mb-5" title="自定义拖拽图标">
<div class="flex items-center justify-center p-4 px-[30%]">
<SliderCaptcha ref="el4" @success="handleSuccess">
<template #actionIcon="{ isPassing }">
<Bell v-if="isPassing" />
<Sun v-else />
</template>
</SliderCaptcha>
<Button class="ml-2" type="primary" @click="handleBtnClick(el4)">
还原
</Button>
</div>
</Card>
<Card class="mb-5" title="自定义文本">
<div class="flex items-center justify-center p-4 px-[30%]">
<SliderCaptcha
ref="el5"
success-text="成功"
text="拖动"
@success="handleSuccess"
/>
<Button class="ml-2" type="primary" @click="handleBtnClick(el5)">
还原
</Button>
</div>
</Card>
<Card class="mb-5" title="自定义内容(slot)">
<div class="flex items-center justify-center p-4 px-[30%]">
<SliderCaptcha ref="el6" @success="handleSuccess">
<template #text="{ isPassing }">
<template v-if="isPassing">
<Bell class="mr-2 size-4" />
成功
</template>
<template v-else>
拖动
<Sun class="ml-2 size-4" />
</template>
</template>
</SliderCaptcha>
<Button class="ml-2" type="primary" @click="handleBtnClick(el6)">
还原
</Button>
</div>
</Card>
</Page>
</template>

View File

@@ -0,0 +1,28 @@
<script setup lang="ts">
import { computed } from 'vue';
import { Page, SliderRotateCaptcha } from '@vben/common-ui';
import { preferences } from '@vben/preferences';
import { useUserStore } from '@vben/stores';
import { Card, message } from 'ant-design-vue';
const userStore = useUserStore();
function handleSuccess() {
message.success('success!');
}
const avatar = computed(() => {
return userStore.userInfo?.avatar || preferences.app.defaultAvatar;
});
</script>
<template>
<Page description="用于前端简单的拖动校验场景" title="滑块旋转校验">
<Card class="mb-5" title="基本示例">
<div class="flex items-center justify-center p-4">
<SliderRotateCaptcha :src="avatar" @success="handleSuccess" />
</div>
</Card>
</Page>
</template>

View File

@@ -0,0 +1,178 @@
<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 'ant-design-vue';
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 w-full items-center justify-center 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-none"
icon="ix:random-filled"
@click="changeNumber"
/>
</template>
</InputNumber>
</FormItem>
</Col>
<Col :span="8">
<FormItem label="禁用动画" name="disabled">
<Switch v-model:checked="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">
<Select.Option
v-for="preset in TransitionPresetsKeys"
:key="preset"
:value="preset"
>
{{ preset }}
</Select.Option>
</Select>
</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>

View File

@@ -0,0 +1,22 @@
<script lang="ts" setup>
import { VBEN_DOC_URL } from '@vben/constants';
import { openWindow } from '@vben/utils';
import { Button } from 'ant-design-vue';
const props = defineProps<{ path: string }>();
function handleClick() {
// 如果没有.html打开页面时可能会出现404
const path =
VBEN_DOC_URL +
(props.path.toLowerCase().endsWith('.html')
? props.path
: `${props.path}.html`);
openWindow(path);
}
</script>
<template>
<Button @click="handleClick">查看组件文档</Button>
</template>

View File

@@ -0,0 +1,47 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { useVbenDrawer } from '@vben/common-ui';
import { Button, message } from 'ant-design-vue';
const list = ref<number[]>([]);
const [Drawer, drawerApi] = useVbenDrawer({
onCancel() {
drawerApi.close();
},
onConfirm() {
message.info('onConfirm');
// drawerApi.close();
},
onOpenChange(isOpen) {
if (isOpen) {
handleUpdate(10);
}
},
});
function handleUpdate(len: number) {
drawerApi.setState({ loading: true });
setTimeout(() => {
list.value = Array.from({ length: len }, (_v, k) => k + 1);
drawerApi.setState({ loading: false });
}, 2000);
}
</script>
<template>
<Drawer title="自动计算高度">
<div
v-for="item in list"
:key="item"
class="even:bg-heavy bg-muted flex-center h-[220px] w-full"
>
{{ item }}
</div>
<template #prepend-footer>
<Button type="link" @click="handleUpdate(6)">点击更新数据</Button>
</template>
</Drawer>
</template>

View File

@@ -0,0 +1,35 @@
<script lang="ts" setup>
import { useVbenDrawer } from '@vben/common-ui';
import { Button, message } from 'ant-design-vue';
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>

View File

@@ -0,0 +1,31 @@
<script lang="ts" setup>
import { useVbenDrawer } from '@vben/common-ui';
import { Button, message } from 'ant-design-vue';
const [Drawer, drawerApi] = useVbenDrawer({
onCancel() {
drawerApi.close();
},
onConfirm() {
message.info('onConfirm');
// drawerApi.close();
},
title: '动态修改配置示例',
});
// const state = drawerApi.useStore();
function handleUpdateTitle() {
drawerApi.setState({ title: '内部动态标题' });
}
</script>
<template>
<Drawer>
<div class="flex-col-center">
<Button class="mb-3" type="primary" @click="handleUpdateTitle()">
内部动态修改标题
</Button>
</div>
</Drawer>
</template>

View File

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

View File

@@ -0,0 +1,48 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { useVbenDrawer } from '@vben/common-ui';
import { Input, message } from 'ant-design-vue';
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();
},
onConfirm() {
message.info('onConfirm');
// drawerApi.close();
},
});
</script>
<template>
<Drawer append-to-main title="基础抽屉示例" title-tooltip="标题提示内容">
<template #extra> extra </template>
此弹窗指定在内容区域打开并且在关闭之后弹窗内容不会被销毁
<Input
v-model:value="value"
placeholder="KeepAlive测试:connectedComponent"
/>
<Form />
</Drawer>
</template>

View File

@@ -0,0 +1,195 @@
<script lang="ts" setup>
import type { DrawerPlacement, DrawerState } from '@vben/common-ui';
import { Page, useVbenDrawer } from '@vben/common-ui';
import { Button, Card } from 'ant-design-vue';
import DocButton from '../doc-button.vue';
import AutoHeightDemo from './auto-height-demo.vue';
import BaseDemo from './base-demo.vue';
import DynamicDemo from './dynamic-demo.vue';
import FormDrawerDemo from './form-drawer-demo.vue';
import inContentDemo from './in-content-demo.vue';
import SharedDataDemo from './shared-data-demo.vue';
defineOptions({ name: 'DrawerExample' });
const [BaseDrawer, baseDrawerApi] = useVbenDrawer({
// 连接抽离的组件
connectedComponent: BaseDemo,
// placement: 'left',
});
const [InContentDrawer, inContentDrawerApi] = useVbenDrawer({
// 连接抽离的组件
connectedComponent: inContentDemo,
// placement: 'left',
});
const [AutoHeightDrawer, autoHeightDrawerApi] = useVbenDrawer({
connectedComponent: AutoHeightDemo,
});
const [DynamicDrawer, dynamicDrawerApi] = useVbenDrawer({
connectedComponent: DynamicDemo,
});
const [SharedDataDrawer, sharedDrawerApi] = useVbenDrawer({
connectedComponent: SharedDataDemo,
});
const [FormDrawer, formDrawerApi] = useVbenDrawer({
connectedComponent: FormDrawerDemo,
});
function openBaseDrawer(placement: DrawerPlacement = 'right') {
baseDrawerApi.setState({ placement }).open();
}
function openBlurDrawer() {
baseDrawerApi.setState({ overlayBlur: 5 }).open();
}
function openInContentDrawer(placement: DrawerPlacement = 'right') {
const state: Partial<DrawerState> = { class: '', placement };
if (placement === 'top') {
// 页面顶部区域的层级只有200所以设置一个低于200的值抽屉从顶部滑出来的时候才比较合适
state.zIndex = 199;
}
inContentDrawerApi.setState(state).open();
}
function openMaxContentDrawer() {
// 这里只是用来演示方便。实际上自己使用的时候可以直接将这些配置卸载Drawer的属性里
inContentDrawerApi.setState({ class: 'w-full', placement: 'right' }).open();
}
function openAutoHeightDrawer() {
autoHeightDrawerApi.open();
}
function openDynamicDrawer() {
dynamicDrawerApi.open();
}
function handleUpdateTitle() {
dynamicDrawerApi.setState({ title: '外部动态标题' }).open();
}
function openSharedDrawer() {
sharedDrawerApi
.setData({
content: '外部传递的数据 content',
payload: '外部传递的数据 payload',
})
.open();
}
function openFormDrawer() {
formDrawerApi
.setData({
// 表单值
values: { field1: 'abc', field2: '123' },
})
.open();
}
</script>
<template>
<Page
auto-content-height
description="抽屉组件通常用于在当前页面上显示一个覆盖层,用以展示重要信息或提供用户交互界面。"
title="抽屉组件示例"
>
<template #extra>
<DocButton path="/components/common-ui/vben-drawer" />
</template>
<BaseDrawer />
<InContentDrawer />
<AutoHeightDrawer />
<DynamicDrawer />
<SharedDataDrawer />
<FormDrawer />
<Card class="mb-4" title="基本使用">
<p class="mb-3">一个基础的抽屉示例</p>
<Button class="mb-2" type="primary" @click="openBaseDrawer('right')">
右侧打开
</Button>
<Button
class="mb-2 ml-2"
type="primary"
@click="openBaseDrawer('bottom')"
>
底部打开
</Button>
<Button class="mb-2 ml-2" type="primary" @click="openBaseDrawer('left')">
左侧打开
</Button>
<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="在内容区域打开">
<p class="mb-3">指定抽屉在内容区域打开不会覆盖顶部和左侧菜单等区域</p>
<Button class="mb-2" type="primary" @click="openInContentDrawer('right')">
右侧打开
</Button>
<Button
class="mb-2 ml-2"
type="primary"
@click="openInContentDrawer('bottom')"
>
底部打开
</Button>
<Button
class="mb-2 ml-2"
type="primary"
@click="openInContentDrawer('left')"
>
左侧打开
</Button>
<Button
class="mb-2 ml-2"
type="primary"
@click="openInContentDrawer('top')"
>
顶部打开
</Button>
<Button class="mb-2 ml-2" type="primary" @click="openMaxContentDrawer">
内容区域全屏打开
</Button>
</Card>
<Card class="mb-4" title="内容高度自适应滚动">
<p class="mb-3">可根据内容自动计算滚动高度</p>
<Button type="primary" @click="openAutoHeightDrawer">打开抽屉</Button>
</Card>
<Card class="mb-4" title="动态配置示例">
<p class="mb-3">通过 setState 动态调整抽屉数据</p>
<Button type="primary" @click="openDynamicDrawer">打开抽屉</Button>
<Button class="ml-2" type="primary" @click="handleUpdateTitle">
从外部修改标题并打开
</Button>
</Card>
<Card class="mb-4" title="内外数据共享示例">
<p class="mb-3">通过共享 sharedData 来进行数据交互</p>
<Button type="primary" @click="openSharedDrawer">
打开抽屉并传递数据
</Button>
</Card>
<Card class="mb-4" title="表单抽屉示例">
<p class="mb-3">打开抽屉并设置表单schema以及数据</p>
<Button type="primary" @click="openFormDrawer">
打开抽屉并设置表单schema以及数据
</Button>
</Card>
</Page>
</template>

View File

@@ -0,0 +1,29 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { useVbenDrawer } from '@vben/common-ui';
import { message } from 'ant-design-vue';
const data = ref();
const [Drawer, drawerApi] = useVbenDrawer({
onCancel() {
drawerApi.close();
},
onConfirm() {
message.info('onConfirm');
// drawerApi.close();
},
onOpenChange(isOpen: boolean) {
if (isOpen) {
data.value = drawerApi.getData<Record<string, any>>();
}
},
});
</script>
<template>
<Drawer title="数据共享示例">
<div class="flex-col-center">外部传递数据 {{ data }}</div>
</Drawer>
</template>

View File

@@ -0,0 +1,46 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { EllipsisText, Page } from '@vben/common-ui';
import { Card } from 'ant-design-vue';
import DocButton from '../doc-button.vue';
const longText = `Vben Admin 是一个基于 Vue3.0、Vite、 TypeScript 的后台解决方案目标是为开发中大型项目提供开箱即用的解决方案。包括二次封装组件、utils、hooks、动态菜单、权限校验、多主题配置、按钮级别权限控制等功能。项目会使用前端较新的技术栈可以作为项目的启动模版以帮助你快速搭建企业级中后台产品原型。也可以作为一个示例用于学习 vue3、vite、ts 等主流技术。该项目会持续跟进最新技术并将其应用在项目中。Vben Admin 是一个基于 Vue3.0、Vite、 TypeScript 的后台解决方案目标是为开发中大型项目提供开箱即用的解决方案。包括二次封装组件、utils、hooks、动态菜单、权限校验、多主题配置、按钮级别权限控制等功能。项目会使用前端较新的技术栈可以作为项目的启动模版以帮助你快速搭建企业级中后台产品原型。也可以作为一个示例用于学习 vue3、vite、ts 等主流技术。该项目会持续跟进最新技术并将其应用在项目中。Vben Admin 是一个基于 Vue3.0、Vite、 TypeScript 的后台解决方案目标是为开发中大型项目提供开箱即用的解决方案。包括二次封装组件、utils、hooks、动态菜单、权限校验、多主题配置、按钮级别权限控制等功能。项目会使用前端较新的技术栈可以作为项目的启动模版以帮助你快速搭建企业级中后台产品原型。也可以作为一个示例用于学习 vue3、vite、ts 等主流技术。该项目会持续跟进最新技术并将其应用在项目中。Vben Admin 是一个基于 Vue3.0、Vite、 TypeScript 的后台解决方案目标是为开发中大型项目提供开箱即用的解决方案。包括二次封装组件、utils、hooks、动态菜单、权限校验、多主题配置、按钮级别权限控制等功能。项目会使用前端较新的技术栈可以作为项目的启动模版以帮助你快速搭建企业级中后台产品原型。也可以作为一个示例用于学习 vue3、vite、ts 等主流技术。该项目会持续跟进最新技术,并将其应用在项目中。`;
const text = ref(longText);
</script>
<template>
<Page
description="用于多行文本省略,支持点击展开和自定义内容。"
title="文本省略组件示例"
>
<template #extra>
<DocButton class="mb-2" path="/components/common-ui/vben-ellipsis-text" />
</template>
<Card class="mb-4" title="基本使用">
<EllipsisText :max-width="500">{{ text }}</EllipsisText>
</Card>
<Card class="mb-4" title="多行省略">
<EllipsisText :line="2">{{ text }}</EllipsisText>
</Card>
<Card class="mb-4" title="点击展开">
<EllipsisText :line="3" expand>{{ text }}</EllipsisText>
</Card>
<Card class="mb-4" title="自定义内容">
<EllipsisText :max-width="240">
住在我心里孤独的 孤独的海怪 痛苦之王 开始厌倦 深海的光 停滞的海浪
<template #tooltip>
<div style="text-align: center">
秦皇岛<br />住在我心里孤独的<br />孤独的海怪 痛苦之王<br />开始厌倦
深海的光 停滞的海浪
</div>
</template>
</EllipsisText>
</Card>
</Page>
</template>

View File

@@ -0,0 +1,274 @@
<script lang="ts" setup>
import type { RefSelectProps } from 'ant-design-vue/es/select';
import { ref } from 'vue';
import { Page } from '@vben/common-ui';
import { Button, Card, message, Space } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
const isReverseActionButtons = ref(false);
const [BaseForm, formApi] = useVbenForm({
// 翻转操作按钮的位置
actionButtonsReverse: isReverseActionButtons.value,
// 所有表单项共用,可单独在表单内覆盖
commonConfig: {
// 所有表单项
componentProps: {
class: 'w-full',
},
},
// 使用 tailwindcss grid布局
// 提交函数
handleSubmit: onSubmit,
// 垂直布局label和input在不同行值为vertical
layout: 'horizontal',
// 水平布局label和input在同一行
schema: [
{
// 组件需要在 #/adapter.ts内注册并加上类型
component: 'Input',
// 对应组件的参数
componentProps: {
placeholder: '请输入用户名',
},
// 字段名
fieldName: 'field1',
// 界面显示的label
label: 'field1',
},
{
component: 'Input',
componentProps: {
placeholder: '请输入',
},
fieldName: 'field2',
label: 'field2',
},
{
component: 'Select',
componentProps: {
allowClear: true,
filterOption: true,
options: [
{
label: '选项1',
value: '1',
},
{
label: '选项2',
value: '2',
},
],
placeholder: '请选择',
showSearch: true,
},
fieldName: 'fieldOptions',
label: '下拉选',
},
],
// 大屏一行显示3个中屏一行显示2个小屏一行显示1个
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
});
function onSubmit(values: Record<string, any>) {
message.success({
content: `form values: ${JSON.stringify(values)}`,
});
}
function handleClick(
action:
| 'batchAddSchema'
| 'batchDeleteSchema'
| 'componentRef'
| 'disabled'
| 'hiddenAction'
| 'hiddenResetButton'
| 'hiddenSubmitButton'
| 'labelWidth'
| 'resetDisabled'
| 'resetLabelWidth'
| 'reverseActionButtons'
| 'showAction'
| 'showResetButton'
| 'showSubmitButton'
| 'updateActionAlign'
| 'updateResetButton'
| 'updateSchema'
| 'updateSubmitButton',
) {
switch (action) {
case 'batchAddSchema': {
formApi.setState((prev) => {
const currentSchema = prev?.schema ?? [];
const newSchema = [];
for (let i = 0; i < 3; i++) {
newSchema.push({
component: 'Input',
componentProps: {
placeholder: '请输入',
},
fieldName: `field${i}${Date.now()}`,
label: `field+`,
});
}
return {
schema: [...currentSchema, ...newSchema],
};
});
break;
}
case 'batchDeleteSchema': {
formApi.setState((prev) => {
const currentSchema = prev?.schema ?? [];
return {
schema: currentSchema.slice(0, -3),
};
});
break;
}
case 'componentRef': {
// 获取下拉组件的实例并调用它的focus方法
formApi.getFieldComponentRef<RefSelectProps>('fieldOptions')?.focus?.();
break;
}
case 'disabled': {
formApi.setState({ commonConfig: { disabled: true } });
break;
}
case 'hiddenAction': {
formApi.setState({ showDefaultActions: false });
break;
}
case 'hiddenResetButton': {
formApi.setState({ resetButtonOptions: { show: false } });
break;
}
case 'hiddenSubmitButton': {
formApi.setState({ submitButtonOptions: { show: false } });
break;
}
case 'labelWidth': {
formApi.setState({
commonConfig: {
labelWidth: 150,
},
});
break;
}
case 'resetDisabled': {
formApi.setState({ commonConfig: { disabled: false } });
break;
}
case 'resetLabelWidth': {
formApi.setState({
commonConfig: {
labelWidth: 100,
},
});
break;
}
case 'reverseActionButtons': {
isReverseActionButtons.value = !isReverseActionButtons.value;
formApi.setState({ actionButtonsReverse: isReverseActionButtons.value });
break;
}
case 'showAction': {
formApi.setState({ showDefaultActions: true });
break;
}
case 'showResetButton': {
formApi.setState({ resetButtonOptions: { show: true } });
break;
}
case 'showSubmitButton': {
formApi.setState({ submitButtonOptions: { show: true } });
break;
}
case 'updateActionAlign': {
formApi.setState({
// 可以自行调整class
actionWrapperClass: 'text-center',
});
break;
}
case 'updateResetButton': {
formApi.setState({
resetButtonOptions: { disabled: true },
});
break;
}
case 'updateSchema': {
formApi.updateSchema([
{
componentProps: {
options: [
{
label: '选项1',
value: '1',
},
{
label: '选项2',
value: '2',
},
{
label: '选项3',
value: '3',
},
],
},
fieldName: 'fieldOptions',
},
]);
message.success('字段 `fieldOptions` 下拉选项更新成功。');
break;
}
case 'updateSubmitButton': {
formApi.setState({
submitButtonOptions: { loading: true },
});
break;
}
}
}
</script>
<template>
<Page description="表单组件api操作示例。" title="表单组件">
<Space class="mb-5 flex-wrap">
<Button @click="handleClick('updateSchema')">updateSchema</Button>
<Button @click="handleClick('labelWidth')">更改labelWidth</Button>
<Button @click="handleClick('resetLabelWidth')">还原labelWidth</Button>
<Button @click="handleClick('disabled')">禁用表单</Button>
<Button @click="handleClick('resetDisabled')">解除禁用</Button>
<Button @click="handleClick('reverseActionButtons')">
翻转操作按钮位置
</Button>
<Button @click="handleClick('hiddenAction')">隐藏操作按钮</Button>
<Button @click="handleClick('showAction')">显示操作按钮</Button>
<Button @click="handleClick('hiddenResetButton')">隐藏重置按钮</Button>
<Button @click="handleClick('showResetButton')">显示重置按钮</Button>
<Button @click="handleClick('hiddenSubmitButton')">隐藏提交按钮</Button>
<Button @click="handleClick('showSubmitButton')">显示提交按钮</Button>
<Button @click="handleClick('updateResetButton')">修改重置按钮</Button>
<Button @click="handleClick('updateSubmitButton')">修改提交按钮</Button>
<Button @click="handleClick('updateActionAlign')">
调整操作按钮位置
</Button>
<Button @click="handleClick('batchAddSchema')"> 批量添加表单项 </Button>
<Button @click="handleClick('batchDeleteSchema')">
批量删除表单项
</Button>
<Button @click="handleClick('componentRef')">下拉组件获取焦点</Button>
</Space>
<Card title="操作示例">
<BaseForm />
</Card>
</Page>
</template>

View File

@@ -0,0 +1,447 @@
<script lang="ts" setup>
import type { UploadFile } from 'ant-design-vue';
import { h, ref, toRaw } from 'vue';
import { Page } from '@vben/common-ui';
import { useDebounceFn } from '@vueuse/core';
import { Button, Card, message, Spin, Tag } from 'ant-design-vue';
import dayjs from 'dayjs';
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 keyword = ref('');
const fetching = ref(false);
// 模拟远程获取数据
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({
// 所有表单项共用,可单独在表单内覆盖
commonConfig: {
// 在label后显示一个冒号
colon: true,
// 所有表单项
componentProps: {
class: 'w-full',
},
},
fieldMappingTime: [['rangePicker', ['startTime', 'endTime'], 'YYYY-MM-DD']],
// 提交函数
handleSubmit: onSubmit,
handleValuesChange(_values, fieldsChanged) {
message.info(`表单以下字段发生变化:${fieldsChanged.join('')}`);
},
// 垂直布局label和input在不同行值为vertical
// 水平布局label和input在同一行
layout: 'horizontal',
schema: [
{
// 组件需要在 #/adapter.ts内注册并加上类型
component: 'Input',
// 对应组件的参数
componentProps: {
placeholder: '请输入用户名',
},
// 字段名
fieldName: 'username',
// 界面显示的label
label: '字符串',
rules: 'required',
},
{
// 组件需要在 #/adapter.ts内注册并加上类型
component: 'ApiSelect',
// 对应组件的参数
componentProps: {
// 菜单接口转options格式
afterFetch: (data: { name: string; path: string }[]) => {
return data.map((item: any) => ({
label: item.name,
value: item.path,
}));
},
// 菜单接口
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,
},
showSearch: true,
};
},
// 字段名
fieldName: 'remoteSearch',
// 界面显示的label
label: '远程搜索',
renderComponentContent: () => {
return {
notFoundContent: fetching.value ? h(Spin) : undefined,
};
},
rules: 'selectRequired',
},
{
component: 'ApiTreeSelect',
// 对应组件的参数
componentProps: {
// 菜单接口
api: getAllMenusApi,
// 菜单接口转options格式
labelField: 'name',
valueField: 'path',
childrenField: 'children',
},
// 字段名
fieldName: 'apiTree',
// 界面显示的label
label: 'ApiTreeSelect',
},
{
component: 'InputPassword',
componentProps: {
placeholder: '请输入密码',
},
fieldName: 'password',
label: '密码',
},
{
component: 'InputNumber',
componentProps: {
placeholder: '请输入',
},
fieldName: 'number',
label: '数字(带后缀)',
suffix: () => '¥',
},
{
component: 'IconPicker',
fieldName: 'icon',
label: '图标',
},
{
colon: false,
component: 'Select',
componentProps: {
allowClear: true,
filterOption: true,
options: [
{
label: '选项1',
value: '1',
},
{
label: '选项2',
value: '2',
},
],
placeholder: '请选择',
showSearch: true,
},
fieldName: 'options',
label: () => h(Tag, { color: 'warning' }, () => '😎自定义:'),
},
{
component: 'RadioGroup',
componentProps: {
options: [
{
label: '选项1',
value: '1',
},
{
label: '选项2',
value: '2',
},
],
},
fieldName: 'radioGroup',
label: '单选组',
},
{
component: 'Radio',
fieldName: 'radio',
label: '',
renderComponentContent: () => {
return {
default: () => ['Radio'],
};
},
},
{
component: 'CheckboxGroup',
componentProps: {
name: 'cname',
options: [
{
label: '选项1',
value: '1',
},
{
label: '选项2',
value: '2',
},
],
},
fieldName: 'checkboxGroup',
label: '多选组',
},
{
component: 'Checkbox',
fieldName: 'checkbox',
label: '',
renderComponentContent: () => {
return {
default: () => ['我已阅读并同意'],
};
},
rules: z
.boolean()
.refine((v) => v, { message: '为什么不同意?勾上它!' }),
},
{
component: 'Mentions',
componentProps: {
options: [
{
label: 'afc163',
value: 'afc163',
},
{
label: 'zombieJ',
value: 'zombieJ',
},
],
placeholder: '请输入',
},
fieldName: 'mentions',
label: '提及',
},
{
component: 'Rate',
fieldName: 'rate',
label: '评分',
},
{
component: 'Switch',
componentProps: {
class: 'w-auto',
},
fieldName: 'switch',
help: () =>
['这是一个多行帮助信息', '第二行', '第三行'].map((v) => h('p', v)),
label: '开关',
},
{
component: 'DatePicker',
fieldName: 'datePicker',
label: '日期选择框',
},
{
component: 'RangePicker',
fieldName: 'rangePicker',
label: '范围选择器',
},
{
component: 'TimePicker',
fieldName: 'timePicker',
label: '时间选择框',
},
{
component: 'TreeSelect',
componentProps: {
allowClear: true,
placeholder: '请选择',
showSearch: true,
treeData: [
{
label: 'root 1',
value: 'root 1',
children: [
{
label: 'parent 1',
value: 'parent 1',
children: [
{
label: 'parent 1-0',
value: 'parent 1-0',
children: [
{
label: 'my leaf',
value: 'leaf1',
},
{
label: 'your leaf',
value: 'leaf2',
},
],
},
{
label: 'parent 1-1',
value: 'parent 1-1',
},
],
},
{
label: 'parent 2',
value: 'parent 2',
},
],
},
],
treeNodeFilterProp: 'label',
},
fieldName: 'treeSelect',
label: '树选择',
},
{
component: 'Upload',
componentProps: {
// 更多属性见https://ant.design/components/upload-cn
accept: '.png,.jpg,.jpeg',
// 自动携带认证信息
customRequest: upload_file,
disabled: false,
maxCount: 1,
multiple: false,
showUploadList: true,
// 上传列表的内建样式,支持四种基本样式 text, picture, picture-card 和 picture-circle
listType: 'picture-card',
},
fieldName: 'files',
label: $t('examples.form.file'),
renderComponentContent: () => {
return {
default: () => $t('examples.form.upload-image'),
};
},
rules: 'required',
},
],
// 大屏一行显示3个中屏一行显示2个小屏一行显示1个
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
});
function onSubmit(values: Record<string, any>) {
const files = toRaw(values.files) as UploadFile[];
const doneFiles = files.filter((file) => file.status === 'done');
const failedFiles = files.filter((file) => file.status !== 'done');
const msg = [
...doneFiles.map((file) => file.response?.url || file.url),
...failedFiles.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;
}
// 如果需要可提交前替换为需要的urls
values.files = doneFiles.map((file) => file.response?.url || file.url);
message.success({
content: `form values: ${JSON.stringify(values)}`,
});
}
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',
password: '2',
radioGroup: '1',
rangePicker: [dayjs('2022-01-01'), dayjs('2022-01-02')],
rate: 3,
switch: true,
timePicker: dayjs('2022-01-01 12:00:00'),
treeSelect: 'leaf1',
username: '1',
});
// 设置单个表单值
baseFormApi.setFieldValue('checkbox', true);
}
</script>
<template>
<Page
content-class="flex flex-col gap-4"
description="表单组件基础示例,请注意,该页面用到的参数代码会添加一些简单注释,方便理解,请仔细查看。"
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>
<Button type="primary" @click="handleSetFormValue">设置表单值</Button>
</template>
<BaseForm />
</Card>
</Page>
</template>

View File

@@ -0,0 +1,111 @@
<script lang="ts" setup>
import { h } from 'vue';
import { Page } from '@vben/common-ui';
import { Card } from 'ant-design-vue';
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>

View File

@@ -0,0 +1,100 @@
<script lang="ts" setup>
import { h, markRaw } from 'vue';
import { Page } from '@vben/common-ui';
import { Card, Input, message } from 'ant-design-vue';
import { useVbenForm, z } from '#/adapter/form';
import TwoFields from './modules/two-fields.vue';
const [Form] = useVbenForm({
// 所有表单项共用,可单独在表单内覆盖
commonConfig: {
// 所有表单项
componentProps: {
class: 'w-full',
},
labelClass: 'w-2/6',
},
fieldMappingTime: [['field4', ['phoneType', 'phoneNumber'], null]],
// 提交函数
handleSubmit: onSubmit,
// 垂直布局label和input在不同行值为vertical
// 水平布局label和input在同一行
layout: 'horizontal',
schema: [
{
// 组件需要在 #/adapter.ts内注册并加上类型
component: 'Input',
fieldName: 'field',
label: '自定义后缀',
suffix: () => h('span', { class: 'text-red-600' }, '元'),
},
{
component: 'Input',
fieldName: 'field1',
label: '自定义组件slot',
renderComponentContent: () => ({
prefix: () => 'prefix',
suffix: () => 'suffix',
}),
},
{
component: h(Input, { placeholder: '请输入Field2' }),
fieldName: 'field2',
label: '自定义组件',
modelPropName: 'value',
rules: 'required',
},
{
component: 'Input',
fieldName: 'field3',
label: '自定义组件(slot)',
rules: 'required',
},
{
component: markRaw(TwoFields),
defaultValue: [undefined, ''],
disabledOnChangeListener: false,
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: '       号码格式不正确',
}),
},
],
// 中屏一行显示2个小屏一行显示1个
wrapperClass: 'grid-cols-1 md:grid-cols-2',
});
function onSubmit(values: Record<string, any>) {
message.success({
content: `form values: ${JSON.stringify(values)}`,
});
}
</script>
<template>
<Page description="表单组件自定义示例" title="表单组件">
<Card title="基础示例">
<Form>
<template #field3="slotProps">
<Input placeholder="请输入" v-bind="slotProps" />
</template>
</Form>
</Card>
</Page>
</template>

View File

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

View File

@@ -0,0 +1,116 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { Page } from '@vben/common-ui';
import { Button, Card, message, Step, Steps, Switch } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
const currentTab = ref(0);
function onFirstSubmit(values: Record<string, any>) {
message.success({
content: `form1 values: ${JSON.stringify(values)}`,
});
currentTab.value = 1;
}
function onSecondReset() {
currentTab.value = 0;
}
function onSecondSubmit(values: Record<string, any>) {
message.success({
content: `form2 values: ${JSON.stringify(values)}`,
});
}
const [FirstForm, firstFormApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
},
handleSubmit: onFirstSubmit,
layout: 'horizontal',
resetButtonOptions: {
show: false,
},
schema: [
{
component: 'Input',
componentProps: {
placeholder: '请输入',
},
fieldName: 'formFirst',
label: '表单1字段',
rules: 'required',
},
],
submitButtonOptions: {
content: '下一步',
},
wrapperClass: 'grid-cols-1 md:grid-cols-1 lg:grid-cols-1',
});
const [SecondForm, secondFormApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
},
handleReset: onSecondReset,
handleSubmit: onSecondSubmit,
layout: 'horizontal',
resetButtonOptions: {
content: '上一步',
},
schema: [
{
component: 'Input',
componentProps: {
placeholder: '请输入',
},
fieldName: 'formSecond',
label: '表单2字段',
rules: 'required',
},
],
wrapperClass: 'grid-cols-1 md:grid-cols-1 lg:grid-cols-1',
});
const needMerge = ref(true);
async function handleMergeSubmit() {
const values = await firstFormApi
.merge(secondFormApi)
.submitAllForm(needMerge.value);
message.success({
content: `merged form values: ${JSON.stringify(values)}`,
});
}
</script>
<template>
<Page
description="表单组件合并示例:在某些场景下,例如分步表单,需要合并多个表单并统一提交。默认情况下,使用 Object.assign 规则合并表单。如果需要特殊处理数据,可以传入 false。"
title="表单组件"
>
<Card title="基础示例">
<template #extra>
<Switch
v-model:checked="needMerge"
checked-children="开启字段合并"
class="mr-4"
un-checked-children="关闭字段合并"
/>
<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>
<div class="p-20">
<FirstForm v-show="currentTab === 0" />
<SecondForm v-show="currentTab === 1" />
</div>
</div>
</Card>
</Page>
</template>

View File

@@ -0,0 +1,42 @@
<script lang="ts" setup>
import { Input, Select } from 'ant-design-vue';
const emit = defineEmits(['blur', 'change']);
const modelValue = defineModel<[string, string]>({
default: () => [undefined, undefined],
});
function onChange() {
emit('change', modelValue.value);
}
</script>
<template>
<div class="flex w-full gap-1">
<Select
v-model:value="modelValue[0]"
class="w-[80px]"
placeholder="类型"
allow-clear
:class="{ 'valid-success': !!modelValue[0] }"
:options="[
{ label: '个人', value: 'personal' },
{ label: '工作', value: 'work' },
{ label: '私密', value: 'private' },
]"
@blur="emit('blur')"
@change="onChange"
/>
<Input
placeholder="请输入11位手机号码"
class="flex-1"
allow-clear
:class="{ 'valid-success': modelValue[1]?.match(/^1[3-9]\d{9}$/) }"
v-model:value="modelValue[1]"
:maxlength="11"
type="tel"
@blur="emit('blur')"
@change="onChange"
/>
</div>
</template>

View File

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

View File

@@ -0,0 +1,245 @@
<script lang="ts" setup>
import { Page } from '@vben/common-ui';
import { Button, Card, message } from 'ant-design-vue';
import { useVbenForm, z } from '#/adapter/form';
const [Form, formApi] = useVbenForm({
// 所有表单项共用,可单独在表单内覆盖
commonConfig: {
// 所有表单项
componentProps: {
class: 'w-full',
},
},
// 提交函数
handleSubmit: onSubmit,
// 垂直布局label和input在不同行值为vertical
// 水平布局label和input在同一行
layout: 'horizontal',
schema: [
{
// 组件需要在 #/adapter.ts内注册并加上类型
component: 'Input',
// 对应组件的参数
componentProps: {
placeholder: '请输入',
},
// 字段名
fieldName: 'field1',
// 界面显示的label
label: '字段1',
rules: 'required',
},
{
component: 'Input',
componentProps: {
placeholder: '请输入',
},
defaultValue: '默认值',
fieldName: 'field2',
label: '默认值(必填)',
rules: 'required',
},
{
component: 'Input',
componentProps: {
placeholder: '请输入',
},
fieldName: 'field3',
label: '默认值(非必填)',
rules: z.string().default('默认值').optional(),
},
{
component: 'Input',
componentProps: {
placeholder: '请输入',
},
fieldName: 'field31',
label: '自定义信息',
rules: z.string().min(1, { message: '最少输入1个字符' }),
},
{
component: 'Input',
// 对应组件的参数
componentProps: {
placeholder: '请输入',
},
// 字段名
fieldName: 'field4',
// 界面显示的label
label: '邮箱',
rules: z.string().email('请输入正确的邮箱'),
},
{
component: 'InputNumber',
componentProps: {
placeholder: '请输入',
},
fieldName: 'number',
label: '数字',
rules: 'required',
},
{
component: 'Select',
componentProps: {
allowClear: true,
filterOption: true,
options: [
{
label: '选项1',
value: '1',
},
{
label: '选项2',
value: '2',
},
],
placeholder: '请选择',
showSearch: true,
},
defaultValue: undefined,
fieldName: 'options',
label: '下拉选',
rules: 'selectRequired',
},
{
component: 'RadioGroup',
componentProps: {
options: [
{
label: '选项1',
value: '1',
},
{
label: '选项2',
value: '2',
},
],
},
fieldName: 'radioGroup',
label: '单选组',
rules: 'selectRequired',
},
{
component: 'CheckboxGroup',
componentProps: {
name: 'cname',
options: [
{
label: '选项1',
value: '1',
},
{
label: '选项2',
value: '2',
},
],
},
fieldName: 'checkboxGroup',
label: '多选组',
rules: 'selectRequired',
},
{
component: 'Checkbox',
fieldName: 'checkbox',
label: '',
renderComponentContent: () => {
return {
default: () => ['我已阅读并同意'],
};
},
rules: z.boolean().refine((value) => value, {
message: '请勾选',
}),
},
{
component: 'DatePicker',
defaultValue: undefined,
fieldName: 'datePicker',
label: '日期选择框',
rules: 'selectRequired',
},
{
component: 'RangePicker',
defaultValue: undefined,
fieldName: 'rangePicker',
label: '区间选择框',
rules: 'selectRequired',
},
{
component: 'InputPassword',
componentProps: {
placeholder: '请输入',
},
fieldName: 'password',
label: '密码',
rules: 'required',
},
{
component: 'Input',
componentProps: {
placeholder: '请输入',
},
fieldName: 'input-blur',
formFieldProps: {
validateOnChange: false,
validateOnModelUpdate: false,
},
help: 'blur时才会触发校验',
label: 'blur触发',
rules: 'required',
},
{
component: 'Input',
componentProps: {
placeholder: '请输入',
},
fieldName: 'input-async',
label: '异步校验',
rules: z
.string()
.min(3, '用户名至少需要3个字符')
.refine(
async (username) => {
// 假设这是一个异步函数,模拟检查用户名是否已存在
const checkUsernameExists = async (
username: string,
): Promise<boolean> => {
await new Promise((resolve) => setTimeout(resolve, 1000));
return username === 'existingUser';
};
const exists = await checkUsernameExists(username);
return !exists;
},
{
message: '用户名已存在',
},
),
},
],
// 大屏一行显示3个中屏一行显示2个小屏一行显示1个
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
});
function onSubmit(values: Record<string, any>) {
message.success({
content: `form values: ${JSON.stringify(values)}`,
});
}
</script>
<template>
<Page description="表单校验示例" title="表单组件">
<Card title="基础组件校验示例">
<template #extra>
<Button @click="() => formApi.validate()">校验表单</Button>
<Button class="mx-2" @click="() => formApi.resetValidate()">
清空校验信息
</Button>
</template>
<Form />
</Card>
</Page>
</template>

View File

@@ -0,0 +1,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": "chatcmpl-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" ]
}
]
}
}
`);

View 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 'ant-design-vue';
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>

View File

@@ -0,0 +1,106 @@
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import { ColPage } from '@vben/common-ui';
import { IconifyIcon } from '@vben/icons';
import {
Alert,
Button,
Card,
Checkbox,
Slider,
Tag,
Tooltip,
} from 'ant-design-vue';
const props = reactive({
leftCollapsedWidth: 5,
leftCollapsible: true,
leftMaxWidth: 50,
leftMinWidth: 20,
leftWidth: 30,
resizable: true,
rightWidth: 70,
splitHandle: false,
splitLine: false,
});
const leftMinWidth = ref(props.leftMinWidth || 1);
const leftMaxWidth = ref(props.leftMaxWidth || 100);
</script>
<template>
<ColPage
auto-content-height
description="ColPage 是一个双列布局组件,支持左侧折叠、拖拽调整宽度等功能。"
v-bind="props"
title="ColPage 双列布局组件"
>
<template #title>
<span class="mr-2 text-2xl font-bold">ColPage 双列布局组件</span>
<Tag color="hsl(var(--destructive))">Alpha</Tag>
</template>
<template #left="{ isCollapsed, expand }">
<div v-if="isCollapsed" @click="expand">
<Tooltip title="点击展开左侧">
<Button shape="circle" type="primary">
<template #icon>
<IconifyIcon class="text-2xl" icon="bi:arrow-right" />
</template>
</Button>
</Tooltip>
</div>
<div
v-else
:style="{ minWidth: '200px' }"
class="border-border bg-card mr-2 rounded-[var(--radius)] border p-2"
>
<p>这里是左侧内容</p>
<p>这里是左侧内容</p>
<p>这里是左侧内容</p>
<p>这里是左侧内容</p>
<p>这里是左侧内容</p>
</div>
</template>
<Card class="ml-2" title="基本使用">
<div class="flex flex-col gap-2">
<div class="flex gap-2">
<Checkbox v-model:checked="props.resizable">可拖动调整宽度</Checkbox>
<Checkbox v-model:checked="props.splitLine">显示拖动分隔线</Checkbox>
<Checkbox v-model:checked="props.splitHandle">显示拖动手柄</Checkbox>
<Checkbox v-model:checked="props.leftCollapsible">
左侧可折叠
</Checkbox>
</div>
<div class="flex items-center gap-2">
<span>左侧最小宽度百分比</span>
<Slider
v-model:value="leftMinWidth"
:max="props.leftMaxWidth - 1"
:min="1"
style="width: 100px"
@after-change="(value) => (props.leftMinWidth = value as number)"
/>
<span>左侧最大宽度百分比:</span>
<Slider
v-model:value="props.leftMaxWidth"
:max="100"
:min="leftMaxWidth + 1"
style="width: 100px"
@after-change="(value) => (props.leftMaxWidth = value as number)"
/>
</div>
<Alert message="实验性的组件" show-icon type="warning">
<template #description>
<p>
双列布局组件是一个在Page组件上扩展的相对基础的布局组件支持左侧折叠当拖拽导致左侧宽度比最小宽度还要小时还可以进入折叠状态、拖拽调整宽度等功能。
</p>
<p>以上宽度设置的数值是百分比最小值为1最大值为100。</p>
<p class="font-bold text-red-600">
这是一个实验性的组件用法可能会发生变动也可能最终不会被采用在其用法正式出现在文档中之前不建议在生产环境中使用
</p>
</template>
</Alert>
</div>
</Card>
</ColPage>
</template>

View File

@@ -0,0 +1,101 @@
<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 'ant-design-vue';
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" tip="加载中...">
<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 h-full w-full items-center justify-center"
>
<Button type="primary" @click="loading = true">默认动画</Button>
</Loading>
</div>
<div class="size-40">
<Loading
:spinning="loading"
class="flex h-full w-full items-center justify-center"
>
<Button type="primary" @click="loading = true">自定义动画1</Button>
<template #icon>
<IconifyIcon
icon="svg-spinners:ring-resize"
class="text-primary size-10"
/>
</template>
</Loading>
</div>
<div class="size-40">
<Loading
:spinning="loading"
class="flex h-full w-full items-center justify-center"
>
<Button type="primary" @click="loading = true">自定义动画2</Button>
<template #icon>
<IconifyIcon
icon="svg-spinners:bars-scale"
class="text-primary size-10"
/>
</template>
</Loading>
</div>
</div>
</Card>
<Card
title="Vben Spinner"
v-spinning="spinningV"
class="mt-4 overflow-hidden"
:body-style="{
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 size-40 items-center justify-center"
>
<Button type="primary" @click="spinning = true">显示Spinner</Button>
</Spinner>
</Card>
</Page>
</template>

View File

@@ -0,0 +1,49 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Button, message } from 'ant-design-vue';
const list = ref<number[]>([]);
const [Modal, modalApi] = useVbenModal({
onCancel() {
modalApi.close();
},
onConfirm() {
message.info('onConfirm');
},
onOpenChange(isOpen) {
if (isOpen) {
handleUpdate();
}
},
});
function handleUpdate(len?: number) {
modalApi.setState({ confirmDisabled: true, loading: true });
setTimeout(() => {
list.value = Array.from(
{ length: len ?? Math.floor(Math.random() * 10) + 1 },
(_v, k) => k + 1,
);
modalApi.setState({ confirmDisabled: false, loading: false });
}, 2000);
}
</script>
<template>
<Modal title="自动计算高度">
<div
v-for="item in list"
:key="item"
class="even:bg-heavy bg-muted flex-center h-[220px] w-full"
>
{{ item }}
</div>
<template #prepend-footer>
<Button type="link" @click="handleUpdate()">点击更新数据</Button>
</template>
</Modal>
</template>

View File

@@ -0,0 +1,34 @@
<script lang="ts" setup>
import { useVbenModal } from '@vben/common-ui';
import { Button, message } from 'ant-design-vue';
const [Modal, modalApi] = useVbenModal({
onCancel() {
modalApi.close();
},
onClosed() {
message.info('onClosed关闭动画结束');
},
onConfirm() {
message.info('onConfirm');
// modalApi.close();
},
onOpened() {
message.info('onOpened打开动画结束');
},
});
function lockModal() {
modalApi.lock();
setTimeout(() => {
modalApi.unlock();
}, 3000);
}
</script>
<template>
<Modal class="w-[600px]" title="基础弹窗示例" title-tooltip="标题提示内容">
base demo
<Button type="primary" @click="lockModal">锁定弹窗</Button>
</Modal>
</template>

View File

@@ -0,0 +1,23 @@
<script lang="ts" setup>
import { ref, watch } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Slider } from 'ant-design-vue';
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>

View File

@@ -0,0 +1,19 @@
<script lang="ts" setup>
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
const [Modal, modalApi] = useVbenModal({
draggable: true,
onCancel() {
modalApi.close();
},
onConfirm() {
message.info('onConfirm');
// modalApi.close();
},
});
</script>
<template>
<Modal title="可拖拽示例"> 鼠标移动到 header 可拖拽弹窗 </Modal>
</template>

View File

@@ -0,0 +1,41 @@
<script lang="ts" setup>
import { useVbenModal } from '@vben/common-ui';
import { Button, message } from 'ant-design-vue';
const [Modal, modalApi] = useVbenModal({
draggable: true,
onCancel() {
modalApi.close();
},
onConfirm() {
message.info('onConfirm');
// modalApi.close();
},
title: '动态修改配置示例',
});
const state = modalApi.useStore();
function handleUpdateTitle() {
modalApi.setState({ title: '内部动态标题' });
}
function handleToggleFullscreen() {
modalApi.setState((prev) => {
return { ...prev, fullscreen: !prev.fullscreen };
});
}
</script>
<template>
<Modal>
<div class="flex-col-center">
<Button class="mb-3" type="primary" @click="handleUpdateTitle()">
内部动态修改标题
</Button>
<Button class="mb-3" type="primary" @click="handleToggleFullscreen()">
{{ state.fullscreen ? '退出全屏' : '打开全屏' }}
</Button>
</div>
</Modal>
</template>

View File

@@ -0,0 +1,91 @@
<script lang="ts" setup>
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
defineOptions({
name: 'FormModelDemo',
});
const [Form, formApi] = useVbenForm({
handleSubmit: onSubmit,
schema: [
{
component: 'Input',
componentProps: {
placeholder: '请输入',
},
fieldName: 'field1',
label: '字段1',
rules: 'required',
},
{
component: 'Input',
componentProps: {
placeholder: '请输入',
},
fieldName: 'field2',
label: '字段2',
rules: 'required',
},
{
component: 'Select',
componentProps: {
options: [
{ label: '选项1', value: '1' },
{ label: '选项2', value: '2' },
],
placeholder: '请输入',
},
fieldName: 'field3',
label: '字段3',
rules: 'required',
},
],
showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
fullscreenButton: false,
onCancel() {
modalApi.close();
},
onConfirm: async () => {
await formApi.validateAndSubmitForm();
// modalApi.close();
},
onOpenChange(isOpen: boolean) {
if (isOpen) {
const { values } = modalApi.getData<Record<string, any>>();
if (values) {
formApi.setValues(values);
}
}
},
title: '内嵌表单示例',
});
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>
<Form />
</Modal>
</template>

View File

@@ -0,0 +1,30 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Input, message } from 'ant-design-vue';
const [Modal, modalApi] = useVbenModal({
destroyOnClose: false,
onCancel() {
modalApi.close();
},
onConfirm() {
message.info('onConfirm');
// modalApi.close();
},
});
const value = ref();
</script>
<template>
<Modal
append-to-main
class="w-[600px]"
title="基础弹窗示例"
title-tooltip="标题提示内容"
>
此弹窗指定在内容区域打开并且在关闭之后弹窗内容不会被销毁
<Input v-model:value="value" placeholder="KeepAlive测试" />
</Modal>
</template>

View File

@@ -0,0 +1,278 @@
<script lang="ts" setup>
import { onBeforeUnmount } from 'vue';
import {
alert,
clearAllAlerts,
confirm,
Page,
prompt,
useVbenModal,
} from '@vben/common-ui';
import { Button, Card, Flex, message } from 'ant-design-vue';
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';
defineOptions({ name: 'ModalExample' });
const [BaseModal, baseModalApi] = useVbenModal({
// 连接抽离的组件
connectedComponent: BaseDemo,
});
const [InContentModal, inContentModalApi] = useVbenModal({
// 连接抽离的组件
connectedComponent: InContentModalDemo,
});
const [AutoHeightModal, autoHeightModalApi] = useVbenModal({
connectedComponent: AutoHeightDemo,
});
const [DragModal, dragModalApi] = useVbenModal({
connectedComponent: DragDemo,
});
const [DynamicModal, dynamicModalApi] = useVbenModal({
connectedComponent: DynamicDemo,
});
const [SharedDataModal, sharedModalApi] = useVbenModal({
connectedComponent: SharedDataDemo,
});
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: FormModalDemo,
});
const [NestedModal, nestedModalApi] = useVbenModal({
connectedComponent: NestedDemo,
});
const [BlurModal, blurModalApi] = useVbenModal({
connectedComponent: BlurDemo,
});
function openBaseModal() {
baseModalApi.open();
}
function openInContentModal() {
inContentModalApi.open();
}
function openAutoHeightModal() {
autoHeightModalApi.open();
}
function openDragModal() {
dragModalApi.open();
}
function openDynamicModal() {
dynamicModalApi.open();
}
function openSharedModal() {
sharedModalApi
.setData({
content: '外部传递的数据 content',
payload: '外部传递的数据 payload',
})
.open();
}
function openNestedModal() {
nestedModalApi.open();
}
function openBlurModal() {
blurModalApi.open();
}
function handleUpdateTitle() {
dynamicModalApi.setState({ title: '外部动态标题' }).open();
}
function openFormModal() {
formModalApi
.setData({
// 表单值
values: { field1: 'abc', field2: '123' },
})
.open();
}
function openAlert() {
alert({
content: '这是一个弹窗',
icon: 'success',
}).then(() => {
message.info('用户关闭了弹窗');
});
}
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>
<template>
<Page
auto-content-height
description="弹窗组件常用于在不离开当前页面的情况下显示额外的信息、表单或操作提示更多api请查看组件文档。"
title="弹窗组件示例"
>
<template #extra>
<DocButton path="/components/common-ui/vben-modal" />
</template>
<BaseModal />
<InContentModal />
<AutoHeightModal />
<DragModal />
<DynamicModal />
<SharedDataModal />
<FormModal />
<NestedModal />
<BlurModal />
<Flex wrap="wrap" class="w-full" gap="10">
<Card class="w-[300px]" title="基本使用">
<p>一个基础的弹窗示例</p>
<template #actions>
<Button type="primary" @click="openBaseModal">打开弹窗</Button>
</template>
</Card>
<Card class="w-[300px]" title="指定容器+关闭后不销毁">
<p>在内容区域打开弹窗的示例</p>
<template #actions>
<Button type="primary" @click="openInContentModal">打开弹窗</Button>
</template>
</Card>
<Card class="w-[300px]" title="内容高度自适应">
<p>可根据内容并自动调整高度</p>
<template #actions>
<Button type="primary" @click="openAutoHeightModal">
打开弹窗
</Button>
</template>
</Card>
<Card class="w-[300px]" title="可拖拽示例">
<p>配置 draggable 可开启拖拽功能</p>
<template #actions>
<Button type="primary" @click="openDragModal"> 打开弹窗 </Button>
</template>
</Card>
<Card class="w-[300px]" 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="w-[300px]" title="内外数据共享示例">
<p>通过共享 sharedData 来进行数据交互</p>
<template #actions>
<Button type="primary" @click="openSharedModal">
打开弹窗并传递数据
</Button>
</template>
</Card>
<Card class="w-[300px]" title="表单弹窗示例">
<p>弹窗与表单结合</p>
<template #actions>
<Button type="primary" @click="openFormModal"> 打开表单弹窗 </Button>
</template>
</Card>
<Card class="w-[300px]" title="嵌套弹窗示例">
<p>在已经打开的弹窗中再次打开弹窗</p>
<template #actions>
<Button type="primary" @click="openNestedModal">打开嵌套弹窗</Button>
</template>
</Card>
<Card class="w-[300px]" title="遮罩模糊示例">
<p>遮罩层应用类似毛玻璃的模糊效果</p>
<template #actions>
<Button type="primary" @click="openBlurModal">打开弹窗</Button>
</template>
</Card>
<Card class="w-[300px]" 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>

View File

@@ -0,0 +1,24 @@
<script lang="ts" setup>
import { useVbenModal } from '@vben/common-ui';
import { Button } from 'ant-design-vue';
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>

View File

@@ -0,0 +1,29 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
const data = ref();
const [Modal, modalApi] = useVbenModal({
onCancel() {
modalApi.close();
},
onConfirm() {
message.info('onConfirm');
// modalApi.close();
},
onOpenChange(isOpen: boolean) {
if (isOpen) {
data.value = modalApi.getData<Record<string, any>>();
}
},
});
</script>
<template>
<Modal title="数据共享示例">
<div class="flex-col-center">外部传递数据 {{ data }}</div>
</Modal>
</template>

View File

@@ -0,0 +1,213 @@
<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 'ant-design-vue';
// 本例子用不到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="使用指令" :body-style="{ 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="使用组件(将内部作为一个整体添加动画)"
:body-style="{ padding: 0 }"
>
<div
class="relative flex min-h-32 items-center justify-center 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 min-h-32 items-center justify-center 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">
<Select.Option
:value="preset"
v-for="preset in presets"
:key="preset"
>
{{ preset }}
</Select.Option>
</Select>
</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="分组动画(每个子元素都会应用相同的独立动画)"
:body-style="{ padding: 0 }"
>
<div
class="relative flex min-h-32 items-center justify-center 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">
<Select.Option
:value="preset"
v-for="preset in presets"
:key="preset"
>
{{ preset }}
</Select.Option>
</Select>
</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>

View File

@@ -0,0 +1,58 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { Page, VResize } from '@vben/common-ui';
const colorMap = ['red', 'green', 'yellow', 'gray'];
type TSize = {
height: number;
left: number;
top: number;
width: number;
};
const sizeList = ref<TSize[]>([
{ height: 200, left: 200, top: 200, width: 200 },
{ height: 300, left: 300, top: 300, width: 300 },
{ height: 400, left: 400, top: 400, width: 400 },
{ height: 500, left: 500, top: 500, width: 500 },
]);
const resize = (size?: TSize, rect?: TSize) => {
if (!size || !rect) return;
size.height = rect.height;
size.left = rect.left;
size.top = rect.top;
size.width = rect.width;
};
</script>
<template>
<Page description="Resize组件基础示例" title="Resize组件">
<div class="m-4 bg-blue-500 p-48 text-xl">
<div v-for="size in sizeList" :key="size.width">
{{
`width: ${size.width}px, height: ${size.height}px, top: ${size.top}px, left: ${size.left}px`
}}
</div>
</div>
<template v-for="(_, idx) of 4" :key="idx">
<VResize
:h="100 * (idx + 1)"
:w="100 * (idx + 1)"
:x="100 * (idx + 1)"
:y="100 * (idx + 1)"
@dragging="(rect) => resize(sizeList[idx], rect)"
@resizing="(rect) => resize(sizeList[idx], rect)"
>
<div
:style="{ backgroundColor: colorMap[idx] }"
class="h-full w-full"
></div>
</VResize>
</template>
</Page>
</template>

View 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 'ant-design-vue';
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="text-secondary-foreground hover:text-secondary-foreground cursor-default"
>
更多配置请
<Button type="link" size="small" @click="goDoc">查看文档</Button>
这里只列出了一些常用的配置
</p>
</template>
</Card>
</Page>
</template>

View File

@@ -0,0 +1,96 @@
<script lang="ts" setup>
import type { VxeGridListeners, VxeGridProps } from '#/adapter/vxe-table';
import { Page } from '@vben/common-ui';
import { Button, message } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import DocButton from '../doc-button.vue';
import { MOCK_TABLE_DATA } from './table-data';
interface RowType {
address: string;
age: number;
id: number;
name: string;
nickname: string;
role: string;
}
const gridOptions: VxeGridProps<RowType> = {
columns: [
{ title: '序号', type: 'seq', width: 50 },
{ field: 'name', title: 'Name' },
{ field: 'age', sortable: true, title: 'Age' },
{ field: 'nickname', title: 'Nickname' },
{ field: 'role', title: 'Role' },
{ field: 'address', showOverflow: true, title: 'Address' },
],
data: MOCK_TABLE_DATA,
pagerConfig: {
enabled: false,
},
sortConfig: {
multiple: true,
},
};
const gridEvents: VxeGridListeners<RowType> = {
cellClick: ({ row }) => {
message.info(`cell-click: ${row.name}`);
},
};
const [Grid, gridApi] = useVbenVxeGrid({ gridEvents, gridOptions });
const showBorder = gridApi.useStore((state) => state.gridOptions?.border);
const showStripe = gridApi.useStore((state) => state.gridOptions?.stripe);
function changeBorder() {
gridApi.setGridOptions({
border: !showBorder.value,
});
}
function changeStripe() {
gridApi.setGridOptions({
stripe: !showStripe.value,
});
}
function changeLoading() {
gridApi.setLoading(true);
setTimeout(() => {
gridApi.setLoading(false);
}, 2000);
}
</script>
<template>
<Page
description="表格组件常用于快速开发数据展示与交互界面示例数据为静态数据。该组件是对vxe-table进行简单的二次封装大部分属性与方法与vxe-table保持一致。"
title="表格基础示例"
>
<template #extra>
<DocButton path="/components/common-ui/vben-vxe-table" />
</template>
<Grid table-title="基础列表" table-title-help="提示">
<!-- <template #toolbar-actions>
<Button class="mr-2" type="primary">左侧插槽</Button>
</template> -->
<template #toolbar-tools>
<Button class="mr-2" type="primary" @click="changeBorder">
{{ showBorder ? '隐藏' : '显示' }}边框
</Button>
<Button class="mr-2" type="primary" @click="changeLoading">
显示loading
</Button>
<Button type="primary" @click="changeStripe">
{{ showStripe ? '隐藏' : '显示' }}斑马纹
</Button>
</template>
</Grid>
</Page>
</template>

View File

@@ -0,0 +1,108 @@
<script lang="ts" setup>
import type { VxeGridProps } from '#/adapter/vxe-table';
import { Page } from '@vben/common-ui';
import { Button, Image, Switch, Tag } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getExampleTableApi } from '#/api';
interface RowType {
category: string;
color: string;
id: string;
imageUrl: string;
open: boolean;
price: string;
productName: string;
releaseDate: string;
status: 'error' | 'success' | 'warning';
}
const gridOptions: VxeGridProps<RowType> = {
checkboxConfig: {
highlight: true,
labelField: 'name',
},
columns: [
{ title: '序号', type: 'seq', width: 50 },
{ field: 'category', title: 'Category', width: 100 },
{
field: 'imageUrl',
slots: { default: 'image-url' },
title: 'Image',
width: 100,
},
{
cellRender: { name: 'CellImage' },
field: 'imageUrl2',
title: 'Render Image',
width: 130,
},
{
field: 'open',
slots: { default: 'open' },
title: 'Open',
width: 100,
},
{
field: 'status',
slots: { default: 'status' },
title: 'Status',
width: 100,
},
{ field: 'color', title: 'Color', width: 100 },
{ field: 'productName', title: 'Product Name', width: 200 },
{ field: 'price', title: 'Price', width: 100 },
{
field: 'releaseDate',
formatter: 'formatDateTime',
title: 'Date',
width: 200,
},
{
cellRender: { name: 'CellLink', props: { text: '编辑' } },
field: 'action',
fixed: 'right',
title: '操作',
width: 120,
},
],
height: 'auto',
keepSource: true,
pagerConfig: {},
proxyConfig: {
ajax: {
query: async ({ page }) => {
return await getExampleTableApi({
page: page.currentPage,
pageSize: page.pageSize,
});
},
},
},
showOverflow: false,
};
const [Grid] = useVbenVxeGrid({ gridOptions });
</script>
<template>
<Page auto-content-height>
<Grid>
<template #image-url="{ row }">
<Image :src="row.imageUrl" height="30" width="30" />
</template>
<template #open="{ row }">
<Switch v-model:checked="row.open" />
</template>
<template #status="{ row }">
<Tag :color="row.color">{{ row.status }}</Tag>
</template>
<template #action>
<Button type="link">编辑</Button>
</template>
</Grid>
</Page>
</template>

View File

@@ -0,0 +1,57 @@
<script lang="ts" setup>
import type { VxeGridProps } from '#/adapter/vxe-table';
import { Page } from '@vben/common-ui';
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> = {
columns: [
{ title: '序号', type: 'seq', width: 50 },
{ editRender: { name: 'input' }, field: 'category', title: 'Category' },
{ editRender: { name: 'input' }, field: 'color', title: 'Color' },
{
editRender: { name: 'input' },
field: 'productName',
title: 'Product Name',
},
{ field: 'price', title: 'Price' },
{ field: 'releaseDate', formatter: 'formatDateTime', title: 'Date' },
],
editConfig: {
mode: 'cell',
trigger: 'click',
},
height: 'auto',
pagerConfig: {},
proxyConfig: {
ajax: {
query: async ({ page }) => {
return await getExampleTableApi({
page: page.currentPage,
pageSize: page.pageSize,
});
},
},
},
showOverflow: true,
};
const [Grid] = useVbenVxeGrid({ gridOptions });
</script>
<template>
<Page auto-content-height>
<Grid />
</Page>
</template>

View File

@@ -0,0 +1,94 @@
<script lang="ts" setup>
import type { VxeGridProps } from '#/adapter/vxe-table';
import { Page } from '@vben/common-ui';
import { Button, message } from 'ant-design-vue';
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> = {
columns: [
{ title: '序号', type: 'seq', width: 50 },
{ editRender: { name: 'input' }, field: 'category', title: 'Category' },
{ editRender: { name: 'input' }, field: 'color', title: 'Color' },
{
editRender: { name: 'input' },
field: 'productName',
title: 'Product Name',
},
{ field: 'price', title: 'Price' },
{ field: 'releaseDate', formatter: 'formatDateTime', title: 'Date' },
{ slots: { default: 'action' }, title: '操作' },
],
editConfig: {
mode: 'row',
trigger: 'click',
},
height: 'auto',
pagerConfig: {},
proxyConfig: {
ajax: {
query: async ({ page }) => {
return await getExampleTableApi({
page: page.currentPage,
pageSize: page.pageSize,
});
},
},
},
showOverflow: true,
};
const [Grid, gridApi] = useVbenVxeGrid({ gridOptions });
function hasEditStatus(row: RowType) {
return gridApi.grid?.isEditByRow(row);
}
function editRowEvent(row: RowType) {
gridApi.grid?.setEditRow(row);
}
async function saveRowEvent(row: RowType) {
await gridApi.grid?.clearEdit();
gridApi.setLoading(true);
setTimeout(() => {
gridApi.setLoading(false);
message.success({
content: `保存成功category=${row.category}`,
});
}, 600);
}
const cancelRowEvent = (_row: RowType) => {
gridApi.grid?.clearEdit();
};
</script>
<template>
<Page auto-content-height>
<Grid>
<template #action="{ row }">
<template v-if="hasEditStatus(row)">
<Button type="link" @click="saveRowEvent(row)">保存</Button>
<Button type="link" @click="cancelRowEvent(row)">取消</Button>
</template>
<template v-else>
<Button type="link" @click="editRowEvent(row)">编辑</Button>
</template>
</template>
</Grid>
</Page>
</template>

View File

@@ -0,0 +1,69 @@
<script lang="ts" setup>
import type { VxeGridProps } from '#/adapter/vxe-table';
import { Page } from '@vben/common-ui';
import { Button } from 'ant-design-vue';
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> = {
columns: [
{ fixed: 'left', title: '序号', type: 'seq', width: 50 },
{ field: 'category', title: 'Category', width: 300 },
{ field: 'color', title: 'Color', width: 300 },
{ field: 'productName', title: 'Product Name', width: 300 },
{ field: 'price', title: 'Price', width: 300 },
{
field: 'releaseDate',
formatter: 'formatDateTime',
title: 'DateTime',
width: 500,
},
{
field: 'action',
fixed: 'right',
slots: { default: 'action' },
title: '操作',
width: 120,
},
],
height: 'auto',
pagerConfig: {},
proxyConfig: {
ajax: {
query: async ({ page }) => {
return await getExampleTableApi({
page: page.currentPage,
pageSize: page.pageSize,
});
},
},
},
rowConfig: {
isHover: true,
},
};
const [Grid] = useVbenVxeGrid({ gridOptions });
</script>
<template>
<Page auto-content-height>
<Grid>
<template #action>
<Button type="link">编辑</Button>
</template>
</Grid>
</Page>
</template>

View File

@@ -0,0 +1,127 @@
<script lang="ts" setup>
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 dayjs from 'dayjs';
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 formOptions: VbenFormProps = {
// 默认展开
collapsed: false,
fieldMappingTime: [['date', ['start', 'end']]],
schema: [
{
component: 'Input',
defaultValue: '1',
fieldName: 'category',
label: 'Category',
},
{
component: 'Input',
fieldName: 'productName',
label: 'ProductName',
},
{
component: 'Input',
fieldName: 'price',
label: 'Price',
},
{
component: 'Select',
componentProps: {
allowClear: true,
options: [
{
label: 'Color1',
value: '1',
},
{
label: 'Color2',
value: '2',
},
],
placeholder: '请选择',
},
fieldName: 'color',
label: 'Color',
},
{
component: 'RangePicker',
defaultValue: [dayjs().subtract(7, 'days'), dayjs()],
fieldName: 'date',
label: 'Date',
},
],
// 控制表单是否显示折叠按钮
showCollapseButton: true,
// 是否在字段值改变时提交表单
submitOnChange: true,
// 按下回车时是否提交表单
submitOnEnter: false,
};
const gridOptions: VxeTableGridOptions<RowType> = {
checkboxConfig: {
highlight: true,
labelField: 'name',
},
columns: [
{ title: '序号', type: 'seq', width: 50 },
{ align: 'left', title: 'Name', type: 'checkbox', width: 100 },
{ field: 'category', title: 'Category' },
{ field: 'color', title: 'Color' },
{ field: 'productName', title: 'Product Name' },
{ field: 'price', title: 'Price' },
{ field: 'releaseDate', formatter: 'formatDateTime', title: 'Date' },
],
exportConfig: {},
height: 'auto',
keepSource: true,
pagerConfig: {},
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
message.success(`Query params: ${JSON.stringify(formValues)}`);
return await getExampleTableApi({
page: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
toolbarConfig: {
custom: true,
export: true,
refresh: true,
resizable: true,
search: true,
zoom: true,
},
};
const [Grid] = useVbenVxeGrid({
formOptions,
gridOptions,
});
</script>
<template>
<Page auto-content-height>
<Grid />
</Page>
</template>

View File

@@ -0,0 +1,81 @@
<script lang="ts" setup>
import type { VxeGridProps } from '#/adapter/vxe-table';
import { Page } from '@vben/common-ui';
import { Button } from 'ant-design-vue';
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: 'name',
},
columns: [
{ title: '序号', type: 'seq', width: 50 },
{ align: 'left', title: 'Name', type: 'checkbox', width: 100 },
{ 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' },
],
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: { code: 'query' },
zoom: true,
},
};
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions,
});
</script>
<template>
<Page auto-content-height>
<Grid table-title="数据列表" table-title-help="提示">
<template #toolbar-tools>
<Button class="mr-2" type="primary" @click="() => gridApi.query()">
刷新当前页面
</Button>
<Button type="primary" @click="() => gridApi.reload()">
刷新并返回第一页
</Button>
</template>
</Grid>
</Page>
</template>

View File

@@ -0,0 +1,172 @@
interface TableRowData {
address: string;
age: number;
id: number;
name: string;
nickname: string;
role: string;
}
const roles = ['User', 'Admin', 'Manager', 'Guest'];
export const MOCK_TABLE_DATA: TableRowData[] = (() => {
const data: TableRowData[] = [];
for (let i = 0; i < 40; i++) {
data.push({
address: `New York${i}`,
age: i + 1,
id: i,
name: `Test${i}`,
nickname: `Test${i}`,
role: roles[Math.floor(Math.random() * roles.length)] as string,
});
}
return data;
})();
export const MOCK_TREE_TABLE_DATA = [
{
date: '2020-08-01',
id: 10_000,
name: 'Test1',
parentId: null,
size: 1024,
type: 'mp3',
},
{
date: '2021-04-01',
id: 10_050,
name: 'Test2',
parentId: null,
size: 0,
type: 'mp4',
},
{
date: '2020-03-01',
id: 24_300,
name: 'Test3',
parentId: 10_050,
size: 1024,
type: 'avi',
},
{
date: '2021-04-01',
id: 20_045,
name: 'Test4',
parentId: 24_300,
size: 600,
type: 'html',
},
{
date: '2021-04-01',
id: 10_053,
name: 'Test5',
parentId: 24_300,
size: 0,
type: 'avi',
},
{
date: '2021-10-01',
id: 24_330,
name: 'Test6',
parentId: 10_053,
size: 25,
type: 'txt',
},
{
date: '2020-01-01',
id: 21_011,
name: 'Test7',
parentId: 10_053,
size: 512,
type: 'pdf',
},
{
date: '2021-06-01',
id: 22_200,
name: 'Test8',
parentId: 10_053,
size: 1024,
type: 'js',
},
{
date: '2020-11-01',
id: 23_666,
name: 'Test9',
parentId: null,
size: 2048,
type: 'xlsx',
},
{
date: '2021-06-01',
id: 23_677,
name: 'Test10',
parentId: 23_666,
size: 1024,
type: 'js',
},
{
date: '2021-06-01',
id: 23_671,
name: 'Test11',
parentId: 23_677,
size: 1024,
type: 'js',
},
{
date: '2021-06-01',
id: 23_672,
name: 'Test12',
parentId: 23_677,
size: 1024,
type: 'js',
},
{
date: '2021-06-01',
id: 23_688,
name: 'Test13',
parentId: 23_666,
size: 1024,
type: 'js',
},
{
date: '2021-06-01',
id: 23_681,
name: 'Test14',
parentId: 23_688,
size: 1024,
type: 'js',
},
{
date: '2021-06-01',
id: 23_682,
name: 'Test15',
parentId: 23_688,
size: 1024,
type: 'js',
},
{
date: '2020-10-01',
id: 24_555,
name: 'Test16',
parentId: null,
size: 224,
type: 'avi',
},
{
date: '2021-06-01',
id: 24_566,
name: 'Test17',
parentId: 24_555,
size: 1024,
type: 'js',
},
{
date: '2021-06-01',
id: 24_577,
name: 'Test18',
parentId: 24_555,
size: 1024,
type: 'js',
},
];

View File

@@ -0,0 +1,62 @@
<script lang="ts" setup>
import type { VxeGridProps } from '#/adapter/vxe-table';
import { Page } from '@vben/common-ui';
import { Button } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { MOCK_TREE_TABLE_DATA } from './table-data';
interface RowType {
date: string;
id: number;
name: string;
parentId: null | number;
size: number;
type: string;
}
const gridOptions: VxeGridProps<RowType> = {
columns: [
{ type: 'seq', width: 70 },
{ field: 'name', minWidth: 300, title: 'Name', treeNode: true },
{ field: 'size', title: 'Size' },
{ field: 'type', title: 'Type' },
{ field: 'date', title: 'Date' },
],
data: MOCK_TREE_TABLE_DATA,
pagerConfig: {
enabled: false,
},
treeConfig: {
parentField: 'parentId',
rowField: 'id',
transform: true,
},
};
const [Grid, gridApi] = useVbenVxeGrid({ gridOptions });
const expandAll = () => {
gridApi.grid?.setAllTreeExpand(true);
};
const collapseAll = () => {
gridApi.grid?.setAllTreeExpand(false);
};
</script>
<template>
<Page>
<Grid table-title="数据列表" table-title-help="提示">
<template #toolbar-tools>
<Button class="mr-2" type="primary" @click="expandAll">
展开全部
</Button>
<Button type="primary" @click="collapseAll"> 折叠全部 </Button>
</template>
</Grid>
</Page>
</template>

View File

@@ -0,0 +1,66 @@
<script lang="ts" setup>
import type { VxeGridProps } from '#/adapter/vxe-table';
import { onMounted } from 'vue';
import { Page } from '@vben/common-ui';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
interface RowType {
id: number;
name: string;
role: string;
sex: string;
}
const gridOptions: VxeGridProps<RowType> = {
columns: [
{ type: 'seq', width: 70 },
{ field: 'name', title: 'Name' },
{ field: 'role', title: 'Role' },
{ field: 'sex', title: 'Sex' },
],
data: [],
height: 'auto',
pagerConfig: {
enabled: false,
},
scrollY: {
enabled: true,
gt: 0,
},
showOverflow: true,
};
const [Grid, gridApi] = useVbenVxeGrid({ gridOptions });
// 模拟行数据
const loadList = (size = 200) => {
try {
const dataList: RowType[] = [];
for (let i = 0; i < size; i++) {
dataList.push({
id: 10_000 + i,
name: `Test${i}`,
role: 'Developer',
sex: '男',
});
}
gridApi.setGridOptions({ data: dataList });
} catch (error) {
console.error('Failed to load data:', error);
// Implement user-friendly error handling
}
};
onMounted(() => {
loadList(1000);
});
</script>
<template>
<Page auto-content-height>
<Grid />
</Page>
</template>