Initial commit: Telegram Management System
Some checks failed
Deploy / deploy (push) Has been cancelled
Some checks failed
Deploy / deploy (push) Has been cancelled
Full-stack web application for Telegram management - Frontend: Vue 3 + Vben Admin - Backend: NestJS - Features: User management, group broadcast, statistics 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
3
frontend-vben/playground/src/views/_core/README.md
Normal file
3
frontend-vben/playground/src/views/_core/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# \_core
|
||||
|
||||
此目录包含应用程序正常运行所需的基本视图。这些视图是应用程序布局中使用的视图。
|
||||
9
frontend-vben/playground/src/views/_core/about/index.vue
Normal file
9
frontend-vben/playground/src/views/_core/about/index.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import { About } from '@vben/common-ui';
|
||||
|
||||
defineOptions({ name: 'About' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<About />
|
||||
</template>
|
||||
@@ -0,0 +1,109 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VbenFormSchema } from '@vben/common-ui';
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import { computed, ref, useTemplateRef } from 'vue';
|
||||
|
||||
import { AuthenticationCodeLogin, z } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
defineOptions({ name: 'CodeLogin' });
|
||||
|
||||
const loading = ref(false);
|
||||
const CODE_LENGTH = 6;
|
||||
const loginRef =
|
||||
useTemplateRef<InstanceType<typeof AuthenticationCodeLogin>>('loginRef');
|
||||
function sendCodeApi(phoneNumber: string) {
|
||||
message.loading({
|
||||
content: $t('page.auth.sendingCode'),
|
||||
duration: 0,
|
||||
key: 'sending-code',
|
||||
});
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
message.success({
|
||||
content: $t('page.auth.codeSentTo', [phoneNumber]),
|
||||
duration: 3,
|
||||
key: 'sending-code',
|
||||
});
|
||||
resolve({ code: '123456', phoneNumber });
|
||||
}, 3000);
|
||||
});
|
||||
}
|
||||
const formSchema = computed((): VbenFormSchema[] => {
|
||||
return [
|
||||
{
|
||||
component: 'VbenInput',
|
||||
componentProps: {
|
||||
placeholder: $t('authentication.mobile'),
|
||||
},
|
||||
fieldName: 'phoneNumber',
|
||||
label: $t('authentication.mobile'),
|
||||
rules: z
|
||||
.string()
|
||||
.min(1, { message: $t('authentication.mobileTip') })
|
||||
.refine((v) => /^\d{11}$/.test(v), {
|
||||
message: $t('authentication.mobileErrortip'),
|
||||
}),
|
||||
},
|
||||
{
|
||||
component: 'VbenPinInput',
|
||||
componentProps: {
|
||||
codeLength: CODE_LENGTH,
|
||||
createText: (countdown: number) => {
|
||||
const text =
|
||||
countdown > 0
|
||||
? $t('authentication.sendText', [countdown])
|
||||
: $t('authentication.sendCode');
|
||||
return text;
|
||||
},
|
||||
handleSendCode: async () => {
|
||||
// 模拟发送验证码
|
||||
// Simulate sending verification code
|
||||
loading.value = true;
|
||||
const formApi = loginRef.value?.getFormApi();
|
||||
if (!formApi) {
|
||||
loading.value = false;
|
||||
throw new Error('formApi is not ready');
|
||||
}
|
||||
await formApi.validateField('phoneNumber');
|
||||
const isPhoneReady = await formApi.isFieldValid('phoneNumber');
|
||||
if (!isPhoneReady) {
|
||||
loading.value = false;
|
||||
throw new Error('Phone number is not Ready');
|
||||
}
|
||||
const { phoneNumber } = await formApi.getValues();
|
||||
await sendCodeApi(phoneNumber);
|
||||
loading.value = false;
|
||||
},
|
||||
placeholder: $t('authentication.code'),
|
||||
},
|
||||
fieldName: 'code',
|
||||
label: $t('authentication.code'),
|
||||
rules: z.string().length(CODE_LENGTH, {
|
||||
message: $t('authentication.codeTip', [CODE_LENGTH]),
|
||||
}),
|
||||
},
|
||||
];
|
||||
});
|
||||
/**
|
||||
* 异步处理登录操作
|
||||
* Asynchronously handle the login process
|
||||
* @param values 登录表单数据
|
||||
*/
|
||||
async function handleLogin(values: Recordable<any>) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(values);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AuthenticationCodeLogin
|
||||
ref="loginRef"
|
||||
:form-schema="formSchema"
|
||||
:loading="loading"
|
||||
@submit="handleLogin"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,42 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VbenFormSchema } from '@vben/common-ui';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { AuthenticationForgetPassword, z } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
defineOptions({ name: 'ForgetPassword' });
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
const formSchema = computed((): VbenFormSchema[] => {
|
||||
return [
|
||||
{
|
||||
component: 'VbenInput',
|
||||
componentProps: {
|
||||
placeholder: 'example@example.com',
|
||||
},
|
||||
fieldName: 'email',
|
||||
label: $t('authentication.email'),
|
||||
rules: z
|
||||
.string()
|
||||
.min(1, { message: $t('authentication.emailTip') })
|
||||
.email($t('authentication.emailValidErrorTip')),
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
function handleSubmit(value: Record<string, any>) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('reset email:', value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AuthenticationForgetPassword
|
||||
:form-schema="formSchema"
|
||||
:loading="loading"
|
||||
@submit="handleSubmit"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,132 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VbenFormSchema } from '@vben/common-ui';
|
||||
import type { BasicOption, Recordable } from '@vben/types';
|
||||
|
||||
import { computed, markRaw, useTemplateRef } from 'vue';
|
||||
|
||||
import { AuthenticationLogin, SliderCaptcha, z } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { useAuthStore } from '#/store';
|
||||
|
||||
defineOptions({ name: 'Login' });
|
||||
|
||||
const authStore = useAuthStore();
|
||||
|
||||
const MOCK_USER_OPTIONS: BasicOption[] = [
|
||||
{
|
||||
label: 'Super',
|
||||
value: 'vben',
|
||||
},
|
||||
{
|
||||
label: 'Admin',
|
||||
value: 'admin',
|
||||
},
|
||||
{
|
||||
label: 'User',
|
||||
value: 'jack',
|
||||
},
|
||||
];
|
||||
|
||||
const formSchema = computed((): VbenFormSchema[] => {
|
||||
return [
|
||||
{
|
||||
component: 'VbenSelect',
|
||||
// componentProps(_values, form) {
|
||||
// return {
|
||||
// 'onUpdate:modelValue': (value: string) => {
|
||||
// const findItem = MOCK_USER_OPTIONS.find(
|
||||
// (item) => item.value === value,
|
||||
// );
|
||||
// if (findItem) {
|
||||
// form.setValues({
|
||||
// password: '123456',
|
||||
// username: findItem.label,
|
||||
// });
|
||||
// }
|
||||
// },
|
||||
// options: MOCK_USER_OPTIONS,
|
||||
// placeholder: $t('authentication.selectAccount'),
|
||||
// };
|
||||
// },
|
||||
componentProps: {
|
||||
options: MOCK_USER_OPTIONS,
|
||||
placeholder: $t('authentication.selectAccount'),
|
||||
},
|
||||
fieldName: 'selectAccount',
|
||||
label: $t('authentication.selectAccount'),
|
||||
rules: z
|
||||
.string()
|
||||
.min(1, { message: $t('authentication.selectAccount') })
|
||||
.optional()
|
||||
.default('vben'),
|
||||
},
|
||||
{
|
||||
component: 'VbenInput',
|
||||
componentProps: {
|
||||
placeholder: $t('authentication.usernameTip'),
|
||||
},
|
||||
dependencies: {
|
||||
trigger(values, form) {
|
||||
if (values.selectAccount) {
|
||||
const findUser = MOCK_USER_OPTIONS.find(
|
||||
(item) => item.value === values.selectAccount,
|
||||
);
|
||||
if (findUser) {
|
||||
form.setValues({
|
||||
password: '123456',
|
||||
username: findUser.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
triggerFields: ['selectAccount'],
|
||||
},
|
||||
fieldName: 'username',
|
||||
label: $t('authentication.username'),
|
||||
rules: z.string().min(1, { message: $t('authentication.usernameTip') }),
|
||||
},
|
||||
{
|
||||
component: 'VbenInputPassword',
|
||||
componentProps: {
|
||||
placeholder: $t('authentication.password'),
|
||||
},
|
||||
fieldName: 'password',
|
||||
label: $t('authentication.password'),
|
||||
rules: z.string().min(1, { message: $t('authentication.passwordTip') }),
|
||||
},
|
||||
{
|
||||
component: markRaw(SliderCaptcha),
|
||||
fieldName: 'captcha',
|
||||
rules: z.boolean().refine((value) => value, {
|
||||
message: $t('authentication.verifyRequiredTip'),
|
||||
}),
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
const loginRef =
|
||||
useTemplateRef<InstanceType<typeof AuthenticationLogin>>('loginRef');
|
||||
|
||||
async function onSubmit(params: Recordable<any>) {
|
||||
authStore.authLogin(params).catch(() => {
|
||||
// 登陆失败,刷新验证码的演示
|
||||
const formApi = loginRef.value?.getFormApi();
|
||||
// 重置验证码组件的值
|
||||
formApi?.setFieldValue('captcha', false, false);
|
||||
// 使用表单API获取验证码组件实例,并调用其resume方法来重置验证码
|
||||
formApi
|
||||
?.getFieldComponentRef<InstanceType<typeof SliderCaptcha>>('captcha')
|
||||
?.resume();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AuthenticationLogin
|
||||
ref="loginRef"
|
||||
:form-schema="formSchema"
|
||||
:loading="authStore.loginLoading"
|
||||
@submit="onSubmit"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,10 @@
|
||||
<script lang="ts" setup>
|
||||
import { AuthenticationQrCodeLogin } from '@vben/common-ui';
|
||||
import { LOGIN_PATH } from '@vben/constants';
|
||||
|
||||
defineOptions({ name: 'QrCodeLogin' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AuthenticationQrCodeLogin :login-path="LOGIN_PATH" />
|
||||
</template>
|
||||
@@ -0,0 +1,96 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VbenFormSchema } from '@vben/common-ui';
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import { computed, h, ref } from 'vue';
|
||||
|
||||
import { AuthenticationRegister, z } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
defineOptions({ name: 'Register' });
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
const formSchema = computed((): VbenFormSchema[] => {
|
||||
return [
|
||||
{
|
||||
component: 'VbenInput',
|
||||
componentProps: {
|
||||
placeholder: $t('authentication.usernameTip'),
|
||||
},
|
||||
fieldName: 'username',
|
||||
label: $t('authentication.username'),
|
||||
rules: z.string().min(1, { message: $t('authentication.usernameTip') }),
|
||||
},
|
||||
{
|
||||
component: 'VbenInputPassword',
|
||||
componentProps: {
|
||||
passwordStrength: true,
|
||||
placeholder: $t('authentication.password'),
|
||||
},
|
||||
fieldName: 'password',
|
||||
label: $t('authentication.password'),
|
||||
renderComponentContent() {
|
||||
return {
|
||||
strengthText: () => $t('authentication.passwordStrength'),
|
||||
};
|
||||
},
|
||||
rules: z.string().min(1, { message: $t('authentication.passwordTip') }),
|
||||
},
|
||||
{
|
||||
component: 'VbenInputPassword',
|
||||
componentProps: {
|
||||
placeholder: $t('authentication.confirmPassword'),
|
||||
},
|
||||
dependencies: {
|
||||
rules(values) {
|
||||
const { password } = values;
|
||||
return z
|
||||
.string({ required_error: $t('authentication.passwordTip') })
|
||||
.min(1, { message: $t('authentication.passwordTip') })
|
||||
.refine((value) => value === password, {
|
||||
message: $t('authentication.confirmPasswordTip'),
|
||||
});
|
||||
},
|
||||
triggerFields: ['password'],
|
||||
},
|
||||
fieldName: 'confirmPassword',
|
||||
label: $t('authentication.confirmPassword'),
|
||||
},
|
||||
{
|
||||
component: 'VbenCheckbox',
|
||||
fieldName: 'agreePolicy',
|
||||
renderComponentContent: () => ({
|
||||
default: () =>
|
||||
h('span', [
|
||||
$t('authentication.agree'),
|
||||
h(
|
||||
'a',
|
||||
{
|
||||
class: 'vben-link ml-1 ',
|
||||
href: '',
|
||||
},
|
||||
`${$t('authentication.privacyPolicy')} & ${$t('authentication.terms')}`,
|
||||
),
|
||||
]),
|
||||
}),
|
||||
rules: z.boolean().refine((value) => !!value, {
|
||||
message: $t('authentication.agreeTip'),
|
||||
}),
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
function handleSubmit(value: Recordable<any>) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('register submit:', value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AuthenticationRegister
|
||||
:form-schema="formSchema"
|
||||
:loading="loading"
|
||||
@submit="handleSubmit"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { Fallback } from '@vben/common-ui';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fallback status="coming-soon" />
|
||||
</template>
|
||||
@@ -0,0 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import { Fallback } from '@vben/common-ui';
|
||||
|
||||
defineOptions({ name: 'Fallback403Demo' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fallback status="403" />
|
||||
</template>
|
||||
@@ -0,0 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import { Fallback } from '@vben/common-ui';
|
||||
|
||||
defineOptions({ name: 'Fallback500Demo' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fallback status="500" />
|
||||
</template>
|
||||
@@ -0,0 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import { Fallback } from '@vben/common-ui';
|
||||
|
||||
defineOptions({ name: 'Fallback404Demo' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fallback status="404" />
|
||||
</template>
|
||||
@@ -0,0 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import { Fallback } from '@vben/common-ui';
|
||||
|
||||
defineOptions({ name: 'FallbackOfflineDemo' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fallback status="offline" />
|
||||
</template>
|
||||
@@ -0,0 +1,98 @@
|
||||
<script lang="ts" setup>
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
||||
onMounted(() => {
|
||||
renderEcharts({
|
||||
grid: {
|
||||
bottom: 0,
|
||||
containLabel: true,
|
||||
left: '1%',
|
||||
right: '1%',
|
||||
top: '2 %',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
areaStyle: {},
|
||||
data: [
|
||||
111, 2000, 6000, 16_000, 33_333, 55_555, 64_000, 33_333, 18_000,
|
||||
36_000, 70_000, 42_444, 23_222, 13_000, 8000, 4000, 1200, 333, 222,
|
||||
111,
|
||||
],
|
||||
itemStyle: {
|
||||
color: '#5ab1ef',
|
||||
},
|
||||
smooth: true,
|
||||
type: 'line',
|
||||
},
|
||||
{
|
||||
areaStyle: {},
|
||||
data: [
|
||||
33, 66, 88, 333, 3333, 6200, 20_000, 3000, 1200, 13_000, 22_000,
|
||||
11_000, 2221, 1201, 390, 198, 60, 30, 22, 11,
|
||||
],
|
||||
itemStyle: {
|
||||
color: '#019680',
|
||||
},
|
||||
smooth: true,
|
||||
type: 'line',
|
||||
},
|
||||
],
|
||||
tooltip: {
|
||||
axisPointer: {
|
||||
lineStyle: {
|
||||
color: '#019680',
|
||||
width: 1,
|
||||
},
|
||||
},
|
||||
trigger: 'axis',
|
||||
},
|
||||
// xAxis: {
|
||||
// axisTick: {
|
||||
// show: false,
|
||||
// },
|
||||
// boundaryGap: false,
|
||||
// data: Array.from({ length: 18 }).map((_item, index) => `${index + 6}:00`),
|
||||
// type: 'category',
|
||||
// },
|
||||
xAxis: {
|
||||
axisTick: {
|
||||
show: false,
|
||||
},
|
||||
boundaryGap: false,
|
||||
data: Array.from({ length: 18 }).map((_item, index) => `${index + 6}:00`),
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
type: 'solid',
|
||||
width: 1,
|
||||
},
|
||||
show: true,
|
||||
},
|
||||
type: 'category',
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
axisTick: {
|
||||
show: false,
|
||||
},
|
||||
max: 80_000,
|
||||
splitArea: {
|
||||
show: true,
|
||||
},
|
||||
splitNumber: 4,
|
||||
type: 'value',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EchartsUI ref="chartRef" />
|
||||
</template>
|
||||
@@ -0,0 +1,82 @@
|
||||
<script lang="ts" setup>
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
||||
onMounted(() => {
|
||||
renderEcharts({
|
||||
legend: {
|
||||
bottom: 0,
|
||||
data: ['访问', '趋势'],
|
||||
},
|
||||
radar: {
|
||||
indicator: [
|
||||
{
|
||||
name: '网页',
|
||||
},
|
||||
{
|
||||
name: '移动端',
|
||||
},
|
||||
{
|
||||
name: 'Ipad',
|
||||
},
|
||||
{
|
||||
name: '客户端',
|
||||
},
|
||||
{
|
||||
name: '第三方',
|
||||
},
|
||||
{
|
||||
name: '其它',
|
||||
},
|
||||
],
|
||||
radius: '60%',
|
||||
splitNumber: 8,
|
||||
},
|
||||
series: [
|
||||
{
|
||||
areaStyle: {
|
||||
opacity: 1,
|
||||
shadowBlur: 0,
|
||||
shadowColor: 'rgba(0,0,0,.2)',
|
||||
shadowOffsetX: 0,
|
||||
shadowOffsetY: 10,
|
||||
},
|
||||
data: [
|
||||
{
|
||||
itemStyle: {
|
||||
color: '#b6a2de',
|
||||
},
|
||||
name: '访问',
|
||||
value: [90, 50, 86, 40, 50, 20],
|
||||
},
|
||||
{
|
||||
itemStyle: {
|
||||
color: '#5ab1ef',
|
||||
},
|
||||
name: '趋势',
|
||||
value: [70, 75, 70, 76, 20, 85],
|
||||
},
|
||||
],
|
||||
itemStyle: {
|
||||
// borderColor: '#fff',
|
||||
borderRadius: 10,
|
||||
borderWidth: 2,
|
||||
},
|
||||
symbolSize: 0,
|
||||
type: 'radar',
|
||||
},
|
||||
],
|
||||
tooltip: {},
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EchartsUI ref="chartRef" />
|
||||
</template>
|
||||
@@ -0,0 +1,46 @@
|
||||
<script lang="ts" setup>
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
||||
onMounted(() => {
|
||||
renderEcharts({
|
||||
series: [
|
||||
{
|
||||
animationDelay() {
|
||||
return Math.random() * 400;
|
||||
},
|
||||
animationEasing: 'exponentialInOut',
|
||||
animationType: 'scale',
|
||||
center: ['50%', '50%'],
|
||||
color: ['#5ab1ef', '#b6a2de', '#67e0e3', '#2ec7c9'],
|
||||
data: [
|
||||
{ name: '外包', value: 500 },
|
||||
{ name: '定制', value: 310 },
|
||||
{ name: '技术支持', value: 274 },
|
||||
{ name: '远程', value: 400 },
|
||||
].sort((a, b) => {
|
||||
return a.value - b.value;
|
||||
}),
|
||||
name: '商业占比',
|
||||
radius: '80%',
|
||||
roseType: 'radius',
|
||||
type: 'pie',
|
||||
},
|
||||
],
|
||||
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
},
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EchartsUI ref="chartRef" />
|
||||
</template>
|
||||
@@ -0,0 +1,65 @@
|
||||
<script lang="ts" setup>
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
||||
onMounted(() => {
|
||||
renderEcharts({
|
||||
legend: {
|
||||
bottom: '2%',
|
||||
left: 'center',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
animationDelay() {
|
||||
return Math.random() * 100;
|
||||
},
|
||||
animationEasing: 'exponentialInOut',
|
||||
animationType: 'scale',
|
||||
avoidLabelOverlap: false,
|
||||
color: ['#5ab1ef', '#b6a2de', '#67e0e3', '#2ec7c9'],
|
||||
data: [
|
||||
{ name: '搜索引擎', value: 1048 },
|
||||
{ name: '直接访问', value: 735 },
|
||||
{ name: '邮件营销', value: 580 },
|
||||
{ name: '联盟广告', value: 484 },
|
||||
],
|
||||
emphasis: {
|
||||
label: {
|
||||
fontSize: '12',
|
||||
fontWeight: 'bold',
|
||||
show: true,
|
||||
},
|
||||
},
|
||||
itemStyle: {
|
||||
// borderColor: '#fff',
|
||||
borderRadius: 10,
|
||||
borderWidth: 2,
|
||||
},
|
||||
label: {
|
||||
position: 'center',
|
||||
show: false,
|
||||
},
|
||||
labelLine: {
|
||||
show: false,
|
||||
},
|
||||
name: '访问来源',
|
||||
radius: ['40%', '65%'],
|
||||
type: 'pie',
|
||||
},
|
||||
],
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
},
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EchartsUI ref="chartRef" />
|
||||
</template>
|
||||
@@ -0,0 +1,55 @@
|
||||
<script lang="ts" setup>
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
||||
onMounted(() => {
|
||||
renderEcharts({
|
||||
grid: {
|
||||
bottom: 0,
|
||||
containLabel: true,
|
||||
left: '1%',
|
||||
right: '1%',
|
||||
top: '2 %',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
barMaxWidth: 80,
|
||||
// color: '#4f69fd',
|
||||
data: [
|
||||
3000, 2000, 3333, 5000, 3200, 4200, 3200, 2100, 3000, 5100, 6000,
|
||||
3200, 4800,
|
||||
],
|
||||
type: 'bar',
|
||||
},
|
||||
],
|
||||
tooltip: {
|
||||
axisPointer: {
|
||||
lineStyle: {
|
||||
// color: '#4f69fd',
|
||||
width: 1,
|
||||
},
|
||||
},
|
||||
trigger: 'axis',
|
||||
},
|
||||
xAxis: {
|
||||
data: Array.from({ length: 12 }).map((_item, index) => `${index + 1}月`),
|
||||
type: 'category',
|
||||
},
|
||||
yAxis: {
|
||||
max: 8000,
|
||||
splitNumber: 4,
|
||||
type: 'value',
|
||||
},
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EchartsUI ref="chartRef" />
|
||||
</template>
|
||||
@@ -0,0 +1,90 @@
|
||||
<script lang="ts" setup>
|
||||
import type { AnalysisOverviewItem } from '@vben/common-ui';
|
||||
import type { TabOption } from '@vben/types';
|
||||
|
||||
import {
|
||||
AnalysisChartCard,
|
||||
AnalysisChartsTabs,
|
||||
AnalysisOverview,
|
||||
} from '@vben/common-ui';
|
||||
import {
|
||||
SvgBellIcon,
|
||||
SvgCakeIcon,
|
||||
SvgCardIcon,
|
||||
SvgDownloadIcon,
|
||||
} from '@vben/icons';
|
||||
|
||||
import AnalyticsTrends from './analytics-trends.vue';
|
||||
import AnalyticsVisitsData from './analytics-visits-data.vue';
|
||||
import AnalyticsVisitsSales from './analytics-visits-sales.vue';
|
||||
import AnalyticsVisitsSource from './analytics-visits-source.vue';
|
||||
import AnalyticsVisits from './analytics-visits.vue';
|
||||
|
||||
const overviewItems: AnalysisOverviewItem[] = [
|
||||
{
|
||||
icon: SvgCardIcon,
|
||||
title: '用户量',
|
||||
totalTitle: '总用户量',
|
||||
totalValue: 120_000,
|
||||
value: 2000,
|
||||
},
|
||||
{
|
||||
icon: SvgCakeIcon,
|
||||
title: '访问量',
|
||||
totalTitle: '总访问量',
|
||||
totalValue: 500_000,
|
||||
value: 20_000,
|
||||
},
|
||||
{
|
||||
icon: SvgDownloadIcon,
|
||||
title: '下载量',
|
||||
totalTitle: '总下载量',
|
||||
totalValue: 120_000,
|
||||
value: 8000,
|
||||
},
|
||||
{
|
||||
icon: SvgBellIcon,
|
||||
title: '使用量',
|
||||
totalTitle: '总使用量',
|
||||
totalValue: 50_000,
|
||||
value: 5000,
|
||||
},
|
||||
];
|
||||
|
||||
const chartTabs: TabOption[] = [
|
||||
{
|
||||
label: '流量趋势',
|
||||
value: 'trends',
|
||||
},
|
||||
{
|
||||
label: '月访问量',
|
||||
value: 'visits',
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-5">
|
||||
<AnalysisOverview :items="overviewItems" />
|
||||
<AnalysisChartsTabs :tabs="chartTabs" class="mt-5">
|
||||
<template #trends>
|
||||
<AnalyticsTrends />
|
||||
</template>
|
||||
<template #visits>
|
||||
<AnalyticsVisits />
|
||||
</template>
|
||||
</AnalysisChartsTabs>
|
||||
|
||||
<div class="mt-5 w-full md:flex">
|
||||
<AnalysisChartCard class="mt-5 md:mr-4 md:mt-0 md:w-1/3" title="访问数量">
|
||||
<AnalyticsVisitsData />
|
||||
</AnalysisChartCard>
|
||||
<AnalysisChartCard class="mt-5 md:mr-4 md:mt-0 md:w-1/3" title="访问来源">
|
||||
<AnalyticsVisitsSource />
|
||||
</AnalysisChartCard>
|
||||
<AnalysisChartCard class="mt-5 md:mt-0 md:w-1/3" title="访问来源">
|
||||
<AnalyticsVisitsSales />
|
||||
</AnalysisChartCard>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
266
frontend-vben/playground/src/views/dashboard/workspace/index.vue
Normal file
266
frontend-vben/playground/src/views/dashboard/workspace/index.vue
Normal file
@@ -0,0 +1,266 @@
|
||||
<script lang="ts" setup>
|
||||
import type {
|
||||
WorkbenchProjectItem,
|
||||
WorkbenchQuickNavItem,
|
||||
WorkbenchTodoItem,
|
||||
WorkbenchTrendItem,
|
||||
} from '@vben/common-ui';
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import {
|
||||
AnalysisChartCard,
|
||||
WorkbenchHeader,
|
||||
WorkbenchProject,
|
||||
WorkbenchQuickNav,
|
||||
WorkbenchTodo,
|
||||
WorkbenchTrends,
|
||||
} from '@vben/common-ui';
|
||||
import { preferences } from '@vben/preferences';
|
||||
import { useUserStore } from '@vben/stores';
|
||||
import { openWindow } from '@vben/utils';
|
||||
|
||||
import AnalyticsVisitsSource from '../analytics/analytics-visits-source.vue';
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
// 这是一个示例数据,实际项目中需要根据实际情况进行调整
|
||||
// url 也可以是内部路由,在 navTo 方法中识别处理,进行内部跳转
|
||||
// 例如:url: /dashboard/workspace
|
||||
const projectItems: WorkbenchProjectItem[] = [
|
||||
{
|
||||
color: '',
|
||||
content: '不要等待机会,而要创造机会。',
|
||||
date: '2021-04-01',
|
||||
group: '开源组',
|
||||
icon: 'carbon:logo-github',
|
||||
title: 'Github',
|
||||
url: 'https://github.com',
|
||||
},
|
||||
{
|
||||
color: '#3fb27f',
|
||||
content: '现在的你决定将来的你。',
|
||||
date: '2021-04-01',
|
||||
group: '算法组',
|
||||
icon: 'ion:logo-vue',
|
||||
title: 'Vue',
|
||||
url: 'https://vuejs.org',
|
||||
},
|
||||
{
|
||||
color: '#e18525',
|
||||
content: '没有什么才能比努力更重要。',
|
||||
date: '2021-04-01',
|
||||
group: '上班摸鱼',
|
||||
icon: 'ion:logo-html5',
|
||||
title: 'Html5',
|
||||
url: 'https://developer.mozilla.org/zh-CN/docs/Web/HTML',
|
||||
},
|
||||
{
|
||||
color: '#bf0c2c',
|
||||
content: '热情和欲望可以突破一切难关。',
|
||||
date: '2021-04-01',
|
||||
group: 'UI',
|
||||
icon: 'ion:logo-angular',
|
||||
title: 'Angular',
|
||||
url: 'https://angular.io',
|
||||
},
|
||||
{
|
||||
color: '#00d8ff',
|
||||
content: '健康的身体是实现目标的基石。',
|
||||
date: '2021-04-01',
|
||||
group: '技术牛',
|
||||
icon: 'bx:bxl-react',
|
||||
title: 'React',
|
||||
url: 'https://reactjs.org',
|
||||
},
|
||||
{
|
||||
color: '#EBD94E',
|
||||
content: '路是走出来的,而不是空想出来的。',
|
||||
date: '2021-04-01',
|
||||
group: '架构组',
|
||||
icon: 'ion:logo-javascript',
|
||||
title: 'Js',
|
||||
url: 'https://developer.mozilla.org/zh-CN/docs/Web/JavaScript',
|
||||
},
|
||||
];
|
||||
|
||||
// 同样,这里的 url 也可以使用以 http 开头的外部链接
|
||||
const quickNavItems: WorkbenchQuickNavItem[] = [
|
||||
{
|
||||
color: '#1fdaca',
|
||||
icon: 'ion:home-outline',
|
||||
title: '首页',
|
||||
url: '/',
|
||||
},
|
||||
{
|
||||
color: '#bf0c2c',
|
||||
icon: 'ion:grid-outline',
|
||||
title: '仪表盘',
|
||||
url: '/dashboard',
|
||||
},
|
||||
{
|
||||
color: '#e18525',
|
||||
icon: 'ion:layers-outline',
|
||||
title: '组件',
|
||||
url: '/demos/features/icons',
|
||||
},
|
||||
{
|
||||
color: '#3fb27f',
|
||||
icon: 'ion:settings-outline',
|
||||
title: '系统管理',
|
||||
url: '/demos/features/login-expired', // 这里的 URL 是示例,实际项目中需要根据实际情况进行调整
|
||||
},
|
||||
{
|
||||
color: '#4daf1bc9',
|
||||
icon: 'ion:key-outline',
|
||||
title: '权限管理',
|
||||
url: '/demos/access/page-control',
|
||||
},
|
||||
{
|
||||
color: '#00d8ff',
|
||||
icon: 'ion:bar-chart-outline',
|
||||
title: '图表',
|
||||
url: '/analytics',
|
||||
},
|
||||
];
|
||||
|
||||
const todoItems = ref<WorkbenchTodoItem[]>([
|
||||
{
|
||||
completed: false,
|
||||
content: `审查最近提交到Git仓库的前端代码,确保代码质量和规范。`,
|
||||
date: '2024-07-30 11:00:00',
|
||||
title: '审查前端代码提交',
|
||||
},
|
||||
{
|
||||
completed: true,
|
||||
content: `检查并优化系统性能,降低CPU使用率。`,
|
||||
date: '2024-07-30 11:00:00',
|
||||
title: '系统性能优化',
|
||||
},
|
||||
{
|
||||
completed: false,
|
||||
content: `进行系统安全检查,确保没有安全漏洞或未授权的访问。 `,
|
||||
date: '2024-07-30 11:00:00',
|
||||
title: '安全检查',
|
||||
},
|
||||
{
|
||||
completed: false,
|
||||
content: `更新项目中的所有npm依赖包,确保使用最新版本。`,
|
||||
date: '2024-07-30 11:00:00',
|
||||
title: '更新项目依赖',
|
||||
},
|
||||
{
|
||||
completed: false,
|
||||
content: `修复用户报告的页面UI显示问题,确保在不同浏览器中显示一致。 `,
|
||||
date: '2024-07-30 11:00:00',
|
||||
title: '修复UI显示问题',
|
||||
},
|
||||
]);
|
||||
const trendItems: WorkbenchTrendItem[] = [
|
||||
{
|
||||
avatar: 'svg:avatar-1',
|
||||
content: `在 <a>开源组</a> 创建了项目 <a>Vue</a>`,
|
||||
date: '刚刚',
|
||||
title: '威廉',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-2',
|
||||
content: `关注了 <a>威廉</a> `,
|
||||
date: '1个小时前',
|
||||
title: '艾文',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-3',
|
||||
content: `发布了 <a>个人动态</a> `,
|
||||
date: '1天前',
|
||||
title: '克里斯',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-4',
|
||||
content: `发表文章 <a>如何编写一个Vite插件</a> `,
|
||||
date: '2天前',
|
||||
title: 'Vben',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-1',
|
||||
content: `回复了 <a>杰克</a> 的问题 <a>如何进行项目优化?</a>`,
|
||||
date: '3天前',
|
||||
title: '皮特',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-2',
|
||||
content: `关闭了问题 <a>如何运行项目</a> `,
|
||||
date: '1周前',
|
||||
title: '杰克',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-3',
|
||||
content: `发布了 <a>个人动态</a> `,
|
||||
date: '1周前',
|
||||
title: '威廉',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-4',
|
||||
content: `推送了代码到 <a>Github</a>`,
|
||||
date: '2021-04-01 20:00',
|
||||
title: '威廉',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-4',
|
||||
content: `发表文章 <a>如何编写使用 Admin Vben</a> `,
|
||||
date: '2021-03-01 20:00',
|
||||
title: 'Vben',
|
||||
},
|
||||
];
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
// 这是一个示例方法,实际项目中需要根据实际情况进行调整
|
||||
// This is a sample method, adjust according to the actual project requirements
|
||||
function navTo(nav: WorkbenchProjectItem | WorkbenchQuickNavItem) {
|
||||
if (nav.url?.startsWith('http')) {
|
||||
openWindow(nav.url);
|
||||
return;
|
||||
}
|
||||
if (nav.url?.startsWith('/')) {
|
||||
router.push(nav.url).catch((error) => {
|
||||
console.error('Navigation failed:', error);
|
||||
});
|
||||
} else {
|
||||
console.warn(`Unknown URL for navigation item: ${nav.title} -> ${nav.url}`);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-5">
|
||||
<WorkbenchHeader
|
||||
:avatar="userStore.userInfo?.avatar || preferences.app.defaultAvatar"
|
||||
>
|
||||
<template #title>
|
||||
早安, {{ userStore.userInfo?.realName }}, 开始您一天的工作吧!
|
||||
</template>
|
||||
<template #description> 今日晴,20℃ - 32℃! </template>
|
||||
</WorkbenchHeader>
|
||||
|
||||
<div class="mt-5 flex flex-col lg:flex-row">
|
||||
<div class="mr-4 w-full lg:w-3/5">
|
||||
<WorkbenchProject :items="projectItems" title="项目" @click="navTo" />
|
||||
<WorkbenchTrends :items="trendItems" class="mt-5" title="最新动态" />
|
||||
</div>
|
||||
<div class="w-full lg:w-2/5">
|
||||
<WorkbenchQuickNav
|
||||
:items="quickNavItems"
|
||||
class="mt-5 lg:mt-0"
|
||||
title="快捷导航"
|
||||
@click="navTo"
|
||||
/>
|
||||
<WorkbenchTodo :items="todoItems" class="mt-5" title="待办事项" />
|
||||
<AnalysisChartCard class="mt-5" title="访问来源">
|
||||
<AnalyticsVisitsSource />
|
||||
</AnalysisChartCard>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
import { Fallback } from '@vben/common-ui';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fallback
|
||||
description="当前页面仅 Admin 账号可见"
|
||||
status="coming-soon"
|
||||
title="页面访问测试"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,157 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { AccessControl, useAccess } from '@vben/access';
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { resetAllStores, useUserStore } from '@vben/stores';
|
||||
|
||||
import { Button, Card } from 'ant-design-vue';
|
||||
|
||||
import { useAuthStore } from '#/store';
|
||||
|
||||
const accounts: Record<string, Recordable<any>> = {
|
||||
admin: {
|
||||
password: '123456',
|
||||
username: 'admin',
|
||||
},
|
||||
super: {
|
||||
password: '123456',
|
||||
username: 'vben',
|
||||
},
|
||||
user: {
|
||||
password: '123456',
|
||||
username: 'jack',
|
||||
},
|
||||
};
|
||||
|
||||
const { accessMode, hasAccessByCodes } = useAccess();
|
||||
const authStore = useAuthStore();
|
||||
const userStore = useUserStore();
|
||||
const router = useRouter();
|
||||
|
||||
function roleButtonType(role: string) {
|
||||
return userStore.userRoles.includes(role) ? 'primary' : 'default';
|
||||
}
|
||||
|
||||
async function changeAccount(role: string) {
|
||||
if (userStore.userRoles.includes(role)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const account = accounts[role];
|
||||
resetAllStores();
|
||||
if (account) {
|
||||
await authStore.authLogin(account, async () => {
|
||||
router.go(0);
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page
|
||||
:title="`${accessMode === 'frontend' ? '前端' : '后端'}按钮访问权限演示`"
|
||||
description="切换不同的账号,观察按钮变化。"
|
||||
>
|
||||
<Card class="mb-5">
|
||||
<template #title>
|
||||
<span class="font-semibold">当前角色:</span>
|
||||
<span class="text-primary mx-4 text-lg">
|
||||
{{ userStore.userRoles?.[0] }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<Button :type="roleButtonType('super')" @click="changeAccount('super')">
|
||||
切换为 Super 账号
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
:type="roleButtonType('admin')"
|
||||
class="mx-4"
|
||||
@click="changeAccount('admin')"
|
||||
>
|
||||
切换为 Admin 账号
|
||||
</Button>
|
||||
<Button :type="roleButtonType('user')" @click="changeAccount('user')">
|
||||
切换为 User 账号
|
||||
</Button>
|
||||
</Card>
|
||||
|
||||
<Card class="mb-5" title="组件形式控制 - 权限码">
|
||||
<AccessControl :codes="['AC_100100']" type="code">
|
||||
<Button class="mr-4"> Super 账号可见 ["AC_100100"] </Button>
|
||||
</AccessControl>
|
||||
<AccessControl :codes="['AC_100030']" type="code">
|
||||
<Button class="mr-4"> Admin 账号可见 ["AC_100030"] </Button>
|
||||
</AccessControl>
|
||||
<AccessControl :codes="['AC_1000001']" type="code">
|
||||
<Button class="mr-4"> User 账号可见 ["AC_1000001"] </Button>
|
||||
</AccessControl>
|
||||
<AccessControl :codes="['AC_100100', 'AC_100030']" type="code">
|
||||
<Button class="mr-4">
|
||||
Super & Admin 账号可见 ["AC_100100","AC_100030"]
|
||||
</Button>
|
||||
</AccessControl>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
v-if="accessMode === 'frontend'"
|
||||
class="mb-5"
|
||||
title="组件形式控制 - 角色"
|
||||
>
|
||||
<AccessControl :codes="['super']" type="role">
|
||||
<Button class="mr-4"> Super 角色可见 </Button>
|
||||
</AccessControl>
|
||||
<AccessControl :codes="['admin']" type="role">
|
||||
<Button class="mr-4"> Admin 角色可见 </Button>
|
||||
</AccessControl>
|
||||
<AccessControl :codes="['user']" type="role">
|
||||
<Button class="mr-4"> User 角色可见 </Button>
|
||||
</AccessControl>
|
||||
<AccessControl :codes="['super', 'admin']" type="role">
|
||||
<Button class="mr-4"> Super & Admin 角色可见 </Button>
|
||||
</AccessControl>
|
||||
</Card>
|
||||
|
||||
<Card class="mb-5" title="函数形式控制">
|
||||
<Button v-if="hasAccessByCodes(['AC_100100'])" class="mr-4">
|
||||
Super 账号可见 ["AC_100100"]
|
||||
</Button>
|
||||
<Button v-if="hasAccessByCodes(['AC_100030'])" class="mr-4">
|
||||
Admin 账号可见 ["AC_100030"]
|
||||
</Button>
|
||||
<Button v-if="hasAccessByCodes(['AC_1000001'])" class="mr-4">
|
||||
User 账号可见 ["AC_1000001"]
|
||||
</Button>
|
||||
<Button v-if="hasAccessByCodes(['AC_100100', 'AC_100030'])" class="mr-4">
|
||||
Super & Admin 账号可见 ["AC_100100","AC_100030"]
|
||||
</Button>
|
||||
</Card>
|
||||
|
||||
<Card class="mb-5" title="指令方式 - 权限码">
|
||||
<Button class="mr-4" v-access:code="['AC_100100']">
|
||||
Super 账号可见 ["AC_100100"]
|
||||
</Button>
|
||||
<Button class="mr-4" v-access:code="['AC_100030']">
|
||||
Admin 账号可见 ["AC_100030"]
|
||||
</Button>
|
||||
<Button class="mr-4" v-access:code="['AC_1000001']">
|
||||
User 账号可见 ["AC_1000001"]
|
||||
</Button>
|
||||
<Button class="mr-4" v-access:code="['AC_100100', 'AC_100030']">
|
||||
Super & Admin 账号可见 ["AC_100100","AC_100030"]
|
||||
</Button>
|
||||
</Card>
|
||||
|
||||
<Card v-if="accessMode === 'frontend'" class="mb-5" title="指令方式 - 角色">
|
||||
<Button class="mr-4" v-access:role="['super']"> Super 角色可见 </Button>
|
||||
<Button class="mr-4" v-access:role="['admin']"> Admin 角色可见 </Button>
|
||||
<Button class="mr-4" v-access:role="['user']"> User 角色可见 </Button>
|
||||
<Button class="mr-4" v-access:role="['super', 'admin']">
|
||||
Super & Admin 角色可见
|
||||
</Button>
|
||||
</Card>
|
||||
</Page>
|
||||
</template>
|
||||
98
frontend-vben/playground/src/views/demos/access/index.vue
Normal file
98
frontend-vben/playground/src/views/demos/access/index.vue
Normal file
@@ -0,0 +1,98 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { useAccess } from '@vben/access';
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { resetAllStores, useUserStore } from '@vben/stores';
|
||||
|
||||
import { Button, Card } from 'ant-design-vue';
|
||||
|
||||
import { useAuthStore } from '#/store';
|
||||
|
||||
const accounts: Record<string, Recordable<any>> = {
|
||||
admin: {
|
||||
password: '123456',
|
||||
username: 'admin',
|
||||
},
|
||||
super: {
|
||||
password: '123456',
|
||||
username: 'vben',
|
||||
},
|
||||
user: {
|
||||
password: '123456',
|
||||
username: 'jack',
|
||||
},
|
||||
};
|
||||
|
||||
const { accessMode, toggleAccessMode } = useAccess();
|
||||
const userStore = useUserStore();
|
||||
const accessStore = useAuthStore();
|
||||
const router = useRouter();
|
||||
|
||||
function roleButtonType(role: string) {
|
||||
return userStore.userRoles.includes(role) ? 'primary' : 'default';
|
||||
}
|
||||
|
||||
async function changeAccount(role: string) {
|
||||
if (userStore.userRoles.includes(role)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const account = accounts[role];
|
||||
resetAllStores();
|
||||
if (account) {
|
||||
await accessStore.authLogin(account, async () => {
|
||||
router.go(0);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function handleToggleAccessMode() {
|
||||
if (!accounts.super) {
|
||||
return;
|
||||
}
|
||||
await toggleAccessMode();
|
||||
resetAllStores();
|
||||
|
||||
await accessStore.authLogin(accounts.super, async () => {
|
||||
setTimeout(() => {
|
||||
router.go(0);
|
||||
}, 150);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page
|
||||
:title="`${accessMode === 'frontend' ? '前端' : '后端'}页面访问权限演示`"
|
||||
description="切换不同的账号,观察左侧菜单变化。"
|
||||
>
|
||||
<Card class="mb-5" title="权限模式">
|
||||
<span class="font-semibold">当前权限模式:</span>
|
||||
<span class="text-primary mx-4">{{
|
||||
accessMode === 'frontend' ? '前端权限控制' : '后端权限控制'
|
||||
}}</span>
|
||||
<Button type="primary" @click="handleToggleAccessMode">
|
||||
切换为{{ accessMode === 'frontend' ? '后端' : '前端' }}权限模式
|
||||
</Button>
|
||||
</Card>
|
||||
<Card title="账号切换">
|
||||
<Button :type="roleButtonType('super')" @click="changeAccount('super')">
|
||||
切换为 Super 账号
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
:type="roleButtonType('admin')"
|
||||
class="mx-4"
|
||||
@click="changeAccount('admin')"
|
||||
>
|
||||
切换为 Admin 账号
|
||||
</Button>
|
||||
<Button :type="roleButtonType('user')" @click="changeAccount('user')">
|
||||
切换为 User 账号
|
||||
</Button>
|
||||
</Card>
|
||||
</Page>
|
||||
</template>
|
||||
@@ -0,0 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
import { Fallback } from '@vben/common-ui';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fallback
|
||||
description="当前页面用户不可见,会被重定向到403页面"
|
||||
status="coming-soon"
|
||||
title="页面访问测试"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
import { Fallback } from '@vben/common-ui';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fallback
|
||||
description="当前页面仅 Super 账号可见"
|
||||
status="coming-soon"
|
||||
title="页面访问测试"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
import { Fallback } from '@vben/common-ui';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fallback
|
||||
description="当前页面仅 User 账号可见"
|
||||
status="coming-soon"
|
||||
title="页面访问测试"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
import { Fallback } from '@vben/common-ui';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fallback
|
||||
description="用于菜单激活显示不同的图标"
|
||||
status="coming-soon"
|
||||
title="激活图标示例"
|
||||
/>
|
||||
</template>
|
||||
117
frontend-vben/playground/src/views/demos/badge/index.vue
Normal file
117
frontend-vben/playground/src/views/demos/badge/index.vue
Normal file
@@ -0,0 +1,117 @@
|
||||
<script lang="ts" setup>
|
||||
import { reactive } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { useAccessStore } from '@vben/stores';
|
||||
|
||||
import { MenuBadge } from '@vben-core/menu-ui';
|
||||
|
||||
import { Button, Card, Radio, RadioGroup } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
|
||||
const colors = [
|
||||
{ label: '预设:默认', value: 'default' },
|
||||
{ label: '预设:关键', value: 'destructive' },
|
||||
{ label: '预设:主要', value: 'primary' },
|
||||
{ label: '预设:成功', value: 'success' },
|
||||
{ label: '自定义', value: 'bg-gray-200 text-black' },
|
||||
];
|
||||
|
||||
const route = useRoute();
|
||||
const accessStore = useAccessStore();
|
||||
const menu = accessStore.getMenuByPath(route.path);
|
||||
const badgeProps = reactive({
|
||||
badge: menu?.badge as string,
|
||||
badgeType: menu?.badge ? 'normal' : (menu?.badgeType as 'dot' | 'normal'),
|
||||
badgeVariants: menu?.badgeVariants as string,
|
||||
});
|
||||
|
||||
const [Form] = useVbenForm({
|
||||
handleValuesChange(values) {
|
||||
badgeProps.badge = values.badge;
|
||||
badgeProps.badgeType = values.badgeType;
|
||||
badgeProps.badgeVariants = values.badgeVariants;
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
buttonStyle: 'solid',
|
||||
options: [
|
||||
{ label: '点徽标', value: 'dot' },
|
||||
{ label: '文字徽标', value: 'normal' },
|
||||
],
|
||||
optionType: 'button',
|
||||
},
|
||||
defaultValue: badgeProps.badgeType,
|
||||
fieldName: 'badgeType',
|
||||
label: '类型',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
maxLength: 4,
|
||||
placeholder: '请输入徽标内容',
|
||||
style: { width: '200px' },
|
||||
},
|
||||
defaultValue: badgeProps.badge,
|
||||
fieldName: 'badge',
|
||||
label: '徽标内容',
|
||||
},
|
||||
{
|
||||
component: 'RadioGroup',
|
||||
defaultValue: badgeProps.badgeVariants,
|
||||
fieldName: 'badgeVariants',
|
||||
label: '颜色',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'action',
|
||||
},
|
||||
],
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
function updateMenuBadge() {
|
||||
if (menu) {
|
||||
menu.badge = badgeProps.badge;
|
||||
menu.badgeType = badgeProps.badgeType;
|
||||
menu.badgeVariants = badgeProps.badgeVariants;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page
|
||||
description="菜单项上可以显示徽标,这些徽标可以主动更新"
|
||||
title="菜单徽标"
|
||||
>
|
||||
<Card title="徽标更新">
|
||||
<Form>
|
||||
<template #badgeVariants="slotProps">
|
||||
<RadioGroup v-bind="slotProps">
|
||||
<Radio
|
||||
v-for="color in colors"
|
||||
:key="color.value"
|
||||
:value="color.value"
|
||||
>
|
||||
<div
|
||||
:title="color.label"
|
||||
class="flex h-[14px] w-[50px] items-center justify-start"
|
||||
>
|
||||
<MenuBadge
|
||||
v-bind="{ ...badgeProps, badgeVariants: color.value }"
|
||||
/>
|
||||
</div>
|
||||
</Radio>
|
||||
</RadioGroup>
|
||||
</template>
|
||||
<template #action>
|
||||
<Button type="primary" @click="updateMenuBadge">更新徽标</Button>
|
||||
</template>
|
||||
</Form>
|
||||
</Card>
|
||||
</Page>
|
||||
</template>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script lang="ts" setup>
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { Fallback } from '@vben/common-ui';
|
||||
|
||||
import { Button } from 'ant-design-vue';
|
||||
|
||||
const router = useRouter();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fallback
|
||||
description="面包屑导航-平级模式-详情页"
|
||||
status="coming-soon"
|
||||
title="注意观察面包屑导航变化"
|
||||
>
|
||||
<template #action>
|
||||
<Button @click="router.go(-1)">返回</Button>
|
||||
</template>
|
||||
</Fallback>
|
||||
</template>
|
||||
@@ -0,0 +1,25 @@
|
||||
<script lang="ts" setup>
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { Fallback } from '@vben/common-ui';
|
||||
|
||||
import { Button } from 'ant-design-vue';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
function details() {
|
||||
router.push({ name: 'BreadcrumbLateralDetailDemo' });
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fallback
|
||||
description="点击查看详情,并观察面包屑导航变化"
|
||||
status="coming-soon"
|
||||
title="面包屑导航-平级模式"
|
||||
>
|
||||
<template #action>
|
||||
<Button type="primary" @click="details">点击查看详情</Button>
|
||||
</template>
|
||||
</Fallback>
|
||||
</template>
|
||||
@@ -0,0 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
import { Fallback } from '@vben/common-ui';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fallback
|
||||
description="面包屑导航-层级模式-详情页"
|
||||
status="coming-soon"
|
||||
title="注意观察面包屑导航变化"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { useClipboard } from '@vueuse/core';
|
||||
import { Button, Card, Input } from 'ant-design-vue';
|
||||
|
||||
const source = ref('Hello');
|
||||
const { copy, text } = useClipboard({ legacy: true, source });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page title="剪切板示例">
|
||||
<Card title="基本使用">
|
||||
<p class="mb-3">
|
||||
Current copied: <code>{{ text || 'none' }}</code>
|
||||
</p>
|
||||
<div class="flex">
|
||||
<Input v-model:value="source" class="mr-3 flex w-[200px]" />
|
||||
<Button type="primary" @click="copy(source)"> Copy </Button>
|
||||
</div>
|
||||
</Card>
|
||||
</Page>
|
||||
</template>
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,100 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import {
|
||||
downloadFileFromBase64,
|
||||
downloadFileFromBlobPart,
|
||||
downloadFileFromImageUrl,
|
||||
downloadFileFromUrl,
|
||||
} from '@vben/utils';
|
||||
|
||||
import { Button, Card } from 'ant-design-vue';
|
||||
|
||||
import { downloadFile1, downloadFile2 } from '#/api/examples/download';
|
||||
|
||||
import imageBase64 from './base64';
|
||||
|
||||
const downloadResult = ref('');
|
||||
|
||||
function getBlob() {
|
||||
downloadFile1().then((res) => {
|
||||
downloadResult.value = `获取Blob成功,长度:${res.size}`;
|
||||
});
|
||||
}
|
||||
|
||||
function getResponse() {
|
||||
downloadFile2().then((res) => {
|
||||
downloadResult.value = `获取Response成功,headers:${JSON.stringify(res.headers)},长度:${res.data.size}`;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page title="文件下载示例">
|
||||
<Card title="根据文件地址下载文件">
|
||||
<Button
|
||||
type="primary"
|
||||
@click="
|
||||
downloadFileFromUrl({
|
||||
source:
|
||||
'https://codeload.github.com/vbenjs/vue-vben-admin-doc/zip/main',
|
||||
target: '_self',
|
||||
})
|
||||
"
|
||||
>
|
||||
Download File
|
||||
</Button>
|
||||
</Card>
|
||||
|
||||
<Card class="my-5" title="根据地址下载图片">
|
||||
<Button
|
||||
type="primary"
|
||||
@click="
|
||||
downloadFileFromImageUrl({
|
||||
source:
|
||||
'https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp',
|
||||
fileName: 'vben-logo.png',
|
||||
})
|
||||
"
|
||||
>
|
||||
Download File
|
||||
</Button>
|
||||
</Card>
|
||||
|
||||
<Card class="my-5" title="base64流下载">
|
||||
<Button
|
||||
type="primary"
|
||||
@click="
|
||||
downloadFileFromBase64({
|
||||
source: imageBase64,
|
||||
fileName: 'image.png',
|
||||
})
|
||||
"
|
||||
>
|
||||
Download Image
|
||||
</Button>
|
||||
</Card>
|
||||
<Card class="my-5" title="文本下载">
|
||||
<Button
|
||||
type="primary"
|
||||
@click="
|
||||
downloadFileFromBlobPart({
|
||||
source: 'text content',
|
||||
fileName: 'test.txt',
|
||||
})
|
||||
"
|
||||
>
|
||||
Download TxT
|
||||
</Button>
|
||||
</Card>
|
||||
|
||||
<Card class="my-5" title="Request download">
|
||||
<Button type="primary" @click="getBlob"> 获取Blob </Button>
|
||||
<Button type="primary" class="ml-4" @click="getResponse">
|
||||
获取Response
|
||||
</Button>
|
||||
<div class="mt-4">{{ downloadResult }}</div>
|
||||
</Card>
|
||||
</Page>
|
||||
</template>
|
||||
@@ -0,0 +1,47 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { useFullscreen } from '@vueuse/core';
|
||||
import { Button, Card } from 'ant-design-vue';
|
||||
|
||||
const domRef = ref<HTMLElement>();
|
||||
|
||||
const { enter, exit, isFullscreen, toggle } = useFullscreen();
|
||||
|
||||
const { isFullscreen: isDomFullscreen, toggle: toggleDom } =
|
||||
useFullscreen(domRef);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page title="全屏示例">
|
||||
<Card title="Window Full Screen">
|
||||
<div class="flex flex-wrap items-center gap-4">
|
||||
<Button :disabled="isFullscreen" type="primary" @click="enter">
|
||||
Enter Window Full Screen
|
||||
</Button>
|
||||
<Button @click="toggle"> Toggle Window Full Screen </Button>
|
||||
|
||||
<Button :disabled="!isFullscreen" danger @click="exit">
|
||||
Exit Window Full Screen
|
||||
</Button>
|
||||
|
||||
<span class="text-nowrap"> Current State: {{ isFullscreen }} </span>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card class="mt-5" title="Dom Full Screen">
|
||||
<Button type="primary" @click="toggleDom"> Enter Dom Full Screen </Button>
|
||||
</Card>
|
||||
|
||||
<div
|
||||
ref="domRef"
|
||||
class="mx-auto mt-10 flex h-64 w-1/2 items-center justify-center rounded-md bg-yellow-400"
|
||||
>
|
||||
<Button class="mr-2" type="primary" @click="toggleDom">
|
||||
{{ isDomFullscreen ? 'Exit Dom Full Screen' : 'Enter Dom Full Screen' }}
|
||||
</Button>
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
||||
@@ -0,0 +1,23 @@
|
||||
<script lang="ts" setup>
|
||||
import { Fallback, VbenButton } from '@vben/common-ui';
|
||||
import { useTabs } from '@vben/hooks';
|
||||
import { X } from '@vben/icons';
|
||||
|
||||
const { closeCurrentTab } = useTabs();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fallback
|
||||
description="当前路由在菜单中不可见"
|
||||
status="coming-soon"
|
||||
title="被隐藏的子菜单"
|
||||
show-back
|
||||
>
|
||||
<template #action>
|
||||
<VbenButton size="lg" @click="closeCurrentTab()">
|
||||
<X class="mr-2 size-4" />
|
||||
关闭当前标签页
|
||||
</VbenButton>
|
||||
</template>
|
||||
</Fallback>
|
||||
</template>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script lang="ts" setup>
|
||||
import { Fallback } from '@vben/common-ui';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fallback
|
||||
:description="`当前路由:${String($route.name)},子菜单不可见`"
|
||||
status="coming-soon"
|
||||
title="隐藏子菜单"
|
||||
>
|
||||
<template #action>
|
||||
<RouterLink to="/demos/features/hide-menu-children/children">
|
||||
打开子路由
|
||||
</RouterLink>
|
||||
</template>
|
||||
</Fallback>
|
||||
</template>
|
||||
@@ -0,0 +1,115 @@
|
||||
<script lang="ts" setup>
|
||||
import { h, ref } from 'vue';
|
||||
|
||||
import { IconPicker, Page } from '@vben/common-ui';
|
||||
import {
|
||||
MdiGithub,
|
||||
MdiGoogle,
|
||||
MdiKeyboardEsc,
|
||||
MdiQqchat,
|
||||
MdiWechat,
|
||||
SvgAvatar1Icon,
|
||||
SvgAvatar2Icon,
|
||||
SvgAvatar3Icon,
|
||||
SvgAvatar4Icon,
|
||||
SvgBellIcon,
|
||||
SvgCakeIcon,
|
||||
SvgCardIcon,
|
||||
SvgDownloadIcon,
|
||||
} from '@vben/icons';
|
||||
|
||||
import { Card, Input } from 'ant-design-vue';
|
||||
|
||||
const iconValue1 = ref('ant-design:trademark-outlined');
|
||||
const iconValue2 = ref('svg:avatar-1');
|
||||
const iconValue3 = ref('mdi:alien-outline');
|
||||
const iconValue4 = ref('mdi-light:book-multiple');
|
||||
|
||||
const inputComponent = h(Input);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page title="图标">
|
||||
<template #description>
|
||||
<div class="text-foreground/80 mt-2">
|
||||
图标可在
|
||||
<a
|
||||
class="text-primary"
|
||||
href="https://icon-sets.iconify.design/"
|
||||
target="_blank"
|
||||
>
|
||||
Iconify
|
||||
</a>
|
||||
中查找,支持多种图标库,如 Material Design, Font Awesome, Jam Icons 等。
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<Card class="mb-5" title="Iconify">
|
||||
<div class="flex items-center gap-5">
|
||||
<MdiGithub class="size-8" />
|
||||
<MdiGoogle class="size-8 text-red-500" />
|
||||
<MdiQqchat class="size-8 text-green-500" />
|
||||
<MdiWechat class="size-8" />
|
||||
<MdiKeyboardEsc class="size-8" />
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card class="mb-5" title="Svg Icons">
|
||||
<div class="flex items-center gap-5">
|
||||
<SvgAvatar1Icon class="size-8" />
|
||||
<SvgAvatar2Icon class="size-8 text-red-500" />
|
||||
<SvgAvatar3Icon class="size-8 text-green-500" />
|
||||
<SvgAvatar4Icon class="size-8" />
|
||||
<SvgCakeIcon class="size-8" />
|
||||
<SvgBellIcon class="size-8" />
|
||||
<SvgCardIcon class="size-8" />
|
||||
<SvgDownloadIcon class="size-8" />
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card class="mb-5" title="Tailwind CSS">
|
||||
<div class="flex items-center gap-5 text-3xl">
|
||||
<span class="icon-[ant-design--alipay-circle-outlined]"></span>
|
||||
<span class="icon-[ant-design--account-book-filled]"></span>
|
||||
<span class="icon-[ant-design--container-outlined]"></span>
|
||||
<span class="icon-[svg-spinners--wind-toy]"></span>
|
||||
<span class="icon-[svg-spinners--blocks-wave]"></span>
|
||||
<span class="icon-[line-md--compass-filled-loop]"></span>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card class="mb-5" title="图标选择器">
|
||||
<div class="mb-5 flex items-center gap-5">
|
||||
<span>原始样式(Iconify):</span>
|
||||
<IconPicker v-model="iconValue1" class="w-[200px]" />
|
||||
</div>
|
||||
<div class="mb-5 flex items-center gap-5">
|
||||
<span>原始样式(svg):</span>
|
||||
<IconPicker v-model="iconValue2" class="w-[200px]" prefix="svg" />
|
||||
</div>
|
||||
<div class="mb-5 flex items-center gap-5">
|
||||
<span>自定义Input:</span>
|
||||
<IconPicker
|
||||
:input-component="inputComponent"
|
||||
v-model="iconValue3"
|
||||
icon-slot="addonAfter"
|
||||
model-value-prop="value"
|
||||
prefix="mdi"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center gap-5">
|
||||
<span>显示为一个Icon:</span>
|
||||
<Input
|
||||
v-model:value="iconValue4"
|
||||
allow-clear
|
||||
placeholder="点击这里选择图标"
|
||||
style="width: 300px"
|
||||
>
|
||||
<template #addonAfter>
|
||||
<IconPicker v-model="iconValue4" prefix="mdi-light" type="icon" />
|
||||
</template>
|
||||
</Input>
|
||||
</div>
|
||||
</Card>
|
||||
</Page>
|
||||
</template>
|
||||
@@ -0,0 +1,39 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { Alert, Button, Card } from 'ant-design-vue';
|
||||
|
||||
import { getBigIntData } from '#/api/examples/json-bigint';
|
||||
|
||||
const response = ref('');
|
||||
function fetchData() {
|
||||
getBigIntData().then((res) => {
|
||||
response.value = res;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<Page
|
||||
title="JSON BigInt Support"
|
||||
description="解析后端返回的长整数(long/bigInt)。代码位置:playground/src/api/request.ts中的transformResponse"
|
||||
>
|
||||
<Card>
|
||||
<Alert>
|
||||
<template #message>
|
||||
有些后端接口返回的ID是长整数,但javascript原生的JSON解析是不支持超过2^53-1的长整数的。
|
||||
这种情况可以建议后端返回数据前将长整数转换为字符串类型。如果后端不接受我们的建议😡……
|
||||
<br />
|
||||
下面的按钮点击后会发起请求,接口返回的JSON数据中的id字段是超出整数范围的数字,已自动将其解析为字符串
|
||||
</template>
|
||||
</Alert>
|
||||
<Button class="mt-4" type="primary" @click="fetchData">发起请求</Button>
|
||||
<div>
|
||||
<pre>
|
||||
{{ response }}
|
||||
</pre>
|
||||
</div>
|
||||
</Card>
|
||||
</Page>
|
||||
</template>
|
||||
@@ -0,0 +1,39 @@
|
||||
<script lang="ts" setup>
|
||||
import type { LoginExpiredModeType } from '@vben/types';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { preferences, updatePreferences } from '@vben/preferences';
|
||||
|
||||
import { Button, Card } from 'ant-design-vue';
|
||||
|
||||
import { getMockStatusApi } from '#/api';
|
||||
|
||||
async function handleClick(type: LoginExpiredModeType) {
|
||||
const loginExpiredMode = preferences.app.loginExpiredMode;
|
||||
|
||||
updatePreferences({ app: { loginExpiredMode: type } });
|
||||
await getMockStatusApi('401');
|
||||
updatePreferences({ app: { loginExpiredMode } });
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page title="登录过期演示">
|
||||
<template #description>
|
||||
<div class="text-foreground/80 mt-2">
|
||||
接口请求遇到401状态码时,需要重新登录。有两种方式:
|
||||
<p>1.转到登录页,登录成功后跳转回原页面</p>
|
||||
<p>
|
||||
2.弹出重新登录弹窗,登录后关闭弹窗,不进行任何页面跳转(刷新后还是会跳转登录页面)
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<Card class="mb-5" title="跳转登录页面方式">
|
||||
<Button type="primary" @click="handleClick('page')"> 点击触发 </Button>
|
||||
</Card>
|
||||
<Card class="mb-5" title="登录弹窗方式">
|
||||
<Button type="primary" @click="handleClick('modal')"> 点击触发 </Button>
|
||||
</Card>
|
||||
</Page>
|
||||
</template>
|
||||
@@ -0,0 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
import { Fallback } from '@vben/common-ui';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fallback
|
||||
description="点击菜单,将会带上参数"
|
||||
status="coming-soon"
|
||||
title="菜单带参示例"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
import { Fallback } from '@vben/common-ui';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fallback
|
||||
description="当前页面已在新窗口内打开"
|
||||
status="coming-soon"
|
||||
title="新窗口打开页面"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,61 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, watchEffect } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { Card, Radio, RadioGroup } from 'ant-design-vue';
|
||||
|
||||
import { getParamsData } from '#/api/examples/params';
|
||||
|
||||
const params = { ids: [2512, 3241, 4255] };
|
||||
const paramsSerializer = ref<'brackets' | 'comma' | 'indices' | 'repeat'>(
|
||||
'brackets',
|
||||
);
|
||||
const response = ref('');
|
||||
const paramsStr = computed(() => {
|
||||
// 写一段代码,从完整的URL中提取参数部分
|
||||
const url = response.value;
|
||||
return new URL(url).searchParams.toString();
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
getParamsData(params, paramsSerializer.value).then((res) => {
|
||||
response.value = res.request.responseURL;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<Page
|
||||
title="请求参数序列化"
|
||||
description="不同的后台接口可能对数组类型的GET参数的解析方式不同,我们预置了几种数组序列化方式,通过配置 paramsSerializer 来实现不同的序列化方式"
|
||||
>
|
||||
<Card>
|
||||
<RadioGroup v-model:value="paramsSerializer" name="paramsSerializer">
|
||||
<Radio value="brackets">brackets</Radio>
|
||||
<Radio value="comma">comma</Radio>
|
||||
<Radio value="indices">indices</Radio>
|
||||
<Radio value="repeat">repeat</Radio>
|
||||
</RadioGroup>
|
||||
<div class="mt-4 flex flex-col gap-4">
|
||||
<div>
|
||||
<h3>需要提交的参数</h3>
|
||||
<div>{{ JSON.stringify(params, null, 2) }}</div>
|
||||
</div>
|
||||
<template v-if="response">
|
||||
<div>
|
||||
<h3>访问地址</h3>
|
||||
<pre>{{ response }}</pre>
|
||||
</div>
|
||||
<div>
|
||||
<h3>参数字符串</h3>
|
||||
<pre>{{ paramsStr }}</pre>
|
||||
</div>
|
||||
<div>
|
||||
<h3>参数解码</h3>
|
||||
<pre>{{ decodeURIComponent(paramsStr) }}</pre>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</Card>
|
||||
</Page>
|
||||
</template>
|
||||
105
frontend-vben/playground/src/views/demos/features/tabs/index.vue
Normal file
105
frontend-vben/playground/src/views/demos/features/tabs/index.vue
Normal file
@@ -0,0 +1,105 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { useTabs } from '@vben/hooks';
|
||||
|
||||
import { Button, Card, Input } from 'ant-design-vue';
|
||||
|
||||
const router = useRouter();
|
||||
const newTabTitle = ref('');
|
||||
|
||||
const {
|
||||
closeAllTabs,
|
||||
closeCurrentTab,
|
||||
closeLeftTabs,
|
||||
closeOtherTabs,
|
||||
closeRightTabs,
|
||||
closeTabByKey,
|
||||
refreshTab,
|
||||
resetTabTitle,
|
||||
setTabTitle,
|
||||
} = useTabs();
|
||||
|
||||
function openTab() {
|
||||
// 这里就是路由跳转,也可以用path
|
||||
router.push({ name: 'VbenAbout' });
|
||||
}
|
||||
|
||||
function openTabWithParams(id: number) {
|
||||
// 这里就是路由跳转,也可以用path
|
||||
router.push({ name: 'FeatureTabDetailDemo', params: { id } });
|
||||
}
|
||||
|
||||
function reset() {
|
||||
newTabTitle.value = '';
|
||||
resetTabTitle();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page description="用于需要操作标签页的场景" title="标签页">
|
||||
<Card class="mb-5" title="打开/关闭标签页">
|
||||
<div class="text-foreground/80 mb-3">
|
||||
如果标签页存在,直接跳转切换。如果标签页不存在,则打开新的标签页。
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<Button type="primary" @click="openTab"> 打开 "关于" 标签页 </Button>
|
||||
<Button type="primary" @click="closeTabByKey('/vben-admin/about')">
|
||||
关闭 "关于" 标签页
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card class="mb-5" title="标签页操作">
|
||||
<div class="text-foreground/80 mb-3">用于动态控制标签页的各种操作</div>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<Button type="primary" @click="closeCurrentTab()">
|
||||
关闭当前标签页
|
||||
</Button>
|
||||
<Button type="primary" @click="closeLeftTabs()">
|
||||
关闭左侧标签页
|
||||
</Button>
|
||||
<Button type="primary" @click="closeRightTabs()">
|
||||
关闭右侧标签页
|
||||
</Button>
|
||||
<Button type="primary" @click="closeAllTabs()"> 关闭所有标签页 </Button>
|
||||
<Button type="primary" @click="closeOtherTabs()">
|
||||
关闭其他标签页
|
||||
</Button>
|
||||
<Button type="primary" @click="refreshTab()"> 刷新当前标签页 </Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card class="mb-5" title="动态标题">
|
||||
<div class="text-foreground/80 mb-3">
|
||||
该操作不会影响页面标题,仅修改Tab标题
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<Input
|
||||
v-model:value="newTabTitle"
|
||||
class="w-40"
|
||||
placeholder="请输入新标题"
|
||||
/>
|
||||
<Button type="primary" @click="() => setTabTitle(newTabTitle)">
|
||||
修改
|
||||
</Button>
|
||||
<Button @click="reset"> 重置 </Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card class="mb-5" title="最大打开数量">
|
||||
<div class="text-foreground/80 mb-3">
|
||||
限制带参数的tab打开的最大数量,由 `route.meta.maxNumOfOpenTab` 控制
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<template v-for="item in 5" :key="item">
|
||||
<Button type="primary" @click="openTabWithParams(item)">
|
||||
打开{{ item }}详情页
|
||||
</Button>
|
||||
</template>
|
||||
</div>
|
||||
</Card>
|
||||
</Page>
|
||||
</template>
|
||||
@@ -0,0 +1,23 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { useTabs } from '@vben/hooks';
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const { setTabTitle } = useTabs();
|
||||
|
||||
const index = computed(() => {
|
||||
return route.params?.id ?? -1;
|
||||
});
|
||||
|
||||
setTabTitle(`No.${index.value} - 详情信息`);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page :title="`标签页${index}详情页`">
|
||||
<template #description> {{ index }} - 详情页内容在此 </template>
|
||||
</Page>
|
||||
</template>
|
||||
@@ -0,0 +1,61 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import { useQuery } from '@tanstack/vue-query';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { getMenuList } from '#/api';
|
||||
|
||||
const queryKey = ['demo', 'api', 'options'];
|
||||
const count = 4;
|
||||
|
||||
const { dataUpdatedAt, promise: fetchDataFn } = useQuery({
|
||||
// 在组件渲染期间预取数据
|
||||
experimental_prefetchInRender: true,
|
||||
// 获取接口数据的函数
|
||||
queryFn: getMenuList,
|
||||
queryKey,
|
||||
// 每次组件挂载时都重新获取数据。如果不需要每次都重新获取就不要设置为always
|
||||
refetchOnMount: 'always',
|
||||
// 缓存时间
|
||||
staleTime: 1000 * 60 * 5,
|
||||
});
|
||||
|
||||
async function fetchOptions() {
|
||||
return await fetchDataFn.value;
|
||||
}
|
||||
|
||||
const schema = [];
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
schema.push({
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: fetchOptions,
|
||||
class: 'w-full',
|
||||
filterOption: (input: string, option: Recordable<any>) => {
|
||||
return option.label.toLowerCase().includes(input.toLowerCase());
|
||||
},
|
||||
labelField: 'name',
|
||||
showSearch: true,
|
||||
valueField: 'id',
|
||||
},
|
||||
fieldName: `field${i}`,
|
||||
label: `Select ${i}`,
|
||||
});
|
||||
}
|
||||
|
||||
const [Form] = useVbenForm({
|
||||
schema,
|
||||
showDefaultActions: false,
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<div class="mb-2 flex gap-2">
|
||||
<div>以下{{ count }}个组件共用一个数据源。</div>
|
||||
<div>缓存更新时间:{{ new Date(dataUpdatedAt).toLocaleString() }}</div>
|
||||
</div>
|
||||
<Form />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,40 @@
|
||||
<script setup lang="ts">
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { refAutoReset } from '@vueuse/core';
|
||||
import { Button, Card, Empty } from 'ant-design-vue';
|
||||
|
||||
import ConcurrencyCaching from './concurrency-caching.vue';
|
||||
import InfiniteQueries from './infinite-queries.vue';
|
||||
import PaginatedQueries from './paginated-queries.vue';
|
||||
import QueryRetries from './query-retries.vue';
|
||||
|
||||
const showCaching = refAutoReset(true, 1000);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page title="Vue Query示例">
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<Card title="分页查询">
|
||||
<PaginatedQueries />
|
||||
</Card>
|
||||
<Card title="无限滚动">
|
||||
<InfiniteQueries class="h-[300px] overflow-auto" />
|
||||
</Card>
|
||||
<Card title="错误重试">
|
||||
<QueryRetries />
|
||||
</Card>
|
||||
<Card
|
||||
title="并发和缓存"
|
||||
v-spinning="!showCaching"
|
||||
:body-style="{ minHeight: '330px' }"
|
||||
>
|
||||
<template #extra>
|
||||
<Button @click="showCaching = false">重新加载</Button>
|
||||
</template>
|
||||
<ConcurrencyCaching v-if="showCaching" />
|
||||
<Empty v-else description="正在加载..." />
|
||||
</Card>
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
||||
@@ -0,0 +1,58 @@
|
||||
<script setup lang="ts">
|
||||
import type { IProducts } from './typing';
|
||||
|
||||
import { useInfiniteQuery } from '@tanstack/vue-query';
|
||||
import { Button } from 'ant-design-vue';
|
||||
|
||||
const LIMIT = 10;
|
||||
const fetchProducts = async ({ pageParam = 0 }): Promise<IProducts> => {
|
||||
const res = await fetch(
|
||||
`https://dummyjson.com/products?limit=${LIMIT}&skip=${pageParam * LIMIT}`,
|
||||
);
|
||||
return res.json();
|
||||
};
|
||||
|
||||
const {
|
||||
data,
|
||||
error,
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
isError,
|
||||
isFetching,
|
||||
isFetchingNextPage,
|
||||
isPending,
|
||||
} = useInfiniteQuery({
|
||||
getNextPageParam: (current, allPages) => {
|
||||
const nextPage = allPages.length + 1;
|
||||
const lastPage = current.skip + current.limit;
|
||||
if (lastPage === current.total) return;
|
||||
return nextPage;
|
||||
},
|
||||
initialPageParam: 0,
|
||||
queryFn: fetchProducts,
|
||||
queryKey: ['products'],
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<span v-if="isPending">加载...</span>
|
||||
<span v-else-if="isError">出错了: {{ error }}</span>
|
||||
<div v-else-if="data">
|
||||
<span v-if="isFetching && !isFetchingNextPage">Fetching...</span>
|
||||
<ul v-for="(group, index) in data.pages" :key="index">
|
||||
<li v-for="product in group.products" :key="product.id">
|
||||
{{ product.title }}
|
||||
</li>
|
||||
</ul>
|
||||
<Button
|
||||
:disabled="!hasNextPage || isFetchingNextPage"
|
||||
@click="() => fetchNextPage()"
|
||||
>
|
||||
<span v-if="isFetchingNextPage">加载中...</span>
|
||||
<span v-else-if="hasNextPage">加载更多</span>
|
||||
<span v-else>没有更多了</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,53 @@
|
||||
<script setup lang="ts">
|
||||
import type { Ref } from 'vue';
|
||||
|
||||
import type { IProducts } from './typing';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { keepPreviousData, useQuery } from '@tanstack/vue-query';
|
||||
import { Button } from 'ant-design-vue';
|
||||
|
||||
const LIMIT = 10;
|
||||
const fetcher = async (page: Ref<number>): Promise<IProducts> => {
|
||||
const res = await fetch(
|
||||
`https://dummyjson.com/products?limit=${LIMIT}&skip=${(page.value - 1) * LIMIT}`,
|
||||
);
|
||||
return res.json();
|
||||
};
|
||||
|
||||
const page = ref(1);
|
||||
const { data, error, isError, isPending, isPlaceholderData } = useQuery({
|
||||
// The data from the last successful fetch is available while new data is being requested.
|
||||
placeholderData: keepPreviousData,
|
||||
queryFn: () => fetcher(page),
|
||||
queryKey: ['products', page],
|
||||
});
|
||||
const prevPage = () => {
|
||||
page.value = Math.max(page.value - 1, 1);
|
||||
};
|
||||
const nextPage = () => {
|
||||
if (!isPlaceholderData.value) {
|
||||
page.value = page.value + 1;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex gap-4">
|
||||
<Button size="small" @click="prevPage">上一页</Button>
|
||||
<p>当前页: {{ page }}</p>
|
||||
<Button size="small" @click="nextPage">下一页</Button>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<div v-if="isPending">加载中...</div>
|
||||
<div v-else-if="isError">出错了: {{ error }}</div>
|
||||
<div v-else-if="data">
|
||||
<ul>
|
||||
<li v-for="item in data.products" :key="item.id">
|
||||
{{ item.title }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useQuery } from '@tanstack/vue-query';
|
||||
import { Button } from 'ant-design-vue';
|
||||
|
||||
const count = ref(-1);
|
||||
async function fetchApi() {
|
||||
count.value += 1;
|
||||
return new Promise((_resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
reject(new Error('something went wrong!'));
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
const { error, isFetching, refetch } = useQuery({
|
||||
enabled: false, // Disable automatic refetching when the query mounts
|
||||
queryFn: fetchApi,
|
||||
queryKey: ['queryKey'],
|
||||
retry: 3, // Will retry failed requests 3 times before displaying an error
|
||||
});
|
||||
|
||||
const onClick = async () => {
|
||||
count.value = -1;
|
||||
await refetch();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Button :loading="isFetching" @click="onClick"> 发起错误重试 </Button>
|
||||
<p v-if="count > 0" class="my-3">重试次数{{ count }}</p>
|
||||
<p>{{ error }}</p>
|
||||
</template>
|
||||
@@ -0,0 +1,18 @@
|
||||
export interface IProducts {
|
||||
limit: number;
|
||||
products: {
|
||||
brand: string;
|
||||
category: string;
|
||||
description: string;
|
||||
discountPercentage: string;
|
||||
id: string;
|
||||
images: string[];
|
||||
price: string;
|
||||
rating: string;
|
||||
stock: string;
|
||||
thumbnail: string;
|
||||
title: string;
|
||||
}[];
|
||||
skip: number;
|
||||
total: number;
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
<script lang="ts" setup>
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { useWatermark } from '@vben/hooks';
|
||||
|
||||
import { Button, Card } from 'ant-design-vue';
|
||||
|
||||
const { destroyWatermark, updateWatermark, watermark } = useWatermark();
|
||||
|
||||
async function recreateWaterMark() {
|
||||
destroyWatermark();
|
||||
await updateWatermark({});
|
||||
}
|
||||
|
||||
async function createWaterMark() {
|
||||
await updateWatermark({
|
||||
advancedStyle: {
|
||||
colorStops: [
|
||||
{
|
||||
color: 'red',
|
||||
offset: 0,
|
||||
},
|
||||
{
|
||||
color: 'blue',
|
||||
offset: 1,
|
||||
},
|
||||
],
|
||||
type: 'linear',
|
||||
},
|
||||
content: `hello my watermark\n${new Date().toLocaleString()}`,
|
||||
globalAlpha: 0.5,
|
||||
gridLayoutOptions: {
|
||||
cols: 2,
|
||||
gap: [20, 20],
|
||||
matrix: [
|
||||
[1, 0],
|
||||
[0, 1],
|
||||
],
|
||||
rows: 2,
|
||||
},
|
||||
height: 200,
|
||||
layout: 'grid',
|
||||
rotate: 22,
|
||||
width: 200,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page title="水印">
|
||||
<template #description>
|
||||
<div class="text-foreground/80 mt-2">
|
||||
水印使用了
|
||||
<a
|
||||
class="text-primary"
|
||||
href="https://zhensherlock.github.io/watermark-js-plus/"
|
||||
target="_blank"
|
||||
>
|
||||
watermark-js-plus
|
||||
</a>
|
||||
开源插件,详细配置可见插件配置。
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<Card title="使用">
|
||||
<Button
|
||||
:disabled="!!watermark"
|
||||
class="mr-2"
|
||||
type="primary"
|
||||
@click="recreateWaterMark"
|
||||
>
|
||||
创建水印
|
||||
</Button>
|
||||
<Button
|
||||
:disabled="!watermark"
|
||||
class="mr-2"
|
||||
type="primary"
|
||||
@click="createWaterMark"
|
||||
>
|
||||
更新水印
|
||||
</Button>
|
||||
<Button :disabled="!watermark" danger @click="destroyWatermark">
|
||||
移除水印
|
||||
</Button>
|
||||
</Card>
|
||||
</Page>
|
||||
</template>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { Fallback } from '@vben/common-ui';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fallback status="coming-soon" />
|
||||
</template>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { Fallback } from '@vben/common-ui';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fallback status="coming-soon" />
|
||||
</template>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { Fallback } from '@vben/common-ui';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fallback status="coming-soon" />
|
||||
</template>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { Fallback } from '@vben/common-ui';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fallback status="coming-soon" />
|
||||
</template>
|
||||
@@ -0,0 +1,229 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import { reactive, ref } from 'vue';
|
||||
|
||||
import {
|
||||
Page,
|
||||
VbenButton,
|
||||
VbenButtonGroup,
|
||||
VbenCheckButtonGroup,
|
||||
} from '@vben/common-ui';
|
||||
import { LoaderCircle, Square, SquareCheckBig } from '@vben/icons';
|
||||
|
||||
import { Button, Card, message } from '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',
|
||||
allowClear: false,
|
||||
} as Recordable<any>);
|
||||
|
||||
const [Form] = useVbenForm({
|
||||
handleValuesChange(values) {
|
||||
Object.keys(values).forEach((k) => {
|
||||
if (k === 'beforeChange') {
|
||||
compProps[k] = values[k] ? beforeChange : undefined;
|
||||
} else {
|
||||
compProps[k] = values[k];
|
||||
}
|
||||
});
|
||||
},
|
||||
commonConfig: {
|
||||
labelWidth: 150,
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '大', value: 'large' },
|
||||
{ label: '中', value: 'middle' },
|
||||
{ label: '小', value: 'small' },
|
||||
],
|
||||
},
|
||||
defaultValue: compProps.size,
|
||||
fieldName: 'size',
|
||||
label: '尺寸',
|
||||
},
|
||||
{
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '无', value: 0 },
|
||||
{ label: '小', value: 5 },
|
||||
{ label: '中', value: 15 },
|
||||
{ label: '大', value: 30 },
|
||||
],
|
||||
},
|
||||
defaultValue: compProps.gap,
|
||||
fieldName: 'gap',
|
||||
label: '间距',
|
||||
},
|
||||
{
|
||||
component: 'Switch',
|
||||
defaultValue: compProps.showIcon,
|
||||
fieldName: 'showIcon',
|
||||
label: '显示图标',
|
||||
},
|
||||
{
|
||||
component: 'Switch',
|
||||
defaultValue: compProps.disabled,
|
||||
fieldName: 'disabled',
|
||||
label: '禁用',
|
||||
},
|
||||
{
|
||||
component: 'Switch',
|
||||
defaultValue: false,
|
||||
fieldName: 'beforeChange',
|
||||
label: '前置回调',
|
||||
},
|
||||
{
|
||||
component: 'Switch',
|
||||
defaultValue: false,
|
||||
fieldName: 'allowClear',
|
||||
label: '允许清除',
|
||||
help: '单选时是否允许取消选中(值为undefined)',
|
||||
},
|
||||
{
|
||||
component: 'InputNumber',
|
||||
defaultValue: 0,
|
||||
fieldName: 'maxCount',
|
||||
label: '最大选中数量',
|
||||
help: '多选时有效,0表示不限制',
|
||||
},
|
||||
],
|
||||
showDefaultActions: false,
|
||||
submitOnChange: true,
|
||||
});
|
||||
|
||||
function onBtnClick(value: any) {
|
||||
const opt = options.find((o) => o.value === value);
|
||||
if (opt) {
|
||||
message.success(`点击了按钮${opt.label},value = ${value}`);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<Page
|
||||
title="VbenButtonGroup 按钮组"
|
||||
description="VbenButtonGroup是一个按钮容器,用于包裹一组按钮,协调整体样式。VbenCheckButtonGroup则可以作为一个表单组件,提供单选或多选功能"
|
||||
>
|
||||
<Card title="基本用法">
|
||||
<template #extra>
|
||||
<Button type="primary" @click="resetValues">清空值</Button>
|
||||
</template>
|
||||
<p class="mt-4">按钮组:</p>
|
||||
<div class="mt-2 flex flex-col gap-2">
|
||||
<VbenButtonGroup v-bind="compProps" border>
|
||||
<VbenButton
|
||||
v-for="btn in options"
|
||||
:key="btn.value"
|
||||
variant="link"
|
||||
@click="onBtnClick(btn.value)"
|
||||
>
|
||||
{{ btn.label }}
|
||||
</VbenButton>
|
||||
</VbenButtonGroup>
|
||||
<VbenButtonGroup v-bind="compProps" border>
|
||||
<VbenButton
|
||||
v-for="btn in options"
|
||||
:key="btn.value"
|
||||
variant="outline"
|
||||
@click="onBtnClick(btn.value)"
|
||||
>
|
||||
{{ btn.label }}
|
||||
</VbenButton>
|
||||
</VbenButtonGroup>
|
||||
</div>
|
||||
<p class="mt-4">单选:{{ radioValue }}</p>
|
||||
<div class="mt-2 flex flex-col gap-2">
|
||||
<VbenCheckButtonGroup
|
||||
v-model="radioValue"
|
||||
:options="options"
|
||||
v-bind="compProps"
|
||||
/>
|
||||
</div>
|
||||
<p class="mt-4">单选插槽:{{ radioValue }}</p>
|
||||
<div class="mt-2 flex flex-col gap-2">
|
||||
<VbenCheckButtonGroup
|
||||
v-model="radioValue"
|
||||
:options="options"
|
||||
v-bind="compProps"
|
||||
>
|
||||
<template #option="{ label, value, data }">
|
||||
<div class="flex items-center">
|
||||
<span>{{ label }}</span>
|
||||
<span class="ml-2 text-gray-400">{{ value }}</span>
|
||||
<span v-if="data.num" class="white ml-2">{{ data.num }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</VbenCheckButtonGroup>
|
||||
</div>
|
||||
<p class="mt-4">多选{{ checkValue }}</p>
|
||||
<div class="mt-2 flex flex-col gap-2">
|
||||
<VbenCheckButtonGroup
|
||||
v-model="checkValue"
|
||||
multiple
|
||||
:options="options"
|
||||
v-bind="compProps"
|
||||
/>
|
||||
</div>
|
||||
<p class="mt-4">自定义图标{{ checkValue }}</p>
|
||||
<div class="mt-2 flex flex-col gap-2">
|
||||
<VbenCheckButtonGroup
|
||||
v-model="checkValue"
|
||||
multiple
|
||||
:options="options"
|
||||
v-bind="compProps"
|
||||
>
|
||||
<template #icon="{ loading, checked }">
|
||||
<LoaderCircle class="animate-spin" v-if="loading" />
|
||||
<SquareCheckBig v-else-if="checked" />
|
||||
<Square v-else />
|
||||
</template>
|
||||
</VbenCheckButtonGroup>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card title="设置" class="mt-4">
|
||||
<Form />
|
||||
</Card>
|
||||
</Page>
|
||||
</template>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
import { Page, SliderTranslateCaptcha } from '@vben/common-ui';
|
||||
|
||||
import { Card, message } from 'ant-design-vue';
|
||||
|
||||
function handleSuccess() {
|
||||
message.success('success!');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page
|
||||
description="用于前端简单的拼图滑块水平拖动校验场景"
|
||||
title="拼图滑块校验"
|
||||
>
|
||||
<Card class="mb-5" title="基本示例">
|
||||
<div class="flex items-center justify-center p-4">
|
||||
<SliderTranslateCaptcha
|
||||
src="https://unpkg.com/@vbenjs/static-source@0.1.7/source/pro-avatar.webp"
|
||||
:canvas-width="420"
|
||||
:canvas-height="420"
|
||||
@success="handleSuccess"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</Page>
|
||||
</template>
|
||||
178
frontend-vben/playground/src/views/examples/count-to/index.vue
Normal file
178
frontend-vben/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
frontend-vben/playground/src/views/examples/doc-button.vue
Normal file
22
frontend-vben/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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
frontend-vben/playground/src/views/examples/drawer/index.vue
Normal file
195
frontend-vben/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>
|
||||
@@ -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>
|
||||
@@ -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
frontend-vben/playground/src/views/examples/form/api.vue
Normal file
274
frontend-vben/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
frontend-vben/playground/src/views/examples/form/basic.vue
Normal file
447
frontend-vben/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>
|
||||
@@ -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
frontend-vben/playground/src/views/examples/form/custom.vue
Normal file
100
frontend-vben/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
frontend-vben/playground/src/views/examples/form/dynamic.vue
Normal file
262
frontend-vben/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
frontend-vben/playground/src/views/examples/form/merge.vue
Normal file
116
frontend-vben/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>
|
||||
@@ -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
frontend-vben/playground/src/views/examples/form/query.vue
Normal file
147
frontend-vben/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
frontend-vben/playground/src/views/examples/form/rules.vue
Normal file
245
frontend-vben/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>
|
||||
@@ -0,0 +1,183 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { Button, Card, Switch } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
|
||||
defineOptions({
|
||||
name: 'ScrollToErrorTest',
|
||||
});
|
||||
|
||||
const scrollEnabled = ref(true);
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
scrollToFirstError: scrollEnabled.value,
|
||||
schema: [
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入用户名',
|
||||
},
|
||||
fieldName: 'username',
|
||||
label: '用户名',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入邮箱',
|
||||
},
|
||||
fieldName: 'email',
|
||||
label: '邮箱',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入手机号',
|
||||
},
|
||||
fieldName: 'phone',
|
||||
label: '手机号',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入地址',
|
||||
},
|
||||
fieldName: 'address',
|
||||
label: '地址',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入备注',
|
||||
},
|
||||
fieldName: 'remark',
|
||||
label: '备注',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入公司名称',
|
||||
},
|
||||
fieldName: 'company',
|
||||
label: '公司名称',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入职位',
|
||||
},
|
||||
fieldName: 'position',
|
||||
label: '职位',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '男', value: 'male' },
|
||||
{ label: '女', value: 'female' },
|
||||
],
|
||||
placeholder: '请选择性别',
|
||||
},
|
||||
fieldName: 'gender',
|
||||
label: '性别',
|
||||
rules: 'selectRequired',
|
||||
},
|
||||
],
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
// 测试 validateAndSubmitForm(验证并提交)
|
||||
async function testValidateAndSubmit() {
|
||||
await formApi.validateAndSubmitForm();
|
||||
}
|
||||
|
||||
// 测试 validate(手动验证整个表单)
|
||||
async function testValidate() {
|
||||
await formApi.validate();
|
||||
}
|
||||
|
||||
// 测试 validateField(验证单个字段)
|
||||
async function testValidateField() {
|
||||
await formApi.validateField('username');
|
||||
}
|
||||
|
||||
// 切换滚动功能
|
||||
function toggleScrollToError() {
|
||||
formApi.setState({ scrollToFirstError: scrollEnabled.value });
|
||||
}
|
||||
|
||||
// 填充部分数据测试
|
||||
async function fillPartialData() {
|
||||
await formApi.resetForm();
|
||||
await formApi.setFieldValue('username', '测试用户');
|
||||
await formApi.setFieldValue('email', 'test@example.com');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page
|
||||
description="测试表单验证失败时自动滚动到错误字段的功能"
|
||||
title="滚动到错误字段测试"
|
||||
>
|
||||
<Card title="功能测试">
|
||||
<template #extra>
|
||||
<div class="flex items-center gap-2">
|
||||
<Switch
|
||||
v-model:checked="scrollEnabled"
|
||||
@change="toggleScrollToError"
|
||||
/>
|
||||
<span>启用滚动到错误字段</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="rounded bg-blue-50 p-4">
|
||||
<h3 class="mb-2 font-medium">测试说明:</h3>
|
||||
<ul class="list-inside list-disc space-y-1 text-sm">
|
||||
<li>所有验证方法在验证失败时都会自动滚动到第一个错误字段</li>
|
||||
<li>可以通过右上角的开关控制是否启用自动滚动功能</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="rounded border p-4">
|
||||
<h4 class="mb-3 font-medium">验证方法测试:</h4>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<Button type="primary" @click="testValidateAndSubmit">
|
||||
测试 validateAndSubmitForm()
|
||||
</Button>
|
||||
<Button @click="testValidate"> 测试 validate() </Button>
|
||||
<Button @click="testValidateField"> 测试 validateField() </Button>
|
||||
</div>
|
||||
<div class="mt-2 text-xs text-gray-500">
|
||||
<p>• validateAndSubmitForm(): 验证表单并提交</p>
|
||||
<p>• validate(): 手动验证整个表单</p>
|
||||
<p>• validateField(): 验证单个字段(这里测试用户名字段)</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded border p-4">
|
||||
<h4 class="mb-3 font-medium">数据填充测试:</h4>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<Button @click="fillPartialData"> 填充部分数据 </Button>
|
||||
<Button @click="() => formApi.resetForm()"> 清空表单 </Button>
|
||||
</div>
|
||||
<div class="mt-2 text-xs text-gray-500">
|
||||
<p>• 填充部分数据后验证,会滚动到第一个错误字段</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Form />
|
||||
</div>
|
||||
</Card>
|
||||
</Page>
|
||||
</template>
|
||||
@@ -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" ]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
`);
|
||||
@@ -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
frontend-vben/playground/src/views/examples/layout/col-page.vue
Normal file
106
frontend-vben/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
frontend-vben/playground/src/views/examples/loading/index.vue
Normal file
101
frontend-vben/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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
frontend-vben/playground/src/views/examples/modal/index.vue
Normal file
278
frontend-vben/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>
|
||||
@@ -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>
|
||||
@@ -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
frontend-vben/playground/src/views/examples/motion/index.vue
Normal file
213
frontend-vben/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
frontend-vben/playground/src/views/examples/resize/basic.vue
Normal file
58
frontend-vben/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
frontend-vben/playground/src/views/examples/tippy/index.vue
Normal file
303
frontend-vben/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>
|
||||
111
frontend-vben/playground/src/views/examples/vxe-table/basic.vue
Normal file
111
frontend-vben/playground/src/views/examples/vxe-table/basic.vue
Normal file
@@ -0,0 +1,111 @@
|
||||
<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<RowType>({
|
||||
// 放开注释查看表单组件的类型
|
||||
// formOptions: {
|
||||
// schema: [
|
||||
// {
|
||||
// component: 'Switch',
|
||||
// fieldName: 'name',
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
gridEvents,
|
||||
gridOptions,
|
||||
});
|
||||
|
||||
// 放开注释查看当前表格实例的类型
|
||||
// gridApi.grid
|
||||
|
||||
const showBorder = gridApi.useStore((state) => state.gridOptions?.border);
|
||||
const showStripe = gridApi.useStore((state) => state.gridOptions?.stripe);
|
||||
|
||||
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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user