refactor: migrate demo applications to playground (#4116)

* chore: detail adjustment

* refactor: Migrate demo applications to playground

* perf: logic optimization

* chore: update docs

* chore: update docs
This commit is contained in:
Vben
2024-08-11 16:09:32 +08:00
committed by GitHub
parent 654bf90c0d
commit b464b87ac5
138 changed files with 3197 additions and 682 deletions

View File

@@ -0,0 +1,33 @@
import { requestClient } from '#/api/request';
export namespace AuthApi {
/** 登录接口参数 */
export interface LoginParams {
password: string;
username: string;
}
/** 登录接口返回值 */
export interface LoginResult {
accessToken: string;
desc: string;
realName: string;
refreshToken: string;
userId: string;
username: string;
}
}
/**
* 登录
*/
export async function loginApi(data: AuthApi.LoginParams) {
return requestClient.post<AuthApi.LoginResult>('/auth/login', data);
}
/**
* 获取用户权限码
*/
export async function getAccessCodesApi() {
return requestClient.get<string[]>('/auth/codes');
}

View File

@@ -0,0 +1,3 @@
export * from './auth';
export * from './menu';
export * from './user';

View File

@@ -0,0 +1,10 @@
import type { RouteRecordStringComponent } from '@vben/types';
import { requestClient } from '#/api/request';
/**
* 获取用户所有菜单
*/
export async function getAllMenusApi() {
return requestClient.get<RouteRecordStringComponent[]>('/menu/all');
}

View File

@@ -0,0 +1,10 @@
import type { UserInfo } from '@vben/types';
import { requestClient } from '#/api/request';
/**
* 获取用户信息
*/
export async function getUserInfoApi() {
return requestClient.get<UserInfo>('/user/info');
}

View File

@@ -0,0 +1 @@
export * from './status';

View File

@@ -0,0 +1,10 @@
import { requestClient } from '#/api/request';
/**
* 模拟任意状态码
*/
async function getMockStatusApi(status: string) {
return requestClient.get('/status', { params: { status } });
}
export { getMockStatusApi };

View File

@@ -0,0 +1,2 @@
export * from './core';
export * from './demos';

View File

@@ -0,0 +1,67 @@
/**
* 该文件可自行根据业务逻辑进行调整
*/
import type { HttpResponse } from '@vben/request';
import { useAppConfig } from '@vben/hooks';
import { preferences } from '@vben/preferences';
import { RequestClient } from '@vben/request';
import { useAccessStore } from '@vben/stores';
import { message } from 'ant-design-vue';
import { useAuthStore } from '#/store';
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
function createRequestClient(baseURL: string) {
const client = new RequestClient({
baseURL,
// 为每个请求携带 Authorization
makeAuthorization: () => {
return {
// 默认
key: 'Authorization',
tokenHandler: () => {
const accessStore = useAccessStore();
return {
refreshToken: `${accessStore.refreshToken}`,
token: `${accessStore.accessToken}`,
};
},
unAuthorizedHandler: async () => {
const accessStore = useAccessStore();
const authStore = useAuthStore();
accessStore.setAccessToken(null);
if (preferences.app.loginExpiredMode === 'modal') {
accessStore.setLoginExpired(true);
} else {
// 退出登录
await authStore.logout();
}
},
};
},
makeErrorMessage: (msg) => message.error(msg),
makeRequestHeaders: () => {
return {
// 为每个请求携带 Accept-Language
'Accept-Language': preferences.app.locale,
};
},
});
client.addResponseInterceptor<HttpResponse>((response) => {
const { data: responseData, status } = response;
const { code, data, message: msg } = responseData;
if (status >= 200 && status < 400 && code === 0) {
return data;
}
throw new Error(`Error ${status}: ${msg}`);
});
return client;
}
export const requestClient = createRequestClient(apiURL);

39
playground/src/app.vue Normal file
View File

@@ -0,0 +1,39 @@
<script lang="ts" setup>
import { computed } from 'vue';
import { useAntdDesignTokens } from '@vben/hooks';
import { preferences, usePreferences } from '@vben/preferences';
import { App, ConfigProvider, theme } from 'ant-design-vue';
import { antdLocale } from '#/locales';
defineOptions({ name: 'App' });
const { isDark } = usePreferences();
const { tokens } = useAntdDesignTokens();
const tokenTheme = computed(() => {
const algorithm = isDark.value
? [theme.darkAlgorithm]
: [theme.defaultAlgorithm];
// antd 紧凑模式算法
if (preferences.app.compact) {
algorithm.push(theme.compactAlgorithm);
}
return {
algorithm,
token: tokens,
};
});
</script>
<template>
<ConfigProvider :locale="antdLocale" :theme="tokenTheme">
<App>
<RouterView />
</App>
</ConfigProvider>
</template>

View File

@@ -0,0 +1,31 @@
import { createApp } from 'vue';
import { registerAccessDirective } from '@vben/access';
import { initStores } from '@vben/stores';
import '@vben/styles';
import '@vben/styles/antd';
import { setupI18n } from '#/locales';
import App from './app.vue';
import { router } from './router';
async function bootstrap(namespace: string) {
const app = createApp(App);
// 国际化 i18n 配置
await setupI18n(app);
// 配置 pinia-tore
await initStores(app, { namespace });
// 安装权限指令
registerAccessDirective(app);
// 配置路由及路由守卫
app.use(router);
app.mount('#app');
}
export { bootstrap };

View File

@@ -0,0 +1,154 @@
<script lang="ts" setup>
import type { NotificationItem } from '@vben/layouts';
import { computed, ref } from 'vue';
import { useRouter } from 'vue-router';
import { AuthenticationLoginExpiredModal } from '@vben/common-ui';
import { LOGIN_PATH, VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants';
import { BookOpenText, CircleHelp, MdiGithub } from '@vben/icons';
import {
BasicLayout,
LockScreen,
Notification,
UserDropdown,
} from '@vben/layouts';
import { preferences } from '@vben/preferences';
import {
resetAllStores,
storeToRefs,
useAccessStore,
useUserStore,
} from '@vben/stores';
import { openWindow } from '@vben/utils';
import { $t } from '#/locales';
import { resetRoutes } from '#/router';
import { useAuthStore } from '#/store';
const notifications = ref<NotificationItem[]>([
{
avatar: 'https://avatar.vercel.sh/vercel.svg?text=VB',
date: '3小时前',
isRead: true,
message: '描述信息描述信息描述信息',
title: '收到了 14 份新周报',
},
{
avatar: 'https://avatar.vercel.sh/1',
date: '刚刚',
isRead: false,
message: '描述信息描述信息描述信息',
title: '朱偏右 回复了你',
},
{
avatar: 'https://avatar.vercel.sh/1',
date: '2024-01-01',
isRead: false,
message: '描述信息描述信息描述信息',
title: '曲丽丽 评论了你',
},
{
avatar: 'https://avatar.vercel.sh/satori',
date: '1天前',
isRead: false,
message: '描述信息描述信息描述信息',
title: '代办提醒',
},
]);
const userStore = useUserStore();
const authStore = useAuthStore();
const accessStore = useAccessStore();
const showDot = computed(() =>
notifications.value.some((item) => !item.isRead),
);
const menus = computed(() => [
{
handler: () => {
openWindow(VBEN_DOC_URL, {
target: '_blank',
});
},
icon: BookOpenText,
text: $t('widgets.document'),
},
{
handler: () => {
openWindow(VBEN_GITHUB_URL, {
target: '_blank',
});
},
icon: MdiGithub,
text: 'GitHub',
},
{
handler: () => {
openWindow(`${VBEN_GITHUB_URL}/issues`, {
target: '_blank',
});
},
icon: CircleHelp,
text: $t('widgets.qa'),
},
]);
const { loginLoading } = storeToRefs(authStore);
const avatar = computed(() => {
return userStore.userInfo?.avatar ?? preferences.app.defaultAvatar;
});
const router = useRouter();
async function handleLogout() {
resetAllStores();
resetRoutes();
await router.replace(LOGIN_PATH);
}
function handleNoticeClear() {
notifications.value = [];
}
function handleMakeAll() {
notifications.value.forEach((item) => (item.isRead = true));
}
</script>
<template>
<BasicLayout @clear-preferences-and-logout="handleLogout">
<template #user-dropdown>
<UserDropdown
:avatar
:menus
:text="userStore.userInfo?.realName"
description="ann.vben@gmail.com"
tag-text="Pro"
@logout="handleLogout"
/>
</template>
<template #notification>
<Notification
:dot="showDot"
:notifications="notifications"
@clear="handleNoticeClear"
@make-all="handleMakeAll"
/>
</template>
<template #extra>
<AuthenticationLoginExpiredModal
v-model:open="accessStore.loginExpired"
:avatar
:loading="loginLoading"
password-placeholder="123456"
username-placeholder="vben"
@submit="authStore.authLogin"
/>
</template>
<template #lock-screen>
<LockScreen :avatar @to-login="handleLogout" />
</template>
</BasicLayout>
</template>

View File

@@ -0,0 +1,8 @@
const BasicLayout = () => import('./basic.vue');
const IFrameView = () => import('@vben/layouts').then((m) => m.IFrameView);
const AuthPageLayout = () =>
import('@vben/layouts').then((m) => m.AuthPageLayout);
export { AuthPageLayout, BasicLayout, IFrameView };

View File

@@ -0,0 +1,3 @@
# locale
每个app使用的国际化可能不同这里用于扩展国际化的功能例如扩展 dayjs、antd组件库的多语言切换以及app本身的国际化文件。

View File

@@ -0,0 +1,94 @@
import type { LocaleSetupOptions, SupportedLanguagesType } from '@vben/locales';
import type { Locale } from 'ant-design-vue/es/locale';
import type { App } from 'vue';
import { ref } from 'vue';
import { $t, setupI18n as coreSetup, loadLocalesMap } from '@vben/locales';
import { preferences } from '@vben/preferences';
import antdEnLocale from 'ant-design-vue/es/locale/en_US';
import antdDefaultLocale from 'ant-design-vue/es/locale/zh_CN';
import dayjs from 'dayjs';
const antdLocale = ref<Locale>(antdDefaultLocale);
const modules = import.meta.glob('./langs/*.json');
const localesMap = loadLocalesMap(modules);
/**
* 加载应用特有的语言包
* 这里也可以改造为从服务端获取翻译数据
* @param lang
*/
async function loadMessages(lang: SupportedLanguagesType) {
const [appLocaleMessages] = await Promise.all([
localesMap[lang]?.(),
loadThirdPartyMessage(lang),
]);
return appLocaleMessages?.default;
}
/**
* 加载第三方组件库的语言包
* @param lang
*/
async function loadThirdPartyMessage(lang: SupportedLanguagesType) {
await Promise.all([loadAntdLocale(lang), loadDayjsLocale(lang)]);
}
/**
* 加载dayjs的语言包
* @param lang
*/
async function loadDayjsLocale(lang: SupportedLanguagesType) {
let locale;
switch (lang) {
case 'zh-CN': {
locale = await import('dayjs/locale/zh-cn');
break;
}
case 'en-US': {
locale = await import('dayjs/locale/en');
break;
}
// 默认使用英语
default: {
locale = await import('dayjs/locale/en');
}
}
if (locale) {
dayjs.locale(locale);
} else {
console.error(`Failed to load dayjs locale for ${lang}`);
}
}
/**
* 加载antd的语言包
* @param lang
*/
async function loadAntdLocale(lang: SupportedLanguagesType) {
switch (lang) {
case 'zh-CN': {
antdLocale.value = antdDefaultLocale;
break;
}
case 'en-US': {
antdLocale.value = antdEnLocale;
break;
}
}
}
async function setupI18n(app: App, options: LocaleSetupOptions = {}) {
await coreSetup(app, {
defaultLocale: preferences.app.locale,
loadMessages,
missingWarn: !import.meta.env.PROD,
...options,
});
}
export { $t, antdLocale, loadMessages, setupI18n };

View File

@@ -0,0 +1,67 @@
{
"page": {
"demos": {
"title": "Demos",
"access": {
"frontendPermissions": "Frontend Permissions",
"backendPermissions": "Backend Permissions",
"pageAccess": "Page Access",
"buttonControl": "Button Control",
"menuVisible403": "Menu Visible(403)",
"superVisible": "Visible to Super",
"adminVisible": "Visible to Admin",
"userVisible": "Visible to User"
},
"nested": {
"title": "Nested Menu",
"menu1": "Menu 1",
"menu2": "Menu 2",
"menu2_1": "Menu 2-1",
"menu3": "Menu 3",
"menu3_1": "Menu 3-1",
"menu3_2": "Menu 3-2",
"menu3_2_1": "Menu 3-2-1"
},
"outside": {
"title": "External Pages",
"embedded": "Embedded",
"externalLink": "External Link"
},
"badge": {
"title": "Menu Badge",
"dot": "Dot Badge",
"text": "Text Badge",
"color": "Badge Color"
},
"activeIcon": {
"title": "Active Menu Icon",
"children": "Children Active Icon"
},
"fallback": {
"title": "Fallback Page"
},
"features": {
"title": "Features",
"hideChildrenInMenu": "Hide Menu Children",
"loginExpired": "Login Expired",
"icons": "Icons",
"watermark": "Watermark",
"tabs": "Tabs",
"tabDetail": "Tab Detail Page"
},
"breadcrumb": {
"navigation": "Breadcrumb Navigation",
"lateral": "Lateral Mode",
"lateralDetail": "Lateral Mode Detail",
"level": "Level Mode",
"levelDetail": "Level Mode Detail"
}
},
"examples": {
"title": "Examples",
"ellipsis": {
"title": "EllipsisText"
}
}
}
}

View File

@@ -0,0 +1,67 @@
{
"page": {
"demos": {
"title": "演示",
"access": {
"frontendPermissions": "前端权限",
"backendPermissions": "后端权限",
"pageAccess": "页面访问",
"buttonControl": "按钮控制",
"menuVisible403": "菜单可见(403)",
"superVisible": "Super 可见",
"adminVisible": "Admin 可见",
"userVisible": "User 可见"
},
"nested": {
"title": "嵌套菜单",
"menu1": "菜单 1",
"menu2": "菜单 2",
"menu2_1": "菜单 2-1",
"menu3": "菜单 3",
"menu3_1": "菜单 3-1",
"menu3_2": "菜单 3-2",
"menu3_2_1": "菜单 3-2-1"
},
"outside": {
"title": "外部页面",
"embedded": "内嵌",
"externalLink": "外链"
},
"badge": {
"title": "菜单徽标",
"dot": "点徽标",
"text": "文本徽标",
"color": "徽标颜色"
},
"activeIcon": {
"title": "菜单激活图标",
"children": "子级激活图标"
},
"fallback": {
"title": "缺省页"
},
"features": {
"title": "功能",
"hideChildrenInMenu": "隐藏子菜单",
"loginExpired": "登录过期",
"icons": "图标",
"watermark": "水印",
"tabs": "标签页",
"tabDetail": "标签详情页"
},
"breadcrumb": {
"navigation": "面包屑导航",
"lateral": "平级模式",
"level": "层级模式",
"levelDetail": "层级模式详情",
"lateralDetail": "平级模式详情"
}
},
"examples": {
"title": "示例",
"ellipsis": {
"title": "文本省略"
}
}
}
}

31
playground/src/main.ts Normal file
View File

@@ -0,0 +1,31 @@
import { initPreferences } from '@vben/preferences';
import { unmountGlobalLoading } from '@vben/utils';
import { overridesPreferences } from './preferences';
/**
* 应用初始化完成之后再进行页面加载渲染
*/
async function initApplication() {
// name用于指定项目唯一标识
// 用于区分不同项目的偏好设置以及存储数据的key前缀以及其他一些需要隔离的数据
const env = import.meta.env.PROD ? 'prod' : 'dev';
const appVersion = import.meta.env.VITE_APP_VERSION;
const namespace = `${import.meta.env.VITE_APP_NAMESPACE}-${appVersion}-${env}`;
// app偏好设置初始化
await initPreferences({
namespace,
overrides: overridesPreferences,
});
// 启动应用并挂载
// vue应用主要逻辑及视图
const { bootstrap } = await import('./bootstrap');
await bootstrap(namespace);
// 移除并销毁loading
unmountGlobalLoading();
}
initApplication();

View File

@@ -0,0 +1,12 @@
import { defineOverridesPreferences } from '@vben/preferences';
/**
* @description 项目配置文件
* 只需要覆盖项目中的一部分配置,不需要的配置不用覆盖,会自动使用默认配置
*/
export const overridesPreferences = defineOverridesPreferences({
// overrides
app: {
name: import.meta.env.VITE_APP_TITLE,
},
});

View File

@@ -0,0 +1,42 @@
import type {
ComponentRecordType,
GenerateMenuAndRoutesOptions,
} from '@vben/types';
import { generateAccessible } from '@vben/access';
import { preferences } from '@vben/preferences';
import { message } from 'ant-design-vue';
import { getAllMenusApi } from '#/api';
import { BasicLayout, IFrameView } from '#/layouts';
import { $t } from '#/locales';
const forbiddenComponent = () => import('#/views/_core/fallback/forbidden.vue');
async function generateAccess(options: GenerateMenuAndRoutesOptions) {
const pageMap: ComponentRecordType = import.meta.glob('../views/**/*.vue');
const layoutMap: ComponentRecordType = {
BasicLayout,
IFrameView,
};
return await generateAccessible(preferences.app.accessMode, {
...options,
fetchMenuListAsync: async () => {
message.loading({
content: `${$t('common.loadingMenu')}...`,
duration: 1.5,
});
return await getAllMenusApi();
},
// 可以指定没有权限跳转403页面
forbiddenComponent,
// 如果 route.meta.menuVisibleWithForbidden = true
layoutMap,
pageMap,
});
}
export { generateAccess };

View File

@@ -0,0 +1,138 @@
import type { Router } from 'vue-router';
import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants';
import { preferences } from '@vben/preferences';
import { useAccessStore, useUserStore } from '@vben/stores';
import { startProgress, stopProgress } from '@vben/utils';
import { useTitle } from '@vueuse/core';
import { $t } from '#/locales';
import { coreRouteNames, dynamicRoutes } from '#/router/routes';
import { useAuthStore } from '#/store';
import { generateAccess } from './access';
/**
* 通用守卫配置
* @param router
*/
function setupCommonGuard(router: Router) {
// 记录已经加载的页面
const loadedPaths = new Set<string>();
router.beforeEach(async (to) => {
to.meta.loaded = loadedPaths.has(to.path);
// 页面加载进度条
if (!to.meta.loaded && preferences.transition.progress) {
startProgress();
}
return true;
});
router.afterEach((to) => {
// 记录页面是否加载,如果已经加载,后续的页面切换动画等效果不在重复执行
if (preferences.tabbar.enable) {
loadedPaths.add(to.path);
}
// 关闭页面加载进度条
if (preferences.transition.progress) {
stopProgress();
}
// 动态修改标题
if (preferences.app.dynamicTitle) {
const { title } = to.meta;
// useTitle(`${$t(title)} - ${preferences.app.name}`);
useTitle(`${$t(title)} - ${preferences.app.name}`);
}
});
}
/**
* 权限访问守卫配置
* @param router
*/
function setupAccessGuard(router: Router) {
router.beforeEach(async (to, from) => {
const accessStore = useAccessStore();
const userStore = useUserStore();
const authStore = useAuthStore();
// 基本路由,这些路由不需要进入权限拦截
if (coreRouteNames.includes(to.name as string)) {
if (to.path === LOGIN_PATH && accessStore.accessToken) {
return decodeURIComponent(
(to.query?.redirect as string) || DEFAULT_HOME_PATH,
);
}
return true;
}
// accessToken 检查
if (!accessStore.accessToken) {
// 明确声明忽略权限访问权限,则可以访问
if (to.meta.ignoreAccess) {
return true;
}
// 没有访问权限,跳转登录页面
if (to.fullPath !== LOGIN_PATH) {
return {
path: LOGIN_PATH,
// 如不需要,直接删除 query
query: { redirect: encodeURIComponent(to.fullPath) },
// 携带当前跳转的页面,登录后重新跳转该页面
replace: true,
};
}
return to;
}
const accessRoutes = accessStore.accessRoutes;
// 是否已经生成过动态路由
if (accessRoutes && accessRoutes.length > 0) {
return true;
}
// 生成路由表
// 当前登录用户拥有的角色标识列表
const userInfo = userStore.userInfo || (await authStore.fetchUserInfo());
const userRoles = userInfo.roles ?? [];
// 生成菜单和路由
const { accessibleMenus, accessibleRoutes } = await generateAccess({
roles: userRoles,
router,
// 则会在菜单中显示但是访问会被重定向到403
routes: dynamicRoutes,
});
// 保存菜单信息和路由信息
accessStore.setAccessMenus(accessibleMenus);
accessStore.setAccessRoutes(accessibleRoutes);
const redirectPath = (from.query.redirect ?? to.fullPath) as string;
return {
...router.resolve(decodeURIComponent(redirectPath)),
replace: true,
};
});
}
/**
* 项目守卫配置
* @param router
*/
function createRouterGuard(router: Router) {
/** 通用 */
setupCommonGuard(router);
/** 权限访问 */
setupAccessGuard(router);
}
export { createRouterGuard };

View File

@@ -0,0 +1,32 @@
import {
createRouter,
createWebHashHistory,
createWebHistory,
} from 'vue-router';
import { resetStaticRoutes } from '@vben/utils';
import { createRouterGuard } from './guard';
import { routes } from './routes';
/**
* @zh_CN 创建vue-router实例
*/
const router = createRouter({
history:
import.meta.env.VITE_ROUTER_HISTORY === 'hash'
? createWebHashHistory(import.meta.env.VITE_BASE)
: createWebHistory(import.meta.env.VITE_BASE),
// 应该添加到路由的初始路由列表。
routes,
scrollBehavior: () => ({ left: 0, top: 0 }),
// 是否应该禁止尾部斜杠。
// strict: true,
});
const resetRoutes = () => resetStaticRoutes(router, routes);
// 创建路由守卫
createRouterGuard(router);
export { resetRoutes, router };

View File

@@ -0,0 +1,86 @@
import type { RouteRecordRaw } from 'vue-router';
import { DEFAULT_HOME_PATH } from '@vben/constants';
import { AuthPageLayout } from '#/layouts';
import { $t } from '#/locales';
import Login from '#/views/_core/authentication/login.vue';
/** 全局404页面 */
const fallbackNotFoundRoute: RouteRecordRaw = {
component: () => import('#/views/_core/fallback/not-found.vue'),
meta: {
hideInBreadcrumb: true,
hideInMenu: true,
hideInTab: true,
title: '404',
},
name: 'FallbackNotFound',
path: '/:path(.*)*',
};
/** 基本路由,这些路由是必须存在的 */
const coreRoutes: RouteRecordRaw[] = [
{
meta: {
title: 'Root',
},
name: 'Root',
path: '/',
redirect: DEFAULT_HOME_PATH,
},
{
component: AuthPageLayout,
meta: {
title: 'Authentication',
},
name: 'Authentication',
path: '/auth',
children: [
{
name: 'Login',
path: 'login',
component: Login,
meta: {
title: $t('page.core.login'),
},
},
{
name: 'CodeLogin',
path: 'code-login',
component: () => import('#/views/_core/authentication/code-login.vue'),
meta: {
title: $t('page.core.codeLogin'),
},
},
{
name: 'QrCodeLogin',
path: 'qrcode-login',
component: () =>
import('#/views/_core/authentication/qrcode-login.vue'),
meta: {
title: $t('page.core.qrcodeLogin'),
},
},
{
name: 'ForgetPassword',
path: 'forget-password',
component: () =>
import('#/views/_core/authentication/forget-password.vue'),
meta: {
title: $t('page.core.forgetPassword'),
},
},
{
name: 'Register',
path: 'register',
component: () => import('#/views/_core/authentication/register.vue'),
meta: {
title: $t('page.core.register'),
},
},
],
},
];
export { coreRoutes, fallbackNotFoundRoute };

View File

@@ -0,0 +1,31 @@
import type { RouteRecordRaw } from 'vue-router';
import { mergeRouteModules, traverseTreeValues } from '@vben/utils';
import { coreRoutes, fallbackNotFoundRoute } from './core';
const dynamicRouteFiles = import.meta.glob('./modules/**/*.ts', {
eager: true,
});
// 有需要可以自行打开注释,并创建文件夹
// const staticRouteFiles = import.meta.glob('./static/**/*.ts', { eager: true });
/** 动态路由 */
const dynamicRoutes: RouteRecordRaw[] = mergeRouteModules(dynamicRouteFiles);
/** 静态路由列表,访问这些页面可以不需要权限 */
// const staticRoutes: RouteRecordRaw[] = mergeRouteModules(staticRouteFiles);
const staticRoutes: RouteRecordRaw[] = [];
/** 路由列表,由基本路由+静态路由组成 */
const routes: RouteRecordRaw[] = [
...coreRoutes,
...staticRoutes,
fallbackNotFoundRoute,
];
/** 基本路由列表,这些路由不需要进入权限拦截 */
const coreRouteNames = traverseTreeValues(coreRoutes, (route) => route.name);
export { coreRouteNames, dynamicRoutes, routes };

View File

@@ -0,0 +1,39 @@
import type { RouteRecordRaw } from 'vue-router';
import { BasicLayout } from '#/layouts';
import { $t } from '#/locales';
const routes: RouteRecordRaw[] = [
{
component: BasicLayout,
meta: {
icon: 'lucide:layout-dashboard',
order: -1,
title: $t('page.dashboard.title'),
},
name: 'Dashboard',
path: '/',
children: [
{
name: 'Analytics',
path: '/analytics',
component: () => import('#/views/dashboard/analytics/index.vue'),
meta: {
affixTab: true,
icon: 'lucide:area-chart',
title: $t('page.dashboard.analytics'),
},
},
{
name: 'Workspace',
path: '/workspace',
component: () => import('#/views/dashboard/workspace/index.vue'),
meta: {
title: $t('page.dashboard.workspace'),
},
},
],
},
];
export default routes;

View File

@@ -0,0 +1,494 @@
import type { RouteRecordRaw } from 'vue-router';
import { BasicLayout, IFrameView } from '#/layouts';
import { $t } from '#/locales';
const routes: RouteRecordRaw[] = [
{
component: BasicLayout,
meta: {
icon: 'ic:baseline-view-in-ar',
keepAlive: true,
order: 1000,
title: $t('page.demos.title'),
},
name: 'Demos',
path: '/demos',
children: [
// 权限控制
{
meta: {
icon: 'mdi:shield-key-outline',
title: $t('page.demos.access.frontendPermissions'),
},
name: 'AccessDemos',
path: '/demos/access',
children: [
{
name: 'AccessPageControlDemo',
path: '/demos/access/page-control',
component: () => import('#/views/demos/access/index.vue'),
meta: {
icon: 'mdi:page-previous-outline',
title: $t('page.demos.access.pageAccess'),
},
},
{
name: 'AccessButtonControlDemo',
path: '/demos/access/button-control',
component: () => import('#/views/demos/access/button-control.vue'),
meta: {
icon: 'mdi:button-cursor',
title: $t('page.demos.access.buttonControl'),
},
},
{
name: 'AccessMenuVisible403Demo',
path: '/demos/access/menu-visible-403',
component: () =>
import('#/views/demos/access/menu-visible-403.vue'),
meta: {
authority: ['no-body'],
icon: 'mdi:button-cursor',
menuVisibleWithForbidden: true,
title: $t('page.demos.access.menuVisible403'),
},
},
{
name: 'AccessSuperVisibleDemo',
path: '/demos/access/super-visible',
component: () => import('#/views/demos/access/super-visible.vue'),
meta: {
authority: ['super'],
icon: 'mdi:button-cursor',
title: $t('page.demos.access.superVisible'),
},
},
{
name: 'AccessAdminVisibleDemo',
path: '/demos/access/admin-visible',
component: () => import('#/views/demos/access/admin-visible.vue'),
meta: {
authority: ['admin'],
icon: 'mdi:button-cursor',
title: $t('page.demos.access.adminVisible'),
},
},
{
name: 'AccessUserVisibleDemo',
path: '/demos/access/user-visible',
component: () => import('#/views/demos/access/user-visible.vue'),
meta: {
authority: ['user'],
icon: 'mdi:button-cursor',
title: $t('page.demos.access.userVisible'),
},
},
],
},
// 功能
{
meta: {
icon: 'mdi:feature-highlight',
title: $t('page.demos.features.title'),
},
name: 'FeaturesDemos',
path: '/demos/features',
children: [
{
name: 'LoginExpiredDemo',
path: '/demos/features/login-expired',
component: () =>
import('#/views/demos/features/login-expired/index.vue'),
meta: {
icon: 'mdi:encryption-expiration',
title: $t('page.demos.features.loginExpired'),
},
},
{
name: 'IconsDemo',
path: '/demos/features/icons',
component: () => import('#/views/demos/features/icons/index.vue'),
meta: {
title: $t('page.demos.features.icons'),
},
},
{
name: 'WatermarkDemo',
path: '/demos/features/watermark',
component: () =>
import('#/views/demos/features/watermark/index.vue'),
meta: {
title: $t('page.demos.features.watermark'),
},
},
{
name: 'FeatureTabsDemo',
path: '/demos/features/tabs',
component: () => import('#/views/demos/features/tabs/index.vue'),
meta: {
icon: 'lucide:app-window',
title: $t('page.demos.features.tabs'),
},
},
{
name: 'FeatureTabDetailDemo',
path: '/demos/features/tabs/detail/:id',
component: () =>
import('#/views/demos/features/tabs/tab-detail.vue'),
meta: {
activePath: '/demos/features/tabs',
hideInMenu: true,
maxNumOfOpenTab: 3,
title: $t('page.demos.features.tabDetail'),
},
},
{
name: 'HideChildrenInMenuParentDemo',
path: '/demos/features/hide-menu-children',
component: () =>
import('#/views/demos/features/hide-menu-children/parent.vue'),
meta: {
hideChildrenInMenu: true,
icon: 'ic:round-menu',
title: $t('page.demos.features.hideChildrenInMenu'),
},
children: [
{
name: 'HideChildrenInMenuChildrenDemo',
path: '/demos/features/hide-menu-children/children',
component: () =>
import(
'#/views/demos/features/hide-menu-children/children.vue'
),
meta: { title: 'HideChildrenInMenuChildrenDemo' },
},
],
},
],
},
// 面包屑导航
{
name: 'BreadcrumbDemos',
path: '/demos/breadcrumb',
meta: {
icon: 'lucide:navigation',
title: $t('page.demos.breadcrumb.navigation'),
},
children: [
{
name: 'BreadcrumbLateralDemo',
path: '/demos/breadcrumb/lateral',
component: () => import('#/views/demos/breadcrumb/lateral.vue'),
meta: {
icon: 'lucide:navigation',
title: $t('page.demos.breadcrumb.lateral'),
},
},
{
name: 'BreadcrumbLateralDetailDemo',
path: '/demos/breadcrumb/lateral-detail',
component: () =>
import('#/views/demos/breadcrumb/lateral-detail.vue'),
meta: {
activePath: '/demos/breadcrumb/lateral',
hideInMenu: true,
title: $t('page.demos.breadcrumb.lateralDetail'),
},
},
{
name: 'BreadcrumbLevelDemo',
path: '/demos/breadcrumb/level',
meta: {
icon: 'lucide:navigation',
title: $t('page.demos.breadcrumb.level'),
},
children: [
{
name: 'BreadcrumbLevelDetailDemo',
path: '/demos/breadcrumb/level/detail',
component: () =>
import('#/views/demos/breadcrumb/level-detail.vue'),
meta: {
title: $t('page.demos.breadcrumb.levelDetail'),
},
},
],
},
],
},
// 缺省页
{
meta: {
icon: 'mdi:lightbulb-error-outline',
title: $t('page.demos.fallback.title'),
},
name: 'FallbackDemos',
path: '/demos/fallback',
children: [
{
name: 'Fallback403Demo',
path: '/demos/fallback/403',
component: () => import('#/views/_core/fallback/forbidden.vue'),
meta: {
icon: 'mdi:do-not-disturb-alt',
title: '403',
},
},
{
name: 'Fallback404Demo',
path: '/demos/fallback/404',
component: () => import('#/views/_core/fallback/not-found.vue'),
meta: {
icon: 'mdi:table-off',
title: '404',
},
},
{
name: 'Fallback500Demo',
path: '/demos/fallback/500',
component: () =>
import('#/views/_core/fallback/internal-error.vue'),
meta: {
icon: 'mdi:server-network-off',
title: '500',
},
},
{
name: 'FallbackOfflineDemo',
path: '/demos/fallback/offline',
component: () => import('#/views/_core/fallback/offline.vue'),
meta: {
icon: 'mdi:offline',
title: $t('fallback.offline'),
},
},
],
},
// 菜单徽标
{
meta: {
badgeType: 'dot',
badgeVariants: 'destructive',
icon: 'lucide:circle-dot',
title: $t('page.demos.badge.title'),
},
name: 'BadgeDemos',
path: '/demos/badge',
children: [
{
name: 'BadgeDotDemo',
component: () => import('#/views/demos/badge/index.vue'),
path: '/demos/badge/dot',
meta: {
badgeType: 'dot',
icon: 'lucide:square-dot',
title: $t('page.demos.badge.dot'),
},
},
{
name: 'BadgeTextDemo',
component: () => import('#/views/demos/badge/index.vue'),
path: '/demos/badge/text',
meta: {
badge: '10',
icon: 'lucide:square-dot',
title: $t('page.demos.badge.text'),
},
},
{
name: 'BadgeColorDemo',
component: () => import('#/views/demos/badge/index.vue'),
path: '/demos/badge/color',
meta: {
badge: 'Hot',
badgeVariants: 'destructive',
icon: 'lucide:square-dot',
title: $t('page.demos.badge.color'),
},
},
],
},
// 菜单激活图标
{
meta: {
activeIcon: 'fluent-emoji:radioactive',
icon: 'bi:radioactive',
title: $t('page.demos.activeIcon.title'),
},
name: 'ActiveIconDemos',
path: '/demos/active-icon',
children: [
{
name: 'ActiveIconDemo',
component: () => import('#/views/demos/active-icon/index.vue'),
path: '/demos/active-icon/children',
meta: {
activeIcon: 'fluent-emoji:radioactive',
icon: 'bi:radioactive',
title: $t('page.demos.activeIcon.children'),
},
},
],
},
// 外部链接
{
meta: {
icon: 'ic:round-settings-input-composite',
title: $t('page.demos.outside.title'),
},
name: 'OutsideDemos',
path: '/demos/outside',
children: [
{
name: 'IframeDemos',
path: '/demos/outside/iframe',
meta: {
icon: 'mdi:newspaper-variant-outline',
title: $t('page.demos.outside.embedded'),
},
children: [
{
name: 'VueDocumentDemo',
path: '/demos/outside/iframe/vue-document',
component: IFrameView,
meta: {
icon: 'logos:vue',
iframeSrc: 'https://cn.vuejs.org/',
keepAlive: true,
title: 'Vue',
},
},
{
name: 'TailwindcssDemo',
path: '/demos/outside/iframe/tailwindcss',
component: IFrameView,
meta: {
icon: 'devicon:tailwindcss',
iframeSrc: 'https://tailwindcss.com/',
// keepAlive: true,
title: 'Tailwindcss',
},
},
],
},
{
name: 'ExternalLinkDemos',
path: '/demos/outside/external-link',
meta: {
icon: 'mdi:newspaper-variant-multiple-outline',
title: $t('page.demos.outside.externalLink'),
},
children: [
{
name: 'ViteDemo',
path: '/demos/outside/external-link/vite',
component: IFrameView,
meta: {
icon: 'logos:vitejs',
link: 'https://vitejs.dev/',
title: 'Vite',
},
},
{
name: 'VueUseDemo',
path: '/demos/outside/external-link/vue-use',
component: IFrameView,
meta: {
icon: 'logos:vueuse',
link: 'https://vueuse.org',
title: 'VueUse',
},
},
],
},
],
},
// 嵌套菜单
{
meta: {
icon: 'ic:round-menu',
title: $t('page.demos.nested.title'),
},
name: 'NestedDemos',
path: '/demos/nested',
children: [
{
name: 'Menu1Demo',
path: '/demos/nested/menu1',
component: () => import('#/views/demos/nested/menu-1.vue'),
meta: {
icon: 'ic:round-menu',
keepAlive: true,
title: $t('page.demos.nested.menu1'),
},
},
{
name: 'Menu2Demo',
path: '/demos/nested/menu2',
meta: {
icon: 'ic:round-menu',
keepAlive: true,
title: $t('page.demos.nested.menu2'),
},
children: [
{
name: 'Menu21Demo',
path: '/demos/nested/menu2/menu2-1',
component: () => import('#/views/demos/nested/menu-2-1.vue'),
meta: {
icon: 'ic:round-menu',
keepAlive: true,
title: $t('page.demos.nested.menu2_1'),
},
},
],
},
{
name: 'Menu3Demo',
path: '/demos/nested/menu3',
meta: {
icon: 'ic:round-menu',
title: $t('page.demos.nested.menu3'),
},
children: [
{
name: 'Menu31Demo',
path: 'menu3-1',
component: () => import('#/views/demos/nested/menu-3-1.vue'),
meta: {
icon: 'ic:round-menu',
keepAlive: true,
title: $t('page.demos.nested.menu3_1'),
},
},
{
name: 'Menu32Demo',
path: 'menu3-2',
meta: {
icon: 'ic:round-menu',
title: $t('page.demos.nested.menu3_2'),
},
children: [
{
name: 'Menu321Demo',
path: '/demos/nested/menu3/menu3-2/menu3-2-1',
component: () =>
import('#/views/demos/nested/menu-3-2-1.vue'),
meta: {
icon: 'ic:round-menu',
keepAlive: true,
title: $t('page.demos.nested.menu3_2_1'),
},
},
],
},
],
},
],
},
],
},
];
export default routes;

View File

@@ -0,0 +1,30 @@
import type { RouteRecordRaw } from 'vue-router';
import { BasicLayout } from '#/layouts';
import { $t } from '#/locales';
const routes: RouteRecordRaw[] = [
{
component: BasicLayout,
meta: {
icon: 'ion:layers-outline',
keepAlive: true,
order: 1000,
title: $t('page.examples.title'),
},
name: 'Examples',
path: '/examples',
children: [
{
name: 'EllipsisDemo',
path: '/examples/ellipsis',
component: () => import('#/views/examples/ellipsis/index.vue'),
meta: {
title: $t('page.examples.ellipsis.title'),
},
},
],
},
];
export default routes;

View File

@@ -0,0 +1,90 @@
import type { RouteRecordRaw } from 'vue-router';
import {
VBEN_ANT_PREVIEW_URL,
VBEN_DOC_URL,
VBEN_ELE_PREVIEW_URL,
VBEN_GITHUB_URL,
VBEN_LOGO_URL,
VBEN_NAIVE_PREVIEW_URL,
} from '@vben/constants';
import { BasicLayout, IFrameView } from '#/layouts';
import { $t } from '#/locales';
const routes: RouteRecordRaw[] = [
{
component: BasicLayout,
meta: {
badgeType: 'dot',
icon: VBEN_LOGO_URL,
order: 9999,
title: $t('page.vben.title'),
},
name: 'VbenProject',
path: '/vben-admin',
children: [
{
name: 'VbenAbout',
path: '/vben-admin/about',
component: () => import('#/views/_core/vben/about/index.vue'),
meta: {
icon: 'lucide:copyright',
title: $t('page.vben.about'),
},
},
{
name: 'VbenDocument',
path: '/vben-admin/document',
component: IFrameView,
meta: {
icon: 'lucide:book-open-text',
link: VBEN_DOC_URL,
title: $t('page.vben.document'),
},
},
{
name: 'VbenGithub',
path: '/vben-admin/github',
component: IFrameView,
meta: {
icon: 'mdi:github',
link: VBEN_GITHUB_URL,
title: 'Github',
},
},
{
name: 'VbenAntdv',
path: '/vben-admin/antdv',
component: IFrameView,
meta: {
badgeType: 'dot',
link: VBEN_ANT_PREVIEW_URL,
title: $t('page.vben.antdv'),
},
},
{
name: 'VbenNaive',
path: '/vben-admin/naive',
component: IFrameView,
meta: {
badgeType: 'dot',
link: VBEN_NAIVE_PREVIEW_URL,
title: $t('page.vben.naive-ui'),
},
},
{
name: 'VbenElementPlus',
path: '/vben-admin/ele',
component: IFrameView,
meta: {
badgeType: 'dot',
link: VBEN_ELE_PREVIEW_URL,
title: $t('page.vben.element-plus'),
},
},
],
},
];
export default routes;

View File

@@ -0,0 +1,111 @@
import type { LoginAndRegisterParams } from '@vben/common-ui';
import type { UserInfo } from '@vben/types';
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants';
import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores';
import { notification } from 'ant-design-vue';
import { defineStore } from 'pinia';
import { getAccessCodesApi, getUserInfoApi, loginApi } from '#/api';
import { $t } from '#/locales';
export const useAuthStore = defineStore('auth', () => {
const accessStore = useAccessStore();
const userStore = useUserStore();
const router = useRouter();
const loginLoading = ref(false);
/**
* 异步处理登录操作
* Asynchronously handle the login process
* @param params 登录表单数据
*/
async function authLogin(
params: LoginAndRegisterParams,
onSuccess?: () => Promise<void> | void,
) {
// 异步处理用户登录操作并获取 accessToken
let userInfo: null | UserInfo = null;
try {
loginLoading.value = true;
const { accessToken, refreshToken } = await loginApi(params);
// 如果成功获取到 accessToken
if (accessToken) {
// 将 accessToken 存储到 accessStore 中
accessStore.setAccessToken(accessToken);
accessStore.setRefreshToken(refreshToken);
// 获取用户信息并存储到 accessStore 中
const [fetchUserInfoResult, accessCodes] = await Promise.all([
fetchUserInfo(),
getAccessCodesApi(),
]);
userInfo = fetchUserInfoResult;
userStore.setUserInfo(userInfo);
accessStore.setAccessCodes(accessCodes);
if (accessStore.loginExpired) {
accessStore.setLoginExpired(false);
} else {
onSuccess
? await onSuccess?.()
: await router.push(userInfo.homePath || DEFAULT_HOME_PATH);
}
if (userInfo?.realName) {
notification.success({
description: `${$t('authentication.loginSuccessDesc')}:${userInfo?.realName}`,
duration: 3,
message: $t('authentication.loginSuccess'),
});
}
}
} finally {
loginLoading.value = false;
}
return {
userInfo,
};
}
async function logout() {
resetAllStores();
accessStore.setLoginExpired(false);
// 回登陆页带上当前路由地址
await router.replace({
path: LOGIN_PATH,
query: {
redirect: encodeURIComponent(router.currentRoute.value.fullPath),
},
});
}
async function fetchUserInfo() {
let userInfo: null | UserInfo = null;
userInfo = await getUserInfoApi();
userStore.setUserInfo(userInfo);
return userInfo;
}
function $reset() {
loginLoading.value = false;
}
return {
$reset,
authLogin,
fetchUserInfo,
loginLoading,
logout,
};
});

View File

@@ -0,0 +1 @@
export * from './auth';

View File

@@ -0,0 +1,3 @@
# \_core
此目录包含应用程序正常运行所需的基本视图。这些视图是应用程序布局中使用的视图。

View File

@@ -0,0 +1,30 @@
<script lang="ts" setup>
import type { LoginCodeParams } from '@vben/common-ui';
import { ref } from 'vue';
import { AuthenticationCodeLogin } from '@vben/common-ui';
import { LOGIN_PATH } from '@vben/constants';
defineOptions({ name: 'CodeLogin' });
const loading = ref(false);
/**
* 异步处理登录操作
* Asynchronously handle the login process
* @param values 登录表单数据
*/
async function handleLogin(values: LoginCodeParams) {
// eslint-disable-next-line no-console
console.log(values);
}
</script>
<template>
<AuthenticationCodeLogin
:loading="loading"
:login-path="LOGIN_PATH"
@submit="handleLogin"
/>
</template>

View File

@@ -0,0 +1,23 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { AuthenticationForgetPassword } from '@vben/common-ui';
import { LOGIN_PATH } from '@vben/constants';
defineOptions({ name: 'ForgetPassword' });
const loading = ref(false);
function handleSubmit(value: string) {
// eslint-disable-next-line no-console
console.log('reset email:', value);
}
</script>
<template>
<AuthenticationForgetPassword
:loading="loading"
:login-path="LOGIN_PATH"
@submit="handleSubmit"
/>
</template>

View File

@@ -0,0 +1,18 @@
<script lang="ts" setup>
import { AuthenticationLogin } from '@vben/common-ui';
import { useAuthStore } from '#/store';
defineOptions({ name: 'Login' });
const authStore = useAuthStore();
</script>
<template>
<AuthenticationLogin
:loading="authStore.loginLoading"
password-placeholder="123456"
username-placeholder="vben"
@submit="authStore.authLogin"
/>
</template>

View File

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

View File

@@ -0,0 +1,25 @@
<script lang="ts" setup>
import type { LoginAndRegisterParams } from '@vben/common-ui';
import { ref } from 'vue';
import { AuthenticationRegister } from '@vben/common-ui';
import { LOGIN_PATH } from '@vben/constants';
defineOptions({ name: 'Register' });
const loading = ref(false);
function handleSubmit(value: LoginAndRegisterParams) {
// eslint-disable-next-line no-console
console.log('register submit:', value);
}
</script>
<template>
<AuthenticationRegister
:loading="loading"
:login-path="LOGIN_PATH"
@submit="handleSubmit"
/>
</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,9 @@
<script lang="ts" setup>
import { Fallback } from '@vben/common-ui';
defineOptions({ name: 'Fallback403Demo' });
</script>
<template>
<Fallback status="403" />
</template>

View File

@@ -0,0 +1,9 @@
<script lang="ts" setup>
import { Fallback } from '@vben/common-ui';
defineOptions({ name: 'Fallback500Demo' });
</script>
<template>
<Fallback status="500" />
</template>

View File

@@ -0,0 +1,9 @@
<script lang="ts" setup>
import { Fallback } from '@vben/common-ui';
defineOptions({ name: 'Fallback404Demo' });
</script>
<template>
<Fallback status="404" />
</template>

View File

@@ -0,0 +1,9 @@
<script lang="ts" setup>
import { Fallback } from '@vben/common-ui';
defineOptions({ name: 'FallbackOfflineDemo' });
</script>
<template>
<Fallback status="offline" />
</template>

View File

@@ -0,0 +1,9 @@
<script lang="ts" setup>
import { About } from '@vben/common-ui';
defineOptions({ name: 'About' });
</script>
<template>
<About />
</template>

View File

@@ -0,0 +1,78 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import { EchartsUI, type EchartsUIType, useEcharts } from '@vben/chart-ui';
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',
},
yAxis: [
{
axisTick: {
show: false,
},
max: 80_000,
type: 'value',
},
],
});
});
</script>
<template>
<EchartsUI ref="chartRef" />
</template>

