mirror of
https://github.com/vbenjs/vue-vben-admin.git
synced 2025-08-26 16:46:19 +08:00
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:
@@ -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"
|
||||
|
@@ -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"
|
||||
}
|
||||
}
|
||||
|
@@ -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,
|
||||
|
@@ -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 };
|
||||
|
@@ -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);
|
||||
|
@@ -41,7 +41,7 @@ class PreferenceManager {
|
||||
|
||||
this.savePreferences = useDebounceFn(
|
||||
(preference: Preferences) => this._savePreferences(preference),
|
||||
100,
|
||||
150,
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -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,
|
||||
|
@@ -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 = '';
|
||||
|
||||
|
@@ -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 首页地址
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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"
|
||||
}
|
||||
}
|
||||
|
@@ -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",
|
||||
|
@@ -38,7 +38,7 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.4.33",
|
||||
"vue": "^3.4.34",
|
||||
"vue-router": "^4.4.0"
|
||||
}
|
||||
}
|
||||
|
42
packages/@core/shared/typings/src/app.d.ts
vendored
42
packages/@core/shared/typings/src/app.d.ts
vendored
@@ -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,
|
||||
};
|
||||
|
@@ -28,6 +28,10 @@ interface MenuRecordBadgeRaw {
|
||||
* 菜单原始对象
|
||||
*/
|
||||
interface MenuRecordRaw extends MenuRecordBadgeRaw {
|
||||
/**
|
||||
* 激活时的图标名
|
||||
*/
|
||||
activeIcon?: string;
|
||||
/**
|
||||
* 子菜单
|
||||
*/
|
||||
|
@@ -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,
|
||||
};
|
||||
|
@@ -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"
|
||||
}
|
||||
}
|
||||
|
@@ -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"
|
||||
}
|
||||
}
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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"
|
||||
|
@@ -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 是否禁用
|
||||
*/
|
||||
|
@@ -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"
|
||||
>
|
||||
|
@@ -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"
|
||||
}
|
||||
}
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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 {
|
||||
|
@@ -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"
|
||||
}
|
||||
}
|
||||
|
@@ -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';
|
||||
|
@@ -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>
|
@@ -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',
|
||||
},
|
||||
];
|
||||
|
@@ -42,6 +42,6 @@
|
||||
"@vben/stores": "workspace:*",
|
||||
"@vben/types": "workspace:*",
|
||||
"@vben/utils": "workspace:*",
|
||||
"vue": "^3.4.33"
|
||||
"vue": "^3.4.34"
|
||||
}
|
||||
}
|
||||
|
@@ -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 };
|
||||
|
@@ -41,6 +41,6 @@
|
||||
"@vben/preferences": "workspace:*",
|
||||
"@vueuse/core": "^10.11.0",
|
||||
"echarts": "^5.5.1",
|
||||
"vue": "^3.4.33"
|
||||
"vue": "^3.4.34"
|
||||
}
|
||||
}
|
||||
|
@@ -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": {
|
||||
|
@@ -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"
|
||||
}
|
||||
}
|
||||
|
@@ -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';
|
||||
|
30
packages/effects/hooks/src/unmount-global-loading.ts
Normal file
30
packages/effects/hooks/src/unmount-global-loading.ts
Normal 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 },
|
||||
); // 确保事件只触发一次
|
||||
}
|
||||
}
|
24
packages/effects/hooks/src/use-app-config.ts
Normal file
24
packages/effects/hooks/src/use-app-config.ts
Normal 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,
|
||||
};
|
||||
}
|
88
packages/effects/hooks/src/use-watermark.ts
Normal file
88
packages/effects/hooks/src/use-watermark.ts
Normal 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,
|
||||
};
|
||||
}
|
@@ -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"]
|
||||
}
|
||||
|
@@ -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"
|
||||
}
|
||||
}
|
||||
|
@@ -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"
|
||||
|
@@ -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>
|
||||
|
||||
|
@@ -57,7 +57,7 @@ function useMixedMenu() {
|
||||
* 侧边菜单激活路径
|
||||
*/
|
||||
const sidebarActive = computed(() => {
|
||||
return route?.meta?.activePath ?? route.path;
|
||||
return (route?.meta?.activePath as string) ?? route.path;
|
||||
});
|
||||
|
||||
/**
|
||||
|
@@ -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"
|
||||
|
@@ -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,
|
||||
|
@@ -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>
|
@@ -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';
|
||||
|
@@ -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"
|
||||
>
|
||||
|
@@ -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',
|
||||
},
|
||||
]);
|
||||
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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="{
|
||||
|
@@ -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"
|
||||
|
@@ -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> = {};
|
||||
|
@@ -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"]
|
||||
}
|
||||
|
@@ -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 {
|
||||
|
@@ -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,
|
||||
};
|
||||
|
@@ -1 +1,5 @@
|
||||
// import { createIconifyIcon } from '@vben-core/icons';
|
||||
|
||||
export * from '@vben-core/icons';
|
||||
|
||||
// export const MdiKeyboardEsc = createIconifyIcon('mdi:keyboard-esc');
|
||||
|
@@ -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');
|
||||
|
@@ -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 };
|
||||
|
@@ -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"
|
||||
}
|
||||
}
|
||||
|
@@ -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);
|
||||
|
||||
|
@@ -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';
|
||||
|
@@ -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"
|
||||
}
|
||||
}
|
||||
|
@@ -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": "启用锁屏"
|
||||
}
|
||||
}
|
||||
|
@@ -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,
|
||||
};
|
||||
|
@@ -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"
|
||||
}
|
||||
}
|
||||
|
@@ -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: [
|
||||
|
16
packages/types/global.d.ts
vendored
16
packages/types/global.d.ts
vendored
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -40,7 +40,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@vben-core/typings": "workspace:*",
|
||||
"vue": "^3.4.33",
|
||||
"vue": "^3.4.34",
|
||||
"vue-router": "^4.4.0"
|
||||
}
|
||||
}
|
||||
|
@@ -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,
|
||||
|
Reference in New Issue
Block a user