This commit is contained in:
dap
2024-08-20 08:44:26 +08:00
99 changed files with 1312 additions and 785 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/design",
"version": "5.1.0",
"version": "5.1.1",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -34,7 +34,7 @@
/* Used for destructive actions such as <Button variant="destructive"> */
--destructive: 0 78% 68%;
--destructive: 359.21 68.47% 56.47%;
--destructive-foreground: 0 0% 98%;
/* Used for success actions such as <message> */
@@ -110,7 +110,7 @@
--muted-foreground: 217.9 10.6% 64.9%;
--accent: 215 27.9% 16.9%;
--accent-foreground: 210 20% 98%;
--destructive: 0 62.8% 30.6%;
--destructive: 359.21 68.47% 56.47%;
--destructive-foreground: 210 20% 98%;
--border: 215 27.9% 16.9%;
--input: 215 27.9% 16.9%;
@@ -136,7 +136,7 @@
--muted-foreground: 240 5% 64.9%;
--accent: 12 6.5% 15.1%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive: 359.21 68.47% 56.47%;
--destructive-foreground: 0 85.7% 97.3%;
--border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%;
@@ -162,7 +162,7 @@
--muted-foreground: 0 0% 63.9%;
--accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive: 359.21 68.47% 56.47%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 14.9%;
--input: 0 0% 14.9%;
@@ -188,7 +188,7 @@
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive: 359.21 68.47% 56.47%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
@@ -214,7 +214,7 @@
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive: 359.21 68.47% 56.47%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
@@ -240,7 +240,7 @@
--muted-foreground: 240 5% 64.9%;
--accent: 12 6.5% 15.1%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive: 359.21 68.47% 56.47%;
--destructive-foreground: 0 85.7% 97.3%;
--border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%;
@@ -266,7 +266,7 @@
--muted-foreground: 240 5% 64.9%;
--accent: 12 6.5% 15.1%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive: 359.21 68.47% 56.47%;
--destructive-foreground: 0 85.7% 97.3%;
--border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%;
@@ -318,7 +318,7 @@
--muted-foreground: 24 5.4% 63.9%;
--accent: 12 6.5% 15.1%;
--accent-foreground: 60 9.1% 97.8%;
--destructive: 0 62.8% 30.6%;
--destructive: 359.21 68.47% 56.47%;
--destructive-foreground: 60 9.1% 97.8%;
--border: 12 6.5% 15.1%;
--input: 12 6.5% 15.1%;
@@ -344,7 +344,7 @@
--muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive: 359.21 68.47% 56.47%;
--destructive-foreground: 0 0% 98%;
--border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%;
@@ -370,7 +370,7 @@
--muted-foreground: 0 0% 63.9%;
--accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive: 359.21 68.47% 56.47%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 14.9%;
--input: 0 0% 14.9%;
@@ -396,7 +396,7 @@
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive: 359.21 68.47% 56.47%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
@@ -422,7 +422,7 @@
--muted-foreground: 217.9 10.6% 64.9%;
--accent: 215 27.9% 16.9%;
--accent-foreground: 210 20% 98%;
--destructive: 0 62.8% 30.6%;
--destructive: 359.21 68.47% 56.47%;
--destructive-foreground: 210 20% 98%;
--border: 215 27.9% 16.9%;
--input: 215 27.9% 16.9%;

View File

@@ -33,7 +33,7 @@
/* Used for destructive actions such as <Button variant="destructive"> */
--destructive: 0 78% 68%;
--destructive: 359.33 100% 65.1%;
--destructive-foreground: 0 0% 98%;
/* Used for success actions such as <message> */

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/icons",
"version": "5.1.0",
"version": "5.1.1",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/shared",
"version": "5.1.0",
"version": "5.1.1",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -5,6 +5,7 @@ export * from './inference';
export * from './letter';
export * from './merge';
export * from './nprogress';
export * from './to';
export * from './tree';
export * from './unique';
export * from './update-css-variables';

View File