View File

@@ -0,0 +1,80 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import { EchartsUI, type EchartsUIType, useEcharts } from '@vben/chart-ui';
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>

View File

@@ -0,0 +1,44 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import { EchartsUI, type EchartsUIType, useEcharts } from '@vben/chart-ui';
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>

View File

@@ -0,0 +1,63 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import { EchartsUI, type EchartsUIType, useEcharts } from '@vben/chart-ui';
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>

View File

@@ -0,0 +1,53 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import { EchartsUI, type EchartsUIType, useEcharts } from '@vben/chart-ui';
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>

View File

@@ -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 AnalyticsVisits from './analytics-visits.vue';
import AnalyticsVisitsData from './analytics-visits-data.vue';
import AnalyticsVisitsSales from './analytics-visits-sales.vue';
import AnalyticsVisitsSource from './analytics-visits-source.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>

View File

@@ -0,0 +1,225 @@
<script lang="ts" setup>
import type {
WorkbenchProjectItem,
WorkbenchQuickNavItem,
WorkbenchTodoItem,
WorkbenchTrendItem,
} from '@vben/common-ui';
import { ref } from 'vue';
import {
AnalysisChartCard,
WorkbenchHeader,
WorkbenchProject,
WorkbenchQuickNav,
WorkbenchTodo,
WorkbenchTrends,
} from '@vben/common-ui';
import { preferences } from '@vben/preferences';
import { useUserStore } from '@vben/stores';
import AnalyticsVisitsSource from '../analytics/analytics-visits-source.vue';
const userStore = useUserStore();
const projectItems: WorkbenchProjectItem[] = [
{
color: '',
content: '不要等待机会,而要创造机会。',
date: '2021-04-01',
group: '开源组',
icon: 'carbon:logo-github',
title: 'Github',
},
{
color: '#3fb27f',
content: '现在的你决定将来的你。',
date: '2021-04-01',
group: '算法组',
icon: 'ion:logo-vue',
title: 'Vue',
},
{
color: '#e18525',
content: '没有什么才能比努力更重要。',
date: '2021-04-01',
group: '上班摸鱼',
icon: 'ion:logo-html5',
title: 'Html5',
},
{
color: '#bf0c2c',
content: '热情和欲望可以突破一切难关。',
date: '2021-04-01',
group: 'UI',
icon: 'ion:logo-angular',
title: 'Angular',
},
{
color: '#00d8ff',
content: '健康的身体是实现目标的基石。',
date: '2021-04-01',
group: '技术牛',
icon: 'bx:bxl-react',
title: 'React',
},
{
color: '#EBD94E',
content: '路是走出来的,而不是空想出来的。',
date: '2021-04-01',
group: '架构组',
icon: 'ion:logo-javascript',
title: 'Js',
},
];
const quickNavItems: WorkbenchQuickNavItem[] = [
{
color: '#1fdaca',
icon: 'ion:home-outline',
title: '首页',
},
{
color: '#bf0c2c',
icon: 'ion:grid-outline',
title: '仪表盘',
},
{
color: '#e18525',
icon: 'ion:layers-outline',
title: '组件',
},
{
color: '#3fb27f',
icon: 'ion:settings-outline',
title: '系统管理',
},
{
color: '#4daf1bc9',
icon: 'ion:key-outline',
title: '权限管理',
},
{
color: '#00d8ff',
icon: 'ion:bar-chart-outline',
title: '图表',
},
];
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',
},
];
</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="项目" />
<WorkbenchTrends :items="trendItems" class="mt-5" title="最新动态" />
</div>
<div class="w-full lg:w-2/5">
<WorkbenchQuickNav :items="quickNavItems" title="快捷导航" />
<WorkbenchTodo :items="todoItems" class="mt-5" title="待办事项" />
<AnalysisChartCard class="mt-5" title="访问来源">
<AnalyticsVisitsSource />
</AnalysisChartCard>
</div>
</div>
</div>
</template>

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,161 @@
<script lang="ts" setup>
import type { LoginAndRegisterParams } from '@vben/common-ui';
import { useRouter } from 'vue-router';
import { AccessControl, useAccess } from '@vben/access';
import { resetAllStores, useUserStore } from '@vben/stores';
import { Button } from 'ant-design-vue';
import { useAuthStore } from '#/store';
const accounts: Record<string, LoginAndRegisterParams> = {
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>
<div class="p-5">
<div class="card-box p-5">
<h1 class="text-xl font-semibold">
{{ accessMode === 'frontend' ? '前端' : '后端' }}页面访问权限演示
</h1>
<div class="text-foreground/80 mt-2">切换不同的账号观察按钮变化</div>
</div>
<div class="card-box mt-5 p-5">
<div class="mb-3">
<span class="text-lg font-semibold">当前角色:</span>
<span class="text-primary mx-4 text-lg">
{{ userStore.userRoles?.[0] }}
</span>
</div>
<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>
</div>
<div class="card-box mt-5 p-5">
<div class="mb-3 text-lg font-semibold">组件形式控制 - 权限码方式</div>
<AccessControl :codes="['AC_100100']" type="code">
<Button class="mr-4"> Super 账号可见 ["AC_1000001"] </Button>
</AccessControl>
<AccessControl :codes="['AC_100030']" type="code">
<Button class="mr-4"> Admin 账号可见 ["AC_100010"] </Button>
</AccessControl>
<AccessControl :codes="['AC_1000001']" type="code">
<Button class="mr-4"> User 账号可见 ["AC_1000001"] </Button>
</AccessControl>
<AccessControl :codes="['AC_100100', 'AC_100010']" type="code">
<Button class="mr-4">
Super & Admin 账号可见 ["AC_100100","AC_1000001"]
</Button>
</AccessControl>
</div>
<div v-if="accessMode === 'frontend'" class="card-box mt-5 p-5">
<div class="mb-3 text-lg font-semibold">组件形式控制 - 用户角色方式</div>
<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>
</div>
<div class="card-box mt-5 p-5">
<div class="mb-3 text-lg font-semibold">函数形式控制</div>
<Button v-if="hasAccessByCodes(['AC_100100'])" class="mr-4">
Super 账号可见 ["AC_1000001"]
</Button>
<Button v-if="hasAccessByCodes(['AC_100030'])" class="mr-4">
Admin 账号可见 ["AC_100010"]
</Button>
<Button v-if="hasAccessByCodes(['AC_1000001'])" class="mr-4">
User 账号可见 ["AC_1000001"]
</Button>
<Button v-if="hasAccessByCodes(['AC_100100', 'AC_1000001'])" class="mr-4">
Super & Admin 账号可见 ["AC_100100","AC_1000001"]
</Button>
</div>
<div class="card-box mt-5 p-5">
<div class="mb-3 text-lg font-semibold">指令方式 - 权限码</div>
<Button class="mr-4" v-access:code="['AC_100100']">
Super 账号可见 ["AC_1000001"]
</Button>
<Button class="mr-4" v-access:code="['AC_100030']">
Admin 账号可见 ["AC_100010"]
</Button>
<Button class="mr-4" v-access:code="['AC_1000001']">
User 账号可见 ["AC_1000001"]
</Button>
<Button class="mr-4" v-access:code="['AC_100100', 'AC_1000001']">
Super & Admin 账号可见 ["AC_100100","AC_1000001"]
</Button>
</div>
<div v-if="accessMode === 'frontend'" class="card-box mt-5 p-5">
<div class="mb-3 text-lg font-semibold">指令方式 - 角色</div>
<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>
</div>
</div>
</template>

View File

@@ -0,0 +1,110 @@
<script lang="ts" setup>
import type { LoginAndRegisterParams } from '@vben/common-ui';
import { useRouter } from 'vue-router';
import { useAccess } from '@vben/access';
import { resetAllStores, useUserStore } from '@vben/stores';
import { Button } from 'ant-design-vue';
import { useAuthStore } from '#/store';
const accounts: Record<string, LoginAndRegisterParams> = {
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>
<div class="p-5">
<div class="card-box p-5">
<h1 class="text-xl font-semibold">
{{ accessMode === 'frontend' ? '前端' : '后端' }}页面访问权限演示
</h1>
<div class="text-foreground/80 mt-2">
切换不同的账号观察左侧菜单变化
</div>
</div>
<div class="card-box mt-5 p-5">
<span class="text-lg font-semibold">当前权限模式:</span>
<span class="text-primary mx-4">{{
accessMode === 'frontend' ? '前端权限控制' : '后端权限控制'
}}</span>
<Button type="primary" @click="handleToggleAccessMode">
切换为{{ accessMode === 'frontend' ? '后端' : '前端' }}权限模式
</Button>
</div>
<div class="card-box mt-5 p-5">
<div class="mb-3">
<span class="text-lg font-semibold">当前账号:</span>
<span class="text-primary mx-4 text-lg">
{{ userStore.userRoles?.[0] }}
</span>
</div>
<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>
</div>
</div>
</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,7 @@
<script lang="ts" setup>
import { Fallback } from '@vben/common-ui';
</script>
<template>
<Fallback description="用于徽标示例" status="coming-soon" title="徽标示例" />
</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,3 @@
<template>
<div>children</div>
</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 {
MdiGithub,
MdiGoogle,
MdiKeyboardEsc,
MdiQqchat,
MdiWechat,
SvgAvatar1Icon,
SvgAvatar2Icon,
SvgAvatar3Icon,
SvgAvatar4Icon,
SvgBellIcon,
SvgCakeIcon,
SvgCardIcon,
SvgDownloadIcon,
} from '@vben/icons';
</script>
<template>
<div class="p-5">
<div class="card-box p-5">
<h1 class="text-xl font-semibold">图标</h1>
<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>
</div>
<div class="card-box mt-5 p-5">
<div class="mb-3 text-lg font-semibold">Iconify</div>
<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>
</div>
<div class="card-box mt-5 p-5">
<div class="mb-3 text-lg font-semibold">Svg Icons</div>
<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>
</div>
</div>
</template>

View File

@@ -0,0 +1,42 @@
<script lang="ts" setup>
import type { LoginExpiredModeType } from '@vben/types';
import { preferences, updatePreferences } from '@vben/preferences';
import { Button } 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>
<div class="p-5">
<div class="card-box p-5">
<h1 class="text-xl font-semibold">登录过期演示</h1>
<div class="text-foreground/80 mt-2">
接口请求遇到401状态码时需要重新登录有两种方式
<div>1.转到登录页登录成功后跳转回原页面</div>
<div>
2.弹出重新登录弹窗登录后关闭弹窗不进行任何页面跳转刷新后调整登录页面
</div>
</div>
</div>
<div class="card-box mt-5 p-5">
<div class="mb-3 text-lg font-semibold">跳转登录页面方式</div>
<Button type="primary" @click="handleClick('page')"> 点击触发 </Button>
</div>
<div class="card-box mt-5 p-5">
<div class="mb-3 text-lg font-semibold">登录弹窗方式</div>
<Button type="primary" @click="handleClick('modal')"> 点击触发 </Button>
</div>
</div>
</template>

View File

@@ -0,0 +1,113 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { useTabs } from '@vben/hooks';
import { Input as AInput, Button } 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>
<div class="p-5">
<div class="card-box p-5">
<h1 class="text-xl font-semibold">标签页</h1>
<div class="text-foreground/80 mt-2">用于需要操作标签页的场景</div>
</div>
<div class="card-box mt-5 p-5">
<div class="text-lg font-semibold">打开/关闭标签页</div>
<div class="text-foreground/80 my-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>
</div>
<div class="card-box mt-5 p-5">
<div class="text-lg font-semibold">标签页操作</div>
<div class="text-foreground/80 my-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>
</div>
<div class="card-box mt-5 p-5">
<div class="text-lg font-semibold">动态标题</div>
<div class="text-foreground/80 my-3">
该操作不会影响页面标题仅修改Tab标题
</div>
<div class="flex flex-wrap items-center gap-3">
<AInput
v-model:value="newTabTitle"
class="w-40"
placeholder="请输入新标题"
/>
<Button type="primary" @click="() => setTabTitle(newTabTitle)">
修改
</Button>
<Button @click="reset"> 重置 </Button>
</div>
</div>
<div class="card-box mt-5 p-5">
<div class="text-lg font-semibold">最大打开数量</div>
<div class="text-foreground/80 my-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>
</div>
</div>
</template>

View File

@@ -0,0 +1,27 @@
<script lang="ts" setup>
import { computed } from 'vue';
import { useRoute } from 'vue-router';
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>
<div class="p-5">
<div class="card-box p-5">
<h1 class="text-xl font-semibold">标签详情页</h1>
<div class="text-foreground/80 mt-2">
<div>{{ index }} - 详情页内容在此</div>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,66 @@
<script lang="ts" setup>
import { useWatermark } from '@vben/hooks';
import { Button } from 'ant-design-vue';
const { destroyWatermark, updateWatermark } = useWatermark();
async function createWaterMark() {
await updateWatermark({
advancedStyle: {
colorStops: [
{
color: 'red',
offset: 0,
},
{
color: 'blue',
offset: 1,
},
],
type: 'linear',
},
content: 'hello my watermark',
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>
<div class="p-5">
<div class="card-box p-5">
<h1 class="text-xl font-semibold">水印</h1>
<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>
</div>
<div class="card-box mt-5 p-5">
<div class="mb-3 flex gap-3 text-lg font-semibold">
<Button type="primary" @click="createWaterMark()">创建水印</Button>
<Button danger @click="destroyWatermark">移除水印</Button>
</div>
</div>
</div>
</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>

View File

@@ -0,0 +1,2 @@
export const longText: string =
'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 等主流技术。该项目会持续跟进最新技术,并将其应用在项目中。';

View File

@@ -0,0 +1,42 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { EllipsisText } from '@vben/common-ui';
import { Collapse, CollapsePanel } from 'ant-design-vue';
import { longText } from './data';
const text = ref(longText);
const activeKey = ref(['1', '2', '3', '4']);
</script>
<template>
<div class="card-box p-5">
<h1 class="mb-5 text-xl font-semibold">文本省略示例</h1>
<div>
<Collapse v-model:activeKey="activeKey">
<CollapsePanel key="1" header="Ellipsis 基本使用">
<EllipsisText :max-width="240">{{ text }}</EllipsisText>
</CollapsePanel>
<CollapsePanel key="2" header="Ellipsis 多行省略">
<EllipsisText :line="2">{{ text }}</EllipsisText>
</CollapsePanel>
<CollapsePanel key="3" header="Ellipsis 点击展开">
<EllipsisText :line="3" expand>{{ text }}</EllipsisText>
</CollapsePanel>
<CollapsePanel key="4" header="Ellipsis 定制 Tooltip 内容">
<EllipsisText :max-width="240">
住在我心里孤独的 孤独的海怪 痛苦之王 开始厌倦 深海的光 停滞的海浪
<template #tooltip>
<div style="text-align: center">
秦皇岛<br />住在我心里孤独的<br />孤独的海怪 痛苦之王<br />开始厌倦
深海的光 停滞的海浪
</div>
</template>
</EllipsisText>
</CollapsePanel>
</Collapse>
</div>
</div>
</template>