feat: Feature/pro docs (#70)

* chore: merge main

* feat: update docs

* feat: remove coze-assistant

* feat: add watermark plugin

* feat: update preferences

* feat: update docs

---------

Co-authored-by: vince <vince292007@gmail.com>
This commit is contained in:
Vben
2024-07-28 14:29:05 +08:00
committed by GitHub
parent 14538f7ed5
commit 376fd17a61
225 changed files with 7731 additions and 1784 deletions

View File

@@ -40,7 +40,7 @@
"@vueuse/core": "^10.11.0",
"radix-vue": "^1.9.2",
"sortablejs": "^1.15.2",
"vue": "^3.4.33"
"vue": "^3.4.34"
},
"devDependencies": {
"@types/sortablejs": "^1.15.8"

View File

@@ -31,6 +31,6 @@
"@vben-core/toolkit": "workspace:*",
"@vben-core/typings": "workspace:*",
"@vueuse/core": "^10.11.0",
"vue": "^3.4.33"
"vue": "^3.4.34"
}
}

View File

@@ -9,14 +9,15 @@ const defaultPreferences: Preferences = {
compact: false,
contentCompact: 'wide',
defaultAvatar:
'https://cdn.jsdelivr.net/npm/@vbenjs/static-source@0.1.3/source/avatar-v1.webp',
'https://unpkg.com/@vbenjs/static-source@0.1.5/source/avatar-v1.webp',
dynamicTitle: true,
enablePreferences: true,
isMobile: false,
layout: 'sidebar-nav',
locale: 'zh-CN',
loginExpiredMode: 'page',
loginExpiredMode: 'modal',
name: 'Vben Admin',
watermark: false,
},
breadcrumb: {
enable: true,
@@ -44,8 +45,7 @@ const defaultPreferences: Preferences = {
},
logo: {
enable: true,
source:
'https://cdn.jsdelivr.net/npm/@vbenjs/static-source@0.1.3/source/logo-v1.webp',
source: 'https://unpkg.com/@vbenjs/static-source@0.1.5/source/logo-v1.webp',
},
navigation: {
accordion: true,
@@ -75,6 +75,9 @@ const defaultPreferences: Preferences = {
keepAlive: true,
persist: true,
showIcon: true,
showMaximize: true,
showMore: true,
showRefresh: true,
styleType: 'chrome',
},
theme: {
@@ -94,7 +97,6 @@ const defaultPreferences: Preferences = {
progress: true,
},
widget: {
aiAssistant: true,
fullscreen: true,
globalSearch: true,
languageToggle: true,

View File

@@ -1,12 +1,4 @@
import type {
BuiltinThemeType,
SupportedLanguagesType,
} from '@vben-core/typings';
interface Language {
key: SupportedLanguagesType;
text: string;
}
import type { BuiltinThemeType } from '@vben-core/typings';
interface BuiltinThemePreset {
color: string;
@@ -15,25 +7,11 @@ interface BuiltinThemePreset {
type: BuiltinThemeType;
}
/**
* Supported languages
*/
const SUPPORT_LANGUAGES: Language[] = [
{
key: 'zh-CN',
text: '简体中文',
},
{
key: 'en-US',
text: 'English',
},
];
const BUILT_IN_THEME_PRESETS: BuiltinThemePreset[] = [
{
color: 'hsl(231 98% 65%)',
type: 'default',
},
// {
// color: 'hsl(231 98% 65%)',
// type: 'default',
// },
{
color: 'hsl(245 82% 67%)',
type: 'violet',
@@ -102,6 +80,6 @@ const BUILT_IN_THEME_PRESETS: BuiltinThemePreset[] = [
export const COLOR_PRESETS = [...BUILT_IN_THEME_PRESETS].slice(0, 7);
export { BUILT_IN_THEME_PRESETS, SUPPORT_LANGUAGES };
export { BUILT_IN_THEME_PRESETS };
export type { BuiltinThemePreset };

View File

@@ -24,17 +24,6 @@ describe('preferences', () => {
preferenceManager = new PreferenceManager();
});
it('initPreferences should initialize preferences with overrides and namespace', async () => {
const overrides = { theme: { colorPrimary: 'hsl(231 98% 65%)' } };
const namespace = 'testNamespace';
await preferenceManager.initPreferences({ namespace, overrides });
expect(preferenceManager.getPreferences().theme.colorPrimary).toBe(
overrides.theme.colorPrimary,
);
});
it('loads default preferences if no saved preferences found', () => {
const preferences = preferenceManager.getPreferences();
expect(preferences).toEqual(defaultPreferences);

View File

@@ -41,7 +41,7 @@ class PreferenceManager {
this.savePreferences = useDebounceFn(
(preference: Preferences) => this._savePreferences(preference),
100,
150,
);
}

View File

@@ -10,11 +10,12 @@ import type {
LoginExpiredModeType,
NavigationStyleType,
PageTransitionType,
SupportedLanguagesType,
TabsStyleType,
ThemeModeType,
} from '@vben-core/typings';
type SupportedLanguagesType = 'en-US' | 'zh-CN';
interface AppPreferences {
/** 权限模式 */
accessMode: AccessModeType;
@@ -44,6 +45,10 @@ interface AppPreferences {
loginExpiredMode: LoginExpiredModeType;
/** 应用名 */
name: string;
/**
* @zh_CN 是否开启水印
*/
watermark: boolean;
}
interface BreadcrumbPreferences {
@@ -149,6 +154,12 @@ interface TabbarPreferences {
persist: boolean;
/** 是否开启多标签页图标 */
showIcon: boolean;
/** 显示最大化按钮 */
showMaximize: boolean;
/** 显示更多按钮 */
showMore: boolean;
/** 显示刷新按钮 */
showRefresh: boolean;
/** 标签页风格 */
styleType: TabsStyleType;
}
@@ -184,8 +195,6 @@ interface TransitionPreferences {
}
interface WidgetPreferences {
/** 是否开启vben助手部件 */
aiAssistant: boolean;
/** 是否启用全屏部件 */
fullscreen: boolean;
/** 是否启用全局搜索部件 */
@@ -249,6 +258,7 @@ export type {
PreferencesKeys,
ShortcutKeyPreferences,
SidebarPreferences,
SupportedLanguagesType,
TabbarPreferences,
ThemePreferences,
TransitionPreferences,

View File

@@ -5,7 +5,7 @@ import {
generatorColorVariables,
} from '@vben-core/toolkit';
import { BUILT_IN_THEME_PRESETS } from './constants';
import { BUILT_IN_THEME_PRESETS, type BuiltinThemePreset } from './constants';
/**
* 更新主题的 CSS 变量以及其他 CSS 变量
@@ -37,9 +37,13 @@ function updateCSSVariables(preferences: Preferences) {
}
// 获取当前的内置主题
const currentBuiltType = BUILT_IN_THEME_PRESETS.find(
(item) => item.type === builtinType,
);
const currentBuiltType = [
{
color: preferences.theme.colorPrimary,
type: 'default',
} as BuiltinThemePreset,
...BUILT_IN_THEME_PRESETS,
].find((item) => item.type === builtinType);
let builtinTypeColorPrimary: string | undefined = '';

View File

@@ -12,7 +12,7 @@ const VBEN_DOC_URL = 'https://doc.vben.pro';
* @zh_CN Vben Logo
*/
const VBEN_LOGO_URL =
'https://cdn.jsdelivr.net/npm/@vbenjs/static-source@0.1.3/source/logo-v1.webp';
'https://unpkg.com/@vbenjs/static-source@0.1.5/source/logo-v1.webp';
/**
* @zh_CN Vben Admin 首页地址

View File

@@ -85,17 +85,21 @@
/* 只有非mac下才进行调整mac下使用默认滚动条 */
html:not([data-platform='macOs']) {
*::-webkit-scrollbar {
::-webkit-scrollbar {
@apply h-[1px] w-[10px];
}
*::-webkit-scrollbar-thumb {
::-webkit-scrollbar-thumb {
@apply bg-border rounded-sm border-none;
}
*::-webkit-scrollbar-track {
::-webkit-scrollbar-track {
@apply rounded-sm border-none bg-transparent shadow-none;
}
::-webkit-scrollbar-button {
@apply hidden;
}
}
}

View File

@@ -35,7 +35,7 @@
},
"dependencies": {
"@iconify/vue": "^4.1.2",
"lucide-vue-next": "^0.414.0",
"vue": "^3.4.33"
"lucide-vue-next": "^0.416.0",
"vue": "^3.4.34"
}
}

View File

@@ -36,7 +36,7 @@
},
"dependencies": {
"@ctrl/tinycolor": "^4.1.0",
"@vue/shared": "^3.4.33",
"@vue/shared": "^3.4.34",
"clsx": "^2.1.1",
"defu": "^6.1.4",
"lodash.clonedeep": "^4.5.0",

View File

@@ -38,7 +38,7 @@
}
},
"dependencies": {
"vue": "^3.4.33",
"vue": "^3.4.34",
"vue-router": "^4.4.0"
}
}

View File

@@ -1,5 +1,3 @@
type SupportedLanguagesType = 'en-US' | 'zh-CN';
type LayoutType =
| 'full-content'
| 'header-nav'
@@ -26,7 +24,8 @@ type BuiltinThemeType =
| 'stone'
| 'violet'
| 'yellow'
| 'zinc';
| 'zinc'
| (Record<never, never> & string);
type ContentCompactType = 'compact' | 'wide';
@@ -34,20 +33,52 @@ type LayoutHeaderModeType = 'auto' | 'auto-scroll' | 'fixed' | 'static';
/**
* 登录过期模式
* 'modal' 弹窗模式 | 'page' 页面模式
* modal 弹窗模式
* page 页面模式
*/
type LoginExpiredModeType = 'modal' | 'page';
/**
* 面包屑样式
* background 背景
* normal 默认
*/
type BreadcrumbStyleType = 'background' | 'normal';
type AccessModeType = 'allow-all' | 'backend' | 'frontend';
/**
* 权限模式
* backend 后端权限模式
* frontend 前端权限模式
*/
type AccessModeType = 'backend' | 'frontend';
/**
* 导航风格
* plain 朴素
* rounded 圆润
*/
type NavigationStyleType = 'plain' | 'rounded';
/**
* 标签栏风格
* brisk 轻快
* card 卡片
* chrome 谷歌
* plain 朴素
*/
type TabsStyleType = 'brisk' | 'card' | 'chrome' | 'plain';
/**
* 页面切换动画
*/
type PageTransitionType = 'fade' | 'fade-down' | 'fade-slide' | 'fade-up';
/**
* 页面切换动画
* panel-center 居中布局
* panel-left 居左布局
* panel-right 居右布局
*/
type AuthPageLayoutType = 'panel-center' | 'panel-left' | 'panel-right';
export type {
@@ -61,7 +92,6 @@ export type {
LoginExpiredModeType,
NavigationStyleType,
PageTransitionType,
SupportedLanguagesType,
TabsStyleType,
ThemeModeType,
};

View File

@@ -28,6 +28,10 @@ interface MenuRecordBadgeRaw {
* 菜单原始对象
*/
interface MenuRecordRaw extends MenuRecordBadgeRaw {
/**
* 激活时的图标名
*/
activeIcon?: string;
/**
* 子菜单
*/

View File

@@ -3,6 +3,10 @@ import type { Router, RouteRecordRaw } from 'vue-router';
import type { Component } from 'vue';
interface RouteMeta {
/**
* 激活图标(菜单/tab
*/
activeIcon?: string;
/**
* 当前激活的菜单,有时候不想激活现有菜单,需要激活父级菜单时使用
* @default false
@@ -13,6 +17,11 @@ interface RouteMeta {
* @default false
*/
affixTab?: boolean;
/**
* 固定标签页的顺序
* @default 0
*/
affixTabOrder?: number;
/**
* 需要特定的角色标识才可以访问
* @default []
@@ -56,10 +65,6 @@ interface RouteMeta {
* @default false
*/
hideInTab?: boolean;
/**
* 路由跳转地址
*/
href?: string;
/**
* 图标(菜单/tab
*/
@@ -87,7 +92,7 @@ interface RouteMeta {
loaded?: boolean;
/**
* 标签页最大打开数量
* @default false
* @default -1
*/
maxNumOfOpenTab?: number;
/**
@@ -126,5 +131,6 @@ export type {
ComponentRecordType,
GenerateMenuAndRoutesOptions,
RouteMeta,
RouteRecordRaw,
RouteRecordStringComponent,
};

View File

@@ -42,6 +42,6 @@
"@vben-core/shadcn-ui": "workspace:*",
"@vben-core/typings": "workspace:*",
"@vueuse/core": "^10.11.0",
"vue": "^3.4.33"
"vue": "^3.4.34"
}
}

View File

@@ -43,6 +43,6 @@
"@vben-core/toolkit": "workspace:*",
"@vben-core/typings": "workspace:*",
"@vueuse/core": "^10.11.0",
"vue": "^3.4.33"
"vue": "^3.4.34"
}
}

View File

@@ -26,6 +26,10 @@ const subMenu = useSubMenuContext();
const { parentMenu, parentPaths } = useMenu();
const active = computed(() => props.path === rootMenu?.activePath);
const menuIcon = computed(() =>
active.value ? props.activeIcon || props.icon : props.icon,
);
const isTopLevelMenuItem = computed(
() => parentMenu.value?.type.name === 'Menu',
);
@@ -94,7 +98,7 @@ onBeforeUnmount(() => {
>
<template #trigger>
<div :class="[nsMenu.be('tooltip', 'trigger')]">
<VbenIcon :class="nsMenu.e('icon')" :icon="icon" fallback />
<VbenIcon :class="nsMenu.e('icon')" :icon="menuIcon" fallback />
<slot></slot>
<span v-if="collapseShowTitle" :class="nsMenu.e('name')">
<slot name="title"></slot>
@@ -109,7 +113,7 @@ onBeforeUnmount(() => {
class="right-2"
v-bind="props"
/>
<VbenIcon :class="nsMenu.e('icon')" :icon="icon" fallback />
<VbenIcon :class="nsMenu.e('icon')" :icon="menuIcon" fallback />
<slot></slot>
<slot name="title"></slot>
</div>

View File

@@ -12,7 +12,7 @@ defineOptions({
name: 'NormalMenu',
});
withDefaults(defineProps<Props>(), {
const props = withDefaults(defineProps<Props>(), {
activePath: '',
collapse: false,
menus: () => [],
@@ -25,6 +25,12 @@ const emit = defineEmits<{
}>();
const { b, e, is } = useNamespace('normal-menu');
function menuIcon(menu: MenuRecordRaw) {
return props.activePath === menu.path
? menu.activeIcon || menu.icon
: menu.icon;
}
</script>
<template>
@@ -44,7 +50,8 @@ const { b, e, is } = useNamespace('normal-menu');
@click="() => emit('select', menu)"
@mouseenter="() => emit('enter', menu)"
>
<VbenIcon :class="e('icon')" :icon="menu.icon" fallback />
<VbenIcon :class="e('icon')" :icon="menuIcon(menu)" fallback />
<span :class="e('name')" class="truncate"> {{ menu.name }}</span>
</li>
</template>

View File

@@ -172,6 +172,10 @@ function handleMouseleave(deepDispatch = false) {
}
}
const menuIcon = computed(() =>
active.value ? props.activeIcon || props.icon : props.icon,
);
const item = reactive({
active,
parentPaths,
@@ -215,7 +219,7 @@ onBeforeUnmount(() => {
<template #trigger>
<SubMenuContent
:class="is('active', active)"
:icon="icon"
:icon="menuIcon"
:is-menu-more="isSubMenuMore"
:is-top-level-menu-submenu="isTopLevelMenuSubmenu"
:level="currentLevel"
@@ -246,7 +250,7 @@ onBeforeUnmount(() => {
<template v-else>
<SubMenuContent
:class="is('active', active)"
:icon="icon"
:icon="menuIcon"
:is-menu-more="isSubMenuMore"
:is-top-level-menu-submenu="isTopLevelMenuSubmenu"
:level="currentLevel"

View File

@@ -50,6 +50,10 @@ interface MenuProps {
}
interface SubMenuProps extends MenuRecordBadgeRaw {
/**
* @zh_CN 激活图标
*/
activeIcon?: string;
/**
* @zh_CN 是否禁用
*/
@@ -65,6 +69,10 @@ interface SubMenuProps extends MenuRecordBadgeRaw {
}
interface MenuItemProps extends MenuRecordBadgeRaw {
/**
* @zh_CN 图标
*/
activeIcon?: string;
/**
* @zh_CN 是否禁用
*/

View File

@@ -31,12 +31,19 @@ const hasChildren = computed(() => {
Reflect.has(menu, 'children') && !!menu.children && menu.children.length > 0
);
});
// function menuIcon(menu: MenuRecordRaw) {
// return props.activePath === menu.path
// ? menu.activeIcon || menu.icon
// : menu.icon;
// }
</script>
<template>
<MenuItem
v-if="!hasChildren"
:key="menu.path"
:active-icon="menu.activeIcon"
:badge="menu.badge"
:badge-type="menu.badgeType"
:badge-variants="menu.badgeVariants"
@@ -48,6 +55,7 @@ const hasChildren = computed(() => {
<SubMenuComp
v-else
:key="`${menu.path}_sub`"
:active-icon="menu.activeIcon"
:icon="menu.icon"
:path="menu.path"
>

View File

@@ -48,8 +48,8 @@
"@vben-core/typings": "workspace:*",
"@vueuse/core": "^10.11.0",
"class-variance-authority": "^0.7.0",
"lucide-vue-next": "^0.414.0",
"lucide-vue-next": "^0.416.0",
"radix-vue": "^1.9.2",
"vue": "^3.4.33"
"vue": "^3.4.34"
}
}

View File

@@ -32,14 +32,14 @@ function handleItemClick(menu: IDropdownMenuItem) {
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuGroup>
<template v-for="menu in menus" :key="menu.key">
<template v-for="menu in menus" :key="menu.value">
<DropdownMenuItem
:disabled="menu.disabled"
class="data-[state=checked]:bg-accent data-[state=checked]:text-accent-foreground text-foreground/80 mb-1 cursor-pointer"
@click="handleItemClick(menu)"
>
<component :is="menu.icon" v-if="menu.icon" class="mr-2 size-4" />
{{ menu.text }}
{{ menu.label }}
</DropdownMenuItem>
<DropdownMenuSeparator v-if="menu.separator" class="bg-border" />
</template>

View File

@@ -30,18 +30,20 @@ function handleItemClick(value: string) {
<template v-for="menu in menus" :key="menu.key">
<DropdownMenuItem
:class="
menu.key === modelValue ? 'bg-accent text-accent-foreground' : ''
menu.value === modelValue
? 'bg-accent text-accent-foreground'
: ''
"
class="data-[state=checked]:bg-accent data-[state=checked]:text-accent-foreground text-foreground/80 mb-1 cursor-pointer"
@click="handleItemClick(menu.key)"
@click="handleItemClick(menu.value)"
>
<component :is="menu.icon" v-if="menu.icon" class="mr-2 size-4" />
<span
v-if="!menu.icon"
:class="menu.key === modelValue ? 'bg-foreground' : ''"
:class="menu.value === modelValue ? 'bg-foreground' : ''"
class="mr-2 size-1.5 rounded-full"
></span>
{{ menu.text }}
{{ menu.label }}
</DropdownMenuItem>
</template>
</DropdownMenuGroup>

View File

@@ -12,17 +12,17 @@ interface VbenDropdownMenuItem {
*/
icon?: Component;
/**
* @zh_CN 唯一标识
* @zh_CN 标题
*/
key: string;
label: string;
/**
* @zh_CN 是否是分割线
*/
separator?: boolean;
/**
* @zh_CN 标题
* @zh_CN 唯一标识
*/
text: string;
value: string;
}
interface DropdownMenuProps {

View File

@@ -41,6 +41,6 @@
"@vben-core/icons": "workspace:*",
"@vben-core/shadcn-ui": "workspace:*",
"@vben-core/typings": "workspace:*",
"vue": "^3.4.33"
"vue": "^3.4.34"
}
}

View File

@@ -1,2 +1,3 @@
export { default as TabsToolMore } from './tool-more.vue';
export { default as TabsToolRefresh } from './tool-refresh.vue';
export { default as TabsToolScreen } from './tool-screen.vue';

View File

@@ -0,0 +1,31 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { RotateCw } from '@vben-core/icons';
const emit = defineEmits<{ refresh: [] }>();
const loading = ref(false);
function handleClick() {
loading.value = true;
setTimeout(() => {
loading.value = false;
}, 1000);
emit('refresh');
}
</script>
<template>
<div
class="flex-center hover:bg-muted hover:text-foreground text-muted-foreground border-border h-full cursor-pointer border-l px-[9px] text-lg font-semibold"
@click="handleClick"
>
<RotateCw
:class="{
'animate-spin duration-1000': loading,
}"
class="size-4"
/>
</div>
</template>

View File

@@ -1,11 +1,28 @@
/**
* @zh_CN 登陆页面 url 地址
*/
const LOGIN_PATH = '/auth/login';
export const LOGIN_PATH = '/auth/login';
/**
* @zh_CN 默认首页地址
*/
const DEFAULT_HOME_PATH = '/analytics';
export const DEFAULT_HOME_PATH = '/analytics';
export { DEFAULT_HOME_PATH, LOGIN_PATH };
export interface LanguageOption {
label: string;
value: 'en-US' | 'zh-CN';
}
/**
* Supported languages
*/
export const SUPPORT_LANGUAGES: LanguageOption[] = [
{
label: '简体中文',
value: 'zh-CN',
},
{
label: 'English',
value: 'en-US',
},
];

View File

@@ -42,6 +42,6 @@
"@vben/stores": "workspace:*",
"@vben/types": "workspace:*",
"@vben/utils": "workspace:*",
"vue": "^3.4.33"
"vue": "^3.4.34"
}
}

View File

@@ -1,10 +1,15 @@
import type { AccessModeType, GenerateMenuAndRoutesOptions } from '@vben/types';
import type {
AccessModeType,
GenerateMenuAndRoutesOptions,
RouteRecordRaw,
} from '@vben/types';
import {
cloneDepp,
generateMenus,
generateRoutesByBackend,
generateRoutesByFrontend,
mapTree,
} from '@vben/utils';
async function generateAccessible(
@@ -38,25 +43,43 @@ async function generateRoutes(
) {
const { forbiddenComponent, roles, routes } = options;
let resultRoutes: RouteRecordRaw[] = routes;
switch (mode) {
// 允许所有路由访问,不做任何过滤处理
case 'allow-all': {
return routes;
}
case 'frontend': {
return await generateRoutesByFrontend(
resultRoutes = await generateRoutesByFrontend(
routes,
roles || [],
forbiddenComponent,
);
break;
}
case 'backend': {
return await generateRoutesByBackend(options);
}
default: {
return routes;
resultRoutes = await generateRoutesByBackend(options);
break;
}
}
/**
* 调整路由树,做以下处理:
* 1. 对未添加redirect的路由添加redirect
*/
resultRoutes = mapTree(resultRoutes, (route) => {
// 如果有redirect或者没有子路由则直接返回
if (route.redirect || !route.children || route.children.length === 0) {
return route;
}
const firstChild = route.children[0];
// 如果子路由不是以/开头,则直接返回,这种情况需要计算全部父级的path才能得出正确的path这里不做处理
if (!firstChild.path || !firstChild.path.startsWith('/')) {
return route;
}
route.redirect = firstChild.path;
return route;
});
return resultRoutes;
}
export { generateAccessible };

View File

@@ -41,6 +41,6 @@
"@vben/preferences": "workspace:*",
"@vueuse/core": "^10.11.0",
"echarts": "^5.5.1",
"vue": "^3.4.33"
"vue": "^3.4.34"
}
}

View File

@@ -46,7 +46,7 @@
"@vben/types": "workspace:*",
"@vueuse/integrations": "^10.11.0",
"qrcode": "^1.5.3",
"vue": "^3.4.33",
"vue": "^3.4.34",
"vue-router": "^4.4.0"
},
"devDependencies": {

View File

@@ -41,6 +41,9 @@
"@vben-core/hooks": "workspace:*",
"@vben/preferences": "workspace:*",
"@vben/stores": "workspace:*",
"vue-router": "^4.4.0"
"@vben/types": "workspace:*",
"vue": "^3.4.34",
"vue-router": "^4.4.0",
"watermark-js-plus": "^1.5.2"
}
}

View File

@@ -1,4 +1,7 @@
export * from './unmount-global-loading';
export * from './use-app-config';
export * from './use-content-maximize';
export * from './use-refresh';
export * from './use-tabs';
export * from './use-watermark';
export * from '@vben-core/hooks';

View File

@@ -0,0 +1,30 @@
/**
* 移除并销毁loading
* 放在这里是而不是放在 index.html 的app标签内是因为这样比较不会生硬渲染过快可能会有闪烁
* 通过先添加css动画隐藏在动画结束后在移除loading节点来改善体验
* 不好的地方是会增加一些代码量
*/
export function unmountGlobalLoading() {
// 查找全局 loading 元素
const loadingElement = document.querySelector('#__app-loading__');
if (loadingElement) {
// 添加隐藏类,触发过渡动画
loadingElement.classList.add('hidden');
// 查找所有需要移除的注入 loading 元素
const injectLoadingElements = document.querySelectorAll(
'[data-app-loading^="inject"]',
);
// 当过渡动画结束时,移除 loading 元素和所有注入的 loading 元素
loadingElement.addEventListener(
'transitionend',
() => {
loadingElement.remove(); // 移除 loading 元素
injectLoadingElements.forEach((el) => el.remove()); // 移除所有注入的 loading 元素
},
{ once: true },
); // 确保事件只触发一次
}
}

View File

@@ -0,0 +1,24 @@
import type {
ApplicationConfig,
VbenAdminProAppConfigRaw,
} from '@vben/types/global';
/**
* 由 vite-inject-app-config 注入的全局配置
*/
export function useAppConfig(
env: Record<string, any>,
isProduction: boolean,
): ApplicationConfig {
// 生产环境下,直接使用 window._VBEN_ADMIN_PRO_APP_CONF_ 全局变量
const config = isProduction
? window._VBEN_ADMIN_PRO_APP_CONF_
: (env as VbenAdminProAppConfigRaw);
const { VITE_GLOB_API_URL, VITE_GLOB_APP_TITLE } = config;
return {
apiURL: VITE_GLOB_API_URL,
appTitle: VITE_GLOB_APP_TITLE,
};
}

View File

@@ -0,0 +1,88 @@
import type { Watermark, WatermarkOptions } from 'watermark-js-plus';
import { nextTick, onUnmounted, ref, watch } from 'vue';
import { preferences } from '@vben/preferences';
const watermark = ref<Watermark>();
const cachedOptions = ref<Partial<WatermarkOptions>>({
advancedStyle: {
colorStops: [
{
color: 'gray',
offset: 0,
},
{
color: 'gray',
offset: 1,
},
],
type: 'linear',
},
// fontSize: '20px',
content: '',
contentType: 'multi-line-text',
globalAlpha: 0.25,
gridLayoutOptions: {
cols: 2,
gap: [20, 20],
matrix: [
[1, 0],
[0, 1],
],
rows: 2,
},
height: 200,
layout: 'grid',
rotate: 30,
width: 160,
});
export function useWatermark() {
async function initWatermark(options: Partial<WatermarkOptions>) {
const { Watermark } = await import('watermark-js-plus');
cachedOptions.value = {
...cachedOptions.value,
...options,
};
watermark.value = new Watermark(cachedOptions.value);
watermark.value?.create();
}
async function updateWatermark(options: Partial<WatermarkOptions>) {
if (!watermark.value || !watermark.value?.check()) {
await initWatermark(options);
} else {
await nextTick();
watermark.value?.changeOptions({
...cachedOptions.value,
...options,
});
}
}
function destroyWatermark() {
watermark.value?.destroy();
}
watch(
() => preferences.app.watermark,
(enable) => {
if (!enable) {
destroyWatermark();
}
},
);
onUnmounted(() => {
destroyWatermark();
});
return {
destroyWatermark,
updateWatermark,
watermark,
};
}

View File

@@ -1,6 +1,9 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@vben/tsconfig/library.json",
"extends": "@vben/tsconfig/web.json",
"compilerOptions": {
"types": ["vite/client", "@vben/types/global"]
},
"include": ["src"],
"exclude": ["node_modules"]
}

View File

@@ -42,6 +42,7 @@
"@vben-core/menu-ui": "workspace:*",
"@vben-core/shadcn-ui": "workspace:*",
"@vben-core/tabs-ui": "workspace:*",
"@vben/constants": "workspace:*",
"@vben/hooks": "workspace:*",
"@vben/icons": "workspace:*",
"@vben/locales": "workspace:*",
@@ -50,7 +51,7 @@
"@vben/types": "workspace:*",
"@vben/utils": "workspace:*",
"@vueuse/core": "^10.11.0",
"vue": "^3.4.33",
"vue": "^3.4.34",
"vue-router": "^4.4.0"
}
}

View File

@@ -1,5 +1,11 @@
<script lang="ts" setup>
import type { RouteLocationNormalizedLoaded } from 'vue-router';
import type {
RouteLocationNormalizedLoaded,
RouteLocationNormalizedLoadedGeneric,
} from 'vue-router';
import { type VNode } from 'vue';
import { RouterView } from 'vue-router';
import { useContentHeight } from '@vben/hooks';
import { preferences, usePreferences } from '@vben/preferences';
@@ -43,6 +49,39 @@ function getTransitionName(_route: RouteLocationNormalizedLoaded) {
// return inTabs && route.meta.loaded ? undefined : transitionName;
return transitionName;
}
/**
* 转换组件,自动添加 name
* @param component
*/
function transformComponent(
component: VNode,
route: RouteLocationNormalizedLoadedGeneric,
) {
const routeName = route.name as string;
// 如果组件没有 name则直接返回
if (!routeName) {
return component;
}
const componentName = (component.type as any).name;
// 已经设置过 name则直接返回
if (componentName) {
return component;
}
// componentName 与 routeName 一致,则直接返回
if (componentName === routeName) {
return component;
}
// 设置 name
component.type ||= {};
(component.type as any).name = routeName;
return component;
}
</script>
<template>
@@ -61,7 +100,7 @@ function getTransitionName(_route: RouteLocationNormalizedLoaded) {
:include="getCachedTabs"
>
<component
:is="Component"
:is="transformComponent(Component, route)"
v-if="renderRouteView"
v-show="!route.meta.iframeSrc"
:key="route.fullPath"

View File

@@ -1,19 +1,20 @@
<script lang="ts" setup>
import { computed } from 'vue';
import { computed, watch } from 'vue';
import { useWatermark } from '@vben/hooks';
import { $t } from '@vben/locales';
import {
preferences,
updatePreferences,
usePreferences,
} from '@vben/preferences';
import { useCoreLockStore } from '@vben/stores';
import { useCoreAccessStore, useCoreLockStore } from '@vben/stores';
import { MenuRecordRaw } from '@vben/types';
import { mapTree } from '@vben/utils';
import { VbenAdminLayout } from '@vben-core/layout-ui';
import { VbenBackTop, VbenLogo } from '@vben-core/shadcn-ui';
import { Breadcrumb, CozeAssistant, Preferences } from '../widgets';
import { Breadcrumb, Preferences } from '../widgets';
import { LayoutContent } from './content';
import { Copyright } from './copyright';
import { LayoutFooter } from './footer';
@@ -40,6 +41,8 @@ const {
layout,
sidebarCollapsed,
} = usePreferences();
const coreAccessStore = useCoreAccessStore();
const { updateWatermark } = useWatermark();
const coreLockStore = useCoreLockStore();
const headerMenuTheme = computed(() => {
@@ -127,6 +130,23 @@ function toggleSidebar() {
function clearPreferencesAndLogout() {
emit('clearPreferencesAndLogout');
}
watch(
() => preferences.app.watermark,
async (val) => {
if (val) {
// await nextTick();
updateWatermark({
content: `${preferences.app.name} 用户名: ${coreAccessStore.userInfo?.username}`,
// parent: contentRef.value,
});
}
},
{
immediate: true,
},
);
</script>
<template>
@@ -174,10 +194,6 @@ function clearPreferencesAndLogout() {
</template>
<template #floating-groups>
<CozeAssistant
v-if="preferences.widget.aiAssistant"
:is-mobile="preferences.app.isMobile"
/>
<VbenBackTop />
</template>

View File

@@ -57,7 +57,7 @@ function useMixedMenu() {
* 侧边菜单激活路径
*/
const sidebarActive = computed(() => {
return route?.meta?.activePath ?? route.path;
return (route?.meta?.activePath as string) ?? route.path;
});
/**

View File

@@ -5,7 +5,12 @@ import { useRoute } from 'vue-router';
import { useContentMaximize, useTabs } from '@vben/hooks';
import { preferences } from '@vben/preferences';
import { useCoreTabbarStore } from '@vben/stores';
import { TabsToolMore, TabsToolScreen, TabsView } from '@vben-core/tabs-ui';
import {
TabsToolMore,
TabsToolRefresh,
TabsToolScreen,
TabsView,
} from '@vben-core/tabs-ui';
import { useTabbar } from './use-tabbar';
@@ -18,7 +23,7 @@ defineProps<{ showIcon?: boolean; theme?: string }>();
const route = useRoute();
const coreTabbarStore = useCoreTabbarStore();
const { toggleMaximize } = useContentMaximize();
const { unpinTab } = useTabs();
const { refreshTab, unpinTab } = useTabs();
const {
createContextMenus,
@@ -29,7 +34,14 @@ const {
} = useTabbar();
const menus = computed(() => {
return createContextMenus(route);
const menus = createContextMenus(route);
return menus.map((item) => {
return {
...item,
label: item.text,
value: item.key,
};
});
});
// 刷新后如果不保持tab状态关闭其他tab
@@ -53,8 +65,13 @@ if (!preferences.tabbar.persist) {
@update:active="handleClick"
/>
<div class="flex-center h-full">
<TabsToolMore :menus="menus" />
<TabsToolRefresh
v-if="preferences.tabbar.showRefresh"
@refresh="refreshTab"
/>
<TabsToolMore v-if="preferences.tabbar.showMore" :menus="menus" />
<TabsToolScreen
v-if="preferences.tabbar.showMaximize"
:screen="preferences.sidebar.hidden"
@change="toggleMaximize"
@update:screen="toggleMaximize"

View File

@@ -42,9 +42,9 @@ const breadcrumbs = computed((): IBreadcrumb[] => {
}
resultBreadcrumb.push({
icon: icon as string,
icon,
path: path || route.path,
title: $t((title || name) as string),
title: title ? $t((title || name) as string) : '',
// items: children.map((child) => {
// return {
// icon: child?.meta?.icon as string,

View File

@@ -1,70 +0,0 @@
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue';
import { useScriptTag } from '@vueuse/core';
interface AssistantProps {
botIcon?: string;
botId?: string;
botTitle?: string;
isMobile?: boolean;
}
const props = withDefaults(defineProps<AssistantProps>(), {
botIcon:
'https://cdn.jsdelivr.net/npm/@vbenjs/static-source@0.1.3/source/avatar-v1-transparent-bg.webp',
botId: '7374674983739621392',
botTitle: 'Vben Admin Assistant',
isMobile: false,
});
let client: any;
const wrapperEl = ref();
const { load, unload } = useScriptTag(
'https://sf-cdn.coze.com/obj/unpkg-va/flow-platform/chat-app-sdk/0.1.0-beta.4/libs/oversea/index.js',
() => {
client = new (window as any).CozeWebSDK.WebChatClient({
componentProps: {
icon: props.botIcon,
layout: props.isMobile ? 'mobile' : 'pc',
// lang: 'zh-CN',
title: props.botTitle,
},
config: {
bot_id: props.botId,
},
el: wrapperEl.value,
});
},
{
manual: true,
},
);
onMounted(() => {
load();
});
onUnmounted(() => {
unload();
client?.destroy();
});
</script>
<template>
<div ref="wrapperEl" class="coze-vben-admin-assistant"></div>
</template>
<style>
.coze-vben-admin-assistant {
position: fixed;
right: 30px;
bottom: 60px;
z-index: 1000;
img {
width: 50px !important;
height: 50px !important;
border-radius: 50%;
}
}
</style>

View File

@@ -1,6 +1,5 @@
export { default as Breadcrumb } from './breadcrumb.vue';
export { default as AuthenticationColorToggle } from './color-toggle.vue';
export { default as CozeAssistant } from './coze-assistant.vue';
export * from './global-search';
export { default as LanguageToggle } from './language-toggle.vue';
export { default as AuthenticationLayoutToggle } from './layout-toggle.vue';

View File

@@ -1,21 +1,16 @@
<script setup lang="ts">
import type { SupportedLanguagesType } from '@vben/types';
import type { SupportedLanguagesType } from '@vben/locales';
import { SUPPORT_LANGUAGES } from '@vben/constants';
import { Languages } from '@vben/icons';
import { loadLocaleMessages } from '@vben/locales';
import {
preferences,
SUPPORT_LANGUAGES,
updatePreferences,
} from '@vben/preferences';
import { preferences, updatePreferences } from '@vben/preferences';
import { VbenDropdownRadioMenu, VbenIconButton } from '@vben-core/shadcn-ui';
defineOptions({
name: 'LanguageToggle',
});
const menus = SUPPORT_LANGUAGES;
async function handleUpdate(value: string) {
const locale = value as SupportedLanguagesType;
updatePreferences({
@@ -23,7 +18,6 @@ async function handleUpdate(value: string) {
locale,
},
});
// 更改预览
await loadLocaleMessages(locale);
}
</script>
@@ -31,7 +25,7 @@ async function handleUpdate(value: string) {
<template>
<div>
<VbenDropdownRadioMenu
:menus="menus"
:menus="SUPPORT_LANGUAGES"
:model-value="preferences.app.locale"
@update:model-value="handleUpdate"
>

View File

@@ -20,18 +20,18 @@ defineOptions({
const menus = computed((): VbenDropdownMenuItem[] => [
{
icon: PanelLeft,
key: 'panel-left',
text: $t('authentication.layout.alignLeft'),
label: $t('authentication.layout.alignLeft'),
value: 'panel-left',
},
{
icon: InspectionPanel,
key: 'panel-center',
text: $t('authentication.layout.center'),
label: $t('authentication.layout.center'),
value: 'panel-center',
},
{
icon: PanelRight,
key: 'panel-right',
text: $t('authentication.layout.alignRight'),
label: $t('authentication.layout.alignRight'),
value: 'panel-right',
},
]);

View File

@@ -1,8 +1,6 @@
<script setup lang="ts">
import type { SelectOption } from '@vben/types';
import { SUPPORT_LANGUAGES } from '@vben/constants';
import { $t } from '@vben/locales';
import { SUPPORT_LANGUAGES } from '@vben/preferences';
import SelectItem from '../select-item.vue';
import SwitchItem from '../switch-item.vue';
@@ -13,18 +11,17 @@ defineOptions({
const appLocale = defineModel<string>('appLocale');
const appDynamicTitle = defineModel<boolean>('appDynamicTitle');
const localeItems: SelectOption[] = SUPPORT_LANGUAGES.map((item) => ({
label: item.text,
value: item.key,
}));
const appWatermark = defineModel<boolean>('appWatermark');
</script>
<template>
<SelectItem v-model="appLocale" :items="localeItems">
<SelectItem v-model="appLocale" :items="SUPPORT_LANGUAGES">
{{ $t('preferences.language') }}
</SelectItem>
<SwitchItem v-model="appDynamicTitle">
{{ $t('preferences.dynamicTitle') }}
</SwitchItem>
<SwitchItem v-model="appWatermark">
{{ $t('preferences.watermark') }}
</SwitchItem>
</template>

View File

@@ -18,6 +18,9 @@ const tabbarShowIcon = defineModel<boolean>('tabbarShowIcon');
const tabbarPersist = defineModel<boolean>('tabbarPersist');
const tabbarDragable = defineModel<boolean>('tabbarDragable');
const tabbarStyleType = defineModel<string>('tabbarStyleType');
const tabbarShowMore = defineModel<boolean>('tabbarShowMore');
const tabbarShowRefresh = defineModel<boolean>('tabbarShowRefresh');
const tabbarShowMaximize = defineModel<boolean>('tabbarShowMaximize');
const styleItems = computed((): SelectOption[] => [
{
@@ -44,9 +47,6 @@ const styleItems = computed((): SelectOption[] => [
<SwitchItem v-model="tabbarEnable" :disabled="disabled">
{{ $t('preferences.tabbar.enable') }}
</SwitchItem>
<SwitchItem v-model="tabbarShowIcon" :disabled="!tabbarEnable">
{{ $t('preferences.tabbar.icon') }}
</SwitchItem>
<SwitchItem v-model="tabbarPersist" :disabled="!tabbarEnable">
{{ $t('preferences.tabbar.persist') }}
</SwitchItem>
@@ -56,4 +56,16 @@ const styleItems = computed((): SelectOption[] => [
<SelectItem v-model="tabbarStyleType" :items="styleItems">
{{ $t('preferences.tabbar.styleType.title') }}
</SelectItem>
<SwitchItem v-model="tabbarShowIcon" :disabled="!tabbarEnable">
{{ $t('preferences.tabbar.icon') }}
</SwitchItem>
<SwitchItem v-model="tabbarShowRefresh" :disabled="!tabbarEnable">
{{ $t('preferences.tabbar.showMore') }}
</SwitchItem>
<SwitchItem v-model="tabbarShowMore" :disabled="!tabbarEnable">
{{ $t('preferences.tabbar.showRefresh') }}
</SwitchItem>
<SwitchItem v-model="tabbarShowMaximize" :disabled="!tabbarEnable">
{{ $t('preferences.tabbar.showMaximize') }}
</SwitchItem>
</template>

View File

@@ -12,7 +12,6 @@ const widgetFullscreen = defineModel<boolean>('widgetFullscreen');
const widgetLanguageToggle = defineModel<boolean>('widgetLanguageToggle');
const widgetNotification = defineModel<boolean>('widgetNotification');
const widgetThemeToggle = defineModel<boolean>('widgetThemeToggle');
const widgetAiAssistant = defineModel<boolean>('widgetAiAssistant');
const widgetSidebarToggle = defineModel<boolean>('widgetSidebarToggle');
const widgetLockScreen = defineModel<boolean>('widgetLockScreen');
</script>
@@ -33,9 +32,6 @@ const widgetLockScreen = defineModel<boolean>('widgetLockScreen');
<SwitchItem v-model="widgetNotification">
{{ $t('preferences.widget.notification') }}
</SwitchItem>
<SwitchItem v-model="widgetAiAssistant">
{{ $t('preferences.widget.aiAssistant') }}
</SwitchItem>
<SwitchItem v-model="widgetLockScreen">
{{ $t('preferences.widget.lockScreen') }}
</SwitchItem>

View File

@@ -8,6 +8,7 @@ import { $t } from '@vben/locales';
import {
BUILT_IN_THEME_PRESETS,
type BuiltinThemePreset,
preferences,
} from '@vben/preferences';
import { convertToHsl, TinyColor } from '@vben/utils';
@@ -25,6 +26,16 @@ const inputValue = computed(() => {
return new TinyColor(themeColorPrimary.value).toHexString();
});
const builtinThemePresets = computed(() => {
return [
{
color: preferences.theme.colorPrimary,
type: 'default',
},
...BUILT_IN_THEME_PRESETS,
];
});
function typeView(name: BuiltinThemeType) {
switch (name) {
case 'default': {
@@ -97,7 +108,7 @@ function selectColor() {
<template>
<div class="flex w-full flex-wrap justify-between">
<template v-for="theme in BUILT_IN_THEME_PRESETS" :key="theme.type">
<template v-for="theme in builtinThemePresets" :key="theme.type">
<div class="flex cursor-pointer flex-col" @click="handleSelect(theme)">
<div
:class="{

View File

@@ -1,4 +1,5 @@
<script setup lang="ts">
import type { SupportedLanguagesType } from '@vben/locales';
import type {
BreadcrumbStyleType,
BuiltinThemeType,
@@ -6,7 +7,6 @@ import type {
LayoutHeaderModeType,
LayoutType,
NavigationStyleType,
SupportedLanguagesType,
ThemeModeType,
} from '@vben/types';
import type { SegmentedItem } from '@vben-core/shadcn-ui';
@@ -61,6 +61,7 @@ const appLayout = defineModel<LayoutType>('appLayout');
const appColorGrayMode = defineModel<boolean>('appColorGrayMode');
const appColorWeakMode = defineModel<boolean>('appColorWeakMode');
const appContentCompact = defineModel<ContentCompactType>('appContentCompact');
const appWatermark = defineModel<boolean>('appWatermark');
const transitionProgress = defineModel<boolean>('transitionProgress');
const transitionName = defineModel<string>('transitionName');
@@ -93,6 +94,9 @@ const breadcrumbHideOnlyOne = defineModel<boolean>('breadcrumbHideOnlyOne');
const tabbarEnable = defineModel<boolean>('tabbarEnable');
const tabbarShowIcon = defineModel<boolean>('tabbarShowIcon');
const tabbarShowMore = defineModel<boolean>('tabbarShowMore');
const tabbarShowRefresh = defineModel<boolean>('tabbarShowRefresh');
const tabbarShowMaximize = defineModel<boolean>('tabbarShowMaximize');
const tabbarPersist = defineModel<boolean>('tabbarPersist');
const tabbarDragable = defineModel<boolean>('tabbarDragable');
const tabbarStyleType = defineModel<string>('tabbarStyleType');
@@ -136,7 +140,6 @@ const widgetFullscreen = defineModel<boolean>('widgetFullscreen');
const widgetLanguageToggle = defineModel<boolean>('widgetLanguageToggle');
const widgetNotification = defineModel<boolean>('widgetNotification');
const widgetThemeToggle = defineModel<boolean>('widgetThemeToggle');
const widgetAiAssistant = defineModel<boolean>('widgetAiAssistant');
const widgetSidebarToggle = defineModel<boolean>('widgetSidebarToggle');
const widgetLockScreen = defineModel<boolean>('widgetLockScreen');
@@ -252,6 +255,7 @@ async function handleReset() {
<General
v-model:app-dynamic-title="appDynamicTitle"
v-model:app-locale="appLocale"
v-model:app-watermark="appWatermark"
/>
</Block>
@@ -342,19 +346,20 @@ async function handleReset() {
"
/>
</Block>
<Block :title="$t('preferences.tabbar.title')">
<Tabbar
v-model:tabbar-dragable="tabbarDragable"
v-model:tabbar-enable="tabbarEnable"
v-model:tabbar-persist="tabbarPersist"
v-model:tabbar-show-icon="tabbarShowIcon"
v-model:tabbar-show-maximize="tabbarShowMaximize"
v-model:tabbar-show-more="tabbarShowMore"
v-model:tabbar-show-refresh="tabbarShowRefresh"
v-model:tabbar-style-type="tabbarStyleType"
/>
</Block>
<Block :title="$t('preferences.widget.title')">
<Widget
v-model:widget-ai-assistant="widgetAiAssistant"
v-model:widget-fullscreen="widgetFullscreen"
v-model:widget-global-search="widgetGlobalSearch"
v-model:widget-language-toggle="widgetLanguageToggle"

View File

@@ -9,7 +9,7 @@ import Preferences from './preferences-sheet.vue';
/**
* preferences 转成 vue props
* preferences.widget.aiAssistant=>widgetAiAssistant
* preferences.widget.fullscreen=>widgetFullscreen
*/
const attrs = computed(() => {
const result: Record<string, any> = {};
@@ -23,7 +23,7 @@ const attrs = computed(() => {
/**
* preferences 转成 vue listener
* preferences.widget.aiAssistant=>@update:widgetAiAssistant
* preferences.widget.fullscreen=>@update:widgetFullscreen
*/
const listen = computed(() => {
const result: Record<string, any> = {};

View File

@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@vben/tsconfig/library.json",
"extends": "@vben/tsconfig/web.json",
"include": ["src"],
"exclude": ["node_modules"]
}

View File

@@ -9,6 +9,7 @@ import type {
import type {
MakeAuthorizationFn,
MakeErrorMessageFn,
MakeRequestHeadersFn,
RequestClientOptions,
} from './types';
@@ -25,6 +26,7 @@ class RequestClient {
private instance: AxiosInstance;
private makeAuthorization: MakeAuthorizationFn | undefined;
private makeErrorMessage: MakeErrorMessageFn | undefined;
private makeRequestHeaders: MakeRequestHeadersFn | undefined;
public addRequestInterceptor: InterceptorManager['addRequestInterceptor'];
public addResponseInterceptor: InterceptorManager['addResponseInterceptor'];
@@ -45,11 +47,17 @@ class RequestClient {
// 默认超时时间
timeout: 10_000,
};
const { makeAuthorization, makeErrorMessage, ...axiosConfig } = options;
const {
makeAuthorization,
makeErrorMessage,
makeRequestHeaders,
...axiosConfig
} = options;
const requestConfig = merge(axiosConfig, defaultConfig);
this.instance = axios.create(requestConfig);
this.makeAuthorization = makeAuthorization;
this.makeRequestHeaders = makeRequestHeaders;
this.makeErrorMessage = makeErrorMessage;
// 实例化拦截器管理器
@@ -85,7 +93,7 @@ class RequestClient {
});
}
private setupAuthorizationInterceptor() {
private setupDefaultResponseInterceptor() {
this.addRequestInterceptor(
(config: InternalAxiosRequestConfig) => {
const authorization = this.makeAuthorization?.(config);
@@ -93,13 +101,19 @@ class RequestClient {
const { token } = authorization.tokenHandler?.() ?? {};
config.headers[authorization.key || 'Authorization'] = token;
}
const requestHeader = this.makeRequestHeaders?.(config);
if (requestHeader) {
for (const [key, value] of Object.entries(requestHeader)) {
config.headers[key] = value;
}
}
return config;
},
(error: any) => Promise.reject(error),
);
}
private setupDefaultResponseInterceptor() {
this.addResponseInterceptor(
(response: AxiosResponse) => {
return response;
@@ -162,15 +176,11 @@ class RequestClient {
private setupInterceptors() {
// 默认拦截器
this.setupAuthorizationInterceptor();
this.setupDefaultResponseInterceptor();
}
/**
* DELETE请求方法
* @param {string} url - 请求的URL
* @param {AxiosRequestConfig} config - 请求配置(可选)
* @returns 返回Promise
*/
public delete<T = any>(url: string, config?: AxiosRequestConfig): Promise<T> {
return this.request<T>(url, { ...config, method: 'DELETE' });
@@ -178,9 +188,6 @@ class RequestClient {
/**
* GET请求方法
* @param {string} url - 请求URL
* @param {AxiosRequestConfig} config - 请求配置,可选
* @returns {Promise<AxiosResponse<T>>} 返回Axios响应Promise
*/
public get<T = any>(url: string, config?: AxiosRequestConfig): Promise<T> {
return this.request<T>(url, { ...config, method: 'GET' });
@@ -188,10 +195,6 @@ class RequestClient {
/**
* POST请求方法
* @param {string} url - 请求URL
* @param {any} data - 请求体数据
* @param {AxiosRequestConfig} config - 请求配置,可选
* @returns {Promise<AxiosResponse<T>>} 返回Axios响应Promise
*/
public post<T = any>(
url: string,
@@ -203,10 +206,6 @@ class RequestClient {
/**
* PUT请求方法
* @param {string} url - 请求的URL
* @param {any} data - 请求体数据
* @param {AxiosRequestConfig} config - 请求配置(可选)
* @returns 返回Promise
*/
public put<T = any>(
url: string,
@@ -218,9 +217,6 @@ class RequestClient {
/**
* 通用的请求方法
* @param {string} url - 请求的URL
* @param {AxiosRequestConfig} config - 请求配置对象
* @returns {Promise<AxiosResponse<T>>} 返回Axios响应Promise
*/
public async request<T>(url: string, config: AxiosRequestConfig): Promise<T> {
try {

View File

@@ -12,10 +12,18 @@ interface MakeAuthorization {
unAuthorizedHandler?: () => Promise<void>;
}
interface MakeRequestHeaders {
'Accept-Language'?: string;
}
type MakeAuthorizationFn = (
config?: InternalAxiosRequestConfig,
) => MakeAuthorization;
type MakeRequestHeadersFn = (
config?: InternalAxiosRequestConfig,
) => MakeRequestHeaders;
type MakeErrorMessageFn = (message: string) => void;
interface RequestClientOptions extends CreateAxiosDefaults {
@@ -27,6 +35,11 @@ interface RequestClientOptions extends CreateAxiosDefaults {
* 用于生成错误消息
*/
makeErrorMessage?: MakeErrorMessageFn;
/**
* 用于生成请求头
*/
makeRequestHeaders?: MakeRequestHeadersFn;
}
interface HttpResponse<T = any> {
@@ -43,6 +56,7 @@ export type {
HttpResponse,
MakeAuthorizationFn,
MakeErrorMessageFn,
MakeRequestHeadersFn,
RequestClientOptions,
RequestContentType,
};

View File

@@ -1 +1,5 @@
// import { createIconifyIcon } from '@vben-core/icons';
export * from '@vben-core/icons';
// export const MdiKeyboardEsc = createIconifyIcon('mdi:keyboard-esc');

View File

@@ -1,12 +1,6 @@
import { createIconifyIcon } from '@vben-core/icons';
import { loadSvgIcons } from './load';
let loaded = false;
if (!loaded) {
loadSvgIcons();
loaded = true;
}
import './load';
const SvgAvatar1Icon = createIconifyIcon('svg:avatar-1');
const SvgAvatar2Icon = createIconifyIcon('svg:avatar-2');

View File

@@ -3,6 +3,12 @@ import {
// addCollection
} from '@vben-core/icons';
let loaded = false;
if (!loaded) {
loadSvgIcons();
loaded = true;
}
// addCollection({
// prefix: 'mdi',
// icons: {
@@ -50,5 +56,3 @@ async function loadSvgIcons() {
}),
);
}
export { loadSvgIcons };

View File

@@ -42,8 +42,7 @@
},
"dependencies": {
"@intlify/core-base": "^9.13.1",
"@vben-core/typings": "workspace:*",
"vue": "^3.4.33",
"vue": "^3.4.34",
"vue-i18n": "^9.13.1"
}
}

View File

@@ -1,7 +1,6 @@
import type { SupportedLanguagesType } from '@vben-core/typings';
import type { Locale } from 'vue-i18n';
import type { ImportLocaleFn } from './typing';
import type { ImportLocaleFn, SupportedLanguagesType } from './typing';
import { unref } from 'vue';
import { createI18n } from 'vue-i18n';
@@ -15,10 +14,7 @@ const i18n = createI18n({
messages: {},
});
const modules = {
'./langs/en-US.json': async () => import('./langs/en-US.json'),
'./langs/zh-CN.json': async () => import('./langs/zh-CN.json'),
};
const modules = import.meta.glob('./langs/*.json');
const localesMap = loadLocalesMap(modules);

View File

@@ -39,5 +39,5 @@ async function setupI18n(app: App, options: LocaleSetupOptions = {}) {
export { $t, i18n, loadLocaleMessages, loadLocalesMap, setupI18n };
export type { CompileError } from '@intlify/core-base';
export { useI18n } from 'vue-i18n';
export type { ImportLocaleFn };
export type { ImportLocaleFn, LocaleSetupOptions, SupportedLanguagesType };
export type { Locale } from 'vue-i18n';

View File

@@ -165,6 +165,7 @@
"general": "General",
"language": "Language",
"dynamicTitle": "Dynamic Title",
"watermark": "Watermark",
"sidebar": {
"title": "Sidebar",
"width": "Width",
@@ -176,6 +177,9 @@
"title": "Tabbar",
"enable": "Enable Tab Bar",
"icon": "Show Tabbar Icon",
"showMore": "Show More Button",
"showRefresh": "Show Refresh Button",
"showMaximize": "Show Maximize Button",
"persist": "Persist Tabs",
"dragable": "Enable Dragable Sort",
"styleType": {
@@ -285,7 +289,6 @@
"languageToggle": "Enable Language Toggle",
"notification": "Enable Notification",
"sidebarToggle": "Enable Sidebar Toggle",
"aiAssistant": "Enable AI Assistant",
"lockScreen": "Enable Lock Screen"
}
}

View File

@@ -165,6 +165,7 @@
"general": "通用",
"language": "语言",
"dynamicTitle": "动态标题",
"watermark": "水印",
"sidebar": {
"title": "侧边栏",
"width": "宽度",
@@ -176,6 +177,9 @@
"title": "标签栏",
"enable": "启用标签栏",
"icon": "显示标签栏图标",
"showMore": "显示更多按钮",
"showRefresh": "显示刷新按钮",
"showMaximize": "显示最大化按钮",
"persist": "持久化标签页",
"dragable": "启动拖拽排序",
"styleType": {
@@ -285,7 +289,6 @@
"languageToggle": "启用语言切换",
"notification": "启用通知",
"sidebarToggle": "启用侧边栏切换",
"aiAssistant": "启用 AI 助手",
"lockScreen": "启用锁屏"
}
}

View File

@@ -1,12 +1,12 @@
import type { SupportedLanguagesType } from '@vben-core/typings';
export type SupportedLanguagesType = 'en-US' | 'zh-CN';
type ImportLocaleFn = () => Promise<{ default: Record<string, string> }>;
export type ImportLocaleFn = () => Promise<{ default: Record<string, string> }>;
type LoadMessageFn = (
export type LoadMessageFn = (
lang: SupportedLanguagesType,
) => Promise<Record<string, string>>;
interface LocaleSetupOptions {
export interface LocaleSetupOptions {
/**
* Default language
* @default zh-CN
@@ -23,10 +23,3 @@ interface LocaleSetupOptions {
*/
missingWarn?: boolean;
}
export type {
ImportLocaleFn,
LoadMessageFn,
LocaleSetupOptions,
SupportedLanguagesType,
};

View File

@@ -42,7 +42,7 @@
"@vben-core/typings": "workspace:*",
"pinia": "2.1.7",
"pinia-plugin-persistedstate": "^3.2.1",
"vue": "^3.4.33",
"vue": "^3.4.34",
"vue-router": "^4.4.0"
}
}

View File

@@ -189,7 +189,7 @@ export const useCoreTabbarStore = defineStore('core-tabbar', {
);
if (index >= 0 && index < this.tabs.length - 1) {
const rightTabs = this.tabs.slice(index + 1, this.tabs.length);
const rightTabs = this.tabs.slice(index + 1);
const paths: string[] = [];
for (const item of rightTabs) {
@@ -399,7 +399,13 @@ export const useCoreTabbarStore = defineStore('core-tabbar', {
},
getters: {
affixTabs(): TabDefinition[] {
return this.tabs.filter((tab) => isAffixTab(tab));
const affixTabs = this.tabs.filter((tab) => isAffixTab(tab));
return affixTabs.sort((a, b) => {
const orderA = (a.meta?.affixTabOrder ?? 0) as number;
const orderB = (b.meta?.affixTabOrder ?? 0) as number;
return orderA - orderB;
});
},
getCachedTabs(): string[] {
return [...this.cachedTabs];
@@ -408,9 +414,8 @@ export const useCoreTabbarStore = defineStore('core-tabbar', {
return [...this.excludeCachedTabs];
},
getTabs(): TabDefinition[] {
const affixTabs = this.tabs.filter((tab) => isAffixTab(tab));
const normalTabs = this.tabs.filter((tab) => !isAffixTab(tab));
return [...affixTabs, ...normalTabs].filter(Boolean);
return [...this.affixTabs, ...normalTabs].filter(Boolean);
},
},
persist: [

View File

@@ -5,3 +5,19 @@ import 'vue-router';
declare module 'vue-router' {
interface RouteMeta extends IRouteMeta {}
}
export interface VbenAdminProAppConfigRaw {
VITE_GLOB_API_URL: string;
VITE_GLOB_APP_TITLE: string;
}
export interface ApplicationConfig {
apiURL: string;
appTitle: string;
}
declare global {
interface Window {
_VBEN_ADMIN_PRO_APP_CONF_: VbenAdminProAppConfigRaw;
}
}

View File

@@ -40,7 +40,7 @@
},
"dependencies": {
"@vben-core/typings": "workspace:*",
"vue": "^3.4.33",
"vue": "^3.4.34",
"vue-router": "^4.4.0"
}
}

View File

@@ -25,6 +25,7 @@ async function generateMenus(
// const path = matchRoute?.path ?? route.path;
const { meta, name: routeName, redirect, children } = route;
const {
activeIcon,
badge,
badgeType,
badgeVariants,
@@ -52,6 +53,7 @@ async function generateMenus(
// 隐藏子菜单
const resultPath = hideChildrenInMenu ? redirect || path : link || path;
return {
activeIcon,
badge,
badgeType,
badgeVariants,