feat(用户中心): 新增用户中心相关功能模块
实现用户中心完整功能,包括: 1. 新增登录页面及登录逻辑 2. 添加个人资料、修改密码、关于我们等子页面 3. 实现头像上传功能 4. 添加js-cookie依赖处理token存储 5. 完善用户信息类型定义和API接口 6. 新增tabbar"我的"入口及相关路由配置 新增工具函数: 1. 添加auth.ts处理认证相关逻辑 2. 实现toast.ts统一消息提示 3. 添加uploadFile.ts处理文件上传 4. 新增isTableBar判断页面是否为tabbar页
This commit is contained in:
81
src/utils/auth.ts
Normal file
81
src/utils/auth.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import Cookie from 'js-cookie'
|
||||
import { isMpWeixin } from './platform'
|
||||
/**
|
||||
* TokeKey的名字
|
||||
*/
|
||||
const TokenKey: string = 'token'
|
||||
|
||||
/**
|
||||
* 获取tokenKeyName
|
||||
* @returns tokenKeyName
|
||||
*/
|
||||
export const getTokenKey = (): string => {
|
||||
return TokenKey
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否登录,即是否有token,不检查Token是否过期和是否有效
|
||||
* @returns 是否登录
|
||||
*/
|
||||
export const isLogin = () => {
|
||||
return !!getToken()
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Token
|
||||
* @returns 令牌
|
||||
*/
|
||||
export const getToken = () => {
|
||||
return getCookieMap<string>(getTokenKey())
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置Token
|
||||
* @param token 令牌
|
||||
*/
|
||||
export const setToken = (token: string) => {
|
||||
setCookieMap(getTokenKey(), token)
|
||||
}
|
||||
/**
|
||||
* 删除Token
|
||||
*/
|
||||
export const removeToken = () => {
|
||||
removeCookieMap(getTokenKey())
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置Cookie
|
||||
* @param key Cookie的key
|
||||
* @param value Cookie的value
|
||||
*/
|
||||
export const setCookieMap = (key: string, value: any) => {
|
||||
if (isMpWeixin) {
|
||||
uni.setStorageSync(key, value)
|
||||
return
|
||||
}
|
||||
Cookie.set(key, value)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Cookie
|
||||
* @param key Cookie的key
|
||||
* @returns Cookie的value
|
||||
*/
|
||||
export const getCookieMap = <T>(key: string) => {
|
||||
if (isMpWeixin) {
|
||||
return uni.getStorageSync(key) as T
|
||||
}
|
||||
return Cookie.get(key) as T
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除Cookie
|
||||
* @param key Cookie的key
|
||||
*/
|
||||
export const removeCookieMap = (key: string) => {
|
||||
if (isMpWeixin) {
|
||||
uni.removeStorageSync(key)
|
||||
return
|
||||
}
|
||||
Cookie.remove(key)
|
||||
}
|
||||
@@ -23,6 +23,26 @@ export const getIsTabbar = () => {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断指定页面是否是 tabbar 页
|
||||
* @param path 页面路径
|
||||
* @returns true: 是 tabbar 页 false: 不是 tabbar 页
|
||||
*/
|
||||
export const isTableBar = (path: string) => {
|
||||
if (!tabBar) {
|
||||
return false
|
||||
}
|
||||
if (!tabBar.list.length) {
|
||||
// 通常有 tabBar 的话,list 不能有空,且至少有2个元素,这里其实不用处理
|
||||
return false
|
||||
}
|
||||
// 这里需要处理一下 path,因为 tabBar 中的 pagePath 是不带 /pages 前缀的
|
||||
if (path.startsWith('/')) {
|
||||
path = path.substring(1)
|
||||
}
|
||||
return !!tabBar.list.find((e) => e.pagePath === path)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前页面路由的 path 路径和 redirectPath 路径
|
||||
* path 如 '/pages/login/index'
|
||||
|
||||
65
src/utils/toast.ts
Normal file
65
src/utils/toast.ts
Normal 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 }),
|
||||
}
|
||||
338
src/utils/uploadFile.ts
Normal file
338
src/utils/uploadFile.ts
Normal file
@@ -0,0 +1,338 @@
|
||||
import { getToken, getTokenKey } from './auth'
|
||||
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 = {
|
||||
/** 用户头像上传地址 */
|
||||
USER_AVATAR: import.meta.env.VITE_SERVER_BASEURL + '/user/avatar',
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用文件上传函数(支持直接传入文件路径)
|
||||
* @param url 上传地址
|
||||
* @param filePath 本地文件路径
|
||||
* @param formData 额外表单数据
|
||||
* @param options 上传选项
|
||||
*/
|
||||
export const 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: UniApp.UploadFileSuccessCallbackResult) => void
|
||||
/** 上传失败回调函数 */
|
||||
onError?: (err: Error | UniApp.GeneralCallbackResult) => void
|
||||
/** 上传完成回调函数(无论成功失败) */
|
||||
onComplete?: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件上传钩子函数
|
||||
* @template T 上传成功后返回的数据类型
|
||||
* @param url 上传地址
|
||||
* @param formData 额外的表单数据
|
||||
* @param options 上传选项
|
||||
* @returns 上传状态和控制对象
|
||||
*/
|
||||
export const 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: UniApp.UploadFileSuccessCallbackResult) => 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
|
||||
[getTokenKey()]: getToken(), // 添加认证token
|
||||
},
|
||||
// 确保文件名称合法
|
||||
success: (uploadFileRes) => {
|
||||
try {
|
||||
// 解析响应数据
|
||||
const result = JSON.parse(uploadFileRes.data)
|
||||
if (result.code === 1) {
|
||||
// 上传成功
|
||||
data.value = result.data as T
|
||||
onSuccess?.(uploadFileRes)
|
||||
} else {
|
||||
// 业务错误
|
||||
const err = new Error(result.message || '上传失败')
|
||||
error.value = true
|
||||
onError?.(err)
|
||||
}
|
||||
} 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