feat(project): 添加vben5前端
This commit is contained in:
@@ -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,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>
|
||||
93
Yi.Vben5.Vue3/playground/src/views/demos/access/index.vue
Normal file
93
Yi.Vben5.Vue3/playground/src/views/demos/access/index.vue
Normal 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>
|
||||
@@ -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>
|
||||
113
Yi.Vben5.Vue3/playground/src/views/demos/badge/index.vue
Normal file
113
Yi.Vben5.Vue3/playground/src/views/demos/badge/index.vue
Normal 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>
|
||||
@@ -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
Yi.Vben5.Vue3/playground/src/views/demos/features/tabs/index.vue
Normal file
105
Yi.Vben5.Vue3/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,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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user