update:调整文件夹

This commit is contained in:
hrz
2025-08-11 15:48:28 +08:00
parent 6e8f9ca22e
commit 8509e62114
138 changed files with 36 additions and 93 deletions
+198
View File
@@ -0,0 +1,198 @@
import { pages, subPackages } from '@/pages.json'
import { isMpWeixin } from './platform'
/**
* 运行时服务端地址覆盖存储键
*/
export const SERVER_BASE_URL_OVERRIDE_KEY = 'server_base_url_override'
/**
* 设置/清除/获取 运行时覆盖的服务端地址
*/
export function setServerBaseUrlOverride(url: string) {
uni.setStorageSync(SERVER_BASE_URL_OVERRIDE_KEY, url)
}
export function clearServerBaseUrlOverride() {
uni.removeStorageSync(SERVER_BASE_URL_OVERRIDE_KEY)
}
export function getServerBaseUrlOverride(): string | null {
const value = uni.getStorageSync(SERVER_BASE_URL_OVERRIDE_KEY)
return value || null
}
export function getLastPage() {
// getCurrentPages() 至少有1个元素,所以不再额外判断
// const lastPage = getCurrentPages().at(-1)
// 上面那个在低版本安卓中打包会报错,所以改用下面这个【虽然我加了 src/interceptions/prototype.ts,但依然报错】
const pages = getCurrentPages()
return pages[pages.length - 1]
}
/**
* 获取当前页面路由的 path 路径和 redirectPath 路径
* path 如 '/pages/login/index'
* redirectPath 如 '/pages/demo/base/route-interceptor'
*/
export function currRoute() {
const lastPage = getLastPage()
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)
}
function 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 function getUrlObj(url: string) {
const [path, queryStr] = url.split('?')
// console.log(path, queryStr)
if (!queryStr) {
return {
path,
query: {},
}
}
const query: Record<string, string> = {}
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 function getAllPages(key = 'needLogin') {
// 这里处理主包
const mainPages = pages
.filter(page => !key || page[key])
.map(page => ({
...page,
path: `/${page.path}`,
}))
// 这里处理分包
const subPages: any[] = []
subPackages.forEach((subPageObj) => {
// console.log(subPageObj)
const { root } = subPageObj
subPageObj.pages
.filter(page => !key || page[key])
.forEach((page: { path: string } & Record<string, any>) => {
subPages.push({
...page,
path: `/${root}/${page.path}`,
})
})
})
const result = [...mainPages, ...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)
/**
* 根据微信小程序当前环境,判断应该获取的 baseUrl
*/
export function getEnvBaseUrl() {
// 若存在用户设置的覆盖地址,优先返回
const override = getServerBaseUrlOverride()
if (override)
return override
// 请求基准地址(默认来源于 env)
let baseUrl = import.meta.env.VITE_SERVER_BASEURL
// # 有些同学可能需要在微信小程序里面根据 develop、trial、release 分别设置上传地址,参考代码如下。
const VITE_SERVER_BASEURL__WEIXIN_DEVELOP = 'https://ukw0y1.laf.run'
const VITE_SERVER_BASEURL__WEIXIN_TRIAL = 'https://ukw0y1.laf.run'
const VITE_SERVER_BASEURL__WEIXIN_RELEASE = 'https://ukw0y1.laf.run'
// 微信小程序端环境区分
if (isMpWeixin) {
const {
miniProgram: { envVersion },
} = uni.getAccountInfoSync()
switch (envVersion) {
case 'develop':
baseUrl = VITE_SERVER_BASEURL__WEIXIN_DEVELOP || baseUrl
break
case 'trial':
baseUrl = VITE_SERVER_BASEURL__WEIXIN_TRIAL || baseUrl
break
case 'release':
baseUrl = VITE_SERVER_BASEURL__WEIXIN_RELEASE || baseUrl
break
}
}
return baseUrl
}
/**
* 根据微信小程序当前环境,判断应该获取的 UPLOAD_BASEURL
*/
export function getEnvBaseUploadUrl() {
// 请求基准地址
let baseUploadUrl = import.meta.env.VITE_UPLOAD_BASEURL
const VITE_UPLOAD_BASEURL__WEIXIN_DEVELOP = 'https://ukw0y1.laf.run/upload'
const VITE_UPLOAD_BASEURL__WEIXIN_TRIAL = 'https://ukw0y1.laf.run/upload'
const VITE_UPLOAD_BASEURL__WEIXIN_RELEASE = 'https://ukw0y1.laf.run/upload'
// 微信小程序端环境区分
if (isMpWeixin) {
const {
miniProgram: { envVersion },
} = uni.getAccountInfoSync()
switch (envVersion) {
case 'develop':
baseUploadUrl = VITE_UPLOAD_BASEURL__WEIXIN_DEVELOP || baseUploadUrl
break
case 'trial':
baseUploadUrl = VITE_UPLOAD_BASEURL__WEIXIN_TRIAL || baseUploadUrl
break
case 'release':
baseUploadUrl = VITE_UPLOAD_BASEURL__WEIXIN_RELEASE || baseUploadUrl
break
}
}
return baseUploadUrl
}
+24
View File
@@ -0,0 +1,24 @@
/*
* @Author: 菲鸽
* @Date: 2024-03-28 19:13:55
* @Last Modified by: 菲鸽
* @Last Modified time: 2024-03-28 19:24:55
*/
export const platform = __UNI_PLATFORM__
export const isH5 = __UNI_PLATFORM__ === 'h5'
export const isApp = __UNI_PLATFORM__ === 'app'
export const isMp = __UNI_PLATFORM__.startsWith('mp-')
export const isMpWeixin = __UNI_PLATFORM__.startsWith('mp-weixin')
export const isMpAplipay = __UNI_PLATFORM__.startsWith('mp-alipay')
export const isMpToutiao = __UNI_PLATFORM__.startsWith('mp-toutiao')
const PLATFORM = {
platform,
isH5,
isApp,
isMp,
isMpWeixin,
isMpAplipay,
isMpToutiao,
}
export default PLATFORM
+78
View File
@@ -0,0 +1,78 @@
import type { CustomRequestOptions } from '@/http/interceptor'
/**
* 请求方法: 主要是对 uni.request 的封装,去适配 openapi-ts-request 的 request 方法
* @param options 请求参数
* @returns 返回 Promise 对象
*/
function http<T>(options: CustomRequestOptions) {
// 1. 返回 Promise 对象
return new Promise<T>((resolve, reject) => {
uni.request({
...options,
dataType: 'json',
// #ifndef MP-WEIXIN
responseType: 'json',
// #endif
// 响应成功
success(res) {
// 状态码 2xx,参考 axios 的设计
if (res.statusCode >= 200 && res.statusCode < 300) {
// 2.1 提取核心数据 res.data
resolve(res.data as T)
}
else if (res.statusCode === 401) {
// 401错误 -> 清理用户信息,跳转到登录页
// userStore.clearUserInfo()
// uni.navigateTo({ url: '/pages/login/login' })
reject(res)
}
else {
// 其他错误 -> 根据后端错误信息轻提示
!options.hideErrorToast
&& uni.showToast({
icon: 'none',
title: (res.data as T & { msg?: string })?.msg || '请求错误',
})
reject(res)
}
},
// 响应失败
fail(err) {
uni.showToast({
icon: 'none',
title: '网络错误,换个网络试试',
})
reject(err)
},
})
})
}
/*
* openapi-ts-request 工具的 request 跨客户端适配方法
*/
export default function request<T = unknown>(
url: string,
options: Omit<CustomRequestOptions, 'url'> & {
params?: Record<string, unknown>
headers?: Record<string, unknown>
},
) {
const requestOptions = {
url,
...options,
}
if (options.params) {
requestOptions.query = requestOptions.params
delete requestOptions.params
}
if (options.headers) {
requestOptions.header = options.headers
delete requestOptions.headers
}
return http<T>(requestOptions)
}
+65
View File
@@ -0,0 +1,65 @@
/**
* toast 弹窗组件
* 支持 success/error/warning/info 四种状态
* 可配置 duration, position 等参数
*/
type ToastType = 'success' | 'error' | 'warning' | 'info'
interface ToastOptions {
type?: ToastType
duration?: number
position?: 'top' | 'middle' | 'bottom'
icon?: 'success' | 'error' | 'none' | 'loading' | 'fail' | 'exception'
message: string
}
export function showToast(options: ToastOptions | string) {
const defaultOptions: ToastOptions = {
type: 'info',
duration: 2000,
position: 'middle',
message: '',
}
const mergedOptions
= typeof options === 'string'
? { ...defaultOptions, message: options }
: { ...defaultOptions, ...options }
// 映射position到uniapp支持的格式
const positionMap: Record<ToastOptions['position'], 'top' | 'bottom' | 'center'> = {
top: 'top',
middle: 'center',
bottom: 'bottom',
}
// 映射图标类型
const iconMap: Record<
ToastType,
'success' | 'error' | 'none' | 'loading' | 'fail' | 'exception'
> = {
success: 'success',
error: 'error',
warning: 'fail',
info: 'none',
}
// 调用uni.showToast显示提示
uni.showToast({
title: mergedOptions.message,
duration: mergedOptions.duration,
position: positionMap[mergedOptions.position],
icon: mergedOptions.icon || iconMap[mergedOptions.type],
mask: true,
})
}
export const toast = {
success: (message: string, options?: Omit<ToastOptions, 'type'>) =>
showToast({ ...options, type: 'success', message }),
error: (message: string, options?: Omit<ToastOptions, 'type'>) =>
showToast({ ...options, type: 'error', message }),
warning: (message: string, options?: Omit<ToastOptions, 'type'>) =>
showToast({ ...options, type: 'warning', message }),
info: (message: string, options?: Omit<ToastOptions, 'type'>) =>
showToast({ ...options, type: 'info', message }),
}
+327
View File
@@ -0,0 +1,327 @@
import { getEnvBaseUrl } from './index'
import { toast } from './toast'
/**
* 文件上传钩子函数使用示例
* @example
* const { loading, error, data, progress, run } = useUpload<IUploadResult>(
* uploadUrl,
* {},
* {
* maxSize: 5, // 最大5MB
* sourceType: ['album'], // 仅支持从相册选择
* onProgress: (p) => console.log(`上传进度:${p}%`),
* onSuccess: (res) => console.log('上传成功', res),
* onError: (err) => console.error('上传失败', err),
* },
* )
*/
/**
* 上传文件的URL配置
*/
export const uploadFileUrl = {
/** 用户头像上传地址(动态读取当前生效的 BaseURL) */
get USER_AVATAR() {
return `${getEnvBaseUrl()}/user/avatar`
},
}
/**
* 通用文件上传函数(支持直接传入文件路径)
* @param url 上传地址
* @param filePath 本地文件路径
* @param formData 额外表单数据
* @param options 上传选项
*/
export function useFileUpload<T = string>(url: string, filePath: string, formData: Record<string, any> = {}, options: Omit<UploadOptions, 'sourceType' | 'sizeType' | 'count'> = {}) {
return useUpload<T>(
url,
formData,
{
...options,
sourceType: ['album'],
sizeType: ['original'],
},
filePath,
)
}
export interface UploadOptions {
/** 最大可选择的图片数量,默认为1 */
count?: number
/** 所选的图片的尺寸,original-原图,compressed-压缩图 */
sizeType?: Array<'original' | 'compressed'>
/** 选择图片的来源,album-相册,camera-相机 */
sourceType?: Array<'album' | 'camera'>
/** 文件大小限制,单位:MB */
maxSize?: number //
/** 上传进度回调函数 */
onProgress?: (progress: number) => void
/** 上传成功回调函数 */
onSuccess?: (res: Record<string, any>) => void
/** 上传失败回调函数 */
onError?: (err: Error | UniApp.GeneralCallbackResult) => void
/** 上传完成回调函数(无论成功失败) */
onComplete?: () => void
}
/**
* 文件上传钩子函数
* @template T 上传成功后返回的数据类型
* @param url 上传地址
* @param formData 额外的表单数据
* @param options 上传选项
* @returns 上传状态和控制对象
*/
export function useUpload<T = string>(url: string, formData: Record<string, any> = {}, options: UploadOptions = {},
/** 直接传入文件路径,跳过选择器 */
directFilePath?: string) {
/** 上传中状态 */
const loading = ref(false)
/** 上传错误状态 */
const error = ref(false)
/** 上传成功后的响应数据 */
const data = ref<T>()
/** 上传进度(0-100 */
const progress = ref(0)
/** 解构上传选项,设置默认值 */
const {
/** 最大可选择的图片数量 */
count = 1,
/** 所选的图片的尺寸 */
sizeType = ['original', 'compressed'],
/** 选择图片的来源 */
sourceType = ['album', 'camera'],
/** 文件大小限制(MB */
maxSize = 10,
/** 进度回调 */
onProgress,
/** 成功回调 */
onSuccess,
/** 失败回调 */
onError,
/** 完成回调 */
onComplete,
} = options
/**
* 检查文件大小是否超过限制
* @param size 文件大小(字节)
* @returns 是否通过检查
*/
const checkFileSize = (size: number) => {
const sizeInMB = size / 1024 / 1024
if (sizeInMB > maxSize) {
toast.warning(`文件大小不能超过${maxSize}MB`)
return false
}
return true
}
/**
* 触发文件选择和上传
* 根据平台使用不同的选择器:
* - 微信小程序使用 chooseMedia
* - 其他平台使用 chooseImage
*/
const run = () => {
if (directFilePath) {
// 直接使用传入的文件路径
loading.value = true
progress.value = 0
uploadFile<T>({
url,
tempFilePath: directFilePath,
formData,
data,
error,
loading,
progress,
onProgress,
onSuccess,
onError,
onComplete,
})
return
}
// #ifdef MP-WEIXIN
// 微信小程序环境下使用 chooseMedia API
uni.chooseMedia({
count,
mediaType: ['image'], // 仅支持图片类型
sourceType,
success: (res) => {
const file = res.tempFiles[0]
// 检查文件大小是否符合限制
if (!checkFileSize(file.size))
return
// 开始上传
loading.value = true
progress.value = 0
uploadFile<T>({
url,
tempFilePath: file.tempFilePath,
formData,
data,
error,
loading,
progress,
onProgress,
onSuccess,
onError,
onComplete,
})
},
fail: (err) => {
console.error('选择媒体文件失败:', err)
error.value = true
onError?.(err)
},
})
// #endif
// #ifndef MP-WEIXIN
// 非微信小程序环境下使用 chooseImage API
uni.chooseImage({
count,
sizeType,
sourceType,
success: (res) => {
console.log('选择图片成功:', res)
// 开始上传
loading.value = true
progress.value = 0
uploadFile<T>({
url,
tempFilePath: res.tempFilePaths[0],
formData,
data,
error,
loading,
progress,
onProgress,
onSuccess,
onError,
onComplete,
})
},
fail: (err) => {
console.error('选择图片失败:', err)
error.value = true
onError?.(err)
},
})
// #endif
}
return { loading, error, data, progress, run }
}
/**
* 文件上传选项接口
* @template T 上传成功后返回的数据类型
*/
interface UploadFileOptions<T> {
/** 上传地址 */
url: string
/** 临时文件路径 */
tempFilePath: string
/** 额外的表单数据 */
formData: Record<string, any>
/** 上传成功后的响应数据 */
data: Ref<T | undefined>
/** 上传错误状态 */
error: Ref<boolean>
/** 上传中状态 */
loading: Ref<boolean>
/** 上传进度(0-100 */
progress: Ref<number>
/** 上传进度回调 */
onProgress?: (progress: number) => void
/** 上传成功回调 */
onSuccess?: (res: Record<string, any>) => void
/** 上传失败回调 */
onError?: (err: Error | UniApp.GeneralCallbackResult) => void
/** 上传完成回调 */
onComplete?: () => void
}
/**
* 执行文件上传
* @template T 上传成功后返回的数据类型
* @param options 上传选项
*/
function uploadFile<T>({
url,
tempFilePath,
formData,
data,
error,
loading,
progress,
onProgress,
onSuccess,
onError,
onComplete,
}: UploadFileOptions<T>) {
try {
// 创建上传任务
const uploadTask = uni.uploadFile({
url,
filePath: tempFilePath,
name: 'file', // 文件对应的 key
formData,
header: {
// H5环境下不需要手动设置Content-Type,让浏览器自动处理multipart格式
// #ifndef H5
'Content-Type': 'multipart/form-data',
// #endif
},
// 确保文件名称合法
success: (uploadFileRes) => {
console.log('上传文件成功:', uploadFileRes)
try {
// 解析响应数据
const { data: _data } = JSON.parse(uploadFileRes.data)
// 上传成功
data.value = _data as T
onSuccess?.(_data)
}
catch (err) {
// 响应解析错误
console.error('解析上传响应失败:', err)
error.value = true
onError?.(new Error('上传响应解析失败'))
}
},
fail: (err) => {
// 上传请求失败
console.error('上传文件失败:', err)
error.value = true
onError?.(err)
},
complete: () => {
// 无论成功失败都执行
loading.value = false
onComplete?.()
},
})
// 监听上传进度
uploadTask.onProgressUpdate((res) => {
progress.value = res.progress
onProgress?.(res.progress)
})
}
catch (err) {
// 创建上传任务失败
console.error('创建上传任务失败:', err)
error.value = true
loading.value = false
onError?.(new Error('创建上传任务失败'))
}
}