物业代码生成
This commit is contained in:
195
playground/src/views/examples/button-group/index.vue
Normal file
195
playground/src/views/examples/button-group/index.vue
Normal 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>
|
@@ -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>
|
117
playground/src/views/examples/captcha/slider-captcha.vue
Normal file
117
playground/src/views/examples/captcha/slider-captcha.vue
Normal 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>
|
@@ -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>
|
178
playground/src/views/examples/count-to/index.vue
Normal file
178
playground/src/views/examples/count-to/index.vue
Normal 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>
|
22
playground/src/views/examples/doc-button.vue
Normal file
22
playground/src/views/examples/doc-button.vue
Normal 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>
|
47
playground/src/views/examples/drawer/auto-height-demo.vue
Normal file
47
playground/src/views/examples/drawer/auto-height-demo.vue
Normal 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>
|
35
playground/src/views/examples/drawer/base-demo.vue
Normal file
35
playground/src/views/examples/drawer/base-demo.vue
Normal 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>
|
31
playground/src/views/examples/drawer/dynamic-demo.vue
Normal file
31
playground/src/views/examples/drawer/dynamic-demo.vue
Normal 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>
|
56
playground/src/views/examples/drawer/form-drawer-demo.vue
Normal file
56
playground/src/views/examples/drawer/form-drawer-demo.vue
Normal 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>
|
48
playground/src/views/examples/drawer/in-content-demo.vue
Normal file
48
playground/src/views/examples/drawer/in-content-demo.vue
Normal 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>
|
195
playground/src/views/examples/drawer/index.vue
Normal file
195
playground/src/views/examples/drawer/index.vue
Normal 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>
|
29
playground/src/views/examples/drawer/shared-data-demo.vue
Normal file
29
playground/src/views/examples/drawer/shared-data-demo.vue
Normal 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>
|
46
playground/src/views/examples/ellipsis/index.vue
Normal file
46
playground/src/views/examples/ellipsis/index.vue
Normal 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>
|
274
playground/src/views/examples/form/api.vue
Normal file
274
playground/src/views/examples/form/api.vue
Normal 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>
|
447
playground/src/views/examples/form/basic.vue
Normal file
447
playground/src/views/examples/form/basic.vue
Normal 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>
|
111
playground/src/views/examples/form/custom-layout.vue
Normal file
111
playground/src/views/examples/form/custom-layout.vue
Normal file
@@ -0,0 +1,111 @@
|
||||
<script lang="ts" setup>
|
||||
import { h } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { Card } from '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>
|
100
playground/src/views/examples/form/custom.vue
Normal file
100
playground/src/views/examples/form/custom.vue
Normal 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>
|
262
playground/src/views/examples/form/dynamic.vue
Normal file
262
playground/src/views/examples/form/dynamic.vue
Normal 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>
|
116
playground/src/views/examples/form/merge.vue
Normal file
116
playground/src/views/examples/form/merge.vue
Normal 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>
|
42
playground/src/views/examples/form/modules/two-fields.vue
Normal file
42
playground/src/views/examples/form/modules/two-fields.vue
Normal 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>
|
147
playground/src/views/examples/form/query.vue
Normal file
147
playground/src/views/examples/form/query.vue
Normal 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>
|
245
playground/src/views/examples/form/rules.vue
Normal file
245
playground/src/views/examples/form/rules.vue
Normal 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>
|
66
playground/src/views/examples/json-viewer/data.ts
Normal file
66
playground/src/views/examples/json-viewer/data.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
export const json1 = {
|
||||
additionalInfo: {
|
||||
author: 'Your Name',
|
||||
debug: true,
|
||||
version: '1.3.10',
|
||||
versionCode: 132,
|
||||
},
|
||||
additionalNotes: 'This JSON is used for demonstration purposes',
|
||||
tools: [
|
||||
{
|
||||
description: 'Description of Tool 1',
|
||||
name: 'Tool 1',
|
||||
},
|
||||
{
|
||||
description: 'Description of Tool 2',
|
||||
name: 'Tool 2',
|
||||
},
|
||||
{
|
||||
description: 'Description of Tool 3',
|
||||
name: 'Tool 3',
|
||||
},
|
||||
{
|
||||
description: 'Description of Tool 4',
|
||||
name: 'Tool 4',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const json2 = JSON.parse(`
|
||||
{
|
||||
"id": "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" ]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
`);
|
51
playground/src/views/examples/json-viewer/index.vue
Normal file
51
playground/src/views/examples/json-viewer/index.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<script lang="ts" setup>
|
||||
import type { JsonViewerAction, JsonViewerValue } from '@vben/common-ui';
|
||||
|
||||
import { JsonViewer, Page } from '@vben/common-ui';
|
||||
|
||||
import { Card, message } from '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>
|
106
playground/src/views/examples/layout/col-page.vue
Normal file
106
playground/src/views/examples/layout/col-page.vue
Normal 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>
|
101
playground/src/views/examples/loading/index.vue
Normal file
101
playground/src/views/examples/loading/index.vue
Normal 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>
|
49
playground/src/views/examples/modal/auto-height-demo.vue
Normal file
49
playground/src/views/examples/modal/auto-height-demo.vue
Normal 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>
|
34
playground/src/views/examples/modal/base-demo.vue
Normal file
34
playground/src/views/examples/modal/base-demo.vue
Normal 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>
|
23
playground/src/views/examples/modal/blur-demo.vue
Normal file
23
playground/src/views/examples/modal/blur-demo.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { Slider } from '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>
|
19
playground/src/views/examples/modal/drag-demo.vue
Normal file
19
playground/src/views/examples/modal/drag-demo.vue
Normal 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>
|
41
playground/src/views/examples/modal/dynamic-demo.vue
Normal file
41
playground/src/views/examples/modal/dynamic-demo.vue
Normal 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>
|
91
playground/src/views/examples/modal/form-modal-demo.vue
Normal file
91
playground/src/views/examples/modal/form-modal-demo.vue
Normal 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>
|
30
playground/src/views/examples/modal/in-content-demo.vue
Normal file
30
playground/src/views/examples/modal/in-content-demo.vue
Normal 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>
|
278
playground/src/views/examples/modal/index.vue
Normal file
278
playground/src/views/examples/modal/index.vue
Normal 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>
|
24
playground/src/views/examples/modal/nested-demo.vue
Normal file
24
playground/src/views/examples/modal/nested-demo.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<script lang="ts" setup>
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { Button } from '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>
|
29
playground/src/views/examples/modal/shared-data-demo.vue
Normal file
29
playground/src/views/examples/modal/shared-data-demo.vue
Normal 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>
|
213
playground/src/views/examples/motion/index.vue
Normal file
213
playground/src/views/examples/motion/index.vue
Normal 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>
|
58
playground/src/views/examples/resize/basic.vue
Normal file
58
playground/src/views/examples/resize/basic.vue
Normal 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>
|
303
playground/src/views/examples/tippy/index.vue
Normal file
303
playground/src/views/examples/tippy/index.vue
Normal file
@@ -0,0 +1,303 @@
|
||||
<script lang="ts" setup>
|
||||
import type { TippyProps } from '@vben/common-ui';
|
||||
|
||||
import { reactive } from 'vue';
|
||||
|
||||
import { Page, Tippy } from '@vben/common-ui';
|
||||
|
||||
import { Button, Card, Flex } from '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>
|
96
playground/src/views/examples/vxe-table/basic.vue
Normal file
96
playground/src/views/examples/vxe-table/basic.vue
Normal 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>
|
108
playground/src/views/examples/vxe-table/custom-cell.vue
Normal file
108
playground/src/views/examples/vxe-table/custom-cell.vue
Normal 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>
|
57
playground/src/views/examples/vxe-table/edit-cell.vue
Normal file
57
playground/src/views/examples/vxe-table/edit-cell.vue
Normal 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>
|
94
playground/src/views/examples/vxe-table/edit-row.vue
Normal file
94
playground/src/views/examples/vxe-table/edit-row.vue
Normal 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>
|
69
playground/src/views/examples/vxe-table/fixed.vue
Normal file
69
playground/src/views/examples/vxe-table/fixed.vue
Normal 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>
|
127
playground/src/views/examples/vxe-table/form.vue
Normal file
127
playground/src/views/examples/vxe-table/form.vue
Normal 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>
|
81
playground/src/views/examples/vxe-table/remote.vue
Normal file
81
playground/src/views/examples/vxe-table/remote.vue
Normal 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>
|
172
playground/src/views/examples/vxe-table/table-data.ts
Normal file
172
playground/src/views/examples/vxe-table/table-data.ts
Normal 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',
|
||||
},
|
||||
];
|
62
playground/src/views/examples/vxe-table/tree.vue
Normal file
62
playground/src/views/examples/vxe-table/tree.vue
Normal 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>
|
66
playground/src/views/examples/vxe-table/virtual.vue
Normal file
66
playground/src/views/examples/vxe-table/virtual.vue
Normal 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>
|
Reference in New Issue
Block a user