refactor(project): re-adjust the overall folder

This commit is contained in:
vince
2024-07-23 00:03:59 +08:00
parent a1a566cb2f
commit 14538f7ed5
281 changed files with 1365 additions and 1659 deletions

View File

@@ -11,7 +11,7 @@
"license": "MIT",
"type": "module",
"scripts": {
"build": "pnpm unbuild",
"#build": "pnpm unbuild",
"prepublishOnly": "npm run build"
},
"files": [
@@ -26,7 +26,8 @@
".": {
"types": "./src/index.ts",
"development": "./src/index.ts",
"default": "./dist/index.mjs"
"#default": "./dist/index.mjs",
"default": "./src/index.ts"
}
},
"publishConfig": {
@@ -37,11 +38,10 @@
}
},
"dependencies": {
"@vben-core/helpers": "workspace:*",
"@vben-core/preferences": "workspace:*",
"@vben-core/stores": "workspace:*",
"@vben-core/toolkit": "workspace:*",
"@vben-core/typings": "workspace:*",
"@vben/preferences": "workspace:*",
"@vben/stores": "workspace:*",
"@vben/types": "workspace:*",
"@vben/utils": "workspace:*",
"vue": "^3.4.33"
}
}

View File

@@ -1,14 +1,11 @@
import type {
AccessModeType,
GenerateMenuAndRoutesOptions,
} from '@vben-core/typings';
import type { AccessModeType, GenerateMenuAndRoutesOptions } from '@vben/types';
import {
cloneDepp,
generateMenus,
generateRoutesByBackend,
generateRoutesByFrontend,
} from '@vben-core/helpers';
import { cloneDepp } from '@vben-core/toolkit';
} from '@vben/utils';
async function generateAccessible(
mode: AccessModeType,

View File

@@ -1,7 +1,7 @@
import { computed } from 'vue';
import { preferences, updatePreferences } from '@vben-core/preferences';
import { useCoreAccessStore } from '@vben-core/stores';
import { preferences, updatePreferences } from '@vben/preferences';
import { useCoreAccessStore } from '@vben/stores';
function useAccess() {
const coreAccessStore = useCoreAccessStore();

View File

@@ -11,7 +11,7 @@
"license": "MIT",
"type": "module",
"scripts": {
"build": "pnpm unbuild",
"#build": "pnpm unbuild",
"prepublishOnly": "npm run build"
},
"files": [
@@ -26,7 +26,8 @@
".": {
"types": "./src/index.ts",
"development": "./src/index.ts",
"default": "./dist/index.mjs"
"#default": "./dist/index.mjs",
"default": "./src/index.ts"
}
},
"publishConfig": {
@@ -37,7 +38,7 @@
}
},
"dependencies": {
"@vben-core/preferences": "workspace:*",
"@vben/preferences": "workspace:*",
"@vueuse/core": "^10.11.0",
"echarts": "^5.5.1",
"vue": "^3.4.33"

View File

@@ -5,7 +5,7 @@ import type EchartsUI from './echarts-ui.vue';
import type { Ref } from 'vue';
import { computed, nextTick, watch } from 'vue';
import { preferences, usePreferences } from '@vben-core/preferences';
import { preferences, usePreferences } from '@vben/preferences';
import {
tryOnUnmounted,

View File

@@ -11,7 +11,7 @@
"license": "MIT",
"type": "module",
"scripts": {
"build": "pnpm unbuild",
"#build": "pnpm unbuild",
"prepublishOnly": "npm run build"
},
"files": [
@@ -26,7 +26,8 @@
".": {
"types": "./src/index.ts",
"development": "./src/index.ts",
"default": "./dist/index.mjs"
"#default": "./dist/index.mjs",
"default": "./src/index.ts"
}
},
"publishConfig": {
@@ -37,11 +38,11 @@
}
},
"dependencies": {
"@vben-core/hooks": "workspace:*",
"@vben-core/icons": "workspace:*",
"@vben-core/locales": "workspace:*",
"@vben-core/shadcn-ui": "workspace:*",
"@vben/constants": "workspace:*",
"@vben/hooks": "workspace:*",
"@vben/icons": "workspace:*",
"@vben/locales": "workspace:*",
"@vben/types": "workspace:*",
"@vueuse/integrations": "^10.11.0",
"qrcode": "^1.5.3",

View File

@@ -23,6 +23,22 @@ withDefaults(defineProps<Props>(), {
title: '关于项目',
});
declare global {
const __VBEN_ADMIN_METADATA__: {
authorEmail: string;
authorName: string;
authorUrl: string;
buildTime: string;
dependencies: Record<string, string>;
description: string;
devDependencies: Record<string, string>;
homepage: string;
license: string;
repositoryUrl: string;
version: string;
};
}
const {
authorEmail,
authorName,

View File

@@ -4,8 +4,7 @@ import type { LoginCodeEmits } from './typings';
import { computed, onBeforeUnmount, reactive, ref } from 'vue';
import { useRouter } from 'vue-router';
import { LOGIN_PATH } from '@vben/constants';
import { $t } from '@vben-core/locales';
import { $t } from '@vben/locales';
import { VbenButton, VbenInput, VbenPinInput } from '@vben-core/shadcn-ui';
import Title from './auth-title.vue';
@@ -27,7 +26,7 @@ defineOptions({
const props = withDefaults(defineProps<Props>(), {
loading: false,
loginPath: LOGIN_PATH,
loginPath: '/auth/login',
});
const emit = defineEmits<{

View File

@@ -2,8 +2,7 @@
import { computed, reactive } from 'vue';
import { useRouter } from 'vue-router';
import { LOGIN_PATH } from '@vben/constants';
import { $t } from '@vben-core/locales';
import { $t } from '@vben/locales';
import { VbenButton, VbenInput } from '@vben-core/shadcn-ui';
import Title from './auth-title.vue';
@@ -25,7 +24,7 @@ defineOptions({
const props = withDefaults(defineProps<Props>(), {
loading: false,
loginPath: LOGIN_PATH,
loginPath: '/auth/login',
});
const emit = defineEmits<{

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { useForwardPropsEmits } from '@vben-core/hooks';
import { useForwardPropsEmits } from '@vben/hooks';
import {
Dialog,
DialogContent,

View File

@@ -2,7 +2,7 @@
import { computed, reactive } from 'vue';
import { useRouter } from 'vue-router';
import { $t } from '@vben-core/locales';
import { $t } from '@vben/locales';
import {
VbenButton,
VbenCheckbox,

View File

@@ -2,8 +2,7 @@
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { LOGIN_PATH } from '@vben/constants';
import { $t } from '@vben-core/locales';
import { $t } from '@vben/locales';
import { VbenButton } from '@vben-core/shadcn-ui';
import { useQRCode } from '@vueuse/integrations/useQRCode';
@@ -27,7 +26,7 @@ defineOptions({
const props = withDefaults(defineProps<Props>(), {
loading: false,
loginPath: LOGIN_PATH,
loginPath: '/auth/login',
});
const router = useRouter();

View File

@@ -4,8 +4,7 @@ import type { RegisterEmits } from './typings';
import { computed, reactive } from 'vue';
import { useRouter } from 'vue-router';
import { LOGIN_PATH } from '@vben/constants';
import { $t } from '@vben-core/locales';
import { $t } from '@vben/locales';
import {
VbenButton,
VbenCheckbox,
@@ -32,7 +31,7 @@ defineOptions({
const props = withDefaults(defineProps<Props>(), {
loading: false,
loginPath: LOGIN_PATH,
loginPath: '/auth/login',
});
const emit = defineEmits<{

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import { MdiGithub, MdiGoogle, MdiQqchat, MdiWechat } from '@vben-core/icons';
import { $t } from '@vben-core/locales';
import { MdiGithub, MdiGoogle, MdiQqchat, MdiWechat } from '@vben/icons';
import { $t } from '@vben/locales';
import { VbenIconButton } from '@vben-core/shadcn-ui';
defineOptions({

View File

@@ -4,8 +4,8 @@ import type { FallbackProps } from './fallback';
import { computed, defineAsyncComponent } from 'vue';
import { useRouter } from 'vue-router';
import { ArrowLeft, RotateCw } from '@vben-core/icons';
import { $t } from '@vben-core/locales';
import { ArrowLeft, RotateCw } from '@vben/icons';
import { $t } from '@vben/locales';
import { VbenButton } from '@vben-core/shadcn-ui';
interface Props extends FallbackProps {}

View File

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

View File

@@ -0,0 +1,19 @@
# @vben/hooks
用于多个 `app` 公用的 hook继承了 `@vben/hooks` 的所有能力。业务上有通用 hooks 可以放在这里。
## 用法
### 添加依赖
```bash
# 进入目标应用目录,例如 apps/xxxx-app
# cd apps/xxxx-app
pnpm add @vben/hooks --workspace
```
### 使用
```ts
import { useNamespace } from '@vben/hooks';
```

View File

@@ -0,0 +1,8 @@
import { defineBuildConfig } from 'unbuild';
export default defineBuildConfig({
clean: true,
declaration: true,
entries: ['src/index'],
externals: ['vue'],
});

View File

@@ -0,0 +1,46 @@
{
"name": "@vben/hooks",
"version": "5.0.0",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {
"type": "git",
"url": "git+https://github.com/vbenjs/vue-vben-admin.git",
"directory": "packages/effects/hooks"
},
"license": "MIT",
"type": "module",
"scripts": {
"#build": "pnpm unbuild"
},
"files": [
"dist"
],
"sideEffects": [
"**/*.css"
],
"main": "./dist/index.mjs",
"module": "./dist/index.mjs",
"exports": {
".": {
"types": "./src/index.ts",
"development": "./src/index.ts",
"#default": "./dist/index.mjs",
"default": "./src/index.ts"
}
},
"publishConfig": {
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.mjs"
}
}
},
"dependencies": {
"@vben-core/hooks": "workspace:*",
"@vben/preferences": "workspace:*",
"@vben/stores": "workspace:*",
"vue-router": "^4.4.0"
}
}

View File

@@ -0,0 +1,4 @@
export * from './use-content-maximize';
export * from './use-refresh';
export * from './use-tabs';
export * from '@vben-core/hooks';

View File

@@ -0,0 +1,24 @@
import { updatePreferences, usePreferences } from '@vben/preferences';
/**
* 主体区域最大化
*/
export function useContentMaximize() {
const { contentIsMaximize } = usePreferences();
function toggleMaximize() {
const isMaximize = contentIsMaximize.value;
updatePreferences({
header: {
hidden: !isMaximize,
},
sidebar: {
hidden: !isMaximize,
},
});
}
return {
contentIsMaximize,
toggleMaximize,
};
}

View File

@@ -0,0 +1,16 @@
import { useRouter } from 'vue-router';
import { useCoreTabbarStore } from '@vben/stores';
export function useRefresh() {
const router = useRouter();
const coreTabbarStore = useCoreTabbarStore();
function refresh() {
coreTabbarStore.refresh(router);
}
return {
refresh,
};
}

View File

@@ -0,0 +1,113 @@
import { type RouteLocationNormalized, useRoute, useRouter } from 'vue-router';
import { useCoreTabbarStore } from '@vben/stores';
export function useTabs() {
const router = useRouter();
const route = useRoute();
const coreTabbarStore = useCoreTabbarStore();
async function closeLeftTabs(tab?: RouteLocationNormalized) {
await coreTabbarStore.closeLeftTabs(tab || route);
}
async function closeAllTabs() {
await coreTabbarStore.closeAllTabs(router);
}
async function closeRightTabs(tab?: RouteLocationNormalized) {
await coreTabbarStore.closeRightTabs(tab || route);
}
async function closeOtherTabs(tab?: RouteLocationNormalized) {
await coreTabbarStore.closeOtherTabs(tab || route);
}
async function closeCurrentTab(tab?: RouteLocationNormalized) {
await coreTabbarStore.closeTab(tab || route, router);
}
async function pinTab(tab?: RouteLocationNormalized) {
await coreTabbarStore.pinTab(tab || route);
}
async function unpinTab(tab?: RouteLocationNormalized) {
await coreTabbarStore.unpinTab(tab || route);
}
async function toggleTabPin(tab?: RouteLocationNormalized) {
await coreTabbarStore.toggleTabPin(tab || route);
}
async function refreshTab() {
await coreTabbarStore.refresh(router);
}
async function openTabInNewWindow(tab?: RouteLocationNormalized) {
coreTabbarStore.openTabInNewWindow(tab || route);
}
async function closeTabByKey(key: string) {
await coreTabbarStore.closeTabByKey(key, router);
}
async function setTabTitle(title: string) {
coreTabbarStore.setUpdateTime();
await coreTabbarStore.setTabTitle(route, title);
}
async function resetTabTitle() {
coreTabbarStore.setUpdateTime();
await coreTabbarStore.resetTabTitle(route);
}
/**
* 获取操作是否禁用
* @param tab
*/
function getTabDisableState(tab: RouteLocationNormalized = route) {
const tabs = coreTabbarStore.getTabs;
const affixTabs = coreTabbarStore.affixTabs;
const index = tabs.findIndex((item) => item.path === tab.path);
const disabled = tabs.length <= 1;
const { meta } = tab;
const affixTab = meta?.affixTab ?? false;
const isCurrentTab = route.path === tab.path;
// 当前处于最左侧或者减去固定标签页的数量等于0
const disabledCloseLeft =
index === 0 || index - affixTabs.length <= 0 || !isCurrentTab;
const disabledCloseRight = !isCurrentTab || index === tabs.length - 1;
const disabledCloseOther =
disabled || !isCurrentTab || tabs.length - affixTabs.length <= 1;
return {
disabledCloseAll: disabled,
disabledCloseCurrent: !!affixTab || disabled,
disabledCloseLeft,
disabledCloseOther,
disabledCloseRight,
disabledRefresh: !isCurrentTab,
};
}
return {
closeAllTabs,
closeCurrentTab,
closeLeftTabs,
closeOtherTabs,
closeRightTabs,
closeTabByKey,
getTabDisableState,
openTabInNewWindow,
pinTab,
refreshTab,
resetTabTitle,
setTabTitle,
toggleTabPin,
unpinTab,
};
}

View File

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

View File

@@ -11,7 +11,7 @@
"license": "MIT",
"type": "module",
"scripts": {
"build": "pnpm unbuild",
"#build": "pnpm unbuild",
"prepublishOnly": "npm run build"
},
"files": [
@@ -26,7 +26,8 @@
".": {
"types": "./src/index.ts",
"development": "./src/index.ts",
"default": "./dist/index.mjs"
"#default": "./dist/index.mjs",
"default": "./src/index.ts"
}
},
"publishConfig": {
@@ -37,18 +38,17 @@
}
},
"dependencies": {
"@vben-core/helpers": "workspace:*",
"@vben-core/hooks": "workspace:*",
"@vben-core/icons": "workspace:*",
"@vben-core/layout-ui": "workspace:*",
"@vben-core/locales": "workspace:*",
"@vben-core/menu-ui": "workspace:*",
"@vben-core/preferences": "workspace:*",
"@vben-core/shadcn-ui": "workspace:*",
"@vben-core/stores": "workspace:*",
"@vben-core/tabs-ui": "workspace:*",
"@vben-core/toolkit": "workspace:*",
"@vben-core/typings": "workspace:*",
"@vben/hooks": "workspace:*",
"@vben/icons": "workspace:*",
"@vben/locales": "workspace:*",
"@vben/preferences": "workspace:*",
"@vben/stores": "workspace:*",
"@vben/types": "workspace:*",
"@vben/utils": "workspace:*",
"@vueuse/core": "^10.11.0",
"vue": "^3.4.33",
"vue-router": "^4.4.0"

View File

@@ -1,8 +1,8 @@
<script setup lang="ts">
import { computed } from 'vue';
import { $t } from '@vben-core/locales';
import { preferences, usePreferences } from '@vben-core/preferences';
import { $t } from '@vben/locales';
import { preferences, usePreferences } from '@vben/preferences';
import AuthenticationFormView from './form.vue';
import SloganIcon from './icons/slogan.vue';

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { preferences } from '@vben-core/preferences';
import { preferences } from '@vben/preferences';
import { Copyright } from '../basic/copyright';
import Toolbar from './toolbar.vue';

View File

@@ -1,10 +1,10 @@
<script lang="ts" setup>
import type { RouteLocationNormalizedLoaded } from 'vue-router';
import { useContentHeight } from '@vben-core/hooks';
import { preferences, usePreferences } from '@vben-core/preferences';
import { useContentHeight } from '@vben/hooks';
import { preferences, usePreferences } from '@vben/preferences';
import { storeToRefs, useCoreTabbarStore } from '@vben/stores';
import { Spinner } from '@vben-core/shadcn-ui';
import { storeToRefs, useCoreTabbarStore } from '@vben-core/stores';
import { IFrameRouterView } from '../../iframe';
import { useContentSpinner } from './use-content-spinner';

View File

@@ -1,7 +1,7 @@
import { computed, ref } from 'vue';
import { useRouter } from 'vue-router';
import { preferences } from '@vben-core/preferences';
import { preferences } from '@vben/preferences';
function useContentSpinner() {
const spinning = ref(false);

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup>
import { preferences, usePreferences } from '@vben-core/preferences';
import { preferences, usePreferences } from '@vben/preferences';
import { useCoreAccessStore } from '@vben/stores';
import { VbenFullScreen } from '@vben-core/shadcn-ui';
import { useCoreAccessStore } from '@vben-core/stores';
import { GlobalSearch, LanguageToggle, ThemeToggle } from '../../widgets';

View File

@@ -1,16 +1,17 @@
<script lang="ts" setup>
import { computed } from 'vue';
import { VbenAdminLayout } from '@vben-core/layout-ui';
import { $t } from '@vben-core/locales';
import { $t } from '@vben/locales';
import {
preferences,
updatePreferences,
usePreferences,
} from '@vben-core/preferences';
} from '@vben/preferences';
import { 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 { mapTree } from '@vben-core/toolkit';
import { MenuRecordRaw } from '@vben-core/typings';
import { Breadcrumb, CozeAssistant, Preferences } from '../widgets';
import { LayoutContent } from './content';
@@ -39,6 +40,7 @@ const {
layout,
sidebarCollapsed,
} = usePreferences();
const coreLockStore = useCoreLockStore();
const headerMenuTheme = computed(() => {
return isDark.value ? 'dark' : 'light';
@@ -293,7 +295,7 @@ function clearPreferencesAndLogout() {
<template #extra>
<slot name="extra"></slot>
<Transition v-if="preferences.widget.lockScreen" name="slide-up">
<slot name="lock-screen"></slot>
<slot v-if="coreLockStore.isLockScreen" name="lock-screen"></slot>
</Transition>
</template>
</VbenAdminLayout>

View File

@@ -1,5 +1,5 @@
<script lang="ts" setup>
import type { MenuRecordRaw } from '@vben-core/typings';
import type { MenuRecordRaw } from '@vben/types';
import { useRoute } from 'vue-router';

View File

@@ -1,5 +1,5 @@
<script lang="ts" setup>
import type { MenuRecordRaw } from '@vben-core/typings';
import type { MenuRecordRaw } from '@vben/types';
import { Menu, MenuProps } from '@vben-core/menu-ui';

View File

@@ -1,11 +1,11 @@
<script lang="ts" setup>
import type { MenuRecordRaw } from '@vben/types';
import type { NormalMenuProps } from '@vben-core/menu-ui';
import type { MenuRecordRaw } from '@vben-core/typings';
import { onBeforeMount } from 'vue';
import { useRoute } from 'vue-router';
import { findMenuByPath } from '@vben-core/helpers';
import { findMenuByPath } from '@vben/utils';
import { NormalMenu } from '@vben-core/menu-ui';
interface Props extends NormalMenuProps {}

View File

@@ -1,11 +1,11 @@
import type { MenuRecordRaw } from '@vben-core/typings';
import type { MenuRecordRaw } from '@vben/types';
import { computed, ref, watch } from 'vue';
import { useRoute } from 'vue-router';
import { findRootMenuByPath } from '@vben-core/helpers';
import { preferences } from '@vben-core/preferences';
import { useCoreAccessStore } from '@vben-core/stores';
import { preferences } from '@vben/preferences';
import { useCoreAccessStore } from '@vben/stores';
import { findRootMenuByPath } from '@vben/utils';
import { useNavigation } from './use-navigation';

View File

@@ -1,11 +1,11 @@
import type { MenuRecordRaw } from '@vben-core/typings';
import type { MenuRecordRaw } from '@vben/types';
import { computed, onBeforeMount, ref, watch } from 'vue';
import { useRoute } from 'vue-router';
import { findRootMenuByPath } from '@vben-core/helpers';
import { preferences, usePreferences } from '@vben-core/preferences';
import { useCoreAccessStore } from '@vben-core/stores';
import { preferences, usePreferences } from '@vben/preferences';
import { useCoreAccessStore } from '@vben/stores';
import { findRootMenuByPath } from '@vben/utils';
import { useNavigation } from './use-navigation';

View File

@@ -1,6 +1,6 @@
import { useRouter } from 'vue-router';
import { isHttpUrl, openWindow } from '@vben-core/toolkit';
import { isHttpUrl, openWindow } from '@vben/utils';
function useNavigation() {
const router = useRouter();

View File

@@ -2,9 +2,9 @@
import { computed } from 'vue';
import { useRoute } from 'vue-router';
import { useContentMaximize, useTabs } from '@vben-core/hooks';
import { preferences } from '@vben-core/preferences';
import { useCoreTabbarStore } from '@vben-core/stores';
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 { useTabbar } from './use-tabbar';

View File

@@ -1,5 +1,5 @@
import type { TabDefinition } from '@vben/types';
import type { IContextMenuItem } from '@vben-core/tabs-ui';
import type { TabDefinition } from '@vben-core/typings';
import type {
RouteLocationNormalized,
RouteLocationNormalizedGeneric,
@@ -8,7 +8,7 @@ import type {
import { computed, ref, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useContentMaximize, useTabs } from '@vben-core/hooks';
import { useContentMaximize, useTabs } from '@vben/hooks';
import {
ArrowLeftToLine,
ArrowRightLeft,
@@ -21,14 +21,14 @@ import {
Minimize2,
RotateCw,
X,
} from '@vben-core/icons';
import { $t, useI18n } from '@vben-core/locales';
} from '@vben/icons';
import { $t, useI18n } from '@vben/locales';
import {
storeToRefs,
useCoreAccessStore,
useCoreTabbarStore,
} from '@vben-core/stores';
import { filterTree } from '@vben-core/toolkit';
} from '@vben/stores';
import { filterTree } from '@vben/utils';
export function useTabbar() {
const router = useRouter();

View File

@@ -4,9 +4,9 @@ import type { RouteLocationNormalized } from 'vue-router';
import { computed, ref } from 'vue';
import { useRoute } from 'vue-router';
import { preferences } from '@vben-core/preferences';
import { preferences } from '@vben/preferences';
import { useCoreTabbarStore } from '@vben/stores';
import { Spinner } from '@vben-core/shadcn-ui';
import { useCoreTabbarStore } from '@vben-core/stores';
defineOptions({ name: 'IFrameRouterView' });

View File

@@ -1,11 +1,11 @@
<script lang="ts" setup>
import type { BreadcrumbStyleType } from '@vben/types';
import type { IBreadcrumb } from '@vben-core/shadcn-ui';
import type { BreadcrumbStyleType } from '@vben-core/typings';
import { computed } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { $t } from '@vben-core/locales';
import { $t } from '@vben/locales';
import { VbenBackgroundBreadcrumb, VbenBreadcrumb } from '@vben-core/shadcn-ui';
interface Props {

View File

@@ -1,12 +1,12 @@
<script setup lang="ts">
import type { BuiltinThemeType } from '@vben-core/typings';
import type { BuiltinThemeType } from '@vben/types';
import { Palette } from '@vben-core/icons';
import { Palette } from '@vben/icons';
import {
COLOR_PRESETS,
preferences,
updatePreferences,
} from '@vben-core/preferences';
} from '@vben/preferences';
import { VbenIconButton } from '@vben-core/shadcn-ui';
defineOptions({

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import type { MenuRecordRaw } from '@vben-core/typings';
import type { MenuRecordRaw } from '@vben/types';
import { onMounted, onUnmounted, ref, watch } from 'vue';
@@ -9,8 +9,9 @@ import {
CornerDownLeft,
MdiKeyboardEsc,
Search,
} from '@vben-core/icons';
import { $t } from '@vben-core/locales';
} from '@vben/icons';
import { $t } from '@vben/locales';
import { isWindowsOs } from '@vben/utils';
import {
Dialog,
DialogContent,
@@ -20,7 +21,6 @@ import {
DialogTitle,
DialogTrigger,
} from '@vben-core/shadcn-ui';
import { isWindowsOs } from '@vben-core/toolkit';
import { useMagicKeys, useToggle, whenever } from '@vueuse/core';

View File

@@ -1,13 +1,13 @@
<script setup lang="ts">
import type { MenuRecordRaw } from '@vben-core/typings';
import type { MenuRecordRaw } from '@vben/types';
import { nextTick, onMounted, ref, shallowRef, watch } from 'vue';
import { useRouter } from 'vue-router';
import { SearchX, X } from '@vben-core/icons';
import { $t } from '@vben-core/locales';
import { SearchX, X } from '@vben/icons';
import { $t } from '@vben/locales';
import { mapTree, traverseTreeValues, uniqueByField } from '@vben/utils';
import { VbenIcon, VbenScrollbar } from '@vben-core/shadcn-ui';
import { mapTree, traverseTreeValues, uniqueByField } from '@vben-core/toolkit';
import { onKeyStroke, useLocalStorage, useThrottleFn } from '@vueuse/core';

View File

@@ -1,13 +1,13 @@
<script setup lang="ts">
import type { SupportedLanguagesType } from '@vben-core/typings';
import type { SupportedLanguagesType } from '@vben/types';
import { Languages } from '@vben-core/icons';
import { loadLocaleMessages } from '@vben-core/locales';
import { Languages } from '@vben/icons';
import { loadLocaleMessages } from '@vben/locales';
import {
SUPPORT_LANGUAGES,
preferences,
SUPPORT_LANGUAGES,
updatePreferences,
} from '@vben-core/preferences';
} from '@vben/preferences';
import { VbenDropdownRadioMenu, VbenIconButton } from '@vben-core/shadcn-ui';
defineOptions({

View File

@@ -1,16 +1,16 @@
<script setup lang="ts">
import type { AuthPageLayoutType } from '@vben/types';
import type { VbenDropdownMenuItem } from '@vben-core/shadcn-ui';
import type { AuthPageLayoutType } from '@vben-core/typings';
import { computed } from 'vue';
import { InspectionPanel, PanelLeft, PanelRight } from '@vben-core/icons';
import { $t } from '@vben-core/locales';
import { InspectionPanel, PanelLeft, PanelRight } from '@vben/icons';
import { $t } from '@vben/locales';
import {
preferences,
updatePreferences,
usePreferences,
} from '@vben-core/preferences';
} from '@vben/preferences';
import { VbenDropdownRadioMenu, VbenIconButton } from '@vben-core/shadcn-ui';
defineOptions({

View File

@@ -1,8 +1,9 @@
<script setup lang="ts">
import { computed, reactive, ref, watchEffect } from 'vue';
import { LockKeyhole } from '@vben-core/icons';
import { $t, useI18n } from '@vben-core/locales';
import { LockKeyhole } from '@vben/icons';
import { $t, useI18n } from '@vben/locales';
import { storeToRefs, useCoreLockStore } from '@vben/stores';
import {
VbenAvatar,
VbenButton,
@@ -13,21 +14,20 @@ import { useDateFormat, useNow } from '@vueuse/core';
interface Props {
avatar?: string;
cachedPassword?: string;
}
defineOptions({
name: 'LockScreen',
});
const props = withDefaults(defineProps<Props>(), {
withDefaults(defineProps<Props>(), {
avatar: '',
cachedPassword: undefined,
});
const emit = defineEmits<{ toLogin: []; unlock: [string] }>();
defineEmits<{ toLogin: [] }>();
const { locale } = useI18n();
const coreLockStore = useCoreLockStore();
const now = useNow();
const meridiem = useDateFormat(now, 'A');
@@ -37,6 +37,7 @@ const date = useDateFormat(now, 'YYYY-MM-DD dddd', { locales: locale.value });
const showUnlockForm = ref(false);
const validPass = ref(true);
const { lockScreenPassword } = storeToRefs(coreLockStore);
const formState = reactive({
password: '',
@@ -56,7 +57,7 @@ const passwordStatus = computed(() => {
});
const errorTip = computed(() => {
return props.cachedPassword === undefined || !formState.password
return lockScreenPassword?.value === undefined || !formState.password
? $t('widgets.lockScreen.placeholder')
: $t('widgets.lockScreen.errorPasswordTip');
});
@@ -72,11 +73,11 @@ function handleSubmit() {
if (passwordStatus.value !== 'default') {
return;
}
if (props.cachedPassword !== formState.password) {
if (lockScreenPassword?.value !== formState.password) {
validPass.value = false;
return;
}
emit('unlock', formState.password);
coreLockStore.unlockScreen();
}
function toggleUnlockForm() {

View File

@@ -1,8 +1,8 @@
<script lang="ts" setup>
import type { NotificationItem } from './types';
import { Bell, MailCheck } from '@vben-core/icons';
import { $t } from '@vben-core/locales';
import { Bell, MailCheck } from '@vben/icons';
import { $t } from '@vben/locales';
import {
VbenButton,
VbenIconButton,

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { $t } from '@vben-core/locales';
import { $t } from '@vben/locales';
import SwitchItem from '../switch-item.vue';

View File

@@ -1,8 +1,8 @@
<script setup lang="ts">
import type { SelectOption } from '@vben-core/typings';
import type { SelectOption } from '@vben/types';
import { $t } from '@vben-core/locales';
import { SUPPORT_LANGUAGES } from '@vben-core/preferences';
import { $t } from '@vben/locales';
import { SUPPORT_LANGUAGES } from '@vben/preferences';
import SelectItem from '../select-item.vue';
import SwitchItem from '../switch-item.vue';

View File

@@ -1,9 +1,9 @@
<script setup lang="ts">
import type { SelectOption } from '@vben-core/typings';
import type { SelectOption } from '@vben/types';
import { useSlots } from 'vue';
import { CircleHelp } from '@vben-core/icons';
import { CircleHelp } from '@vben/icons';
import { Input, VbenTooltip } from '@vben-core/shadcn-ui';
defineOptions({

View File

@@ -1,9 +1,9 @@
<script setup lang="ts">
import type { SelectOption } from '@vben-core/typings';
import type { SelectOption } from '@vben/types';
import { computed } from 'vue';
import { $t } from '@vben-core/locales';
import { $t } from '@vben/locales';
import SwitchItem from '../switch-item.vue';
import ToggleItem from '../toggle-item.vue';

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import { type Component, computed } from 'vue';
import { $t } from '@vben-core/locales';
import { $t } from '@vben/locales';
import { ContentCompact, ContentWide } from '../../icons';

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import { computed } from 'vue';
import { $t } from '@vben-core/locales';
import { $t } from '@vben/locales';
import InputItem from '../input-item.vue';
import SwitchItem from '../switch-item.vue';

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { $t } from '@vben-core/locales';
import { $t } from '@vben/locales';
import SwitchItem from '../switch-item.vue';

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import type { LayoutHeaderModeType, SelectOption } from '@vben-core/typings';
import type { LayoutHeaderModeType, SelectOption } from '@vben/types';
import { $t } from '@vben-core/locales';
import { $t } from '@vben/locales';
import SelectItem from '../select-item.vue';
import SwitchItem from '../switch-item.vue';

View File

@@ -1,10 +1,10 @@
<script setup lang="ts">
import type { LayoutType } from '@vben-core/typings';
import type { LayoutType } from '@vben/types';
import { type Component, computed } from 'vue';
import { CircleHelp } from '@vben-core/icons';
import { $t } from '@vben-core/locales';
import { CircleHelp } from '@vben/icons';
import { $t } from '@vben/locales';
import { VbenTooltip } from '@vben-core/shadcn-ui';
import {

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import type { SelectOption } from '@vben-core/typings';
import type { SelectOption } from '@vben/types';
import { $t } from '@vben-core/locales';
import { $t } from '@vben/locales';
import SwitchItem from '../switch-item.vue';
import ToggleItem from '../toggle-item.vue';

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { $t } from '@vben-core/locales';
import { $t } from '@vben/locales';
import NumberFieldItem from '../number-field-item.vue';
import SwitchItem from '../switch-item.vue';

View File

@@ -1,8 +1,8 @@
<script setup lang="ts">
import { computed } from 'vue';
import { $t } from '@vben-core/locales';
import { SelectOption } from '@vben-core/typings';
import { $t } from '@vben/locales';
import { SelectOption } from '@vben/types';
import SelectItem from '../select-item.vue';
import SwitchItem from '../switch-item.vue';

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { $t } from '@vben-core/locales';
import { $t } from '@vben/locales';
import SwitchItem from '../switch-item.vue';

View File

@@ -1,9 +1,9 @@
<script setup lang="ts">
import type { SelectOption } from '@vben-core/typings';
import type { SelectOption } from '@vben/types';
import { useSlots } from 'vue';
import { CircleHelp } from '@vben-core/icons';
import { CircleHelp } from '@vben/icons';
import {
NumberField,
NumberFieldContent,

View File

@@ -1,9 +1,9 @@
<script setup lang="ts">
import type { SelectOption } from '@vben-core/typings';
import type { SelectOption } from '@vben/types';
import { useSlots } from 'vue';
import { CircleHelp } from '@vben-core/icons';
import { CircleHelp } from '@vben/icons';
import {
Select,
SelectContent,

View File

@@ -1,8 +1,8 @@
<script setup lang="ts">
import { computed } from 'vue';
import { $t } from '@vben-core/locales';
import { isWindowsOs } from '@vben-core/toolkit';
import { $t } from '@vben/locales';
import { isWindowsOs } from '@vben/utils';
import SwitchItem from '../switch-item.vue';

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import { useSlots } from 'vue';
import { CircleHelp } from '@vben-core/icons';
import { CircleHelp } from '@vben/icons';
import { Switch, VbenTooltip } from '@vben-core/shadcn-ui';
defineOptions({

View File

@@ -1,15 +1,15 @@
<script setup lang="ts">
import type { BuiltinThemeType } from '@vben-core/typings';
import type { BuiltinThemeType } from '@vben/types';
import { computed, ref } from 'vue';
import { UserRoundPen } from '@vben-core/icons';
import { $t } from '@vben-core/locales';
import { UserRoundPen } from '@vben/icons';
import { $t } from '@vben/locales';
import {
BUILT_IN_THEME_PRESETS,
type BuiltinThemePreset,
} from '@vben-core/preferences';
import { TinyColor, convertToHsl } from '@vben-core/toolkit';
} from '@vben/preferences';
import { convertToHsl, TinyColor } from '@vben/utils';
defineOptions({
name: 'PreferenceBuiltinTheme',

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { $t } from '@vben-core/locales';
import { $t } from '@vben/locales';
import SwitchItem from '../switch-item.vue';

View File

@@ -1,10 +1,10 @@
<script setup lang="ts">
import type { ThemeModeType } from '@vben-core/typings';
import type { ThemeModeType } from '@vben/types';
import type { Component } from 'vue';
import { MoonStar, Sun, SunMoon } from '@vben-core/icons';
import { $t } from '@vben-core/locales';
import { MoonStar, Sun, SunMoon } from '@vben/icons';
import { $t } from '@vben/locales';
import SwitchItem from '../switch-item.vue';

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import type { SelectOption } from '@vben-core/typings';
import type { SelectOption } from '@vben/types';
import { ToggleGroup, ToggleGroupItem } from '@vben-core/shadcn-ui';

View File

@@ -1,5 +1,4 @@
<script setup lang="ts">
import type { SegmentedItem } from '@vben-core/shadcn-ui';
import type {
BreadcrumbStyleType,
BuiltinThemeType,
@@ -9,24 +8,25 @@ import type {
NavigationStyleType,
SupportedLanguagesType,
ThemeModeType,
} from '@vben-core/typings';
} from '@vben/types';
import type { SegmentedItem } from '@vben-core/shadcn-ui';
import { computed, ref } from 'vue';
import { Copy, RotateCw, SwatchBook } from '@vben-core/icons';
import { $t, loadLocaleMessages } from '@vben-core/locales';
import { Copy, RotateCw, SwatchBook } from '@vben/icons';
import { $t, loadLocaleMessages } from '@vben/locales';
import {
clearPreferencesCache,
preferences,
resetPreferences,
usePreferences,
} from '@vben-core/preferences';
} from '@vben/preferences';
import {
useToast,
VbenButton,
VbenIconButton,
VbenSegmented,
VbenSheet,
useToast,
} from '@vben-core/shadcn-ui';
import { useClipboard } from '@vueuse/core';

View File

@@ -1,9 +1,9 @@
<script lang="ts" setup>
import { computed } from 'vue';
import { loadLocaleMessages } from '@vben-core/locales';
import { preferences, updatePreferences } from '@vben-core/preferences';
import { capitalizeFirstLetter } from '@vben-core/toolkit';
import { loadLocaleMessages } from '@vben/locales';
import { preferences, updatePreferences } from '@vben/preferences';
import { capitalizeFirstLetter } from '@vben/utils';
import Preferences from './preferences-sheet.vue';

View File

@@ -1,13 +1,13 @@
<script lang="ts" setup>
import type { ThemeModeType } from '@vben-core/typings';
import type { ThemeModeType } from '@vben/types';
import { MoonStar, Sun, SunMoon } from '@vben-core/icons';
import { $t } from '@vben-core/locales';
import { MoonStar, Sun, SunMoon } from '@vben/icons';
import { $t } from '@vben/locales';
import {
preferences,
updatePreferences,
usePreferences,
} from '@vben-core/preferences';
} from '@vben/preferences';
import {
ToggleGroup,
ToggleGroupItem,

View File

@@ -1,12 +1,14 @@
<script setup lang="ts">
import type { AnyFunction } from '@vben-core/typings';
import type { AnyFunction } from '@vben/types';
import type { Component } from 'vue';
import { computed, ref } from 'vue';
import { LockKeyhole, LogOut, SwatchBook } from '@vben-core/icons';
import { $t } from '@vben-core/locales';
import { preferences, usePreferences } from '@vben-core/preferences';
import { LockKeyhole, LogOut, SwatchBook } from '@vben/icons';
import { $t } from '@vben/locales';
import { preferences, usePreferences } from '@vben/preferences';
import { useCoreLockStore } from '@vben/stores';
import { isWindowsOs } from '@vben/utils';
import {
Badge,
DropdownMenu,
@@ -20,7 +22,6 @@ import {
VbenAvatar,
VbenIcon,
} from '@vben-core/shadcn-ui';
import { isWindowsOs } from '@vben-core/toolkit';
import { useMagicKeys, whenever } from '@vueuse/core';
@@ -69,7 +70,7 @@ const props = withDefaults(defineProps<Props>(), {
text: '',
});
const emit = defineEmits<{ lockScreen: [string]; logout: [] }>();
const emit = defineEmits<{ logout: [] }>();
const openPopover = ref(false);
const openDialog = ref(false);
const openLock = ref(false);
@@ -79,6 +80,7 @@ const {
globalLogoutShortcutKey,
globalPreferencesShortcutKey,
} = usePreferences();
const coreLockStore = useCoreLockStore();
const { handleOpenPreference } = useOpenPreferences();
const altView = computed(() => (isWindowsOs() ? 'Alt' : '⌥'));
@@ -109,7 +111,7 @@ function handleSubmitLock({
lockScreenPassword: string;
}) {
openLock.value = false;
emit('lockScreen', lockScreenPassword);
coreLockStore.lockScreen(lockScreenPassword);
}
function handleLogout() {
// emit

View File

@@ -0,0 +1,7 @@
import { defineBuildConfig } from 'unbuild';
export default defineBuildConfig({
clean: true,
declaration: true,
entries: ['src/index'],
});

View File

@@ -0,0 +1,49 @@
{
"name": "@vben/request",
"version": "5.0.0",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {
"type": "git",
"url": "git+https://github.com/vbenjs/vue-vben-admin.git",
"directory": "packages/effects/request"
},
"license": "MIT",
"type": "module",
"scripts": {
"#build": "pnpm unbuild"
},
"files": [
"dist"
],
"sideEffects": [
"**/*.css"
],
"main": "./dist/index.mjs",
"module": "./dist/index.mjs",
"exports": {
".": {
"types": "./src/index.ts",
"development": "./src/index.ts",
"#default": "./dist/index.mjs",
"default": "./src/index.ts"
}
},
"publishConfig": {
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.mjs"
}
}
},
"dependencies": {
"@vben/locales": "workspace:*",
"@vben/utils": "workspace:*",
"axios": "^1.7.2",
"vue-request": "^2.0.4"
},
"devDependencies": {
"axios-mock-adapter": "^1.22.0"
}
}

View File

@@ -0,0 +1,3 @@
export * from './request-client';
export * from './use-request';
export * from 'axios';

View File

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

View File

@@ -0,0 +1,84 @@
import type { AxiosRequestConfig } from 'axios';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { FileDownloader } from './downloader';
describe('fileDownloader', () => {
let fileDownloader: FileDownloader;
const mockAxiosInstance = {
get: vi.fn(),
} as any;
beforeEach(() => {
fileDownloader = new FileDownloader(mockAxiosInstance);
});
it('should create an instance of FileDownloader', () => {
expect(fileDownloader).toBeInstanceOf(FileDownloader);
});
it('should download a file and return a Blob', async () => {
const url = 'https://example.com/file';
const mockBlob = new Blob(['file content'], { type: 'text/plain' });
const mockResponse: Blob = mockBlob;
mockAxiosInstance.get.mockResolvedValueOnce(mockResponse);
const result = await fileDownloader.download(url);
expect(result).toBeInstanceOf(Blob);
expect(result).toEqual(mockBlob);
expect(mockAxiosInstance.get).toHaveBeenCalledWith(url, {
responseType: 'blob',
});
});
it('should merge provided config with default config', async () => {
const url = 'https://example.com/file';
const mockBlob = new Blob(['file content'], { type: 'text/plain' });
const mockResponse: Blob = mockBlob;
mockAxiosInstance.get.mockResolvedValueOnce(mockResponse);
const customConfig: AxiosRequestConfig = {
headers: { 'Custom-Header': 'value' },
};
const result = await fileDownloader.download(url, customConfig);
expect(result).toBeInstanceOf(Blob);
expect(result).toEqual(mockBlob);
expect(mockAxiosInstance.get).toHaveBeenCalledWith(url, {
...customConfig,
responseType: 'blob',
});
});
it('should handle errors gracefully', async () => {
const url = 'https://example.com/file';
mockAxiosInstance.get.mockRejectedValueOnce(new Error('Network Error'));
await expect(fileDownloader.download(url)).rejects.toThrow('Network Error');
});
it('should handle empty URL gracefully', async () => {
const url = '';
mockAxiosInstance.get.mockRejectedValueOnce(
new Error('Request failed with status code 404'),
);
await expect(fileDownloader.download(url)).rejects.toThrow(
'Request failed with status code 404',
);
});
it('should handle null URL gracefully', async () => {
const url = null as unknown as string;
mockAxiosInstance.get.mockRejectedValueOnce(
new Error('Request failed with status code 404'),
);
await expect(fileDownloader.download(url)).rejects.toThrow(
'Request failed with status code 404',
);
});
});

View File

@@ -0,0 +1,30 @@
import type { AxiosRequestConfig, AxiosResponse } from 'axios';
import type { RequestClient } from '../request-client';
class FileDownloader {
private client: RequestClient;
constructor(client: RequestClient) {
this.client = client;
}
public async download(
url: string,
config?: AxiosRequestConfig,
): Promise<AxiosResponse<Blob>> {
const finalConfig: AxiosRequestConfig = {
...config,
responseType: 'blob',
};
const response = await this.client.get<AxiosResponse<Blob>>(
url,
finalConfig,
);
return response;
}
}
export { FileDownloader };

View File

@@ -0,0 +1,41 @@
import {
AxiosInstance,
AxiosResponse,
type InternalAxiosRequestConfig,
} from 'axios';
const errorHandler = (res: Error) => Promise.reject(res);
class InterceptorManager {
private axiosInstance: AxiosInstance;
constructor(instance: AxiosInstance) {
this.axiosInstance = instance;
}
addRequestInterceptor(
fulfilled: (
config: InternalAxiosRequestConfig,
) => InternalAxiosRequestConfig | Promise<InternalAxiosRequestConfig>,
rejected?: (error: any) => any,
) {
this.axiosInstance.interceptors.request.use(
fulfilled,
rejected || errorHandler,
);
}
addResponseInterceptor<T = any>(
fulfilled: (
response: AxiosResponse<T>,
) => AxiosResponse | Promise<AxiosResponse>,
rejected?: (error: any) => any,
) {
this.axiosInstance.interceptors.response.use(
fulfilled,
rejected || errorHandler,
);
}
}
export { InterceptorManager };

View File

@@ -0,0 +1,118 @@
import type { AxiosRequestConfig, AxiosResponse } from 'axios';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { FileUploader } from './uploader';
describe('fileUploader', () => {
let fileUploader: FileUploader;
// Mock the AxiosInstance
const mockAxiosInstance = {
post: vi.fn(),
} as any;
beforeEach(() => {
fileUploader = new FileUploader(mockAxiosInstance);
});
it('should create an instance of FileUploader', () => {
expect(fileUploader).toBeInstanceOf(FileUploader);
});
it('should upload a file and return the response', async () => {
const url = 'https://example.com/upload';
const file = new File(['file content'], 'test.txt', { type: 'text/plain' });
const mockResponse: AxiosResponse = {
config: {} as any,
data: { success: true },
headers: {},
status: 200,
statusText: 'OK',
};
(
mockAxiosInstance.post as unknown as ReturnType<typeof vi.fn>
).mockResolvedValueOnce(mockResponse);
const result = await fileUploader.upload(url, file);
expect(result).toEqual(mockResponse);
expect(mockAxiosInstance.post).toHaveBeenCalledWith(
url,
expect.any(FormData),
{
headers: {
'Content-Type': 'multipart/form-data',
},
},
);
});
it('should merge provided config with default config', async () => {
const url = 'https://example.com/upload';
const file = new File(['file content'], 'test.txt', { type: 'text/plain' });
const mockResponse: AxiosResponse = {
config: {} as any,
data: { success: true },
headers: {},
status: 200,
statusText: 'OK',
};
(
mockAxiosInstance.post as unknown as ReturnType<typeof vi.fn>
).mockResolvedValueOnce(mockResponse);
const customConfig: AxiosRequestConfig = {
headers: { 'Custom-Header': 'value' },
};
const result = await fileUploader.upload(url, file, customConfig);
expect(result).toEqual(mockResponse);
expect(mockAxiosInstance.post).toHaveBeenCalledWith(
url,
expect.any(FormData),
{
headers: {
'Content-Type': 'multipart/form-data',
'Custom-Header': 'value',
},
},
);
});
it('should handle errors gracefully', async () => {
const url = 'https://example.com/upload';
const file = new File(['file content'], 'test.txt', { type: 'text/plain' });
(
mockAxiosInstance.post as unknown as ReturnType<typeof vi.fn>
).mockRejectedValueOnce(new Error('Network Error'));
await expect(fileUploader.upload(url, file)).rejects.toThrow(
'Network Error',
);
});
it('should handle empty URL gracefully', async () => {
const url = '';
const file = new File(['file content'], 'test.txt', { type: 'text/plain' });
(
mockAxiosInstance.post as unknown as ReturnType<typeof vi.fn>
).mockRejectedValueOnce(new Error('Request failed with status code 404'));
await expect(fileUploader.upload(url, file)).rejects.toThrow(
'Request failed with status code 404',
);
});
it('should handle null URL gracefully', async () => {
const url = null as unknown as string;
const file = new File(['file content'], 'test.txt', { type: 'text/plain' });
(
mockAxiosInstance.post as unknown as ReturnType<typeof vi.fn>
).mockRejectedValueOnce(new Error('Request failed with status code 404'));
await expect(fileUploader.upload(url, file)).rejects.toThrow(
'Request failed with status code 404',
);
});
});

View File

@@ -0,0 +1,32 @@
import type { AxiosRequestConfig, AxiosResponse } from 'axios';
import type { RequestClient } from '../request-client';
class FileUploader {
private client: RequestClient;
constructor(client: RequestClient) {
this.client = client;
}
public async upload(
url: string,
file: Blob | File,
config?: AxiosRequestConfig,
): Promise<AxiosResponse> {
const formData = new FormData();
formData.append('file', file);
const finalConfig: AxiosRequestConfig = {
...config,
headers: {
'Content-Type': 'multipart/form-data',
...config?.headers,
},
};
return this.client.post(url, formData, finalConfig);
}
}
export { FileUploader };

View File

@@ -0,0 +1,97 @@
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { RequestClient } from './request-client';
describe('requestClient', () => {
let mock: MockAdapter;
let requestClient: RequestClient;
beforeEach(() => {
mock = new MockAdapter(axios);
requestClient = new RequestClient();
});
afterEach(() => {
mock.reset();
});
it('should successfully make a GET request', async () => {
mock.onGet('test/url').reply(200, { data: 'response' });
const response = await requestClient.get('test/url');
expect(response.data).toEqual({ data: 'response' });
});
it('should successfully make a POST request', async () => {
const postData = { key: 'value' };
const mockData = { data: 'response' };
mock.onPost('/test/post', postData).reply(200, mockData);
const response = await requestClient.post('/test/post', postData);
expect(response.data).toEqual(mockData);
});
it('should successfully make a PUT request', async () => {
const putData = { key: 'updatedValue' };
const mockData = { data: 'updated response' };
mock.onPut('/test/put', putData).reply(200, mockData);
const response = await requestClient.put('/test/put', putData);
expect(response.data).toEqual(mockData);
});
it('should successfully make a DELETE request', async () => {
const mockData = { data: 'delete response' };
mock.onDelete('/test/delete').reply(200, mockData);
const response = await requestClient.delete('/test/delete');
expect(response.data).toEqual(mockData);
});
it('should handle network errors', async () => {
mock.onGet('/test/error').networkError();
try {
await requestClient.get('/test/error');
expect(true).toBe(false);
} catch (error: any) {
expect(error.isAxiosError).toBe(true);
expect(error.message).toBe('Network Error');
}
});
it('should handle timeout', async () => {
mock.onGet('/test/timeout').timeout();
try {
await requestClient.get('/test/timeout');
expect(true).toBe(false);
} catch (error: any) {
expect(error.isAxiosError).toBe(true);
expect(error.code).toBe('ECONNABORTED');
}
});
it('should successfully upload a file', async () => {
const fileData = new Blob(['file contents'], { type: 'text/plain' });
mock.onPost('/test/upload').reply((config) => {
return config.data instanceof FormData && config.data.has('file')
? [200, { data: 'file uploaded' }]
: [400, { error: 'Bad Request' }];
});
const response = await requestClient.upload('/test/upload', fileData);
expect(response.data).toEqual({ data: 'file uploaded' });
});
it('should successfully download a file as a blob', async () => {
const mockFileContent = new Blob(['mock file content'], {
type: 'text/plain',
});
mock.onGet('/test/download').reply(200, mockFileContent);
const res = await requestClient.download('/test/download');
expect(res.data).toBeInstanceOf(Blob);
});
});

View File

@@ -0,0 +1,238 @@
import type {
AxiosInstance,
AxiosRequestConfig,
AxiosResponse,
CreateAxiosDefaults,
InternalAxiosRequestConfig,
} from 'axios';
import type {
MakeAuthorizationFn,
MakeErrorMessageFn,
RequestClientOptions,
} from './types';
import { $t } from '@vben/locales';
import { merge } from '@vben/utils';
import axios from 'axios';
import { FileDownloader } from './modules/downloader';
import { InterceptorManager } from './modules/interceptor';
import { FileUploader } from './modules/uploader';
class RequestClient {
private instance: AxiosInstance;
private makeAuthorization: MakeAuthorizationFn | undefined;
private makeErrorMessage: MakeErrorMessageFn | undefined;
public addRequestInterceptor: InterceptorManager['addRequestInterceptor'];
public addResponseInterceptor: InterceptorManager['addResponseInterceptor'];
public download: FileDownloader['download'];
public upload: FileUploader['upload'];
/**
* 构造函数用于创建Axios实例
* @param options - Axios请求配置可选
*/
constructor(options: RequestClientOptions = {}) {
this.bindMethods();
// 合并默认配置和传入的配置
const defaultConfig: CreateAxiosDefaults = {
headers: {
'Content-Type': 'application/json;charset=utf-8',
},
// 默认超时时间
timeout: 10_000,
};
const { makeAuthorization, makeErrorMessage, ...axiosConfig } = options;
const requestConfig = merge(axiosConfig, defaultConfig);
this.instance = axios.create(requestConfig);
this.makeAuthorization = makeAuthorization;
this.makeErrorMessage = makeErrorMessage;
// 实例化拦截器管理器
const interceptorManager = new InterceptorManager(this.instance);
this.addRequestInterceptor =
interceptorManager.addRequestInterceptor.bind(interceptorManager);
this.addResponseInterceptor =
interceptorManager.addResponseInterceptor.bind(interceptorManager);
// 实例化文件上传器
const fileUploader = new FileUploader(this);
this.upload = fileUploader.upload.bind(fileUploader);
// 实例化文件下载器
const fileDownloader = new FileDownloader(this);
this.download = fileDownloader.download.bind(fileDownloader);
// 设置默认的拦截器
this.setupInterceptors();
}
private bindMethods() {
const propertyNames = Object.getOwnPropertyNames(
Object.getPrototypeOf(this),
);
propertyNames.forEach((propertyName) => {
const propertyValue = (this as any)[propertyName];
if (
typeof propertyValue === 'function' &&
propertyName !== 'constructor'
) {
(this as any)[propertyName] = propertyValue.bind(this);
}
});
}
private setupAuthorizationInterceptor() {
this.addRequestInterceptor(
(config: InternalAxiosRequestConfig) => {
const authorization = this.makeAuthorization?.(config);
if (authorization) {
const { token } = authorization.tokenHandler?.() ?? {};
config.headers[authorization.key || 'Authorization'] = token;
}
return config;
},
(error: any) => Promise.reject(error),
);
}
private setupDefaultResponseInterceptor() {
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.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' });
}
/**
* 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' });
}
/**
* POST请求方法
* @param {string} url - 请求URL
* @param {any} data - 请求体数据
* @param {AxiosRequestConfig} config - 请求配置,可选
* @returns {Promise<AxiosResponse<T>>} 返回Axios响应Promise
*/
public post<T = any>(
url: string,
data?: any,
config?: AxiosRequestConfig,
): Promise<T> {
return this.request<T>(url, { ...config, data, method: 'POST' });
}
/**
* PUT请求方法
* @param {string} url - 请求的URL
* @param {any} data - 请求体数据
* @param {AxiosRequestConfig} config - 请求配置(可选)
* @returns 返回Promise
*/
public put<T = any>(
url: string,
data?: any,
config?: AxiosRequestConfig,
): Promise<T> {
return this.request<T>(url, { ...config, data, method: 'PUT' });
}
/**
* 通用的请求方法
* @param {string} url - 请求的URL
* @param {AxiosRequestConfig} config - 请求配置对象
* @returns {Promise<AxiosResponse<T>>} 返回Axios响应Promise
*/
public async request<T>(url: string, config: AxiosRequestConfig): Promise<T> {
try {
const response: AxiosResponse<T> = await this.instance({
url,
...config,
});
return response as T;
} catch (error: any) {
throw error.response ? error.response.data : error;
}
}
}
export { RequestClient };

View File

@@ -0,0 +1,48 @@
import type { CreateAxiosDefaults, InternalAxiosRequestConfig } from 'axios';
type RequestContentType =
| 'application/json;charset=utf-8'
| 'application/octet-stream;charset=utf-8'
| '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 MakeAuthorizationFn = (
config?: InternalAxiosRequestConfig,
) => MakeAuthorization;
type MakeErrorMessageFn = (message: string) => void;
interface RequestClientOptions extends CreateAxiosDefaults {
/**
* 用于生成Authorization
*/
makeAuthorization?: MakeAuthorizationFn;
/**
* 用于生成错误消息
*/
makeErrorMessage?: MakeErrorMessageFn;
}
interface HttpResponse<T = any> {
/**
* 0 表示成功 其他表示失败
* 0 means success, others means fail
*/
code: number;
data: T;
message: string;
}
export type {
HttpResponse,
MakeAuthorizationFn,
MakeErrorMessageFn,
RequestClientOptions,
RequestContentType,
};

View File

@@ -0,0 +1,11 @@
// import { setGlobalOptions, } from 'vue-request';
// setGlobalOptions({
// manual: true,
// // ...
// });
/**
* @see https://www.attojs.com/guide/documentation/globalOptions.html
*/
export * from 'vue-request';

View File

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