@@ -0,0 +1,21 @@
/**
* @param { Readonly<Promise> } promise
* @param {object=} errorExt - Additional Information you can pass to the err object
* @return { Promise }
*/
export async function to<T, U = Error>(
promise: Readonly<Promise<T>>,
errorExt?: object,
): Promise<[null, T] | [U, undefined]> {
try {
const data = await promise;
const result: [null, T] = [null, data];
return result;
} catch (error) {
if (errorExt) {
const parsedError = Object.assign({}, error, errorExt);
return [parsedError as U, undefined];
}
return [error as U, undefined];
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/typings",
"version": "5.1.0",
"version": "5.1.1",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/composables",
"version": "5.1.0",
"version": "5.1.1",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/preferences",
"version": "5.1.0",
"version": "5.1.1",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -14,6 +14,7 @@ const defaultPreferences: Preferences = {
dynamicTitle: true,
enableCheckUpdates: true,
enablePreferences: true,
enableRefreshToken: false,
isMobile: false,
layout: 'sidebar-nav',
locale: 'zh-CN',

View File

@@ -40,6 +40,10 @@ interface AppPreferences {
enableCheckUpdates: boolean;
/** 是否显示偏好设置 */
enablePreferences: boolean;
/**
* @zh_CN 是否开启refreshToken
*/
enableRefreshToken: boolean;
/** 是否移动端 */
isMobile: boolean;
/** 布局方式 */

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/layout-ui",
"version": "5.1.0",
"version": "5.1.1",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -2,9 +2,6 @@
import type { CSSProperties } from 'vue';
import { computed, useSlots } from 'vue';
import { Menu } from '@vben-core/icons';
import { VbenIconButton } from '@vben-core/shadcn-ui';
interface Props {
/**
* 横屏
@@ -14,11 +11,6 @@ interface Props {
* 高度
*/
height: number;
/**
* 是否混合导航
* @default false
*/
isMixedNav: boolean;
/**
* 是否移动端
*/
@@ -27,11 +19,6 @@ interface Props {
* 是否显示
*/
show: boolean;
/**
* 是否显示关闭菜单按钮
*/
showToggleBtn: boolean;
/**
* 侧边菜单宽度
*/
@@ -52,8 +39,6 @@ interface Props {
const props = withDefaults(defineProps<Props>(), {});
const emit = defineEmits<{ openMenu: []; toggleSidebar: [] }>();
const slots = useSlots();
const style = computed((): CSSProperties => {
@@ -72,10 +57,6 @@ const logoStyle = computed((): CSSProperties => {
minWidth: `${props.isMobile ? 40 : props.sidebarWidth}px`,
};
});
function handleToggleMenu() {
props.isMobile ? emit('openMenu') : emit('toggleSidebar');
}
</script>
<template>
@@ -87,13 +68,9 @@ function handleToggleMenu() {
<div v-if="slots.logo" :style="logoStyle">
<slot name="logo"></slot>
</div>
<VbenIconButton
v-if="showToggleBtn || isMobile"
class="my-0 ml-2 mr-1 rounded-md"
@click="handleToggleMenu"
>
<Menu class="size-4" />
</VbenIconButton>
<slot name="toggle-button"> </slot>
<slot></slot>
</header>
</template>

View File

@@ -4,6 +4,9 @@ import type { VbenLayoutProps } from './vben-layout';
import type { CSSProperties } from 'vue';
import { computed, ref, watch } from 'vue';
import { Menu } from '@vben-core/icons';
import { VbenIconButton } from '@vben-core/shadcn-ui';
import { useMouse, useScroll, useThrottleFn } from '@vueuse/core';
import {
@@ -330,11 +333,12 @@ const maskStyle = computed((): CSSProperties => {
const showHeaderToggleButton = computed(() => {
return (
props.headerToggleSidebarButton &&
isSideMode.value &&
!isSidebarMixedNav.value &&
!isMixedNav.value &&
!props.isMobile
props.isMobile ||
(props.headerToggleSidebarButton &&
isSideMode.value &&
!isSidebarMixedNav.value &&
!isMixedNav.value &&
!props.isMobile)
);
});
@@ -421,8 +425,12 @@ function handleClickMask() {
sidebarCollapse.value = true;
}
function handleOpenMenu() {
sidebarCollapse.value = false;
function handleHeaderToggle() {
if (props.isMobile) {
sidebarCollapse.value = false;
} else {
emit('toggleSidebar');
}
}
</script>
@@ -473,27 +481,36 @@ function handleOpenMenu() {
class="flex flex-1 flex-col overflow-hidden transition-all duration-300 ease-in"
>
<div
:class="{
'shadow-[0_16px_24px_hsl(var(--background))]': scrollY > 20,
}"
:style="headerWrapperStyle"
class="overflow-hidden shadow-[0_16px_24px_hsl(var(--background))] transition-all duration-200"
class="overflow-hidden transition-all duration-200"
>
<LayoutHeader
v-if="headerVisible"
:full-width="!isSideMode"
:height="headerHeight"
:is-mixed-nav="isMixedNav"
:is-mobile="isMobile"
:show="!isFullContent && !headerHidden"
:show-toggle-btn="showHeaderToggleButton"
:sidebar-width="sidebarWidth"
:theme="headerTheme"
:width="mainStyle.width"
:z-index="headerZIndex"
@open-menu="handleOpenMenu"
@toggle-sidebar="() => emit('toggleSidebar')"
>
<template v-if="showHeaderLogo" #logo>
<slot name="logo"></slot>
</template>
<template #toggle-button>
<VbenIconButton
v-if="showHeaderToggleButton"
class="my-0 ml-2 mr-1 rounded-md"
@click="handleHeaderToggle"
>
<Menu class="size-4" />
</VbenIconButton>
</template>
<slot name="header"></slot>
</LayoutHeader>

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/menu-ui",
"version": "5.1.0",
"version": "5.1.1",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,5 +1,5 @@
<script lang="ts" setup>
import type { MenuItemProps, MenuItemRegistered } from '../interface';
import type { MenuItemProps, MenuItemRegistered } from '../types';
import { computed, onBeforeUnmount, onMounted, reactive, useSlots } from 'vue';

View File

@@ -6,7 +6,7 @@ import type {
MenuItemRegistered,
MenuProps,
MenuProvider,
} from '../interface';
} from '../types';
import {
computed,

View File

@@ -1,5 +1,5 @@
<script lang="ts" setup>
import type { MenuItemProps } from '../interface';
import type { MenuItemProps } from '../types';
import { computed } from 'vue';

View File

@@ -1,11 +1,7 @@
<script lang="ts" setup>
import type { HoverCardContentProps } from '@vben-core/shadcn-ui';
import type {
MenuItemRegistered,
MenuProvider,
SubMenuProps,
} from '../interface';
import type { MenuItemRegistered, MenuProvider, SubMenuProps } from '../types';
import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue';
@@ -74,7 +70,6 @@ const contentProps = computed((): HoverCardContentProps => {
collisionPadding: { top: 20 },
side,
sideOffset: isHorizontal ? 5 : 10,
// sideOffset: 10,
};
});
@@ -216,7 +211,7 @@ onBeforeUnmount(() => {
]"
:content-props="contentProps"
:open="true"
:open-delay="30"
:open-delay="0"
>
<template #trigger>
<SubMenuContent

View File

@@ -1,4 +1,4 @@
import type { MenuProvider, SubMenuProvider } from '../interface';
import type { MenuProvider, SubMenuProvider } from '../types';
import { getCurrentInstance, inject, provide } from 'vue';

View File

@@ -1,4 +1,4 @@
import type { SubMenuProvider } from '../interface';
import type { SubMenuProvider } from '../types';
import { computed, getCurrentInstance } from 'vue';

View File

@@ -1,3 +1,3 @@
export * from './components/normal-menu';
export type * from './interface';
export { default as Menu } from './menu.vue';
export type * from './types';

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import type { MenuRecordRaw } from '@vben-core/typings';
import type { MenuProps } from './interface';
import type { MenuProps } from './types';
import { useForwardProps } from '@vben-core/composables';

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/shadcn-ui",
"version": "5.1.0",
"version": "5.1.1",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/tabs-ui",
"version": "5.1.0",
"version": "5.1.1",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -121,7 +121,7 @@ const tabsView = computed((): TabConfig[] => {
/>
<Pin
v-show="tab.affixTab && tabsView.length > 1 && tab.closable"
class="hover:text-accent-foreground text-accent-foreground/80 group-[.is-active]:text-accent-foreground mt-[2px] size-3.5 cursor-pointer rounded-full transition-all"
class="hover:text-accent-foreground text-accent-foreground/80 group-[.is-active]:text-accent-foreground mt-[1px] size-3.5 cursor-pointer rounded-full transition-all"
@click.stop="() => emit('unpin', tab)"
/>
</div>
@@ -150,18 +150,6 @@ const tabsView = computed((): TabConfig[] => {
<style scoped>
.tabs-chrome {
/* .dragging { */
/* .tabs-chrome__item-main {
@apply pr-0;
} */
/* .tabs-chrome__extra {
@apply hidden;
} */
/* } */
&__item:not(.dragging) {
@apply cursor-pointer;

View File

@@ -100,7 +100,7 @@ const tabsView = computed((): TabConfig[] => {
/>
<Pin
v-show="tab.affixTab && tabsView.length > 1 && tab.closable"
class="hover:bg-accent hover:stroke-accent-foreground group-[.is-active]:text-primary dark:group-[.is-active]:text-accent-foreground mt-[2px] size-3.5 cursor-pointer rounded-full transition-all"
class="hover:bg-accent hover:stroke-accent-foreground group-[.is-active]:text-primary dark:group-[.is-active]:text-accent-foreground mt-[1px] size-3.5 cursor-pointer rounded-full transition-all"
@click.stop="() => emit('unpin', tab)"
/>
</div>

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/constants",
"version": "5.1.0",
"version": "5.1.1",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/access",
"version": "5.1.0",
"version": "5.1.1",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/chart-ui",
"version": "5.1.0",
"version": "5.1.1",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/common-ui",
"version": "5.1.0",
"version": "5.1.1",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/hooks",
"version": "5.1.0",
"version": "5.1.1",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -18,6 +18,7 @@ export function useAntdDesignTokens() {
colorBgLayout: '',
colorBgMask: '',
colorBorder: '',
colorBorderSecondary: '',
colorError: '',
colorInfo: '',
colorPrimary: '',
@@ -48,7 +49,8 @@ export function useAntdDesignTokens() {
getCssVariableValue('--primary-foreground');
tokens.colorBorder = getCssVariableValue('--border');
tokens.colorBorderSecondary = tokens.colorBorder =
getCssVariableValue('--border');
tokens.colorBgElevated = getCssVariableValue('--popover');

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/layouts",
"version": "5.1.0",
"version": "5.1.1",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/request",
"version": "5.1.0",
"version": "5.1.1",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,2 +1,3 @@
export * from './preset-interceptors';
export * from './request-client';
export type * from './types';

View File

@@ -1,10 +1,19 @@
import type {
AxiosInstance,
AxiosResponse,
InternalAxiosRequestConfig,
} from 'axios';
import type { AxiosInstance, AxiosResponse } from 'axios';
const errorHandler = (res: Error) => Promise.reject(res);
import type {
RequestInterceptorConfig,
ResponseInterceptorConfig,
} from '../types';
const defaultRequestInterceptorConfig: RequestInterceptorConfig = {
fulfilled: (response) => response,
rejected: (error) => Promise.reject(error),
};
const defaultResponseInterceptorConfig: ResponseInterceptorConfig = {
fulfilled: (response: AxiosResponse) => response,
rejected: (error) => Promise.reject(error),
};
class InterceptorManager {
private axiosInstance: AxiosInstance;
@@ -13,28 +22,18 @@ class InterceptorManager {
this.axiosInstance = instance;
}
addRequestInterceptor(
fulfilled: (
config: InternalAxiosRequestConfig,
) => InternalAxiosRequestConfig | Promise<InternalAxiosRequestConfig>,
rejected?: (error: any) => any,
) {
this.axiosInstance.interceptors.request.use(
fulfilled,
rejected || errorHandler,
);
addRequestInterceptor({
fulfilled,
rejected,
}: RequestInterceptorConfig = defaultRequestInterceptorConfig) {
this.axiosInstance.interceptors.request.use(fulfilled, rejected);
}
addResponseInterceptor<T = any>(
fulfilled: (
response: AxiosResponse<T>,
) => AxiosResponse | Promise<AxiosResponse>,
rejected?: (error: any) => any,
) {
this.axiosInstance.interceptors.response.use(
fulfilled,
rejected || errorHandler,
);
addResponseInterceptor<T = any>({
fulfilled,
rejected,
}: ResponseInterceptorConfig<T> = defaultResponseInterceptorConfig) {
this.axiosInstance.interceptors.response.use(fulfilled, rejected);
}
}

View File

@@ -0,0 +1,124 @@
import type { RequestClient } from './request-client';
import type { MakeErrorMessageFn, ResponseInterceptorConfig } from './types';
import { $t } from '@vben/locales';
import axios from 'axios';
export const authenticateResponseInterceptor = ({
client,
doReAuthenticate,
doRefreshToken,
enableRefreshToken,
formatToken,
}: {
client: RequestClient;
doReAuthenticate: () => Promise<void>;
doRefreshToken: () => Promise<string>;
enableRefreshToken: boolean;
formatToken: (token: string) => null | string;
}): ResponseInterceptorConfig => {
return {
rejected: async (error) => {
const { config, response } = error;
// 如果不是 401 错误,直接抛出异常
if (response?.status !== 401) {
throw error;
}
// 判断是否启用了 refreshToken 功能
// 如果没有启用或者已经是重试请求了,直接跳转到重新登录
if (!enableRefreshToken || config.__isRetryRequest) {
await doReAuthenticate();
throw error;
}
// 如果正在刷新 token则将请求加入队列等待刷新完成
if (client.isRefreshing) {
return new Promise((resolve) => {
client.refreshTokenQueue.push((newToken: string) => {
config.headers.Authorization = formatToken(newToken);
resolve(client.request(config.url, { ...config }));
});
});
}
// 标记开始刷新 token
client.isRefreshing = true;
// 标记当前请求为重试请求,避免无限循环
config.__isRetryRequest = true;
try {
const newToken = await doRefreshToken();
// 处理队列中的请求
client.refreshTokenQueue.forEach((callback) => callback(newToken));
// 清空队列
client.refreshTokenQueue = [];
return client.request(error.config.url, { ...error.config });
} catch (refreshError) {
// 如果刷新 token 失败,处理错误(如强制登出或跳转登录页面)
client.refreshTokenQueue.forEach((callback) => callback(''));
client.refreshTokenQueue = [];
console.error('Refresh token failed, please login again.');
throw refreshError;
} finally {
client.isRefreshing = false;
}
},
};
};
export const errorMessageResponseInterceptor = (
makeErrorMessage?: MakeErrorMessageFn,
): ResponseInterceptorConfig => {
return {
rejected: (error: any) => {
if (axios.isCancel(error)) {
return Promise.reject(error);
}
const err: string = error?.toString?.() ?? '';
let errMsg = '';
if (err?.includes('Network Error')) {
errMsg = $t('fallback.http.networkError');
} else if (error?.message?.includes?.('timeout')) {
errMsg = $t('fallback.http.requestTimeout');
}
if (errMsg) {
makeErrorMessage?.(errMsg);
return Promise.reject(error);
}
let errorMessage = error?.response?.data?.error?.message ?? '';
const status = error?.response?.status;
switch (status) {
case 400: {
errorMessage = $t('fallback.http.badRequest');
break;
}
case 401: {
errorMessage = $t('fallback.http.unauthorized');
break;
}
case 403: {
errorMessage = $t('fallback.http.forbidden');
break;
}
case 404: {
errorMessage = $t('fallback.http.notFound');
break;
}
case 408: {
errorMessage = $t('fallback.http.requestTimeout');
break;
}
default: {
errorMessage = $t('fallback.http.internalServerError');
}
}
makeErrorMessage?.(errorMessage);
return Promise.reject(error);
},
};
};

View File

@@ -3,17 +3,8 @@ import type {
AxiosRequestConfig,
AxiosResponse,
CreateAxiosDefaults,
InternalAxiosRequestConfig,
} from 'axios';
import type {
MakeAuthorizationFn,
MakeErrorMessageFn,
MakeRequestHeadersFn,
RequestClientOptions,
} from './types';
import { $t } from '@vben/locales';
import { merge } from '@vben/utils';
import axios from 'axios';
@@ -21,16 +12,19 @@ import axios from 'axios';
import { FileDownloader } from './modules/downloader';
import { InterceptorManager } from './modules/interceptor';
import { FileUploader } from './modules/uploader';
import { type RequestClientOptions } from './types';
class RequestClient {
private instance: AxiosInstance;
private makeAuthorization: MakeAuthorizationFn | undefined;
private makeErrorMessage: MakeErrorMessageFn | undefined;
private makeRequestHeaders: MakeRequestHeadersFn | undefined;
private readonly instance: AxiosInstance;
public addRequestInterceptor: InterceptorManager['addRequestInterceptor'];
public addResponseInterceptor: InterceptorManager['addResponseInterceptor'];
public download: FileDownloader['download'];
// 是否正在刷新token
public isRefreshing = false;
// 刷新token队列
public refreshTokenQueue: ((token: string) => void)[] = [];
public upload: FileUploader['upload'];
/**
@@ -38,7 +32,6 @@ class RequestClient {
* @param options - Axios请求配置可选
*/
constructor(options: RequestClientOptions = {}) {
this.bindMethods();
// 合并默认配置和传入的配置
const defaultConfig: CreateAxiosDefaults = {
headers: {
@@ -47,18 +40,11 @@ class RequestClient {
// 默认超时时间
timeout: 10_000,
};
const {
makeAuthorization,
makeErrorMessage,
makeRequestHeaders,
...axiosConfig
} = options;
const { ...axiosConfig } = options;
const requestConfig = merge(axiosConfig, defaultConfig);
this.instance = axios.create(requestConfig);
this.makeAuthorization = makeAuthorization;
this.makeRequestHeaders = makeRequestHeaders;
this.makeErrorMessage = makeErrorMessage;
this.bindMethods();
// 实例化拦截器管理器
const interceptorManager = new InterceptorManager(this.instance);
@@ -73,9 +59,6 @@ class RequestClient {
// 实例化文件下载器
const fileDownloader = new FileDownloader(this);
this.download = fileDownloader.download.bind(fileDownloader);
// 设置默认的拦截器
this.setupInterceptors();
}
private bindMethods() {
@@ -93,93 +76,6 @@ class RequestClient {
});
}
private setupDefaultResponseInterceptor() {
this.addRequestInterceptor(
(config: InternalAxiosRequestConfig) => {
const authorization = this.makeAuthorization?.(config);
if (authorization) {
const { token } = authorization.tokenHandler?.() ?? {};
config.headers[authorization.key || 'Authorization'] =
`Bearer ${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),
);
this.addResponseInterceptor(
(response: AxiosResponse) => {
return response;
},
(error: any) => {
if (axios.isCancel(error)) {
return Promise.reject(error);
}
const err: string = error?.toString?.() ?? '';
let errMsg = '';
if (err?.includes('Network Error')) {
errMsg = $t('fallback.http.networkError');
} else if (error?.message?.includes?.('timeout')) {
errMsg = $t('fallback.http.requestTimeout');
}
if (errMsg) {
this.makeErrorMessage?.(errMsg);
return Promise.reject(error);
}
let errorMessage = error?.response?.data?.error?.message ?? '';
const status = error?.response?.status;
switch (status) {
case 400: {
errorMessage = $t('fallback.http.badRequest');
break;
}
case 401: {
errorMessage = $t('fallback.http.unauthorized');
this.makeAuthorization?.().unAuthorizedHandler?.();
break;
}
case 403: {
errorMessage = $t('fallback.http.forbidden');
break;
}
// 404请求不存在
case 404: {
errorMessage = $t('fallback.http.notFound');
break;
}
case 408: {
errorMessage = $t('fallback.http.requestTimeout');
break;
}
default: {
errorMessage = $t('fallback.http.internalServerError');
}
}
this.makeErrorMessage?.(errorMessage);
return Promise.reject(error);
},
);
}
private setupInterceptors() {
// 默认拦截器
this.setupDefaultResponseInterceptor();
}
/**
* DELETE请求方法
*/

View File

@@ -1,4 +1,8 @@
import type { CreateAxiosDefaults, InternalAxiosRequestConfig } from 'axios';
import type {
AxiosResponse,
CreateAxiosDefaults,
InternalAxiosRequestConfig,
} from 'axios';
type RequestContentType =
| 'application/json;charset=utf-8'
@@ -6,42 +10,26 @@ type RequestContentType =
| 'application/x-www-form-urlencoded;charset=utf-8'
| 'multipart/form-data;charset=utf-8';
interface MakeAuthorization {
key?: string;
tokenHandler: () => { refreshToken: string; token: string } | null;
unAuthorizedHandler?: () => Promise<void>;
type RequestClientOptions = CreateAxiosDefaults;
interface RequestInterceptorConfig {
fulfilled?: (
config: InternalAxiosRequestConfig,
) =>
| InternalAxiosRequestConfig<any>
| Promise<InternalAxiosRequestConfig<any>>;
rejected?: (error: any) => any;
}
interface MakeRequestHeaders {
'Accept-Language'?: string;
interface ResponseInterceptorConfig<T = any> {
fulfilled?: (
response: AxiosResponse<T>,
) => AxiosResponse | Promise<AxiosResponse>;
rejected?: (error: any) => any;
}
type MakeAuthorizationFn = (
config?: InternalAxiosRequestConfig,
) => MakeAuthorization;
type MakeRequestHeadersFn = (
config?: InternalAxiosRequestConfig,
) => MakeRequestHeaders;
type MakeErrorMessageFn = (message: string) => void;
interface RequestClientOptions extends CreateAxiosDefaults {
/**
* 用于生成Authorization
*/
makeAuthorization?: MakeAuthorizationFn;
/**
* 用于生成错误消息
*/
makeErrorMessage?: MakeErrorMessageFn;
/**
* 用于生成请求头
*/
makeRequestHeaders?: MakeRequestHeadersFn;
}
interface HttpResponse<T = any> {
code: number;
data: T;
@@ -50,11 +38,11 @@ interface HttpResponse<T = any> {
export type {
HttpResponse,
MakeAuthorizationFn,
MakeErrorMessageFn,
MakeRequestHeadersFn,
RequestClientOptions,
RequestContentType,
RequestInterceptorConfig,
ResponseInterceptorConfig,
};
export type ErrorMessageMode = 'message' | 'modal' | 'none' | undefined;

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/icons",
"version": "5.1.0",
"version": "5.1.1",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/locales",
"version": "5.1.0",
"version": "5.1.1",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/preferences",
"version": "5.1.0",
"version": "5.1.1",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/stores",
"version": "5.1.0",
"version": "5.1.1",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -47,7 +47,7 @@ export const useTabbarStore = defineStore('core-tabbar', {
return !paths.includes(getTabPath(item));
});
this.updateCacheTab();
this.updateCacheTabs();
},
/**
* @zh_CN 关闭标签页
@@ -141,7 +141,7 @@ export const useTabbarStore = defineStore('core-tabbar', {
this.tabs.splice(tabIndex, 1, mergedTab);
}
this.updateCacheTab();
this.updateCacheTabs();
},
/**
* @zh_CN 关闭所有标签页
@@ -150,7 +150,7 @@ export const useTabbarStore = defineStore('core-tabbar', {
const newTabs = this.tabs.filter((tab) => isAffixTab(tab));
this.tabs = newTabs.length > 0 ? newTabs : [...this.tabs].splice(0, 1);
await this._goToDefaultTab(router);
this.updateCacheTab();
this.updateCacheTabs();
},
/**
* @zh_CN 关闭左侧标签页
@@ -230,7 +230,7 @@ export const useTabbarStore = defineStore('core-tabbar', {
// 关闭不是激活选项卡
if (getTabPath(currentRoute.value) !== getTabPath(tab)) {
this._close(tab);
this.updateCacheTab();
this.updateCacheTabs();
return;
}
const index = this.getTabs.findIndex(
@@ -339,7 +339,7 @@ export const useTabbarStore = defineStore('core-tabbar', {
);
if (findTab) {
findTab.meta.newTabTitle = undefined;
await this.updateCacheTab();
await this.updateCacheTabs();
}
},
@@ -367,7 +367,7 @@ export const useTabbarStore = defineStore('core-tabbar', {
if (findTab) {
findTab.meta.newTabTitle = title;
await this.updateCacheTab();
await this.updateCacheTabs();
}
},
@@ -417,7 +417,7 @@ export const useTabbarStore = defineStore('core-tabbar', {
/**
* 根据当前打开的选项卡更新缓存
*/
async updateCacheTab() {
async updateCacheTabs() {
const cacheMap = new Set<string>();
for (const tab of this.tabs) {
@@ -426,7 +426,7 @@ export const useTabbarStore = defineStore('core-tabbar', {
if (!keepAlive) {
continue;
}
tab.matched.forEach((t, i) => {
(tab.matched || []).forEach((t, i) => {
if (i > 0) {
cacheMap.add(t.name as string);
}

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/styles",
"version": "5.1.0",
"version": "5.1.1",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -6,3 +6,8 @@
overscroll-behavior: none;
color: inherit;
}
.ant-message-notice-content,
.ant-notification-notice {
@apply dark:border-border/60 dark:border;
}

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/types",
"version": "5.1.0",
"version": "5.1.1",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/utils",
"version": "5.1.0",
"version": "5.1.1",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {