hookehuyr

feat(profile): 添加编辑资料页面功能

- 新增编辑资料页面,包含头像、昵称、手机号等表单字段
- 实现头像上传、日期选择、学校选择等功能
- 添加手机号绑定弹框和验证码发送逻辑
- 更新路由配置和组件类型声明
......@@ -11,6 +11,7 @@ declare module 'vue' {
NutButton: typeof import('@nutui/nutui-taro')['Button']
NutCol: typeof import('@nutui/nutui-taro')['Col']
NutConfigProvider: typeof import('@nutui/nutui-taro')['ConfigProvider']
NutDatePicker: typeof import('@nutui/nutui-taro')['DatePicker']
NutForm: typeof import('@nutui/nutui-taro')['Form']
NutFormItem: typeof import('@nutui/nutui-taro')['FormItem']
NutImagePreview: typeof import('@nutui/nutui-taro')['ImagePreview']
......@@ -20,6 +21,8 @@ declare module 'vue' {
NutNavbar: typeof import('@nutui/nutui-taro')['Navbar']
NutPicker: typeof import('@nutui/nutui-taro')['Picker']
NutPopup: typeof import('@nutui/nutui-taro')['Popup']
NutRadio: typeof import('@nutui/nutui-taro')['Radio']
NutRadioGroup: typeof import('@nutui/nutui-taro')['RadioGroup']
NutRow: typeof import('@nutui/nutui-taro')['Row']
NutSearchbar: typeof import('@nutui/nutui-taro')['Searchbar']
NutSticky: typeof import('@nutui/nutui-taro')['Sticky']
......@@ -28,6 +31,7 @@ declare module 'vue' {
NutTabPane: typeof import('@nutui/nutui-taro')['TabPane']
NutTabs: typeof import('@nutui/nutui-taro')['Tabs']
NutTextarea: typeof import('@nutui/nutui-taro')['Textarea']
NutToast: typeof import('@nutui/nutui-taro')['Toast']
Picker: typeof import('./src/components/time-picker-data/picker.vue')['default']
PosterBuilder: typeof import('./src/components/PosterBuilder/index.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
......
/*
* @Date: 2025-06-28 10:33:00
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-07-02 14:02:10
* @LastEditTime: 2025-07-02 15:05:28
* @FilePath: /jgdl/src/app.config.js
* @Description: 文件描述
*/
......@@ -12,6 +12,7 @@ export default {
'pages/sell/index',
'pages/messages/index',
'pages/profile/index',
'pages/editProfile/index',
'pages/auth/index',
],
subpackages: [ // 配置在tabBar中的页面不能分包写到subpackages中去
......
<!--
* @Date: 2022-09-19 14:11:06
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-07-01 10:56:38
* @FilePath: /myApp/src/pages/demo/index.vue
* @LastEditTime: 2025-07-02 14:50:57
* @FilePath: /jgdl/src/pages/demo/index.vue
* @Description: 文件描述
-->
<template>
......@@ -10,7 +10,7 @@
</template>
<script setup>
import '@tarojs/taro/html.css'
// import '@tarojs/taro/html.css'
import { ref } from "vue";
import "./index.less";
......
/*
* @Date: 2025-07-02 14:50:25
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-07-02 15:22:00
* @FilePath: /jgdl/src/pages/editProfile/index.config.js
* @Description: 文件描述
*/
export default {
navigationBarTitleText: '编辑资料',
usingComponents: {
},
}
.edit-profile-page {
background-color: #f5f5f5;
min-height: 100vh;
}
// 头像区域
.avatar-section {
display: flex;
flex-direction: column;
align-items: center;
padding: 48rpx 0;
background-color: #fff;
margin-bottom: 20rpx;
.avatar-container {
position: relative;
margin-bottom: 32rpx;
.avatar-image {
width: 192rpx;
height: 192rpx;
border-radius: 50%;
object-fit: cover;
}
.camera-btn {
position: absolute;
bottom: 0;
right: 0;
width: 48rpx;
height: 48rpx;
background-color: #fff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
.camera-icon {
width: 32rpx;
height: 32rpx;
color: #666;
}
}
}
.change-avatar-btn {
background-color: #f5f5f5;
padding: 16rpx 40rpx;
border-radius: 48rpx;
.change-avatar-text {
color: #666;
font-size: 28rpx;
}
}
}
// 表单容器
.form-container {
background-color: #fff;
margin-bottom: 40rpx;
:deep(.nut-form-item) {
padding: 32rpx 32rpx;
border-bottom: 1rpx solid #f0f0f0;
&:last-child {
border-bottom: none;
}
.nut-form-item__label {
color: #333;
font-size: 32rpx;
font-weight: normal;
}
.nut-input {
text-align: right;
.nut-input__inner {
color: #666;
font-size: 32rpx;
}
}
}
// 手机号项
.phone-item {
display: flex;
align-items: center;
justify-content: flex-end;
width: 100%;
.phone-value {
color: #666;
font-size: 32rpx;
margin-right: 16rpx;
}
.arrow-icon {
width: 32rpx;
height: 32rpx;
color: #ccc;
}
}
// 生日项
.birthday-item {
display: flex;
align-items: center;
justify-content: flex-end;
width: 100%;
.birthday-value {
color: #666;
font-size: 32rpx;
margin-right: 16rpx;
}
.arrow-icon {
width: 32rpx;
height: 32rpx;
color: #ccc;
}
}
// 学校项
.school-item {
display: flex;
align-items: center;
justify-content: flex-end;
width: 100%;
.school-value {
color: #666;
font-size: 32rpx;
margin-right: 16rpx;
}
.arrow-icon {
width: 32rpx;
height: 32rpx;
color: #ccc;
}
}
// 性别单选组
:deep(.nut-radio-group) {
display: flex;
justify-content: flex-end;
gap: 64rpx;
.nut-radio {
.nut-radio__label {
color: #666;
font-size: 32rpx;
}
&.nut-radio--checked {
.nut-radio__icon {
color: #f97316;
}
.nut-radio__label {
color: #333;
}
}
}
}
}
// 保存按钮区域
.save-section {
padding: 0 32rpx 80rpx;
:deep(.nut-button) {
height: 96rpx;
border-radius: 48rpx;
font-size: 32rpx;
font-weight: 500;
}
}
// 手机号绑定弹框
.phone-dialog {
padding: 48rpx 32rpx 32rpx;
border-radius: 16rpx;
.dialog-title {
text-align: center;
font-size: 36rpx;
font-weight: 500;
color: #333;
margin-bottom: 48rpx;
}
.dialog-content {
margin-bottom: 48rpx;
.phone-input {
margin-bottom: 32rpx;
:deep(.nut-input__inner) {
border: 1rpx solid #e0e0e0;
border-radius: 8rpx;
padding: 24rpx 16rpx;
font-size: 32rpx;
}
}
.code-row {
display: flex;
gap: 16rpx;
align-items: center;
.code-input {
flex: 1;
:deep(.nut-input__inner) {
border: 1rpx solid #e0e0e0;
border-radius: 8rpx;
padding: 24rpx 16rpx;
font-size: 32rpx;
}
}
.code-btn {
:deep(.nut-button__warp) {
padding: 24rpx 32rpx;
font-size: 28rpx;
white-space: nowrap;
}
}
}
}
.dialog-actions {
display: flex;
gap: 24rpx;
.cancel-btn,
.confirm-btn {
flex: 1;
:deep(.nut-button__warp) {
height: 80rpx;
border-radius: 8rpx;
font-size: 32rpx;
}
}
.cancel-btn {
:deep(.nut-button__warp) {
background-color: #f5f5f5;
color: #666;
border: none;
}
}
}
}
// 响应式适配
@media (max-width: 750rpx) {
.avatar-section {
padding: 40rpx 0;
.avatar-container {
.avatar-image {
width: 160rpx;
height: 160rpx;
}
.camera-btn {
width: 40rpx;
height: 40rpx;
.camera-icon {
width: 24rpx;
height: 24rpx;
}
}
}
}
.form-container {
:deep(.nut-form-item) {
padding: 24rpx 24rpx;
.nut-form-item__label {
font-size: 28rpx;
}
.nut-input {
.nut-input__inner {
font-size: 28rpx;
}
}
}
.phone-item,
.birthday-item,
.school-item {
.phone-value,
.birthday-value,
.school-value {
font-size: 28rpx;
}
}
}
.save-section {
padding: 0 24rpx 60rpx;
:deep(.nut-button) {
height: 80rpx;
font-size: 28rpx;
}
}
}
<template>
<view class="edit-profile-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">
<nut-input
v-model="formData.nickname"
placeholder="请输入昵称"
input-align="right"
/>
</nut-form-item>
<!-- 手机号 -->
<nut-form-item label="手机号" prop="phone">
<view class="phone-item" @click="showPhoneDialog">
<text class="phone-value">{{ formData.phone || '未绑定' }}</text>
<Right class="arrow-icon" />
</view>
</nut-form-item>
<!-- 性别 -->
<nut-form-item label="性别" prop="gender" body-align="right">
<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">
<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">
<view class="school-item" @click="showSchoolPicker">
<text class="school-value">{{ formData.school || '请选择学校' }}</text>
<Right class="arrow-icon" />
</view>
</nut-form-item>
<!-- 微信号 -->
<nut-form-item label="微信号" prop="wechat">
<nut-input
v-model="formData.wechat"
placeholder="请输入微信号"
input-align="right"
>
</nut-input>
</nut-form-item>
</view>
</nut-form>
<!-- 保存按钮 -->
<view class="save-section mt-32">
<nut-button
color="#f97316"
size="large"
block
@click="handleSave"
>
保存
</nut-button>
</view>
<!-- 手机号绑定弹框 -->
<nut-popup v-model:visible="phoneDialogVisible" position="center" :style="{ width: '80%' }">
<view class="phone-dialog">
<view class="dialog-title">重新绑定手机号</view>
<view class="dialog-content">
<nut-input
v-model="newPhone"
placeholder="请输入新手机号"
type="tel"
class="phone-input"
/>
<view class="code-row">
<nut-input
v-model="verifyCode"
placeholder="验证码"
class="code-input"
/>
<nut-button
size="small"
:disabled="codeCountdown > 0"
@click="sendCode"
class="code-btn"
>
{{ codeCountdown > 0 ? `${codeCountdown}s` : '发送验证码' }}
</nut-button>
</view>
</view>
<view class="dialog-actions">
<nut-button
size="small"
@click="phoneDialogVisible = false"
class="cancel-btn"
>
取消
</nut-button>
<nut-button
size="small"
color="#f97316"
@click="confirmPhoneChange"
class="confirm-btn"
>
确认
</nut-button>
</view>
</view>
</nut-popup>
<!-- 日期选择器 -->
<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="保存成功"
type="success"
/>
</view>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import Taro from '@tarojs/taro'
import { RectLeft, Right } from '@nutui/icons-vue-taro'
import './index.less'
// 主题配置
const themeVars = ref({
navbarBackground: '#fb923c',
navbarColor: '#ffffff',
})
// 默认头像
const defaultAvatar = 'https://randomuser.me/api/portraits/men/32.jpg'
// 表单数据
const formData = reactive({
avatar: '',
nickname: '张先生',
phone: '139 2233 8888',
gender: '男',
birthday: '',
school: '上海理工大学',
wechat: ''
})
// 弹框控制
const phoneDialogVisible = ref(false)
const datePickerVisible = ref(false)
const schoolPickerVisible = ref(false)
const avatarPreviewVisible = ref(false)
const toastVisible = ref(false)
// 手机号相关
const newPhone = ref('')
const verifyCode = ref('')
const codeCountdown = ref(0)
// 日期选择
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 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 showPhoneDialog = () => {
phoneDialogVisible.value = true
newPhone.value = ''
verifyCode.value = ''
}
// 发送验证码
const sendCode = () => {
if (!newPhone.value) {
Taro.showToast({
title: '请输入手机号',
icon: 'none'
})
return
}
// 模拟发送验证码
codeCountdown.value = 60
const timer = setInterval(() => {
codeCountdown.value--
if (codeCountdown.value <= 0) {
clearInterval(timer)
}
}, 1000)
Taro.showToast({
title: '验证码已发送',
icon: 'success'
})
}
// 确认手机号更改
const confirmPhoneChange = () => {
if (!newPhone.value || !verifyCode.value) {
Taro.showToast({
title: '请填写完整信息',
icon: 'none'
})
return
}
formData.phone = newPhone.value
phoneDialogVisible.value = false
Taro.showToast({
title: '手机号绑定成功',
icon: 'success'
})
}
// 显示日期选择器
const showDatePicker = () => {
datePickerVisible.value = true
}
// 确认日期选择
const onDateConfirm = ({ selectedValue }) => {
const date = new Date(selectedValue[0], selectedValue[1] - 1, selectedValue[2])
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 handleSave = () => {
// 这里可以添加表单验证
console.log('保存数据:', formData)
toastVisible.value = true
setTimeout(() => {
toastVisible.value = false
goBack()
}, 1500)
}
// 初始化
onMounted(() => {
// 可以在这里加载用户数据
})
</script>
<script>
export default {
name: 'EditProfilePage'
}
</script>
......@@ -103,7 +103,7 @@ const userStats = ref({
*/
const onEditProfile = () => {
Taro.navigateTo({
url: '/pages/edit-profile/index'
url: '/pages/editProfile/index'
})
}
......