diff --git a/src/interceptors/index.ts b/src/interceptors/index.ts new file mode 100644 index 0000000..477545a --- /dev/null +++ b/src/interceptors/index.ts @@ -0,0 +1,2 @@ +export { routeInterceptor } from './route' +export { requestInterceptor } from './request' diff --git a/src/interceptors/request.ts b/src/interceptors/request.ts new file mode 100644 index 0000000..8e18f62 --- /dev/null +++ b/src/interceptors/request.ts @@ -0,0 +1,53 @@ +/* eslint-disable no-param-reassign */ +import qs from 'qs' +import { useUserStore } from '@/store' + +export type CustomRequestOptions = UniApp.RequestOptions & { + query?: Record +} & IUniUploadFileOptions // 添加uni.uploadFile参数类型 + +// 请求基地址 +const baseURL = import.meta.env.VITE_SERVER_BASEURL + +// 拦截器配置 +const httpInterceptor = { + // 拦截前触发 + invoke(options: CustomRequestOptions) { + // 接口请求支持通过 query 参数配置 queryString + if (options.query) { + const queryStr = qs.stringify(options.query) + if (options.url.includes('?')) { + options.url += `&${queryStr}` + } else { + options.url += `?${queryStr}` + } + } + + // 1. 非 http 开头需拼接地址 + if (!options.url.startsWith('http')) { + options.url = baseURL + options.url + } + // 2. 请求超时 + options.timeout = 10000 // 10s + // 3. 添加小程序端请求头标识 + options.header = { + platform: 'mp-weixin', // 可选值与 uniapp 定义的平台一致,告诉后台来源 + ...options.header, + } + // 4. 添加 token 请求头标识 + const userStore = useUserStore() + const { token } = userStore.userInfo as unknown as IUserInfo + if (token) { + options.header.Authorization = `Bearer ${token}` + } + }, +} + +export const requestInterceptor = { + install() { + // 拦截 request 请求 + uni.addInterceptor('request', httpInterceptor) + // 拦截 uploadFile 文件上传 + uni.addInterceptor('uploadFile', httpInterceptor) + }, +} diff --git a/src/interceptors/route.ts b/src/interceptors/route.ts new file mode 100644 index 0000000..e103bee --- /dev/null +++ b/src/interceptors/route.ts @@ -0,0 +1,54 @@ +/** + * by 菲鸽 on 2024-03-06 + * 路由拦截,通常也是登录拦截 + * 可以设置路由白名单,或者黑名单,看业务需要选哪一个 + * 我这里应为大部分都可以随便进入,所以使用黑名单 + */ +import { useUserStore } from '@/store' +import { getNeedLoginPages, needLoginPages as _needLoginPages } from '@/utils' + +// TODO Check +const loginRoute = '/pages/login/index' + +const isLogined = () => { + const userStore = useUserStore() + return userStore.isLogined +} + +const isDev = import.meta.env.DEV + +// 黑名单登录拦截器 - (适用于大部分页面不需要登录,少部分页面需要登录) +const navigateToInterceptor = { + // 注意,这里的url是 '/' 开头的,如 '/pages/index/index',跟 'pages.json' 里面的 path 不同 + invoke({ url }: { url: string }) { + console.log(url) // /pages/route-interceptor/index?name=feige&age=30 + const path = url.split('?')[0] + let needLoginPages: string[] = [] + // 为了防止开发时出现BUG,这里每次都获取一下。生产环境可以移到函数外,性能更好 + if (isDev) { + needLoginPages = getNeedLoginPages() + } else { + needLoginPages = _needLoginPages + } + console.log(needLoginPages.includes(path)) + + if (needLoginPages.includes(path)) { + const isLogin = isLogined() + if (isLogin) { + return true + } + const redirectRoute = `${loginRoute}?redirect=${encodeURIComponent(url)}` + uni.navigateTo({ url: redirectRoute }) + return false + } + return true + }, +} + +export const routeInterceptor = { + install() { + uni.addInterceptor('navigateTo', navigateToInterceptor) + uni.addInterceptor('reLaunch', navigateToInterceptor) + uni.addInterceptor('redirectTo', navigateToInterceptor) + }, +} diff --git a/src/main.ts b/src/main.ts index 8f52bb4..290b7be 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3,6 +3,7 @@ import uvUI from '@climblee/uv-ui' import App from './App.vue' import store from './store' import i18n from './locale/index' +import { routeInterceptor, requestInterceptor } from './interceptors' import 'virtual:svg-icons-register' import 'virtual:uno.css' import '@/style/index.scss' @@ -12,6 +13,8 @@ export function createApp() { app.use(store) app.use(i18n) app.use(uvUI) + app.use(routeInterceptor) + app.use(requestInterceptor) return { app, } diff --git a/src/store/user.ts b/src/store/user.ts index 9118687..b5eca5f 100644 --- a/src/store/user.ts +++ b/src/store/user.ts @@ -14,16 +14,19 @@ export const useUserStore = defineStore( } const clearUserInfo = () => { - userInfo.value = undefined + userInfo.value = { ...initState } } + // 一般没有reset需求,不需要的可以删除 const reset = () => { userInfo.value = { ...initState } } + const isLogined = computed(() => !!userInfo.value.token) return { userInfo, setUserInfo, clearUserInfo, + isLogined, reset, } }, diff --git a/src/utils/http.ts b/src/utils/http.ts index 04cd7a4..c34a6d3 100644 --- a/src/utils/http.ts +++ b/src/utils/http.ts @@ -1,52 +1,4 @@ -/* eslint-disable no-param-reassign */ -import qs from 'qs' -import { useUserStore } from '@/store' -import { IResData, IUserInfo } from '@/typings' - -type CustomRequestOptions = UniApp.RequestOptions & { query?: Record } - -// 请求基地址 -const baseURL = import.meta.env.VITE_SERVER_BASEURL -// console.log(import.meta.env) - -// 拦截器配置 -const httpInterceptor = { - // 拦截前触发 - invoke(options: CustomRequestOptions) { - // 接口请求支持通过 query 参数配置 queryString - if (options.query) { - const queryStr = qs.stringify(options.query) - if (options.url.includes('?')) { - options.url += `&${queryStr}` - } else { - options.url += `?${queryStr}` - } - } - - // 1. 非 http 开头需拼接地址 - if (!options.url.startsWith('http')) { - options.url = baseURL + options.url - } - // 2. 请求超时 - options.timeout = 10000 // 10s - // 3. 添加小程序端请求头标识 - options.header = { - platform: 'mp-weixin', // 可选值与 uniapp 定义的平台一致,告诉后台来源 - ...options.header, - } - // 4. 添加 token 请求头标识 - const userStore = useUserStore() - const { token } = userStore.userInfo as unknown as IUserInfo - if (token) { - options.header.Authorization = `Bearer ${token}` - } - }, -} - -// 拦截 request 请求 -uni.addInterceptor('request', httpInterceptor) -// 拦截 uploadFile 文件上传 -uni.addInterceptor('uploadFile', httpInterceptor) +import { CustomRequestOptions } from '@/interceptors/request' export const http = (options: CustomRequestOptions) => { // 1. 返回 Promise 对象 @@ -89,4 +41,41 @@ export const http = (options: CustomRequestOptions) => { }) } -export default http +// uni.uploadFile封装 +export const uniFileUpload = (options: CustomRequestOptions) => { + // 1. 返回 Promise 对象 + return new Promise>((resolve, reject) => { + uni.uploadFile({ + ...options, + // 响应成功 + success(res) { + // 状态码 2xx,参考 axios 的设计 + if (res.statusCode >= 200 && res.statusCode < 300) { + // 文件上传接口的rea.data的类型为string,这里转一下 + const resData = JSON.parse(res.data) as IResData + resolve(resData) + } else if (res.statusCode === 401) { + // 401错误 -> 清理用户信息,跳转到登录页 + // userStore.clearUserInfo() + // uni.navigateTo({ url: '/pages/login/login' }) + reject(res) + } else { + // 其他错误 -> 根据后端错误信息轻提示 + uni.showToast({ + icon: 'none', + title: '文件上传错误', + }) + reject(res) + } + }, + // 响应失败 + fail(err) { + uni.showToast({ + icon: 'none', + title: '网络错误,换个网络试试', + }) + reject(err) + }, + }) + }) +} diff --git a/src/utils/index.ts b/src/utils/index.ts index a6f8265..6cf3d2b 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -25,6 +25,98 @@ export const testI18n = () => { content: t('app.name'), }) } +/* + * 获取当前页面路由的 path 路劲和 redirectPath 路径 + * path 如 ‘/pages/login/index’ + * redirectPath 如 ‘/pages/demo/base/route-interceptor’ + */ +export const currRoute = () => { + const pages = getCurrentPages() + console.log('pages:', pages) + + const lastPage = getArrElementByIdx(pages, -1) + const currRoute = (lastPage as any).$page + // console.log('lastPage.$page:', currRoute) + // console.log('lastPage.$page.fullpath:', currRoute.fullPath) + // console.log('lastPage.$page.options:', currRoute.options) + // console.log('lastPage.options:', (lastPage as any).options) + // 经过多端测试,只有 fullPath 靠谱,其他都不靠谱 + const { fullPath } = currRoute as { fullPath: string } + console.log(fullPath) + // eg: /pages/login/index?redirect=%2Fpages%2Fdemo%2Fbase%2Froute-interceptor (小程序) + // eg: /pages/login/index?redirect=%2Fpages%2Froute-interceptor%2Findex%3Fname%3Dfeige%26age%3D30(h5) + return getUrlObj(fullPath) +} + +const ensureDecodeURIComponent = (url: string) => { + if (url.startsWith('%')) { + return ensureDecodeURIComponent(decodeURIComponent(url)) + } + return url +} +/** + * 解析 url 得到 path 和 query + * 比如输入url: /pages/login/index?redirect=%2Fpages%2Fdemo%2Fbase%2Froute-interceptor + * 输出: {path: /pages/login/index, query: {redirect: /pages/demo/base/route-interceptor}} + */ +export const getUrlObj = (url: string) => { + const [path, queryStr] = url.split('?') + console.log(path, queryStr) + + const query: Record = {} + queryStr.split('&').forEach((item) => { + const [key, value] = item.split('=') + console.log(key, value) + query[key] = ensureDecodeURIComponent(value) // 这里需要统一 decodeURIComponent 一下,可以兼容h5和微信y + }) + return { path, query } +} +/** + * 得到所有的需要登录的pages,包括主包和分包的 + * 这里设计得通用一点,可以传递key作为判断依据,默认是 needLogin, 与 route-block 配对使用 + * 如果没有传 key,则表示所有的pages,如果传递了 key, 则表示通过 key 过滤 + */ +export const getAllPages = (key = 'needLogin') => { + // 这里处理主包 + const pages = [ + ...pagesJson.pages + .filter((page) => !key || page[key]) + .map((page) => ({ + ...page, + path: `/${page.path}`, + })), + ] + // 这里处理分包 + const subPages: any[] = [] + pagesJson.subPackages.forEach((subPageObj) => { + // console.log(subPageObj) + const { root } = subPageObj + + subPageObj.pages + .filter((page) => !key || page[key]) + .forEach((page: { path: string } & Record) => { + subPages.push({ + ...page, + path: `/${root}/${page.path}`, + }) + }) + }) + const result = [...pages, ...subPages] + console.log(`getAllPages by ${key} result: `, result) + return result +} + +/** + * 得到所有的需要登录的pages,包括主包和分包的 + * 只得到 path 数组 + */ +export const getNeedLoginPages = (): string[] => getAllPages('needLogin').map((page) => page.path) + +/** + * 得到所有的需要登录的pages,包括主包和分包的 + * 只得到 path 数组 + */ +export const needLoginPages: string[] = getAllPages('needLogin').map((page) => page.path) export const getArrElementByIdx = (arr: any[], index: number) => { if (index < 0) return arr[arr.length + index]