hookehuyr

feat(register): 添加注册页面及表单功能

实现注册页面UI及表单验证功能,包括头像上传、手机验证码发送、生日和学校选择等
/*
* @Date: 2025-06-28 10:33:00
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-07-02 15:05:28
* @LastEditTime: 2025-07-02 16:01:00
* @FilePath: /jgdl/src/app.config.js
* @Description: 文件描述
*/
......@@ -13,6 +13,7 @@ export default {
'pages/messages/index',
'pages/profile/index',
'pages/editProfile/index',
'pages/register/index',
'pages/auth/index',
],
subpackages: [ // 配置在tabBar中的页面不能分包写到subpackages中去
......
/*
* @Date: 2025-07-02 16:00:16
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-07-02 16:00:37
* @FilePath: /jgdl/src/pages/register/index.config.js
* @Description: 文件描述
*/
export default {
navigationBarTitleText: '完善信息',
usingComponents: {
},
}
/* 注册页面样式 */
.register-page {
min-height: 100vh;
background-color: #f5f5f5;
padding-bottom: 200rpx;
}
/* 头像区域 */
.avatar-section {
display: flex;
flex-direction: column;
align-items: center;
padding: 60rpx 0;
background-color: #fff;
margin-bottom: 20rpx;
}
.avatar-container {
position: relative;
margin-bottom: 20rpx;
}
.avatar-image {
width: 160rpx;
height: 160rpx;
border-radius: 80rpx;
border: 4rpx solid #f0f0f0;
}
.camera-btn {
position: absolute;
bottom: 0;
right: 0;
width: 48rpx;
height: 48rpx;
background-color: #f97316;
border-radius: 24rpx;
display: flex;
align-items: center;
justify-content: center;
border: 4rpx solid #fff;
}
.camera-icon {
width: 24rpx;
height: 24rpx;
color: #fff;
}
.change-avatar-btn {
padding: 16rpx 32rpx;
background-color: #f97316;
border-radius: 40rpx;
}
.change-avatar-text {
color: #fff;
font-size: 28rpx;
}
/* 表单容器 */
.form-container {
background-color: #fff;
margin: 0 24rpx 40rpx;
border-radius: 16rpx;
overflow: hidden;
}
/* 手机号输入容器 */
.phone-input-container {
display: flex;
align-items: center;
width: 100%;
}
.phone-input {
flex: 1;
margin-right: 20rpx;
}
.code-btn {
flex-shrink: 0;
min-width: 160rpx;
height: 64rpx;
background-color: #f97316;
color: #fff;
border-radius: 8rpx;
font-size: 24rpx;
}
.code-btn:disabled {
background-color: #ccc;
color: #999;
}
/* 生日选择项 */
.birthday-item {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 20rpx 0;
}
.birthday-value {
color: #333;
font-size: 28rpx;
}
.arrow-icon {
width: 24rpx;
height: 24rpx;
color: #999;
}
/* 学校选择项 */
.school-item {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 20rpx 0;
}
.school-value {
color: #333;
font-size: 28rpx;
}
/* 性别单选组件右对齐 */
// .form-container :deep(.nut-form-item:nth-child(4) .nut-form-item__body) {
// justify-content: flex-end;
// }
/* 注册按钮区域 */
.register-section {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 40rpx 24rpx;
background-color: #fff;
border-top: 1rpx solid #eee;
z-index: 100;
}
/* 表单项样式调整 */
.form-container :deep(.nut-form-item) {
padding: 24rpx 32rpx;
border-bottom: 1rpx solid #f0f0f0;
}
// .form-container :deep(.nut-form-item:last-child) {
// border-bottom: none;
// }
.form-container .nut-form-item__label {
font-size: 28rpx;
color: #333;
font-weight: 500;
min-width: 140rpx;
}
.form-container :deep(.nut-input__inner) {
font-size: 28rpx;
color: #333;
}
// .form-container :deep(.nut-input__inner::placeholder) {
// color: #999;
// font-size: 28rpx;
// }
/* 单选按钮样式 */
.form-container :deep(.nut-radio-group) {
gap: 40rpx;
}
.form-container :deep(.nut-radio__label) {
font-size: 28rpx;
color: #333;
}
.form-container :deep(.nut-radio__icon) {
width: 32rpx;
height: 32rpx;
}
/* 弹窗样式调整 */
:deep(.nut-popup) {
border-radius: 24rpx 24rpx 0 0;
}
:deep(.nut-picker__title) {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
:deep(.nut-date-picker__title) {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
/* 按钮样式 */
:deep(.nut-button--large) {
height: 88rpx;
border-radius: 44rpx;
font-size: 32rpx;
font-weight: 600;
}
/* Toast 样式 */
:deep(.nut-toast) {
z-index: 9999;
}
/* 导航栏样式 */
:deep(.nut-navbar) {
background-color: #f97316;
color: #fff;
}
:deep(.nut-navbar__title) {
color: #fff;
font-size: 36rpx;
font-weight: 600;
}
:deep(.nut-navbar__left) {
color: #fff;
}
/* 响应式适配 */
@media screen and (max-width: 375px) {
.avatar-image {
width: 140rpx;
height: 140rpx;
border-radius: 70rpx;
}
.camera-btn {
width: 40rpx;
height: 40rpx;
border-radius: 20rpx;
}
.camera-icon {
width: 20rpx;
height: 20rpx;
}
.form-container :deep(.nut-form-item) {
padding: 20rpx 24rpx;
}
.form-container :deep.nut-form-item__label {
font-size: 26rpx;
min-width: 120rpx;
}
.form-container :deep(.nut-input__inner) {
font-size: 26rpx;
}
}
/* 加载状态 */
.register-section :deep(.nut-button--loading) {
opacity: 0.7;
}
/* 禁用状态 */
.register-section :deep(.nut-button--disabled) {
background-color: #ccc !important;
color: #999 !important;
}
/* 表单验证错误样式 */
// .form-container :deep(.nut-form-item--error .nut-form-item__label) {
// color: #ff4757;
// }
// .form-container :deep(.nut-form-item--error .nut-input__inner) {
// border-color: #ff4757;
// }
/* 占位符颜色调整 */
.birthday-value,
.school-value {
color: #999;
}
// .birthday-item:has(.birthday-value:not(:empty)),
// .school-item:has(.school-value:not(:empty)) {
// .birthday-value,
// .school-value {
// color: #333;
// }
// }
<template>
<view class="register-page">
<!-- 头像区域 -->
<view class="avatar-section">
<view class="avatar-container">
<image
:src="formData.avatar || defaultAvatar"
class="avatar-image"
mode="aspectFill"
@click="previewAvatar"
/>
</view>
<view class="change-avatar-btn" @click="changeAvatar">
<text class="change-avatar-text">上传头像</text>
</view>
</view>
<!-- 表单内容 -->
<nut-form ref="formRef" :model-value="formData">
<view class="form-container">
<!-- 昵称 -->
<nut-form-item label="昵称" prop="nickname" required :rules="[{ required: true, message: '请输入昵称' }]">
<nut-input
v-model="formData.nickname"
placeholder="请输入昵称"
input-align="right"
clearable
/>
</nut-form-item>
<!-- 手机号 -->
<nut-form-item label="手机号" prop="phone" required :rules="phoneRules">
<view class="phone-input-container">
<nut-input
v-model="formData.phone"
placeholder="请输入手机号"
type="tel"
maxlength="11"
input-align="right"
clearable
class="phone-input"
/>
<nut-button
size="small"
:disabled="codeCountdown > 0 || !isPhoneValid"
@click="sendCode"
class="code-btn"
>
{{ codeCountdown > 0 ? `${codeCountdown}s` : '获取验证码' }}
</nut-button>
</view>
</nut-form-item>
<!-- 验证码 -->
<nut-form-item label="验证码" prop="verifyCode" required :rules="[{ required: true, message: '请输入验证码' }]">
<nut-input
v-model="formData.verifyCode"
placeholder="请输入验证码"
type="number"
maxlength="6"
input-align="right"
clearable
/>
</nut-form-item>
<!-- 性别 -->
<nut-form-item label="性别" prop="gender" body-align="right" required :rules="[{ required: true, message: '请选择性别' }]">
<nut-radio-group v-model="formData.gender" direction="horizontal">
<nut-radio label="男">男</nut-radio>
<nut-radio label="女">女</nut-radio>
</nut-radio-group>
</nut-form-item>
<!-- 生日 -->
<nut-form-item label="生日" prop="birthday" label-position="top">
<view class="birthday-item" @click="showDatePicker">
<text class="birthday-value">{{ formData.birthday || '请选择生日' }}</text>
<Right class="arrow-icon" />
</view>
</nut-form-item>
<!-- 所在学校 -->
<nut-form-item label="所在学校" prop="school" required :rules="[{ required: true, message: '请选择学校' }]" label-position="top">
<view class="school-item" @click="showSchoolPicker">
<text class="school-value">{{ formData.school || '请选择学校' }}</text>
<Right class="arrow-icon" />
</view>
</nut-form-item>
</view>
</nut-form>
<!-- 注册按钮 -->
<view class="register-section">
<nut-button
color="#f97316"
size="large"
block
@click="handleRegister"
:loading="isRegistering"
>
{{ isRegistering ? '保存中...' : '保存' }}
</nut-button>
</view>
<!-- 日期选择器 -->
<nut-popup v-model:visible="datePickerVisible" position="bottom">
<nut-date-picker
v-model="dateValue"
title="选择生日"
@confirm="onDateConfirm"
@cancel="datePickerVisible = false"
/>
</nut-popup>
<!-- 学校选择器 -->
<nut-popup v-model:visible="schoolPickerVisible" position="bottom">
<nut-picker
v-model="schoolValue"
:columns="schoolOptions"
title="选择学校"
@confirm="onSchoolConfirm"
@cancel="schoolPickerVisible = false"
/>
</nut-popup>
<!-- 头像预览 -->
<nut-image-preview
v-model:show="avatarPreviewVisible"
:images="[formData.avatar || defaultAvatar]"
/>
<!-- 成功提示 -->
<nut-toast
v-model:visible="toastVisible"
:msg="toastMessage"
:type="toastType"
/>
</view>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import Taro from '@tarojs/taro'
import { RectLeft, Camera, Right } from '@nutui/icons-vue-taro'
import './index.less'
// 主题配置
const themeVars = {
navbarBackground: '#f97316',
navbarColor: '#fff'
}
// 默认头像
const defaultAvatar = 'https://randomuser.me/api/portraits/men/32.jpg'
// 表单数据
const formData = reactive({
avatar: '',
nickname: '',
phone: '',
verifyCode: '',
gender: '',
birthday: '',
school: ''
})
// 弹框控制
const datePickerVisible = ref(false)
const schoolPickerVisible = ref(false)
const avatarPreviewVisible = ref(false)
const toastVisible = ref(false)
const toastMessage = ref('')
const toastType = ref('success')
// 验证码相关
const codeCountdown = ref(0)
const isRegistering = ref(false)
// 日期选择
const dateValue = ref(new Date())
// 学校选择
const schoolValue = ref([])
const schoolOptions = ref([
[
{ text: '上海理工大学', value: '上海理工大学' },
{ text: '上海大学', value: '上海大学' },
{ text: '华东理工大学', value: '华东理工大学' },
{ text: '上海交通大学', value: '上海交通大学' },
{ text: '复旦大学', value: '复旦大学' },
{ text: '同济大学', value: '同济大学' },
{ text: '华东师范大学', value: '华东师范大学' },
{ text: '上海财经大学', value: '上海财经大学' }
]
])
// 手机号验证规则
const phoneRules = [
{ required: true, message: '请输入手机号' },
{
pattern: /^1[3-9]\d{9}$/,
message: '请输入正确的手机号格式'
}
]
// 计算属性:手机号是否有效
const isPhoneValid = computed(() => {
return /^1[3-9]\d{9}$/.test(formData.phone)
})
/**
* 返回上一页
*/
const goBack = () => {
Taro.navigateBack()
}
/**
* 更换头像
*/
const changeAvatar = () => {
Taro.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
formData.avatar = res.tempFilePaths[0]
}
})
}
/**
* 预览头像
*/
const previewAvatar = () => {
avatarPreviewVisible.value = true
}
/**
* 发送验证码
*/
const sendCode = () => {
if (!isPhoneValid.value) {
showToast('请输入正确的手机号', 'error')
return
}
// 模拟发送验证码
codeCountdown.value = 60
const timer = setInterval(() => {
codeCountdown.value--
if (codeCountdown.value <= 0) {
clearInterval(timer)
}
}, 1000)
showToast('验证码已发送', 'success')
}
/**
* 显示日期选择器
*/
const showDatePicker = () => {
datePickerVisible.value = true
}
/**
* 确认日期选择
*/
const onDateConfirm = ({ selectedValue }) => {
formData.birthday = `${selectedValue[0]}-${String(selectedValue[1]).padStart(2, '0')}-${String(selectedValue[2]).padStart(2, '0')}`
datePickerVisible.value = false
}
/**
* 显示学校选择器
*/
const showSchoolPicker = () => {
schoolPickerVisible.value = true
}
/**
* 确认学校选择
*/
const onSchoolConfirm = ({ selectedOptions }) => {
formData.school = selectedOptions[0].text
schoolPickerVisible.value = false
}
/**
* 显示提示信息
*/
const showToast = (message, type = 'success') => {
toastMessage.value = message
toastType.value = type
toastVisible.value = true
}
/**
* 表单验证
*/
const validateForm = () => {
if (!formData.nickname.trim()) {
showToast('请输入昵称', 'error')
return false
}
if (!isPhoneValid.value) {
showToast('请输入正确的手机号', 'error')
return false
}
if (!formData.verifyCode.trim()) {
showToast('请输入验证码', 'error')
return false
}
if (!formData.gender) {
showToast('请选择性别', 'error')
return false
}
if (!formData.school) {
showToast('请选择学校', 'error')
return false
}
return true
}
/**
* 处理注册
*/
const handleRegister = async () => {
if (!validateForm()) {
return
}
isRegistering.value = true
try {
// 模拟注册API调用
await new Promise(resolve => setTimeout(resolve, 2000))
// TODO: 这里应该调用实际的注册API
// const result = await registerAPI(formData)
showToast('注册成功', 'success')
setTimeout(() => {
// 注册成功后跳转到登录页面或首页
Taro.navigateBack()
}, 1500)
} catch (error) {
showToast('注册失败,请重试', 'error')
} finally {
isRegistering.value = false
}
}
// 初始化
onMounted(() => {
// 可以在这里进行初始化操作
})
</script>
<script>
export default {
name: 'RegisterPage'
}
</script>