topfans/frontend/pages/profile/setNickname.vue
2026-06-02 21:10:35 +08:00

637 lines
13 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="set-nickname-container">
<!-- 背景图片 -->
<image class="background-image" src="/static/background/starbook.jpg" mode="aspectFill"></image>
<view class="background-overlay"></view>
<!-- 内容区域 -->
<view class="content-wrapper">
<!-- 导航栏 -->
<view class="nav-bar">
<view class="nav-back" @click="goBack">
<text class="back-icon"></text>
</view>
<text class="nav-title">设置头像与昵称</text>
</view>
<!-- 表单区域居中部分 -->
<view class="form-container">
<!-- 头像点击可上传 -->
<view class="avatar-wrapper" @tap="handleAvatarClick">
<Avatar :nickname="nickname" :size="200" :borderWidth="8" :avatarUrl="userAvatarUrl" />
<view class="avatar-edit-badge">
<text class="avatar-edit-icon"></text>
</view>
</view>
<!-- 昵称输入框 -->
<view class="input-wrapper">
<input
class="input-field"
type="text"
v-model="nickname"
placeholder="请输入昵称"
maxlength="20"
placeholder-class="input-placeholder"
/>
</view>
<!-- 错误提示 -->
<view v-if="errorMessage" class="error-message">
<text>{{ errorMessage }}</text>
</view>
<!-- 下一步按钮(圆形箭头) -->
<view class="next-button-wrapper">
<button class="btn-next" @click="handleNext">
<text class="arrow-icon">→</text>
</button>
</view>
</view>
</view>
<!-- 头像上传弹窗 -->
<view class="avatar-modal" v-if="showAvatarModal" @tap="closeAvatarModal">
<view class="modal-content" @tap.stop>
<view class="modal-title">设置头像</view>
<!-- 头像预览 -->
<view class="avatar-preview">
<Avatar :nickname="nickname" :size="180" :borderWidth="6" :avatarUrl="userAvatarUrl" />
</view>
<!-- 上传按钮 -->
<button class="upload-avatar-btn" @tap="handleUploadAvatar" :disabled="uploadingAvatar">
{{ uploadingAvatar ? '上传中...' : '上传头像' }}
</button>
<view class="upload-hint">支持JPGPNG格式大小不超过10MB</view>
<view class="modal-buttons">
<button class="modal-btn-cancel" @tap="closeAvatarModal">取消</button>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue';
import { useStore } from 'vuex';
import Avatar from '../components/Avatar.vue';
import { checkNicknameApi, getPublicOssSignatureApi } from '@/utils/api.js';
import { validateNickname } from '@/utils/validator.js';
const store = useStore();
// 响应式数据
const nickname = ref('');
const errorMessage = ref('');
const isChecking = ref(false);
// 头像上传相关
const userAvatarUrl = ref('');
const showAvatarModal = ref(false);
const uploadingAvatar = ref(false);
const goToAuthPage = () => {
const hasRegisterDraft = Boolean(uni.getStorageSync('temp_register_mobile'));
const authPageUrl = hasRegisterDraft ? '/pages/register/register' : '/pages/login/login';
uni.reLaunch({
url: authPageUrl
});
};
// 返回上一页
const goBack = () => {
const pages = getCurrentPages();
// 有上一页时正常返回,否则兜底回登录/注册页
if (pages.length > 1) {
uni.navigateBack({
fail: goToAuthPage
});
return;
}
goToAuthPage();
};
// 打开头像上传弹窗
const handleAvatarClick = () => {
showAvatarModal.value = true;
};
// 关闭头像上传弹窗
const closeAvatarModal = () => {
if (uploadingAvatar.value) return;
showAvatarModal.value = false;
};
// 选择并上传头像
const handleUploadAvatar = () => {
uni.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: async (res) => {
const tempFilePath = res.tempFilePaths[0];
uni.getFileInfo({
filePath: tempFilePath,
success: async (fileInfo) => {
if (fileInfo.size > 10 * 1024 * 1024) {
uni.showToast({
title: '图片大小不能超过10MB',
icon: 'none'
});
return;
}
await uploadAvatarToOss(tempFilePath);
},
fail: (error) => {
console.error('获取文件信息失败:', error);
uni.showToast({
title: '获取文件信息失败',
icon: 'none'
});
}
});
},
fail: (error) => {
console.error('选择图片失败:', error);
}
});
};
// 通过公开 OSS 签名接口上传头像
const uploadAvatarToOss = async (filePath) => {
try {
uploadingAvatar.value = true;
uni.showLoading({ title: '上传中...', mask: true });
// 1. 命名空间:使用注册手机号,便于注册成功后定位/迁移
const mobile = uni.getStorageSync('temp_register_mobile') || 'anon';
const signRes = await getPublicOssSignatureApi('register', mobile);
if (signRes.code !== 200) {
throw new Error(signRes.message || '获取签名失败');
}
// 2. 上传到 OSS用后端返回的 key每次唯一避免覆盖 / 缓存命中)
const ossKey = signRes.data.key
uni.uploadFile({
url: signRes.data.host,
filePath: filePath,
name: 'file',
formData: {
key: ossKey,
policy: signRes.data.policy,
success_action_status: '200',
'x-oss-credential': signRes.data.x_oss_credential,
'x-oss-date': signRes.data.x_oss_date,
'x-oss-security-token': signRes.data.security_token,
'x-oss-signature': signRes.data.signature,
'x-oss-signature-version': signRes.data.x_oss_signature_version
},
success: (uploadRes) => {
uni.hideLoading();
if (uploadRes.statusCode === 200 || uploadRes.statusCode === 204) {
userAvatarUrl.value = `${signRes.data.host}/${ossKey}`;
uni.showToast({
title: '头像已选择',
icon: 'success'
});
closeAvatarModal();
} else {
uni.showToast({
title: `上传失败,状态码: ${uploadRes.statusCode}`,
icon: 'none',
duration: 2000
});
}
uploadingAvatar.value = false;
},
fail: (error) => {
console.error('OSS上传失败:', error);
uni.hideLoading();
uni.showToast({
title: '上传失败',
icon: 'none',
duration: 2000
});
uploadingAvatar.value = false;
}
});
} catch (error) {
console.error('上传头像失败:', error);
uni.hideLoading();
uni.showToast({
title: error.message || '上传失败',
icon: 'none',
duration: 2000
});
uploadingAvatar.value = false;
}
};
// 下一步
const handleNext = async () => {
// 验证昵称
const trimmedNickname = nickname.value.trim();
const validation = validateNickname(trimmedNickname);
if (!validation.valid) {
errorMessage.value = validation.message;
uni.showToast({
title: validation.message,
icon: 'none'
});
return;
}
errorMessage.value = '';
// 防止重复点击
if (isChecking.value) return;
isChecking.value = true;
try {
// 检查昵称是否已被注册
const res = await checkNicknameApi(trimmedNickname);
if (res.data.exists === true) {
errorMessage.value = '该昵称已被注册';
uni.showToast({
title: '该昵称已被注册',
icon: 'none'
});
isChecking.value = false;
return;
}
// 暂存昵称到临时存储
uni.setStorageSync('temp_register_nickname', trimmedNickname);
// 获取临时存储的注册信息
const mobile = uni.getStorageSync('temp_register_mobile');
const password = uni.getStorageSync('temp_register_password');
const star_id = 87; // 默认身份
const verify_token = uni.getStorageSync('temp_register_verify_token') || '';
const avatar_url = userAvatarUrl.value || '';
// 验证数据完整性
if (!mobile || !password || !trimmedNickname || !star_id) {
uni.showToast({
title: '注册信息不完整,请重新注册',
icon: 'none'
});
uni.removeStorageSync('temp_register_mobile');
uni.removeStorageSync('temp_register_password');
uni.removeStorageSync('temp_register_nickname');
uni.removeStorageSync('temp_register_verify_token');
setTimeout(() => {
uni.reLaunch({
url: '/pages/register/register'
});
}, 1500);
isChecking.value = false;
return;
}
// 显示加载提示
uni.showLoading({
title: '注册中...',
mask: true
});
// 调用注册 API
await store.dispatch('user/register', {
mobile,
password,
star_id,
nickname: trimmedNickname,
verify_token,
avatar_url
});
uni.hideLoading();
// 清除临时数据
uni.removeStorageSync('temp_register_mobile');
uni.removeStorageSync('temp_register_password');
uni.removeStorageSync('temp_register_nickname');
uni.removeStorageSync('temp_register_verify_token');
// 设置新用户标记
uni.setStorageSync('is_new_user', true);
// 跳转到主页
uni.reLaunch({
url: '/pages/square/square'
});
} catch (error) {
uni.hideLoading();
// 处理昵称已存在的情况409错误
if (error.code === 409 || error.message.includes('昵称') || error.message.includes('已存在')) {
uni.showModal({
title: '昵称已存在',
content: '该昵称已被使用,请返回修改昵称',
showCancel: false,
confirmText: '知道了'
});
} else {
errorMessage.value = error.message || '注册失败,请重试';
uni.showToast({
title: errorMessage.value,
icon: 'none'
});
}
} finally {
isChecking.value = false;
}
};
</script>
<style scoped>
.set-nickname-container {
position: relative;
width: 100%;
min-height: 100vh;
height: 100vh;
overflow: hidden;
display: flex;
flex-direction: column;
}
.background-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
transform: scale(1.1);
}
.background-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.3);
}
.content-wrapper {
position: relative;
z-index: 1;
width: 100%;
flex: 1;
min-height: 100%;
display: flex;
flex-direction: column;
padding-top: 80rpx;
box-sizing: border-box;
}
/* 导航栏 */
.nav-bar {
display: flex;
align-items: center;
padding: 0 40rpx 40rpx 40rpx;
position: relative;
}
.nav-back {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
}
.back-icon {
font-size: 48rpx;
font-weight: bold;
color: #fff;
}
.nav-title {
flex: 1;
text-align: center;
font-size: 36rpx;
font-weight: 500;
color: #000000;
margin-right: 60rpx;
}
.form-container {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
max-width: 600rpx;
margin: 0 auto;
padding: 0 60rpx;
}
.avatar-wrapper {
position: relative;
width: 100%;
margin-bottom: 80rpx;
display: flex;
justify-content: center;
align-items: center;
}
.avatar-edit-badge {
position: absolute;
right: 30%;
bottom: 0;
width: 56rpx;
height: 56rpx;
border-radius: 50%;
background: linear-gradient(165deg, #F0E4B1 0%, #F08399 50%, #B94E73 90%, #834B9E 100%);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.3);
border: 4rpx solid #fff;
}
.avatar-edit-icon {
color: #fff;
font-size: 28rpx;
font-weight: bold;
line-height: 1;
}
.input-wrapper {
position: relative;
width: 100%;
height: 100rpx;
margin-bottom: 40rpx;
background: rgba(255, 255, 255, 0.9);
border-radius: 50rpx;
display: flex;
align-items: center;
padding: 0 40rpx;
box-sizing: border-box;
}
.input-field {
flex: 1;
height: 100%;
font-size: 32rpx;
color: #333333;
}
.input-placeholder {
color: #999999;
}
.error-message {
width: 100%;
padding: 20rpx 0;
margin-bottom: 20rpx;
text-align: center;
}
.error-message text {
font-size: 28rpx;
color: #ff4444;
}
.next-button-wrapper {
width: 100%;
margin-top: 60rpx;
display: flex;
justify-content: center;
align-items: center;
}
.btn-next {
width: 100rpx;
height: 100rpx;
border-radius: 50%;
background: #000000;
border: none;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.3);
padding: 0;
transition: transform 0.2s ease;
}
.btn-next:active {
transform: scale(1.15);
}
.btn-next::after {
border: none;
}
.arrow-icon {
font-size: 48rpx;
font-weight: 900;
color: #e6e6e6;
line-height: 1;
}
/* 头像上传弹窗 */
.avatar-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-content {
width: 80%;
max-width: 600rpx;
background-image: url('/static/starbookcontent/beijing.png');
background-size: cover;
background-position: center bottom;
border-radius: 30rpx;
padding: 60rpx 40rpx;
box-sizing: border-box;
}
.modal-title {
font-size: 36rpx;
font-weight: bold;
color: #333333;
text-align: center;
margin-bottom: 40rpx;
}
.avatar-preview {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 40rpx;
}
.upload-avatar-btn {
width: 100%;
height: 88rpx;
background: linear-gradient(165deg, #F0E4B1 0%, #F08399 50%, #B94E73 90%, #834B9E 100%);
border-radius: 44rpx;
color: #ffffff;
font-size: 32rpx;
font-weight: 500;
border: none;
margin-bottom: 20rpx;
}
.upload-avatar-btn::after {
border: none;
}
.upload-avatar-btn:disabled {
opacity: 0.6;
}
.upload-hint {
font-size: 24rpx;
color: #999999;
text-align: center;
margin-bottom: 40rpx;
}
.modal-buttons {
display: flex;
gap: 20rpx;
}
.modal-btn-cancel {
flex: 1;
height: 88rpx;
line-height: 88rpx;
border-radius: 44rpx;
font-size: 32rpx;
font-weight: 500;
border: none;
background: #f5f5f5;
color: #666666;
}
.modal-btn-cancel::after {
border: none;
}
.modal-btn-cancel:active {
background: #e0e0e0;
}
</style>