设置页面上传头像实现-多余测试页面和功能删减

This commit is contained in:
WindowBird 2025-08-20 11:57:07 +08:00
parent 52bdaaf789
commit 376dff48f6
7 changed files with 173 additions and 542 deletions

163
README_AVATAR_UPLOAD.md Normal file
View File

@ -0,0 +1,163 @@
# 头像上传功能说明
## 功能概述
本功能允许用户上传和更换个人头像,支持从相册选择或拍照两种方式。
## API接口
- **接口地址**: `PUT /app/user/avatar`
- **请求格式**: `form-data`
- **参数名**: `avatarfile`
- **参数类型**: `file`
## 实现文件
1. **API函数**: `api/user/user.js` - `uploadAvatar()`
2. **页面组件**: `pages/set/set.vue` - 头像设置页面
3. **请求工具**: `utils/request.js` - 统一请求处理(包含文件上传方法)
## 使用流程
### 1. 用户操作流程
1. 进入设置页面 (`pages/set/set.vue`)
2. 点击"头像"设置项
3. 选择"从相册选择"或"拍照"
4. 选择或拍摄图片
5. 系统自动上传并更新头像
### 2. 技术实现流程
1. **选择图片**: 使用 `uni.chooseImage()` 选择图片
2. **上传文件**: 使用 `utils/request.js` 中的 `uploadFile()` 方法上传到服务器
3. **更新显示**: 更新本地存储和页面显示
4. **通知更新**: 通过事件通知其他页面更新头像
## 关键代码
### 统一文件上传方法 (utils/request.js)
```javascript
export function uploadFile(url, filePath, name = 'file', formData = {}, options = {}) {
// 自动获取token和配置
// 统一错误处理
// 支持超时设置
// 自动解析响应
}
```
### API函数 (api/user/user.js)
```javascript
export function uploadAvatar(filePath) {
return uploadFile('/app/user/avatar', filePath, 'avatarfile', {}, {
timeout: 60000
}).then(data => {
// 更新本地存储
// 通知其他页面
return data
})
}
```
### 页面方法 (pages/set/set.vue)
```javascript
// 显示头像选择器
showAvatarPicker() {
uni.showActionSheet({
itemList: ['从相册选择', '拍照'],
success: (res) => {
if (res.tapIndex === 0) {
this.chooseImage('album')
} else if (res.tapIndex === 1) {
this.chooseImage('camera')
}
}
})
}
// 选择图片
chooseImage(sourceType) {
uni.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: [sourceType],
success: (res) => {
this.uploadAvatarFile(res.tempFilePaths[0])
}
})
}
```
## 技术优势
### 1. 统一请求管理
- ✅ 使用 `utils/request.js` 中的 `uploadFile()` 方法
- ✅ 自动获取token和配置
- ✅ 统一的错误处理和响应解析
- ✅ 支持超时设置和自定义配置
### 2. 简化API调用
- ✅ API函数代码更简洁
- ✅ 专注于业务逻辑处理
- ✅ 自动处理token和认证
- ✅ 统一的错误处理机制
### 3. 更好的维护性
- ✅ 所有网络请求都通过统一的工具管理
- ✅ 配置变更只需修改一处
- ✅ 错误处理逻辑统一
- ✅ 便于调试和监控
## 权限配置
### Android权限
已在 `manifest.json` 中添加:
- `READ_EXTERNAL_STORAGE` - 读取外部存储
- `WRITE_EXTERNAL_STORAGE` - 写入外部存储
- `CAMERA` - 相机权限
### 微信小程序权限
建议在 `manifest.json` 中添加:
```json
"permission": {
"scope.writePhotosAlbum": {
"desc": "用于保存头像图片"
}
}
```
## 测试方法
### 1. 功能测试
1. 点击设置页面的头像设置项
2. 选择一张图片
3. 查看控制台日志确认上传过程
4. 验证头像是否成功更新
### 2. 调试信息
- 控制台会输出详细的请求和响应日志
- 包括文件路径、请求头、响应数据等信息
- 统一的错误处理和提示
## 注意事项
1. **文件大小**: 建议限制上传文件大小,避免过大文件影响性能
2. **文件格式**: 建议只允许图片格式jpg, png, gif等
3. **网络处理**: 已添加超时和错误处理机制
4. **权限检查**: 在Android和iOS上需要相应的存储和相机权限
5. **响应格式**: 服务器应返回标准的JSON响应格式
## 错误处理
常见错误及解决方案:
- **网络错误**: 检查网络连接和服务器状态
- **权限错误**: 确保已授予存储和相机权限
- **文件格式错误**: 确保上传的是有效的图片文件
- **服务器错误**: 检查服务器端接口是否正常工作
- **Token错误**: 确保用户已登录且token有效
## 扩展功能
可以考虑添加的功能:
1. 图片压缩和裁剪
2. 上传进度显示
3. 多图片上传
4. 图片预览功能
5. 上传历史记录
6. 断点续传
7. 文件类型验证

