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:
@@ -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,
|
||||
};
|
||||
|
Reference in New Issue
Block a user