初始化=商城+金融,用于演示.
This commit is contained in:
166
src/utils/debounce.ts
Normal file
166
src/utils/debounce.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
// fork from https://github.com/toss/es-toolkit/blob/main/src/function/debounce.ts
|
||||
// 文档可查看:https://es-toolkit.dev/reference/function/debounce.html
|
||||
// 如需要 throttle 功能,可 copy https://github.com/toss/es-toolkit/blob/main/src/function/throttle.ts
|
||||
|
||||
interface DebounceOptions {
|
||||
/**
|
||||
* An optional AbortSignal to cancel the debounced function.
|
||||
*/
|
||||
signal?: AbortSignal
|
||||
|
||||
/**
|
||||
* An optional array specifying whether the function should be invoked on the leading edge, trailing edge, or both.
|
||||
* If `edges` includes "leading", the function will be invoked at the start of the delay period.
|
||||
* If `edges` includes "trailing", the function will be invoked at the end of the delay period.
|
||||
* If both "leading" and "trailing" are included, the function will be invoked at both the start and end of the delay period.
|
||||
* @default ["trailing"]
|
||||
*/
|
||||
edges?: Array<'leading' | 'trailing'>
|
||||
}
|
||||
|
||||
export interface DebouncedFunction<F extends (...args: any[]) => void> {
|
||||
(...args: Parameters<F>): void
|
||||
|
||||
/**
|
||||
* Schedules the execution of the debounced function after the specified debounce delay.
|
||||
* This method resets any existing timer, ensuring that the function is only invoked
|
||||
* after the delay has elapsed since the last call to the debounced function.
|
||||
* It is typically called internally whenever the debounced function is invoked.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
schedule: () => void
|
||||
|
||||
/**
|
||||
* Cancels any pending execution of the debounced function.
|
||||
* This method clears the active timer and resets any stored context or arguments.
|
||||
*/
|
||||
cancel: () => void
|
||||
|
||||
/**
|
||||
* Immediately invokes the debounced function if there is a pending execution.
|
||||
* This method executes the function right away if there is a pending execution.
|
||||
*/
|
||||
flush: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a debounced function that delays invoking the provided function until after `debounceMs` milliseconds
|
||||
* have elapsed since the last time the debounced function was invoked. The debounced function also has a `cancel`
|
||||
* method to cancel any pending execution.
|
||||
*
|
||||
* @template F - The type of function.
|
||||
* @param {F} func - The function to debounce.
|
||||
* @param {number} debounceMs - The number of milliseconds to delay.
|
||||
* @param {DebounceOptions} options - The options object
|
||||
* @param {AbortSignal} options.signal - An optional AbortSignal to cancel the debounced function.
|
||||
* @returns A new debounced function with a `cancel` method.
|
||||
*
|
||||
* @example
|
||||
* const debouncedFunction = debounce(() => {
|
||||
* console.log('Function executed');
|
||||
* }, 1000);
|
||||
*
|
||||
* // Will log 'Function executed' after 1 second if not called again in that time
|
||||
* debouncedFunction();
|
||||
*
|
||||
* // Will not log anything as the previous call is canceled
|
||||
* debouncedFunction.cancel();
|
||||
*
|
||||
* // With AbortSignal
|
||||
* const controller = new AbortController();
|
||||
* const signal = controller.signal;
|
||||
* const debouncedWithSignal = debounce(() => {
|
||||
* console.log('Function executed');
|
||||
* }, 1000, { signal });
|
||||
*
|
||||
* debouncedWithSignal();
|
||||
*
|
||||
* // Will cancel the debounced function call
|
||||
* controller.abort();
|
||||
*/
|
||||
export function debounce<F extends (...args: any[]) => void>(
|
||||
func: F,
|
||||
debounceMs: number,
|
||||
{ signal, edges }: DebounceOptions = {},
|
||||
): DebouncedFunction<F> {
|
||||
let pendingThis: any
|
||||
let pendingArgs: Parameters<F> | null = null
|
||||
|
||||
const leading = edges != null && edges.includes('leading')
|
||||
const trailing = edges == null || edges.includes('trailing')
|
||||
|
||||
const invoke = () => {
|
||||
if (pendingArgs !== null) {
|
||||
func.apply(pendingThis, pendingArgs)
|
||||
pendingThis = undefined
|
||||
pendingArgs = null
|
||||
}
|
||||
}
|
||||
|
||||
const onTimerEnd = () => {
|
||||
if (trailing) {
|
||||
invoke()
|
||||
}
|
||||
|
||||
// eslint-disable-next-line ts/no-use-before-define
|
||||
cancel()
|
||||
}
|
||||
|
||||
let timeoutId: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
const schedule = () => {
|
||||
if (timeoutId != null) {
|
||||
clearTimeout(timeoutId)
|
||||
}
|
||||
|
||||
timeoutId = setTimeout(() => {
|
||||
timeoutId = null
|
||||
|
||||
onTimerEnd()
|
||||
}, debounceMs)
|
||||
}
|
||||
|
||||
const cancelTimer = () => {
|
||||
if (timeoutId !== null) {
|
||||
clearTimeout(timeoutId)
|
||||
timeoutId = null
|
||||
}
|
||||
}
|
||||
|
||||
const cancel = () => {
|
||||
cancelTimer()
|
||||
pendingThis = undefined
|
||||
pendingArgs = null
|
||||
}
|
||||
|
||||
const flush = () => {
|
||||
invoke()
|
||||
}
|
||||
|
||||
const debounced = function (this: any, ...args: Parameters<F>) {
|
||||
if (signal?.aborted) {
|
||||
return
|
||||
}
|
||||
|
||||
// eslint-disable-next-line ts/no-this-alias
|
||||
pendingThis = this
|
||||
pendingArgs = args
|
||||
|
||||
const isFirstCall = timeoutId == null
|
||||
|
||||
schedule()
|
||||
|
||||
if (leading && isFirstCall) {
|
||||
invoke()
|
||||
}
|
||||
}
|
||||
|
||||
debounced.schedule = schedule
|
||||
debounced.cancel = cancel
|
||||
debounced.flush = flush
|
||||
|
||||
signal?.addEventListener('abort', cancel, { once: true })
|
||||
|
||||
return debounced
|
||||
}
|
||||
159
src/utils/index.ts
Normal file
159
src/utils/index.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
import type { PageMetaDatum, SubPackages } from '@uni-helper/vite-plugin-uni-pages'
|
||||
import { isMpWeixin } from '@uni-helper/uni-env'
|
||||
import { pages, subPackages } from '@/pages.json'
|
||||
|
||||
export type PageInstance = Page.PageInstance<AnyObject, object> & { $page: Page.PageInstance<AnyObject, object> & { fullPath: string } }
|
||||
|
||||
export function getLastPage() {
|
||||
// getCurrentPages() 至少有1个元素,所以不再额外判断
|
||||
// const lastPage = getCurrentPages().at(-1)
|
||||
// 上面那个在低版本安卓中打包会报错,所以改用下面这个【虽然我加了 src/interceptions/prototype.ts,但依然报错】
|
||||
const pages = getCurrentPages()
|
||||
return pages[pages.length - 1] as PageInstance
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前页面路由的 path 路径和 redirectPath 路径
|
||||
* path 如 '/pages/login/login'
|
||||
* redirectPath 如 '/pages/demo/base/route-interceptor'
|
||||
*/
|
||||
export function currRoute() {
|
||||
const lastPage = getLastPage() as PageInstance
|
||||
if (!lastPage) {
|
||||
return {
|
||||
path: '',
|
||||
query: {},
|
||||
}
|
||||
}
|
||||
const currRoute = lastPage.$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
|
||||
// console.log(fullPath)
|
||||
// eg: /pages/login/login?redirect=%2Fpages%2Fdemo%2Fbase%2Froute-interceptor (小程序)
|
||||
// eg: /pages/login/login?redirect=%2Fpages%2Froute-interceptor%2Findex%3Fname%3Dfeige%26age%3D30(h5)
|
||||
return parseUrlToObj(fullPath)
|
||||
}
|
||||
|
||||
export function ensureDecodeURIComponent(url: string) {
|
||||
if (url.startsWith('%')) {
|
||||
return ensureDecodeURIComponent(decodeURIComponent(url))
|
||||
}
|
||||
return url
|
||||
}
|
||||
/**
|
||||
* 解析 url 得到 path 和 query
|
||||
* 比如输入url: /pages/login/login?redirect=%2Fpages%2Fdemo%2Fbase%2Froute-interceptor
|
||||
* 输出: {path: /pages/login/login, query: {redirect: /pages/demo/base/route-interceptor}}
|
||||
*/
|
||||
export function parseUrlToObj(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 作为判断依据,默认是 excludeLoginPath, 与 route-block 配对使用
|
||||
* 如果没有传 key,则表示所有的 pages,如果传递了 key, 则表示通过 key 过滤
|
||||
*/
|
||||
export function getAllPages(key?: string) {
|
||||
// 这里处理主包
|
||||
const mainPages = (pages as PageMetaDatum[])
|
||||
.filter(page => !key || page[key])
|
||||
.map(page => ({
|
||||
...page,
|
||||
path: `/${page.path}`,
|
||||
}))
|
||||
|
||||
// 这里处理分包
|
||||
const subPages: PageMetaDatum[] = []
|
||||
;(subPackages as SubPackages).forEach((subPageObj) => {
|
||||
// console.log(subPageObj)
|
||||
const { root } = subPageObj
|
||||
|
||||
subPageObj.pages
|
||||
.filter(page => !key || page[key])
|
||||
.forEach((page) => {
|
||||
subPages.push({
|
||||
...page,
|
||||
path: `/${root}/${page.path}`,
|
||||
})
|
||||
})
|
||||
})
|
||||
const result = [...mainPages, ...subPages]
|
||||
// console.log(`getAllPages by ${key} result: `, result)
|
||||
return result
|
||||
}
|
||||
|
||||
export function getCurrentPageI18nKey() {
|
||||
const routeObj = currRoute()
|
||||
const currPage = (pages as PageMetaDatum[]).find(page => `/${page.path}` === routeObj.path)
|
||||
if (!currPage) {
|
||||
console.warn('路由不正确')
|
||||
return ''
|
||||
}
|
||||
console.log(currPage)
|
||||
console.log(currPage.style.navigationBarTitleText)
|
||||
return currPage.style?.navigationBarTitleText || ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据微信小程序当前环境,判断应该获取的 baseUrl
|
||||
*/
|
||||
export function getEnvBaseUrl() {
|
||||
// 请求基准地址
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否是双token模式
|
||||
*/
|
||||
export const isDoubleTokenMode = import.meta.env.VITE_AUTH_MODE === 'double'
|
||||
|
||||
/**
|
||||
* 首页路径,通过 page.json 里面的 type 为 home 的页面获取,如果没有,则默认是第一个页面
|
||||
* 通常为 /pages/index/index
|
||||
*/
|
||||
export const HOME_PAGE = `/${(pages as PageMetaDatum[]).find(page => page.type === 'home')?.path || (pages as PageMetaDatum[])[0].path}`
|
||||
38
src/utils/systemInfo.ts
Normal file
38
src/utils/systemInfo.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
/* eslint-disable import/no-mutable-exports */
|
||||
// 获取屏幕边界到安全区域距离
|
||||
let systemInfo
|
||||
let safeAreaInsets
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
// 微信小程序使用新的API
|
||||
systemInfo = uni.getWindowInfo()
|
||||
safeAreaInsets = systemInfo.safeArea
|
||||
? {
|
||||
top: systemInfo.safeArea.top,
|
||||
right: systemInfo.windowWidth - systemInfo.safeArea.right,
|
||||
bottom: systemInfo.windowHeight - systemInfo.safeArea.bottom,
|
||||
left: systemInfo.safeArea.left,
|
||||
}
|
||||
: null
|
||||
// #endif
|
||||
|
||||
// #ifndef MP-WEIXIN
|
||||
// 其他平台继续使用uni API
|
||||
systemInfo = uni.getSystemInfoSync()
|
||||
safeAreaInsets = systemInfo.safeAreaInsets
|
||||
// #endif
|
||||
|
||||
console.log('systemInfo', systemInfo)
|
||||
// 微信里面打印
|
||||
// pixelRatio: 3
|
||||
// safeArea: {top: 47, left: 0, right: 390, bottom: 810, width: 390, …}
|
||||
// safeAreaInsets: {top: 47, left: 0, right: 0, bottom: 34}
|
||||
// screenHeight: 844
|
||||
// screenTop: 91
|
||||
// screenWidth: 390
|
||||
// statusBarHeight: 47
|
||||
// windowBottom: 0
|
||||
// windowHeight: 753
|
||||
// windowTop: 0
|
||||
// windowWidth: 390
|
||||
export { safeAreaInsets, systemInfo }
|
||||
44
src/utils/toLoginPage.ts
Normal file
44
src/utils/toLoginPage.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { getLastPage } from '@/utils'
|
||||
import { debounce } from '@/utils/debounce'
|
||||
|
||||
interface ToLoginPageOptions {
|
||||
/**
|
||||
* 跳转模式, uni.navigateTo | uni.reLaunch
|
||||
* @default 'navigateTo'
|
||||
*/
|
||||
mode?: 'navigateTo' | 'reLaunch'
|
||||
/**
|
||||
* 查询参数
|
||||
* @example '?redirect=/pages/home/index'
|
||||
*/
|
||||
queryString?: string
|
||||
}
|
||||
|
||||
// TODO: 自己增加登录页
|
||||
const LOGIN_PAGE = '/pages/login/index'
|
||||
|
||||
/**
|
||||
* 跳转到登录页, 带防抖处理
|
||||
*
|
||||
* 如果要立即跳转,不做延时,可以使用 `toLoginPage.flush()` 方法
|
||||
*/
|
||||
export const toLoginPage = debounce((options: ToLoginPageOptions = {}) => {
|
||||
const { mode = 'navigateTo', queryString = '' } = options
|
||||
|
||||
const url = `${LOGIN_PAGE}${queryString}`
|
||||
|
||||
// 获取当前页面路径
|
||||
const currentPage = getLastPage()
|
||||
const currentPath = `/${currentPage.route}`
|
||||
// 如果已经在登录页,则不跳转
|
||||
if (currentPath === LOGIN_PAGE) {
|
||||
return
|
||||
}
|
||||
|
||||
if (mode === 'navigateTo') {
|
||||
uni.navigateTo({ url })
|
||||
}
|
||||
else {
|
||||
uni.reLaunch({ url })
|
||||
}
|
||||
}, 500)
|
||||
29
src/utils/updateManager.wx.ts
Normal file
29
src/utils/updateManager.wx.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
export default () => {
|
||||
if (!wx.canIUse('getUpdateManager')) {
|
||||
return
|
||||
}
|
||||
|
||||
const updateManager = wx.getUpdateManager()
|
||||
|
||||
updateManager.onCheckForUpdate((res) => {
|
||||
// 请求完新版本信息的回调
|
||||
console.log('版本信息', res)
|
||||
})
|
||||
|
||||
updateManager.onUpdateReady(() => {
|
||||
wx.showModal({
|
||||
title: '更新提示',
|
||||
content: '新版本已经准备好,是否重启应用?',
|
||||
success(res) {
|
||||
if (res.confirm) {
|
||||
// 新的版本已经下载好,调用 applyUpdate 应用新版本并重启
|
||||
updateManager.applyUpdate()
|
||||
}
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
updateManager.onUpdateFailed(() => {
|
||||
// 新版本下载失败
|
||||
})
|
||||
}
|
||||
325
src/utils/uploadFile.ts
Normal file
325
src/utils/uploadFile.ts
Normal file
@@ -0,0 +1,325 @@
|
||||
/**
|
||||
* 文件上传钩子函数使用示例
|
||||
* @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 = {
|
||||
/** 用户头像上传地址 */
|
||||
USER_AVATAR: `${import.meta.env.VITE_SERVER_BASEURL}/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) {
|
||||
uni.showToast({
|
||||
title: `文件大小不能超过${maxSize}MB`,
|
||||
icon: 'none',
|
||||
})
|
||||
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('创建上传任务失败'))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user