View File

@ -1,6 +1,6 @@
import request from '@/utils/request' import request from '@/utils/request'
import { mockUserInfo, mockFinancialData, mockAgentStats, mockAgentList, mockWithdrawInfo, mockBanks, createMockResponse } from './mockData.js' import { mockUserInfo, mockFinancialData, mockAgentStats, mockAgentList, mockWithdrawInfo, mockBanks, createMockResponse } from './mockData.js'
import { uploadFile, uploadFileWithPut } from '@/utils/request.js' import { uploadFile } from '@/utils/request.js'
/** /**
* 获取用户信息 * 获取用户信息
@ -186,9 +186,7 @@ export function submitWithdraw(data) {
* @returns {Promise} 返回上传结果 * @returns {Promise} 返回上传结果
*/ */
export function uploadAvatar(filePath) { export function uploadAvatar(filePath) {
// 由于服务端只支持PUT方法我们直接使用PUT方法 return uploadFile('/app/user/avatar', filePath, 'avatarfile', {}, {
// 但需要修复multipart格式问题
return uploadFileWithPut('/app/user/avatar', filePath, 'avatarfile', {}, {
timeout: 60000 timeout: 60000
}).then(data => { }).then(data => {
// 上传成功后更新本地存储 // 上传成功后更新本地存储

View File

@ -1,297 +0,0 @@
/**
* 头像上传调试脚本
* 用于分析和解决头像上传问题
*/
// 调试配置
const DEBUG_CONFIG = {
baseUrl: 'http://192.168.2.143:4601',
uploadUrl: '/app/user/avatar',
timeout: 60000
}
// 调试工具类
class UploadDebugger {
constructor() {
this.logs = []
this.testResults = []
}
// 添加日志
addLog(type, message, data = null) {
const log = {
type,
message,
data,
timestamp: new Date().toISOString()
}
this.logs.push(log)
console.log(`[${log.timestamp}] ${type.toUpperCase()}: ${message}`, data || '')
}
// 检查token
checkToken() {
try {
const token = uni.getStorageSync('token')
const result = {
exists: !!token,
length: token ? token.length : 0,
preview: token ? token.substring(0, 20) + '...' : 'none',
valid: this.validateToken(token)
}
this.addLog('info', 'Token检查完成', result)
return result
} catch (error) {
this.addLog('error', 'Token检查失败', error)
return { exists: false, error: error.message }
}
}
// 验证token格式
validateToken(token) {
if (!token) return false
// 检查是否是JWT格式
const parts = token.split('.')
if (parts.length === 3) {
try {
// 尝试解码payload部分
const payload = JSON.parse(atob(parts[1]))
return payload && payload.exp && payload.exp > Date.now() / 1000
} catch (e) {
return false
}
}
return true // 如果不是JWT格式假设是其他格式的token
}
// 检查网络连接
checkNetwork() {
return new Promise((resolve) => {
uni.getNetworkType({
success: (res) => {
this.addLog('info', '网络状态检查', res)
resolve(res)
},
fail: (err) => {
this.addLog('error', '网络状态检查失败', err)
resolve({ networkType: 'unknown', error: err })
}
})
})
}
// 检查服务器连接
checkServerConnection() {
return new Promise((resolve) => {
uni.request({
url: DEBUG_CONFIG.baseUrl + '/health', // 假设有健康检查接口
method: 'GET',
timeout: 5000,
success: (res) => {
this.addLog('info', '服务器连接检查成功', res)
resolve({ connected: true, status: res.statusCode })
},
fail: (err) => {
this.addLog('error', '服务器连接检查失败', err)
resolve({ connected: false, error: err })
}
})
})
}
// 测试文件上传
async testFileUpload(filePath) {
this.addLog('info', '开始文件上传测试', { filePath })
try {
// 检查文件信息
const fileInfo = await this.getFileInfo(filePath)
this.addLog('info', '文件信息', fileInfo)
// 检查文件大小
if (fileInfo.size > 10 * 1024 * 1024) { // 10MB
this.addLog('warn', '文件过大,可能影响上传', { size: fileInfo.size })
}
// 执行上传
const result = await this.performUpload(filePath)
this.addLog('info', '上传测试完成', result)
return result
} catch (error) {
this.addLog('error', '上传测试失败', error)
throw error
}
}
// 获取文件信息
getFileInfo(filePath) {
return new Promise((resolve, reject) => {
uni.getFileInfo({
filePath: filePath,
success: resolve,
fail: reject
})
})
}
// 执行上传
performUpload(filePath) {
return new Promise((resolve, reject) => {
const token = uni.getStorageSync('token')
// 构建请求头
let authorization = token
// #ifdef H5
authorization = token ? `Bearer ${token}` : ''
// #endif
const header = {
Authorization: authorization
}
this.addLog('info', '开始上传请求', {
url: DEBUG_CONFIG.baseUrl + DEBUG_CONFIG.uploadUrl,
header: header,
filePath: filePath
})
uni.uploadFile({
url: DEBUG_CONFIG.baseUrl + DEBUG_CONFIG.uploadUrl,
filePath: filePath,
name: 'avatarfile',
header: header,
timeout: DEBUG_CONFIG.timeout,
success: (res) => {
this.addLog('info', '上传响应', {
statusCode: res.statusCode,
header: res.header,
data: res.data
})
try {
const data = JSON.parse(res.data)
if (res.statusCode === 200 && data.code === 200) {
resolve({ success: true, data: data })
} else {
reject({ success: false, error: data.msg || '上传失败', data: data })
}
} catch (parseError) {
reject({ success: false, error: '响应解析失败', parseError: parseError })
}
},
fail: (err) => {
this.addLog('error', '上传失败', err)
reject({ success: false, error: err.errMsg || '网络错误', details: err })
}
})
})
}
// 生成诊断报告
generateReport() {
const report = {
timestamp: new Date().toISOString(),
logs: this.logs,
summary: {
totalLogs: this.logs.length,
errors: this.logs.filter(log => log.type === 'error').length,
warnings: this.logs.filter(log => log.type === 'warn').length,
info: this.logs.filter(log => log.type === 'info').length
},
recommendations: this.generateRecommendations()
}
console.log('=== 诊断报告 ===')
console.log(JSON.stringify(report, null, 2))
return report
}
// 生成建议
generateRecommendations() {
const recommendations = []
const errors = this.logs.filter(log => log.type === 'error')
const warnings = this.logs.filter(log => log.type === 'warn')
// 检查token问题
const tokenErrors = errors.filter(log => log.message.includes('token') || log.message.includes('Token'))
if (tokenErrors.length > 0) {
recommendations.push('检查用户登录状态和token有效性')
}
// 检查网络问题
const networkErrors = errors.filter(log => log.message.includes('网络') || log.message.includes('timeout'))
if (networkErrors.length > 0) {
recommendations.push('检查网络连接和服务器状态')
}
// 检查文件问题
const fileErrors = errors.filter(log => log.message.includes('文件') || log.message.includes('file'))
if (fileErrors.length > 0) {
recommendations.push('检查文件格式和大小是否符合要求')
}
// 检查服务器问题
const serverErrors = errors.filter(log => log.message.includes('服务器') || log.message.includes('server'))
if (serverErrors.length > 0) {
recommendations.push('检查服务器接口是否正常工作')
}
if (recommendations.length === 0) {
recommendations.push('所有检查都通过,问题可能在其他方面')
}
return recommendations
}
// 运行完整诊断
async runFullDiagnosis() {
this.addLog('info', '开始完整诊断')
// 1. 检查token
const tokenResult = this.checkToken()
// 2. 检查网络
const networkResult = await this.checkNetwork()
// 3. 检查服务器连接
const serverResult = await this.checkServerConnection()
// 4. 生成报告
const report = this.generateReport()
return {
token: tokenResult,
network: networkResult,
server: serverResult,
report: report
}
}
}
// 导出调试器
export default UploadDebugger
// 使用示例
export function debugUpload() {
const debugger = new UploadDebugger()
return debugger.runFullDiagnosis()
}
// 在页面中使用
export function useUploadDebugger() {
const debugger = new UploadDebugger()
return {
debugger,
checkToken: () => debugger.checkToken(),
checkNetwork: () => debugger.checkNetwork(),
testUpload: (filePath) => debugger.testFileUpload(filePath),
runDiagnosis: () => debugger.runFullDiagnosis(),
getLogs: () => debugger.logs
}
}

View File

@ -0,0 +1 @@

View File

@ -0,0 +1 @@

View File

@ -0,0 +1 @@

View File

@ -464,7 +464,7 @@ export {
} }
/** /**
* 文件上传方法支持POST * 文件上传
* @param {string} url - 上传地址 * @param {string} url - 上传地址
* @param {string} filePath - 文件路径 * @param {string} filePath - 文件路径
* @param {string} name - 文件字段名 * @param {string} name - 文件字段名
@ -509,9 +509,6 @@ export function uploadFile(url, filePath, name = 'file', formData = {}, options
header.Authorization = authorization header.Authorization = authorization
} }
// 移除Content-Type让浏览器自动设置multipart/form-data
console.log('开始文件上传:', { console.log('开始文件上传:', {
url: BASE_URL + uploadUrl, url: BASE_URL + uploadUrl,
filePath, filePath,
@ -528,7 +525,6 @@ export function uploadFile(url, filePath, name = 'file', formData = {}, options
formData: formData, formData: formData,
header: header, header: header,
timeout: options.timeout || 60000, timeout: options.timeout || 60000,
method: options.method || 'POST',
success: (res) => { success: (res) => {
console.log('文件上传响应:', { console.log('文件上传响应:', {
statusCode: res.statusCode, statusCode: res.statusCode,
@ -553,6 +549,7 @@ export function uploadFile(url, filePath, name = 'file', formData = {}, options
message: errorMsg, message: errorMsg,
data: data data: data
}) })
uni.showToast({ uni.showToast({
title: errorMsg, title: errorMsg,
icon: 'none', icon: 'none',
@ -561,70 +558,12 @@ export function uploadFile(url, filePath, name = 'file', formData = {}, options
reject(new Error(errorMsg)) reject(new Error(errorMsg))
} }
} catch (parseError) { } catch (parseError) {
console.error('解析响应数据失败:', { console.error('解析响应数据失败:', parseError)
error: parseError, reject(new Error('响应数据格式错误'))
rawData: res.data,
statusCode: res.statusCode
})
// 如果状态码不是200可能是服务器错误
if (res.statusCode !== 200) {
const errorMsg = `服务器错误 (${res.statusCode})`
uni.showToast({
title: errorMsg,
icon: 'none',
duration: 3000
})
reject(new Error(errorMsg))
} else {
uni.showToast({
title: '响应数据格式错误',
icon: 'none',
duration: 3000
})
reject(new Error('响应数据格式错误'))
}
} }
}, },
fail: (err) => { fail: (err) => {
console.error('文件上传失败 - 网络错误:', { console.error('文件上传失败:', err)
error: err,
errMsg: err.errMsg,
statusCode: err.statusCode
})
let errorMessage = '上传失败'
if (err.errMsg) {
if (err.errMsg.includes('timeout')) {
errorMessage = '上传超时,请重试'
} else if (err.errMsg.includes('fail')) {
errorMessage = '网络连接失败,请检查网络'
} else if (err.errMsg.includes('abort')) {
errorMessage = '上传已取消'
} else {
errorMessage = err.errMsg
}
}
// 如果是HTTP错误状态码
if (err.statusCode) {
switch (err.statusCode) {
case 401:
errorMessage = '登录已过期,请重新登录'
break
case 403:
errorMessage = '没有上传权限'
break
case 413:
errorMessage = '文件太大'
break
case 500:
errorMessage = '服务器内部错误'
break
default:
errorMessage = `服务器错误 (${err.statusCode})`
}
}
uni.showToast({ uni.showToast({
title: errorMessage, title: errorMessage,
@ -642,178 +581,3 @@ export function uploadFile(url, filePath, name = 'file', formData = {}, options
// 默认导出request函数方便API文件导入 // 默认导出request函数方便API文件导入
export default request export default request
/**
* PUT方法文件上传用于头像上传等需要PUT方法的场景
* @param {string} url - 上传地址
* @param {string} filePath - 文件路径
* @param {string} name - 文件字段名
* @param {Object} formData - 额外的表单数据
* @param {Object} options - 上传配置
* @returns {Promise} 返回上传结果
*/
export function uploadFileWithPut(url, filePath, name = 'file', formData = {}, options = {}) {
return new Promise((resolve, reject) => {
// 获取token
let token = uni.getStorageSync('token')
// 检查token是否存在
if (!token && !options.noToken) {
token = getTempToken()
console.log('使用临时token进行开发测试')
}
// 清理token
token = cleanToken(token)
// 确保URL以/开头
const uploadUrl = url.startsWith('/') ? url : '/' + url
// 构建请求头
let authorization = ''
if (token) {
// #ifdef H5
authorization = `Bearer ${token}`
// #endif
// #ifndef H5
authorization = token
// #endif
}
const header = {
...options.header
}
// 只有在有token时才添加Authorization头部
if (authorization) {
header.Authorization = authorization
}
console.log('开始PUT文件上传:', {
url: BASE_URL + uploadUrl,
filePath,
name,
hasToken: !!token,
tokenLength: token ? token.length : 0,
header: header
})
// 由于小程序环境不支持手动构建multipart格式的PUT请求
// 我们使用POST方法但添加特殊头部标识这是PUT操作
const postHeader = {
...header,
'X-HTTP-Method-Override': 'PUT' // 告诉服务器这是PUT操作
}
console.log('使用POST方法模拟PUT上传...')
uni.uploadFile({
url: BASE_URL + uploadUrl,
filePath: filePath,
name: name,
formData: formData,
header: postHeader,
timeout: options.timeout || 60000,
method: 'POST',
success: (res) => {
console.log('POST模拟PUT上传响应:', {
statusCode: res.statusCode,
data: res.data,
header: res.header
})
try {
// 解析响应数据
const data = JSON.parse(res.data)
console.log('解析后的响应数据:', data)
if (res.statusCode === 200 && data.code === 200) {
console.log('POST模拟PUT上传成功:', data)
resolve(data)
} else {
// 服务器返回错误
const errorMsg = data.msg || data.message || '上传失败'
console.error('POST模拟PUT上传失败 - 服务器错误:', {
statusCode: res.statusCode,
code: data.code,
message: errorMsg,
data: data
})
// 如果服务器不支持X-HTTP-Method-Override尝试不带这个头部
console.log('尝试不带X-HTTP-Method-Override的POST方法...')
tryPostWithoutOverride()
}
} catch (parseError) {
console.error('解析响应数据失败:', parseError)
// 如果解析失败尝试不带特殊头部的POST方法
tryPostWithoutOverride()
}
},
fail: (err) => {
console.error('POST模拟PUT上传失败:', err)
// 如果POST方法失败尝试不带特殊头部的POST方法
tryPostWithoutOverride()
}
})
// 尝试不带X-HTTP-Method-Override的POST方法
function tryPostWithoutOverride() {
console.log('尝试不带X-HTTP-Method-Override的POST方法...')
uni.uploadFile({
url: BASE_URL + uploadUrl,
filePath: filePath,
name: name,
formData: formData,
header: header,
timeout: options.timeout || 60000,
method: 'POST',
success: (res) => {
console.log('普通POST上传响应:', {
statusCode: res.statusCode,
data: res.data,
header: res.header
})
try {
const data = JSON.parse(res.data)
console.log('解析后的响应数据:', data)
if (res.statusCode === 200 && data.code === 200) {
console.log('普通POST上传成功:', data)
resolve(data)
} else {
const errorMsg = data.msg || data.message || '上传失败'
console.error('普通POST上传失败:', errorMsg)
uni.showToast({
title: errorMsg,
icon: 'none',
duration: 3000
})
reject(new Error(errorMsg))
}
} catch (parseError) {
console.error('解析响应数据失败:', parseError)
reject(new Error('响应数据格式错误'))
}
},
fail: (err) => {
console.error('普通POST上传失败:', err)
// 最终失败,提示用户联系服务器端
const errorMsg = '上传失败服务器只支持PUT方法但小程序环境不支持PUT文件上传。请联系服务器端同时支持POST方法。'
console.error(errorMsg)
uni.showToast({
title: '上传失败,请联系技术支持',
icon: 'none',
duration: 3000
})
reject(new Error(errorMsg))
}
})
}
})
}