mi-assessment/uniapp/pages/mine/profile/index.vue
zpc 3f179e5682 feat(upload): 头像直传COS + 修复用户资料接口404
后端:
- Model层新增UploadSetting配置模型
- Core层新增IUploadConfigService/UploadConfigService,从Admin库读取COS配置生成预签名URL
- Api层新增UploadController,提供POST /api/upload/presignedUrl接口
- ServiceModule注册UploadConfigService服务

前端:
- api/user.js修复接口路径:updateProfileupdate_userinfo,upload/imageupload/presignedUrl
- 新增utils/upload.js COS直传工具(获取预签名URL直传COS返回文件URL)
- 个人资料页改为:选图直传COS保存时提交headimg URL到update_userinfo
2026-02-20 23:21:56 +08:00

326 lines
7.1 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="profile-page">
<!-- 页面内容 -->
<view class="page-content">
<!-- 头像区域 - 居中展示右下角编辑图标 -->
<view class="avatar-section" @click="handleChangeAvatar">
<view class="avatar-wrapper">
<image
class="avatar"
:src="avatarUrl || '/static/mine/icon-user.png'"
mode="aspectFill"
/>
<image class="edit-icon" src="/static/mine/icon-user-icon-edit.png" mode="aspectFit" />
</view>
</view>
<!-- 昵称 -->
<view class="form-group">
<text class="form-label">昵称</text>
<input
class="form-input"
type="text"
v-model="formData.nickname"
placeholder="用户昵称"
maxlength="20"
/>
</view>
<!-- UID只读 -->
<view class="form-group">
<text class="form-label">UID</text>
<input
class="form-input readonly"
type="text"
:value="userStore.uid || '--'"
disabled
/>
</view>
<!-- 保存按钮 -->
<view class="btn-section">
<button class="btn-save" :disabled="saving" @click="handleSave">保存</button>
</view>
</view>
<!-- 加载中 -->
<view v-if="loading" class="loading-mask">
<view class="loading-content">
<view class="loading-spinner"></view>
<text class="loading-text">{{ loadingText }}</text>
</view>
</view>
</view>
</template>
<script setup>
/**
* 个人资料页面
* 头像直传COS昵称通过update_userinfo接口保存
* UID 仅展示不可修改
*/
import { ref, reactive, onMounted } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { useUserStore } from '@/store/user.js'
import { updateUserInfo } from '@/api/user.js'
import { chooseAndUploadImage } from '@/utils/upload.js'
const userStore = useUserStore()
// 状态
const loading = ref(false)
const saving = ref(false)
const loadingText = ref('加载中...')
// 头像URL本地临时展示 + 上传后的COS地址
const avatarUrl = ref('')
// 待提交的COS头像地址上传成功后赋值
const pendingAvatarUrl = ref('')
// 表单数据
const formData = reactive({
nickname: ''
})
/**
* 初始化表单数据
*/
function initFormData() {
formData.nickname = userStore.nickname || ''
avatarUrl.value = userStore.avatar || ''
pendingAvatarUrl.value = ''
}
/**
* 选择并上传头像到COS
*/
async function handleChangeAvatar() {
try {
loading.value = true
loadingText.value = '上传中...'
const fileUrl = await chooseAndUploadImage()
// 上传成功,更新预览和待提交地址
avatarUrl.value = fileUrl
pendingAvatarUrl.value = fileUrl
uni.showToast({ title: '头像上传成功', icon: 'success' })
} catch (error) {
if (error.message !== '用户取消选择') {
console.error('上传头像失败:', error)
uni.showToast({ title: error.message || '上传失败', icon: 'none' })
}
} finally {
loading.value = false
}
}
/**
* 保存资料
* 提交昵称和头像URL到 POST /api/update_userinfo
*/
async function handleSave() {
const nickname = formData.nickname.trim()
const hasNicknameChange = nickname && nickname !== userStore.nickname
const hasAvatarChange = !!pendingAvatarUrl.value
if (!hasNicknameChange && !hasAvatarChange) {
uni.showToast({ title: '资料未修改', icon: 'none' })
return
}
if (!nickname) {
uni.showToast({ title: '请输入昵称', icon: 'none' })
return
}
try {
saving.value = true
loading.value = true
loadingText.value = '保存中...'
// 构建请求参数(对应后端 UpdateUserInfoRequest
const params = {}
if (hasNicknameChange) params.nickname = nickname
if (hasAvatarChange) params.headimg = pendingAvatarUrl.value
const res = await updateUserInfo(params)
if (res.code === 0) {
// 更新本地store
const updateData = {}
if (hasNicknameChange) updateData.nickname = nickname
if (hasAvatarChange) updateData.avatar = pendingAvatarUrl.value
userStore.updateUserInfo(updateData)
pendingAvatarUrl.value = ''
uni.showToast({ title: '保存成功', icon: 'success' })
} else {
throw new Error(res.message || '保存失败')
}
} catch (error) {
console.error('保存资料失败:', error)
uni.showToast({ title: error.message || '保存失败', icon: 'none' })
} finally {
saving.value = false
loading.value = false
}
}
// Lifecycle
onShow(() => {
initFormData()
})
onMounted(() => {
userStore.restoreFromStorage()
initFormData()
})
</script>
<style lang="scss" scoped>
@import '@/styles/variables.scss';
$save-btn-color: #F5A623;
$save-btn-active: #E09518;
.profile-page {
min-height: 100vh;
background-color: $bg-color;
}
.page-content {
padding: $spacing-lg $spacing-xl;
}
// 头像区域
.avatar-section {
display: flex;
justify-content: center;
padding: $spacing-xl 0;
.avatar-wrapper {
position: relative;
width: 160rpx;
height: 160rpx;
.avatar {
width: 160rpx;
height: 160rpx;
border-radius: 50%;
background-color: $bg-gray;
}
.edit-icon {
position: absolute;
right: 0;
bottom: 0;
width: 48rpx;
height: 48rpx;
}
}
}
// 表单组
.form-group {
margin-bottom: $spacing-lg;
.form-label {
display: block;
font-size: $font-size-md;
color: $text-color;
margin-bottom: $spacing-sm;
}
.form-input {
width: 100%;
height: 88rpx;
padding: 0 $spacing-lg;
font-size: $font-size-md;
color: $text-color;
background-color: $bg-white;
border: 2rpx solid $border-color;
border-radius: $border-radius-md;
box-sizing: border-box;
&.readonly {
color: $text-placeholder;
background-color: $bg-gray;
}
}
}
// 保存按钮
.btn-section {
margin-top: $spacing-xl;
.btn-save {
width: 100%;
height: 88rpx;
line-height: 88rpx;
background-color: $save-btn-color;
border-radius: 44rpx;
font-size: $font-size-lg;
color: $text-white;
border: none;
font-weight: $font-weight-medium;
&::after {
border: none;
}
&:active {
background-color: $save-btn-active;
}
&[disabled] {
background-color: $text-disabled;
}
}
}
// 加载中
.loading-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.3);
display: flex;
align-items: center;
justify-content: center;
z-index: 1001;
}
.loading-content {
display: flex;
flex-direction: column;
align-items: center;
padding: $spacing-xl;
background-color: $bg-white;
border-radius: $border-radius-lg;
.loading-spinner {
width: 60rpx;
height: 60rpx;
border: 4rpx solid $border-color;
border-top-color: $primary-color;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
.loading-text {
margin-top: $spacing-md;
font-size: $font-size-sm;
color: $text-secondary;
}
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
</style>