diff --git a/build/proxy.ts b/build/proxy.ts index d11829d..19fe5fd 100644 --- a/build/proxy.ts +++ b/build/proxy.ts @@ -3,7 +3,7 @@ import type { ProxyOptions } from 'vite' /** 不同请求服务的环境配置 */ export const proxyConfig: Record<ServiceEnvType, ServiceEnvConfig> = { dev: { - url: 'http://localhost:3000', + url: 'https://mock.apifox.com/m1/4071143-0-default', urlPattern: '/url-pattern', }, test: { @@ -11,7 +11,7 @@ export const proxyConfig: Record<ServiceEnvType, ServiceEnvConfig> = { urlPattern: '/url-pattern', }, prod: { - url: 'http://localhost:8080', + url: 'https://mock.apifox.com/m1/4071143-0-default', urlPattern: '/url-pattern', }, } diff --git a/src/layouts/components/tab/TabBar.vue b/src/layouts/components/tab/TabBar.vue index 3cb5d85..8d1f885 100644 --- a/src/layouts/components/tab/TabBar.vue +++ b/src/layouts/components/tab/TabBar.vue @@ -1,7 +1,8 @@ <script setup lang="ts"> import type { RouteLocationNormalized } from 'vue-router' -import { useAppStore, useTabStore } from '@/store' +import { NIcon } from 'naive-ui' import { renderIcon } from '@/utils' +import { useAppStore, useTabStore } from '@/store' const tabStore = useTabStore() const appStore = useAppStore() @@ -90,6 +91,17 @@ function handleContextMenu(e: MouseEvent, route: RouteLocationNormalized) { function onClickoutside() { showDropdown.value = false } + +function renderDropTabsLabel(option: any) { + return option.meta.title +} +function renderDropTabsIcon(option: any) { + return renderIcon(option.meta.icon)!() +} + +function handleDropTabs(key: string, option: any) { + router.push(option.path) +} </script> <template> @@ -121,6 +133,20 @@ function onClickoutside() { <e-icon :icon="item.meta.icon" /> {{ item.meta.title }} </div> </n-tab> + <template #suffix> + <n-dropdown + :options="tabStore.allTabs" + :render-label="renderDropTabsLabel" + :render-icon="renderDropTabsIcon" + trigger="click" + size="small" + @select="handleDropTabs" + > + <n-button tertiary circle type="primary"> + <i-icon-park-outline-application-menu /> + </n-button> + </n-dropdown> + </template> </n-tabs> <n-dropdown placement="bottom-start" diff --git a/src/service/api/test.ts b/src/service/api/test.ts index 69c64c4..3690e33 100644 --- a/src/service/api/test.ts +++ b/src/service/api/test.ts @@ -11,7 +11,7 @@ export function fetchPost(data: any) { } /* formPost方法测试 */ export function fetchFormPost(data: any) { - const methodInstance = alovaInstance.Post('/postAPI', data) + const methodInstance = alovaInstance.Post('/postFormAPI', data) methodInstance.meta = { isFormPost: true, } @@ -49,10 +49,20 @@ export function dictData() { export function getBlob() { const methodInstance = blankInstance.Get('https://images.unsplash.com/photo-1663529628961-80aa6ebcd157?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=764&q=80') methodInstance.meta = { - isDownload: true, + // 标识为bolb数据 + isBlob: true, } return methodInstance } + +/* 带进度的下载文件 */ +export function downloadFile(url: string) { + const methodInstance = blankInstance.Get(url, { + // 开启下载进度 + enableDownload: true, + }) + return methodInstance +} /* 测试状态码500失败 */ export function FailedRequest() { return alovaInstance.Get('/serverError') diff --git a/src/service/http/alova.ts b/src/service/http/alova.ts index 4a42f3e..4db0613 100644 --- a/src/service/http/alova.ts +++ b/src/service/http/alova.ts @@ -63,7 +63,7 @@ export function createAlovaInstance( if (status === 200) { // 返回blob数据 - if (method.meta?.isDownload) + if (method.meta?.isBlob) return response.blob() // 返回json数据 diff --git a/src/service/http/index.ts b/src/service/http/index.ts index 763ea15..0aacb51 100644 --- a/src/service/http/index.ts +++ b/src/service/http/index.ts @@ -5,12 +5,8 @@ const { url, urlPattern } = proxyConfig[import.meta.env.MODE] const isHttpProxy = import.meta.env.VITE_HTTP_PROXY === 'Y' || false -export const request = createAlovaInstance({ - baseURL: isHttpProxy ? urlPattern : url, -}) - export const alovaInstance = createAlovaInstance({ - baseURL: 'https://mock.apifox.com/m1/4071143-0-default', + baseURL: isHttpProxy ? urlPattern : url, }) export const blankInstance = createAlovaInstance({ diff --git a/src/store/auth.ts b/src/store/auth.ts index 0f99982..ab33c06 100644 --- a/src/store/auth.ts +++ b/src/store/auth.ts @@ -4,20 +4,15 @@ import { fetchLogin, fetchUserInfo } from '@/service' import { router } from '@/router' import { local } from '@/utils' -const emptyInfo: Auth.UserInfo = { - id: 0, - userName: '', - nickName: '', - avatar: '', - role: 'user', +interface AuthStatus { + userInfo: Auth.UserInfo | null + token: string } export const useAuthStore = defineStore('auth-store', { - state: () => { + state: (): AuthStatus => { return { - userInfo: local.get('userInfo') || emptyInfo, + userInfo: local.get('userInfo'), token: local.get('token') || '', - refreshToken: local.get('refreshToken') || '', - loginLoading: false, } }, getters: { @@ -58,16 +53,12 @@ export const useAuthStore = defineStore('auth-store', { /* 用户登录 */ async login(username: string, password: string) { - this.loginLoading = true const { error, data } = await fetchLogin({ username, password }) - if (error) { - this.loginLoading = false + if (error) return - } + // 处理登录信息 await this.handleAfterLogin(data) - - this.loginLoading = false }, /* 登录后的处理函数 */ @@ -91,7 +82,7 @@ export const useAuthStore = defineStore('auth-store', { // 触发用户提示 window.$notification?.success({ title: '登录成功!', - content: `欢迎回来😊,${this.userInfo.nickName}!`, + content: `欢迎回来😊,${this.userInfo?.nickName}!`, duration: 3000, }) return @@ -108,7 +99,6 @@ export const useAuthStore = defineStore('auth-store', { local.set('token', accessToken) local.set('refreshToken', refreshToken) this.token = accessToken - this.refreshToken = refreshToken const { error, data } = await fetchUserInfo({ id }) if (error) return catchSuccess diff --git a/src/store/route.ts b/src/store/route.ts index 629abba..98d970c 100644 --- a/src/store/route.ts +++ b/src/store/route.ts @@ -13,19 +13,17 @@ import { BasicLayout } from '@/layouts/index' interface RoutesStatus { isInitAuthRoute: boolean menus: any - userRoutes: AppRoute.RowRoute[] + rowRoutes: AppRoute.RowRoute[] activeMenu: string | null - authRouteMode: ImportMetaEnv['VITE_AUTH_ROUTE_MODE'] cacheRoutes: string[] } export const useRouteStore = defineStore('route-store', { state: (): RoutesStatus => { return { - userRoutes: [], isInitAuthRoute: false, menus: [], + rowRoutes: [], activeMenu: null, - authRouteMode: import.meta.env.VITE_AUTH_ROUTE_MODE, cacheRoutes: [], } }, @@ -38,30 +36,12 @@ export const useRouteStore = defineStore('route-store', { /* 删除后面添加的路由 */ router.removeRoute('appRoot') }, - /* 判断当前路由和子路由中是否存在为routeName的路由 */ - hasPathinAllPath(routeName: string, userRoutes: AppRoute.Route) { - if (userRoutes.name === routeName) - return true - - if (userRoutes.children && userRoutes.children.length !== 0) { - const arr: boolean[] = [] - userRoutes.children.forEach((item) => { - arr.push(this.hasPathinAllPath(routeName, item)) - }) - return arr.some((item) => { - return item - }) - } - return false - }, /* 设置当前高亮的菜单key */ setActiveMenu(key: string) { this.activeMenu = key }, /* 生成侧边菜单的数据 */ createMenus(userRoutes: AppRoute.RowRoute[]) { - this.userRoutes = userRoutes - const resultMenus = clone(userRoutes).map(i => construct(i)) as AppRoute.Route[] /** 过滤不需要显示的菜单 */ const visibleMenus = resultMenus.filter(route => !route.meta.hide) @@ -139,7 +119,7 @@ export const useRouteStore = defineStore('route-store', { } }) }, - createDynamicRoutes(routes: AppRoute.RowRoute[]) { + createRoutes(routes: AppRoute.RowRoute[]) { const { hasPermission } = usePermission() // 结构化meta字段 let resultRouter = clone(routes).map(i => construct(i)) as AppRoute.Route[] @@ -175,44 +155,38 @@ export const useRouteStore = defineStore('route-store', { } // 根据角色过滤后的插入根路由中 appRootRoute.children = resultRouter as unknown as RouteRecordRaw[] - return appRootRoute - }, - /* 初始化动态路由 */ - async initDynamicRoute() { - // 根据用户id来获取用户的路由 - const userInfo = local.get('userInfo') - - if (!userInfo || !userInfo.id) - return - - const { data } = await fetchUserRoutes({ - id: userInfo.id, - }) - - if (!data) - return - // 根据用户返回的路由表来生成真实路由 - const appRoutes = this.createDynamicRoutes(data) - // 生成侧边菜单 - this.createMenus(data) // 插入路由表 - router.addRoute(appRoutes) - }, - /* 初始化静态路由 */ - initStaticRoute() { - // 根据静态路由表来生成真实路由 - const appRoutes = this.createDynamicRoutes(staticRoutes) - // 生成侧边菜单 - this.createMenus(staticRoutes) - // 插入路由表 - router.addRoute(appRoutes) + router.addRoute(appRootRoute) }, + async initRouteInfo() { + if (import.meta.env.VITE_AUTH_ROUTE_MODE === 'dynamic') { + // 根据用户id来获取用户的路由 + const userInfo = local.get('userInfo') + if (!userInfo || !userInfo.id) + return + + const { data } = await fetchUserRoutes({ + id: userInfo.id, + }) + + if (!data) + return + + this.rowRoutes = data + } + else { + this.rowRoutes = staticRoutes + } + }, async initAuthRoute() { this.isInitAuthRoute = false - if (this.authRouteMode === 'dynamic') - await this.initDynamicRoute() - else this.initStaticRoute() + // 初始化路由信息 + await this.initRouteInfo() + // 生成真实路由并插入 + this.createRoutes(this.rowRoutes) + // 生成侧边菜单 + this.createMenus(this.rowRoutes) this.isInitAuthRoute = true }, diff --git a/src/store/tab.ts b/src/store/tab.ts index 7d6b748..7116d9a 100644 --- a/src/store/tab.ts +++ b/src/store/tab.ts @@ -14,6 +14,9 @@ export const useTabStore = defineStore('tab-store', { currentTabPath: '', } }, + getters: { + allTabs: state => [...state.pinTabs, ...state.tabs], + }, actions: { addTab(route: RouteLocationNormalized) { // 根据meta确定是否不添加,可用于错误页,登录页等 diff --git a/src/typings/service.d.ts b/src/typings/service.d.ts index f96cef6..f4b04ce 100644 --- a/src/typings/service.d.ts +++ b/src/typings/service.d.ts @@ -20,7 +20,7 @@ declare namespace Service { successCode?: number | string } - type RequestErrorType = 'Alova' | 'Response' | 'Business' + type RequestErrorType = 'Response' | 'Business' type RequestCode = string | number interface RequestError { diff --git a/src/views/login/components/Login/index.vue b/src/views/login/components/Login/index.vue index 3185094..df7d507 100644 --- a/src/views/login/components/Login/index.vue +++ b/src/views/login/components/Login/index.vue @@ -1,5 +1,6 @@ <script setup lang="ts"> import type { FormInst } from 'naive-ui' +import { useRequest } from 'alova' import { local } from '@/utils' import { useAuthStore } from '@/store' @@ -34,20 +35,23 @@ const formValue = ref({ code: '1234', }) const isRemember = ref(false) +const isLoading = ref(false) const formRef = ref<FormInst | null>(null) function handleLogin() { - formRef.value?.validate((errors) => { + formRef.value?.validate(async (errors) => { if (errors) return + isLoading.value = true const { account, pwd } = formValue.value if (isRemember.value) local.set('login_account', { account, pwd }) else local.remove('login_account') - authStore.login(account, pwd) + await authStore.login(account, pwd) + isLoading.value = false }) } function checkUserAccount() { @@ -95,7 +99,7 @@ checkUserAccount() 忘记密码? </n-button> </div> - <n-button block type="primary" size="large" :loading="authStore.loginLoading" @click="handleLogin"> + <n-button block type="primary" size="large" :loading="isLoading" @click="handleLogin"> 登录 </n-button> <n-button type="primary" text @click="toOtherForm('register')"> diff --git a/src/views/test/test1/index.vue b/src/views/test/test1/index.vue index edbc169..6f31559 100644 --- a/src/views/test/test1/index.vue +++ b/src/views/test/test1/index.vue @@ -5,6 +5,7 @@ import { FailedResponse, FailedResponseWithoutTip, dictData, + downloadFile, expiredTokenRequest, fetachGet, fetchDelete, @@ -17,14 +18,14 @@ import { } from '@/service' const msg = ref() -const { data, send } = useRequest(fetachGet({ a: 112211 }), { +const { data: fetachGetData, send: sendFetachGet } = useRequest(fetachGet({ a: 112211 }), { // 当immediate为false时,默认不发出 immediate: false, }) function handleRequestHook() { - send() - msg.value = data.value + sendFetachGet() + msg.value = fetachGetData.value } function pinterEnv() { msg.value = import.meta.env @@ -126,6 +127,17 @@ function getBlobFile() { document.body.removeChild(eleLink) }) } +// 下载大文件获取进度 +const downloadPath = ref('https://suqiqi.oss-cn-beijing.aliyuncs.com/test/video/1.mp4') +const { downloading, abort: abortDownloadFile, send: sendDownloadFile } = useRequest(downloadFile(downloadPath.value), { + // 当immediate为false时,默认不发出 + immediate: false, +}) +const downloadProcess = computed(() => { + if (!downloading.value.loaded) + return 0 + return Math.floor(downloading.value.loaded / downloading.value.total * 100) +}) </script> <template> @@ -180,6 +192,18 @@ function getBlobFile() { click </n-button> </n-descriptions-item> + <n-descriptions-item label="带进度的下载文件" span="3"> + <n-input v-model:value="downloadPath" /> + <div>文件大小:{{ downloading.total }}B</div> + <div>已下载:{{ downloading.loaded }}B</div> + <n-progress type="line" indicator-placement="inside" :percentage="downloadProcess" /> + <n-button strong secondary @click="sendDownloadFile"> + 开始下载 + </n-button> + <n-button strong secondary type="warning" @click="abortDownloadFile"> + 中断下载 + </n-button> + </n-descriptions-item> <n-descriptions-item label="转换请求数据"> <n-button strong secondary type="success" @click="getDictData"> click