637 lines
13 KiB
Vue
637 lines
13 KiB
Vue
<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">支持JPG、PNG格式,大小不超过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>
|