hookehuyr

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

实现注册页面UI及表单验证功能,包括头像上传、手机验证码发送、生日和学校选择等
1 /* 1 /*
2 * @Date: 2025-06-28 10:33:00 2 * @Date: 2025-06-28 10:33:00
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2025-07-02 15:05:28 4 + * @LastEditTime: 2025-07-02 16:01:00
5 * @FilePath: /jgdl/src/app.config.js 5 * @FilePath: /jgdl/src/app.config.js
6 * @Description: 文件描述 6 * @Description: 文件描述
7 */ 7 */
...@@ -13,6 +13,7 @@ export default { ...@@ -13,6 +13,7 @@ export default {
13 'pages/messages/index', 13 'pages/messages/index',
14 'pages/profile/index', 14 'pages/profile/index',
15 'pages/editProfile/index', 15 'pages/editProfile/index',
16 + 'pages/register/index',
16 'pages/auth/index', 17 'pages/auth/index',
17 ], 18 ],
18 subpackages: [ // 配置在tabBar中的页面不能分包写到subpackages中去 19 subpackages: [ // 配置在tabBar中的页面不能分包写到subpackages中去
......
1 +/*
2 + * @Date: 2025-07-02 16:00:16
3 + * @LastEditors: hookehuyr hookehuyr@gmail.com
4 + * @LastEditTime: 2025-07-02 16:00:37
5 + * @FilePath: /jgdl/src/pages/register/index.config.js
6 + * @Description: 文件描述
7 + */
8 +export default {
9 + navigationBarTitleText: '完善信息',
10 + usingComponents: {
11 + },
12 +}
1 +/* 注册页面样式 */
2 +.register-page {
3 + min-height: 100vh;
4 + background-color: #f5f5f5;
5 + padding-bottom: 200rpx;
6 +}
7 +
8 +/* 头像区域 */
9 +.avatar-section {
10 + display: flex;
11 + flex-direction: column;
12 + align-items: center;
13 + padding: 60rpx 0;
14 + background-color: #fff;
15 + margin-bottom: 20rpx;
16 +}
17 +
18 +.avatar-container {
19 + position: relative;
20 + margin-bottom: 20rpx;
21 +}
22 +
23 +.avatar-image {
24 + width: 160rpx;
25 + height: 160rpx;
26 + border-radius: 80rpx;
27 + border: 4rpx solid #f0f0f0;
28 +}
29 +
30 +.camera-btn {
31 + position: absolute;
32 + bottom: 0;
33 + right: 0;
34 + width: 48rpx;
35 + height: 48rpx;
36 + background-color: #f97316;
37 + border-radius: 24rpx;
38 + display: flex;
39 + align-items: center;
40 + justify-content: center;
41 + border: 4rpx solid #fff;
42 +}
43 +
44 +.camera-icon {
45 + width: 24rpx;
46 + height: 24rpx;
47 + color: #fff;
48 +}
49 +
50 +.change-avatar-btn {
51 + padding: 16rpx 32rpx;
52 + background-color: #f97316;
53 + border-radius: 40rpx;
54 +}
55 +
56 +.change-avatar-text {
57 + color: #fff;
58 + font-size: 28rpx;
59 +}
60 +
61 +/* 表单容器 */
62 +.form-container {
63 + background-color: #fff;
64 + margin: 0 24rpx 40rpx;
65 + border-radius: 16rpx;
66 + overflow: hidden;
67 +}
68 +
69 +/* 手机号输入容器 */
70 +.phone-input-container {
71 + display: flex;
72 + align-items: center;
73 + width: 100%;
74 +}
75 +
76 +.phone-input {
77 + flex: 1;
78 + margin-right: 20rpx;
79 +}
80 +
81 +.code-btn {
82 + flex-shrink: 0;
83 + min-width: 160rpx;
84 + height: 64rpx;
85 + background-color: #f97316;
86 + color: #fff;
87 + border-radius: 8rpx;
88 + font-size: 24rpx;
89 +}
90 +
91 +.code-btn:disabled {
92 + background-color: #ccc;
93 + color: #999;
94 +}
95 +
96 +/* 生日选择项 */
97 +.birthday-item {
98 + display: flex;
99 + align-items: center;
100 + justify-content: space-between;
101 + width: 100%;
102 + padding: 20rpx 0;
103 +}
104 +
105 +.birthday-value {
106 + color: #333;
107 + font-size: 28rpx;
108 +}
109 +
110 +.arrow-icon {
111 + width: 24rpx;
112 + height: 24rpx;
113 + color: #999;
114 +}
115 +
116 +/* 学校选择项 */
117 +.school-item {
118 + display: flex;
119 + align-items: center;
120 + justify-content: space-between;
121 + width: 100%;
122 + padding: 20rpx 0;
123 +}
124 +
125 +.school-value {
126 + color: #333;
127 + font-size: 28rpx;
128 +}
129 +
130 +/* 性别单选组件右对齐 */
131 +// .form-container :deep(.nut-form-item:nth-child(4) .nut-form-item__body) {
132 +// justify-content: flex-end;
133 +// }
134 +
135 +/* 注册按钮区域 */
136 +.register-section {
137 + position: fixed;
138 + bottom: 0;
139 + left: 0;
140 + right: 0;
141 + padding: 40rpx 24rpx;
142 + background-color: #fff;
143 + border-top: 1rpx solid #eee;
144 + z-index: 100;
145 +}
146 +
147 +/* 表单项样式调整 */
148 +.form-container :deep(.nut-form-item) {
149 + padding: 24rpx 32rpx;
150 + border-bottom: 1rpx solid #f0f0f0;
151 +}
152 +
153 +// .form-container :deep(.nut-form-item:last-child) {
154 +// border-bottom: none;
155 +// }
156 +
157 +.form-container .nut-form-item__label {
158 + font-size: 28rpx;
159 + color: #333;
160 + font-weight: 500;
161 + min-width: 140rpx;
162 +}
163 +
164 +.form-container :deep(.nut-input__inner) {
165 + font-size: 28rpx;
166 + color: #333;
167 +}
168 +
169 +// .form-container :deep(.nut-input__inner::placeholder) {
170 +// color: #999;
171 +// font-size: 28rpx;
172 +// }
173 +
174 +/* 单选按钮样式 */
175 +.form-container :deep(.nut-radio-group) {
176 + gap: 40rpx;
177 +}
178 +
179 +.form-container :deep(.nut-radio__label) {
180 + font-size: 28rpx;
181 + color: #333;
182 +}
183 +
184 +.form-container :deep(.nut-radio__icon) {
185 + width: 32rpx;
186 + height: 32rpx;
187 +}
188 +
189 +/* 弹窗样式调整 */
190 +:deep(.nut-popup) {
191 + border-radius: 24rpx 24rpx 0 0;
192 +}
193 +
194 +:deep(.nut-picker__title) {
195 + font-size: 32rpx;
196 + font-weight: 600;
197 + color: #333;
198 +}
199 +
200 +:deep(.nut-date-picker__title) {
201 + font-size: 32rpx;
202 + font-weight: 600;
203 + color: #333;
204 +}
205 +
206 +/* 按钮样式 */
207 +:deep(.nut-button--large) {
208 + height: 88rpx;
209 + border-radius: 44rpx;
210 + font-size: 32rpx;
211 + font-weight: 600;
212 +}
213 +
214 +/* Toast 样式 */
215 +:deep(.nut-toast) {
216 + z-index: 9999;
217 +}
218 +
219 +/* 导航栏样式 */
220 +:deep(.nut-navbar) {
221 + background-color: #f97316;
222 + color: #fff;
223 +}
224 +
225 +:deep(.nut-navbar__title) {
226 + color: #fff;
227 + font-size: 36rpx;
228 + font-weight: 600;
229 +}
230 +
231 +:deep(.nut-navbar__left) {
232 + color: #fff;
233 +}
234 +
235 +/* 响应式适配 */
236 +@media screen and (max-width: 375px) {
237 + .avatar-image {
238 + width: 140rpx;
239 + height: 140rpx;
240 + border-radius: 70rpx;
241 + }
242 +
243 + .camera-btn {
244 + width: 40rpx;
245 + height: 40rpx;
246 + border-radius: 20rpx;
247 + }
248 +
249 + .camera-icon {
250 + width: 20rpx;
251 + height: 20rpx;
252 + }
253 +
254 + .form-container :deep(.nut-form-item) {
255 + padding: 20rpx 24rpx;
256 + }
257 +
258 + .form-container :deep.nut-form-item__label {
259 + font-size: 26rpx;
260 + min-width: 120rpx;
261 + }
262 +
263 + .form-container :deep(.nut-input__inner) {
264 + font-size: 26rpx;
265 + }
266 +}
267 +
268 +/* 加载状态 */
269 +.register-section :deep(.nut-button--loading) {
270 + opacity: 0.7;
271 +}
272 +
273 +/* 禁用状态 */
274 +.register-section :deep(.nut-button--disabled) {
275 + background-color: #ccc !important;
276 + color: #999 !important;
277 +}
278 +
279 +/* 表单验证错误样式 */
280 +// .form-container :deep(.nut-form-item--error .nut-form-item__label) {
281 +// color: #ff4757;
282 +// }
283 +
284 +// .form-container :deep(.nut-form-item--error .nut-input__inner) {
285 +// border-color: #ff4757;
286 +// }
287 +
288 +/* 占位符颜色调整 */
289 +.birthday-value,
290 +.school-value {
291 + color: #999;
292 +}
293 +
294 +// .birthday-item:has(.birthday-value:not(:empty)),
295 +// .school-item:has(.school-value:not(:empty)) {
296 +// .birthday-value,
297 +// .school-value {
298 +// color: #333;
299 +// }
300 +// }
1 +<template>
2 + <view class="register-page">
3 + <!-- 头像区域 -->
4 + <view class="avatar-section">
5 + <view class="avatar-container">
6 + <image
7 + :src="formData.avatar || defaultAvatar"
8 + class="avatar-image"
9 + mode="aspectFill"
10 + @click="previewAvatar"
11 + />
12 + </view>
13 + <view class="change-avatar-btn" @click="changeAvatar">
14 + <text class="change-avatar-text">上传头像</text>
15 + </view>
16 + </view>
17 +
18 + <!-- 表单内容 -->
19 + <nut-form ref="formRef" :model-value="formData">
20 + <view class="form-container">
21 + <!-- 昵称 -->
22 + <nut-form-item label="昵称" prop="nickname" required :rules="[{ required: true, message: '请输入昵称' }]">
23 + <nut-input
24 + v-model="formData.nickname"
25 + placeholder="请输入昵称"
26 + input-align="right"
27 + clearable
28 + />
29 + </nut-form-item>
30 +
31 + <!-- 手机号 -->
32 + <nut-form-item label="手机号" prop="phone" required :rules="phoneRules">
33 + <view class="phone-input-container">
34 + <nut-input
35 + v-model="formData.phone"
36 + placeholder="请输入手机号"
37 + type="tel"
38 + maxlength="11"
39 + input-align="right"
40 + clearable
41 + class="phone-input"
42 + />
43 + <nut-button
44 + size="small"
45 + :disabled="codeCountdown > 0 || !isPhoneValid"
46 + @click="sendCode"
47 + class="code-btn"
48 + >
49 + {{ codeCountdown > 0 ? `${codeCountdown}s` : '获取验证码' }}
50 + </nut-button>
51 + </view>
52 + </nut-form-item>
53 +
54 + <!-- 验证码 -->
55 + <nut-form-item label="验证码" prop="verifyCode" required :rules="[{ required: true, message: '请输入验证码' }]">
56 + <nut-input
57 + v-model="formData.verifyCode"
58 + placeholder="请输入验证码"
59 + type="number"
60 + maxlength="6"
61 + input-align="right"
62 + clearable
63 + />
64 + </nut-form-item>
65 +
66 + <!-- 性别 -->
67 + <nut-form-item label="性别" prop="gender" body-align="right" required :rules="[{ required: true, message: '请选择性别' }]">
68 + <nut-radio-group v-model="formData.gender" direction="horizontal">
69 + <nut-radio label="男">男</nut-radio>
70 + <nut-radio label="女">女</nut-radio>
71 + </nut-radio-group>
72 + </nut-form-item>
73 +
74 + <!-- 生日 -->
75 + <nut-form-item label="生日" prop="birthday" label-position="top">
76 + <view class="birthday-item" @click="showDatePicker">
77 + <text class="birthday-value">{{ formData.birthday || '请选择生日' }}</text>
78 + <Right class="arrow-icon" />
79 + </view>
80 + </nut-form-item>
81 +
82 + <!-- 所在学校 -->
83 + <nut-form-item label="所在学校" prop="school" required :rules="[{ required: true, message: '请选择学校' }]" label-position="top">
84 + <view class="school-item" @click="showSchoolPicker">
85 + <text class="school-value">{{ formData.school || '请选择学校' }}</text>
86 + <Right class="arrow-icon" />
87 + </view>
88 + </nut-form-item>
89 + </view>
90 + </nut-form>
91 +
92 + <!-- 注册按钮 -->
93 + <view class="register-section">
94 + <nut-button
95 + color="#f97316"
96 + size="large"
97 + block
98 + @click="handleRegister"
99 + :loading="isRegistering"
100 + >
101 + {{ isRegistering ? '保存中...' : '保存' }}
102 + </nut-button>
103 + </view>
104 +
105 + <!-- 日期选择器 -->
106 + <nut-popup v-model:visible="datePickerVisible" position="bottom">
107 + <nut-date-picker
108 + v-model="dateValue"
109 + title="选择生日"
110 + @confirm="onDateConfirm"
111 + @cancel="datePickerVisible = false"
112 + />
113 + </nut-popup>
114 +
115 + <!-- 学校选择器 -->
116 + <nut-popup v-model:visible="schoolPickerVisible" position="bottom">
117 + <nut-picker
118 + v-model="schoolValue"
119 + :columns="schoolOptions"
120 + title="选择学校"
121 + @confirm="onSchoolConfirm"
122 + @cancel="schoolPickerVisible = false"
123 + />
124 + </nut-popup>
125 +
126 + <!-- 头像预览 -->
127 + <nut-image-preview
128 + v-model:show="avatarPreviewVisible"
129 + :images="[formData.avatar || defaultAvatar]"
130 + />
131 +
132 + <!-- 成功提示 -->
133 + <nut-toast
134 + v-model:visible="toastVisible"
135 + :msg="toastMessage"
136 + :type="toastType"
137 + />
138 + </view>
139 +</template>
140 +
141 +<script setup>
142 +import { ref, reactive, computed, onMounted } from 'vue'
143 +import Taro from '@tarojs/taro'
144 +import { RectLeft, Camera, Right } from '@nutui/icons-vue-taro'
145 +import './index.less'
146 +
147 +// 主题配置
148 +const themeVars = {
149 + navbarBackground: '#f97316',
150 + navbarColor: '#fff'
151 +}
152 +
153 +// 默认头像
154 +const defaultAvatar = 'https://randomuser.me/api/portraits/men/32.jpg'
155 +
156 +// 表单数据
157 +const formData = reactive({
158 + avatar: '',
159 + nickname: '',
160 + phone: '',
161 + verifyCode: '',
162 + gender: '',
163 + birthday: '',
164 + school: ''
165 +})
166 +
167 +// 弹框控制
168 +const datePickerVisible = ref(false)
169 +const schoolPickerVisible = ref(false)
170 +const avatarPreviewVisible = ref(false)
171 +const toastVisible = ref(false)
172 +const toastMessage = ref('')
173 +const toastType = ref('success')
174 +
175 +// 验证码相关
176 +const codeCountdown = ref(0)
177 +const isRegistering = ref(false)
178 +
179 +// 日期选择
180 +const dateValue = ref(new Date())
181 +
182 +// 学校选择
183 +const schoolValue = ref([])
184 +const schoolOptions = ref([
185 + [
186 + { text: '上海理工大学', value: '上海理工大学' },
187 + { text: '上海大学', value: '上海大学' },
188 + { text: '华东理工大学', value: '华东理工大学' },
189 + { text: '上海交通大学', value: '上海交通大学' },
190 + { text: '复旦大学', value: '复旦大学' },
191 + { text: '同济大学', value: '同济大学' },
192 + { text: '华东师范大学', value: '华东师范大学' },
193 + { text: '上海财经大学', value: '上海财经大学' }
194 + ]
195 +])
196 +
197 +// 手机号验证规则
198 +const phoneRules = [
199 + { required: true, message: '请输入手机号' },
200 + {
201 + pattern: /^1[3-9]\d{9}$/,
202 + message: '请输入正确的手机号格式'
203 + }
204 +]
205 +
206 +// 计算属性:手机号是否有效
207 +const isPhoneValid = computed(() => {
208 + return /^1[3-9]\d{9}$/.test(formData.phone)
209 +})
210 +
211 +/**
212 + * 返回上一页
213 + */
214 +const goBack = () => {
215 + Taro.navigateBack()
216 +}
217 +
218 +/**
219 + * 更换头像
220 + */
221 +const changeAvatar = () => {
222 + Taro.chooseImage({
223 + count: 1,
224 + sizeType: ['compressed'],
225 + sourceType: ['album', 'camera'],
226 + success: (res) => {
227 + formData.avatar = res.tempFilePaths[0]
228 + }
229 + })
230 +}
231 +
232 +/**
233 + * 预览头像
234 + */
235 +const previewAvatar = () => {
236 + avatarPreviewVisible.value = true
237 +}
238 +
239 +/**
240 + * 发送验证码
241 + */
242 +const sendCode = () => {
243 + if (!isPhoneValid.value) {
244 + showToast('请输入正确的手机号', 'error')
245 + return
246 + }
247 +
248 + // 模拟发送验证码
249 + codeCountdown.value = 60
250 + const timer = setInterval(() => {
251 + codeCountdown.value--
252 + if (codeCountdown.value <= 0) {
253 + clearInterval(timer)
254 + }
255 + }, 1000)
256 +
257 + showToast('验证码已发送', 'success')
258 +}
259 +
260 +/**
261 + * 显示日期选择器
262 + */
263 +const showDatePicker = () => {
264 + datePickerVisible.value = true
265 +}
266 +
267 +/**
268 + * 确认日期选择
269 + */
270 +const onDateConfirm = ({ selectedValue }) => {
271 + formData.birthday = `${selectedValue[0]}-${String(selectedValue[1]).padStart(2, '0')}-${String(selectedValue[2]).padStart(2, '0')}`
272 + datePickerVisible.value = false
273 +}
274 +
275 +/**
276 + * 显示学校选择器
277 + */
278 +const showSchoolPicker = () => {
279 + schoolPickerVisible.value = true
280 +}
281 +
282 +/**
283 + * 确认学校选择
284 + */
285 +const onSchoolConfirm = ({ selectedOptions }) => {
286 + formData.school = selectedOptions[0].text
287 + schoolPickerVisible.value = false
288 +}
289 +
290 +/**
291 + * 显示提示信息
292 + */
293 +const showToast = (message, type = 'success') => {
294 + toastMessage.value = message
295 + toastType.value = type
296 + toastVisible.value = true
297 +}
298 +
299 +/**
300 + * 表单验证
301 + */
302 +const validateForm = () => {
303 + if (!formData.nickname.trim()) {
304 + showToast('请输入昵称', 'error')
305 + return false
306 + }
307 +
308 + if (!isPhoneValid.value) {
309 + showToast('请输入正确的手机号', 'error')
310 + return false
311 + }
312 +
313 + if (!formData.verifyCode.trim()) {
314 + showToast('请输入验证码', 'error')
315 + return false
316 + }
317 +
318 + if (!formData.gender) {
319 + showToast('请选择性别', 'error')
320 + return false
321 + }
322 +
323 + if (!formData.school) {
324 + showToast('请选择学校', 'error')
325 + return false
326 + }
327 +
328 + return true
329 +}
330 +
331 +/**
332 + * 处理注册
333 + */
334 +const handleRegister = async () => {
335 + if (!validateForm()) {
336 + return
337 + }
338 +
339 + isRegistering.value = true
340 +
341 + try {
342 + // 模拟注册API调用
343 + await new Promise(resolve => setTimeout(resolve, 2000))
344 +
345 + // TODO: 这里应该调用实际的注册API
346 + // const result = await registerAPI(formData)
347 +
348 + showToast('注册成功', 'success')
349 +
350 + setTimeout(() => {
351 + // 注册成功后跳转到登录页面或首页
352 + Taro.navigateBack()
353 + }, 1500)
354 +
355 + } catch (error) {
356 + showToast('注册失败,请重试', 'error')
357 + } finally {
358 + isRegistering.value = false
359 + }
360 +}
361 +
362 +// 初始化
363 +onMounted(() => {
364 + // 可以在这里进行初始化操作
365 +})
366 +</script>
367 +
368 +<script>
369 +export default {
370 + name: 'RegisterPage'
371 +}
372 +</script>