feat(project): 添加vben5前端

This commit is contained in:
wcg
2026-01-04 13:45:07 +08:00
parent 2c0689fe02
commit 51ee3fb460
839 changed files with 74231 additions and 0 deletions

View File

@@ -0,0 +1,11 @@
<script lang="ts" setup>
import { Fallback } from '@vben/common-ui';
</script>
<template>
<Fallback
description="当前页面仅 Admin 账号可见"
status="coming-soon"
title="页面访问测试"
/>
</template>

View File

@@ -0,0 +1,155 @@
<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();
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>

View File

@@ -0,0 +1,93 @@
<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();
await accessStore.authLogin(account, async () => {
router.go(0);
});
}
async function handleToggleAccessMode() {
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>

View File

@@ -0,0 +1,11 @@
<script lang="ts" setup>
import { Fallback } from '@vben/common-ui';
</script>
<template>
<Fallback
description="当前页面用户不可见会被重定向到403页面"
status="coming-soon"
title="页面访问测试"
/>
</template>

View File

@@ -0,0 +1,11 @@
<script lang="ts" setup>
import { Fallback } from '@vben/common-ui';
</script>
<template>
<Fallback
description="当前页面仅 Super 账号可见"
status="coming-soon"
title="页面访问测试"
/>
</template>

View File

@@ -0,0 +1,11 @@
<script lang="ts" setup>
import { Fallback } from '@vben/common-ui';
</script>
<template>
<Fallback
description="当前页面仅 User 账号可见"
status="coming-soon"
title="页面访问测试"
/>
</template>

View File

@@ -0,0 +1,11 @@
<script lang="ts" setup>
import { Fallback } from '@vben/common-ui';
</script>
<template>
<Fallback
description="用于菜单激活显示不同的图标"
status="coming-soon"
title="激活图标示例"
/>
</template>

View File

@@ -0,0 +1,113 @@
<script lang="ts" setup>
import { useVbenForm } from '#/adapter/form';
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 { reactive } from 'vue';
import { useRoute } from 'vue-router';
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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -0,0 +1,11 @@
<script lang="ts" setup>
import { Fallback } from '@vben/common-ui';
</script>
<template>
<Fallback
description="面包屑导航-层级模式-详情页"
status="coming-soon"
title="注意观察面包屑导航变化"
/>
</template>

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -0,0 +1,11 @@
<script lang="ts" setup>
import { Fallback } from '@vben/common-ui';
</script>
<template>
<Fallback
description="点击菜单,将会带上参数"
status="coming-soon"
title="菜单带参示例"
/>
</template>

View File

@@ -0,0 +1,11 @@
<script lang="ts" setup>
import { Fallback } from '@vben/common-ui';
</script>
<template>
<Fallback
description="当前页面已在新窗口内打开"
status="coming-soon"
title="新窗口打开页面"
/>
</template>

View File

@@ -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>

View 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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -0,0 +1,52 @@
<script setup lang="ts">
import type { Ref } from 'vue';
import type { IProducts } from './typing';
import { keepPreviousData, useQuery } from '@tanstack/vue-query';
import { Button } from 'ant-design-vue';
import { ref } from '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>

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -0,0 +1,7 @@
<script lang="ts" setup>
import { Fallback } from '@vben/common-ui';
</script>
<template>
<Fallback status="coming-soon" />
</template>

View File

@@ -0,0 +1,7 @@
<script lang="ts" setup>
import { Fallback } from '@vben/common-ui';
</script>
<template>
<Fallback status="coming-soon" />
</template>

View File

@@ -0,0 +1,7 @@
<script lang="ts" setup>
import { Fallback } from '@vben/common-ui';
</script>
<template>
<Fallback status="coming-soon" />
</template>

View File

@@ -0,0 +1,7 @@
<script lang="ts" setup>
import { Fallback } from '@vben/common-ui';
</script>
<template>
<Fallback status="coming-soon" />
</template>