xiangyixiangqin/miniapp/pages/profile/edit.vue
2026-01-18 18:13:01 +08:00

1833 lines
48 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="profile-edit-page">
<!-- 固定头部区域 -->
<view class="fixed-header">
<!-- 自定义导航栏带渐变背景 -->
<view class="custom-navbar" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="navbar-content">
<view class="navbar-back" @click="handleBack">
<text class="back-icon"></text>
</view>
<text class="navbar-title">填写资料</text>
<view class="navbar-placeholder"></view>
</view>
</view>
<!-- 步骤指示器白色背景不在渐变内 -->
<view class="step-indicator">
<view
v-for="(step, index) in steps"
:key="index"
class="step-item"
:class="{ active: currentStep === index, completed: currentStep > index }"
>
<view class="step-dot">{{ index + 1 }}</view>
<text class="step-label">{{ step.label }}</text>
</view>
</view>
</view>
<!-- 占位区域 -->
<view class="header-placeholder" :style="{ height: headerHeight + 'px' }"></view>
<!-- 步骤内容 -->
<view class="step-content">
<!-- 步骤1: 基础信息 -->
<view v-show="currentStep === 0" class="form-section">
<view class="section-title">基础信息</view>
<!-- 照片上传 -->
<view class="photo-upload-section">
<view class="photo-label">上传照片最多5张</view>
<view class="photo-grid">
<view
v-for="(photo, index) in formData.photos"
:key="index"
class="photo-item"
>
<image :src="photo.url" mode="aspectFill" class="photo-image" />
<view class="photo-delete" @click="handleDeletePhoto(index)">×</view>
</view>
<view
v-if="formData.photos.length < 5"
class="photo-add"
@click="handleChoosePhoto"
>
<text class="add-icon">+</text>
<text class="add-text">添加照片</text>
</view>
</view>
</view>
<!-- 照片公开设置 -->
<view class="form-item">
<text class="form-label">照片是否公开</text>
<view class="radio-group">
<view
class="radio-item"
:class="{ active: formData.isPhotoPublic }"
@click="formData.isPhotoPublic = true"
>
<view class="radio-dot"></view>
<text>公开</text>
</view>
<view
class="radio-item"
:class="{ active: !formData.isPhotoPublic }"
@click="formData.isPhotoPublic = false"
>
<view class="radio-dot"></view>
<text>私密</text>
</view>
</view>
</view>
<!-- 关系 -->
<view class="form-item">
<text class="form-label required">您与相亲者的关系</text>
<picker
:value="relationshipIndex"
:range="relationshipOptions"
range-key="label"
@change="handleRelationshipChange"
>
<view class="picker-value">
{{ relationshipOptions[relationshipIndex]?.label || '请选择' }}
<text class="picker-arrow">▼</text>
</view>
</picker>
</view>
<!-- 姓氏 -->
<view class="form-item">
<text class="form-label required">姓氏</text>
<input
v-model="formData.surname"
placeholder="请输入姓氏"
maxlength="2"
@blur="handleSurnameBlur"
/>
</view>
<!-- 昵称(自动生成) -->
<view class="form-item">
<text class="form-label">昵称(自动生成)</text>
<input
:value="formData.nickname"
disabled
placeholder="填写关系和姓氏后自动生成"
placeholder-class="input-placeholder"
/>
</view>
<!-- 相亲者性别 -->
<view class="form-item">
<text class="form-label required">相亲者性别</text>
<view class="radio-group">
<view
class="radio-item"
:class="{ active: formData.childGender === 1 }"
@click="formData.childGender = 1"
>
<view class="radio-dot"></view>
<text>男</text>
</view>
<view
class="radio-item"
:class="{ active: formData.childGender === 2 }"
@click="formData.childGender = 2"
>
<view class="radio-dot"></view>
<text>女</text>
</view>
</view>
</view>
<!-- 出生年份 -->
<view class="form-item">
<text class="form-label required">出生年份</text>
<picker
:value="birthYearIndex"
:range="birthYearOptions"
@change="handleBirthYearChange"
>
<view class="picker-value">
{{ formData.birthYear || '请选择' }}
<text class="picker-arrow">▼</text>
</view>
</picker>
</view>
<!-- 籍贯 -->
<view class="form-item">
<text class="form-label">籍贯</text>
<picker
mode="region"
:value="[formData.homeProvince, formData.homeCity, formData.homeDistrict]"
@change="handleHomeLocationChange"
>
<view class="picker-value">
{{ homeLocationText || '请选择' }}
<text class="picker-arrow">▼</text>
</view>
</picker>
</view>
</view>
<!-- 步骤2: 详细信息 -->
<view v-show="currentStep === 1" class="form-section">
<view class="section-title">详细信息</view>
<!-- 学历 -->
<view class="form-item">
<text class="form-label required">学历</text>
<picker
:value="educationIndex"
:range="educationOptions"
range-key="label"
@change="handleEducationChange"
>
<view class="picker-value">
{{ educationOptions[educationIndex]?.label || '请选择' }}
<text class="picker-arrow">▼</text>
</view>
</picker>
</view>
<!-- 工作地点 -->
<view class="form-item">
<text class="form-label required">工作地点</text>
<picker
mode="region"
:value="[formData.workProvince, formData.workCity, formData.workDistrict]"
@change="handleWorkLocationChange"
>
<view class="picker-value">
{{ workLocationText || '请选择' }}
<text class="picker-arrow">▼</text>
</view>
</picker>
</view>
<!-- 职业 -->
<view class="form-item">
<text class="form-label required">职业</text>
<input
v-model="formData.occupation"
placeholder="请输入职业"
maxlength="20"
/>
</view>
<!-- 月收入 -->
<view class="form-item">
<text class="form-label required">月收入</text>
<picker
:value="incomeIndex"
:range="incomeOptions"
range-key="label"
@change="handleIncomeChange"
>
<view class="picker-value">
{{ incomeOptions[incomeIndex]?.label || '请选择' }}
<text class="picker-arrow">▼</text>
</view>
</picker>
</view>
<!-- 身高 -->
<view class="form-item">
<text class="form-label required">身高(cm)</text>
<picker
:value="heightIndex"
:range="heightOptions"
@change="handleHeightChange"
>
<view class="picker-value">
{{ formData.height ? formData.height + 'cm' : '请选择' }}
<text class="picker-arrow">▼</text>
</view>
</picker>
</view>
<!-- 体重 -->
<view class="form-item">
<text class="form-label">体重(kg)</text>
<picker
:value="weightIndex"
:range="weightOptions"
@change="handleWeightChange"
>
<view class="picker-value">
{{ formData.weight ? formData.weight + 'kg' : '请选择' }}
<text class="picker-arrow">▼</text>
</view>
</picker>
</view>
<!-- 房产状态 -->
<view class="form-item">
<text class="form-label required">房产状态</text>
<picker
:value="houseIndex"
:range="houseOptions"
range-key="label"
@change="handleHouseChange"
>
<view class="picker-value">
{{ houseOptions[houseIndex]?.label || '请选择' }}
<text class="picker-arrow">▼</text>
</view>
</picker>
</view>
<!-- 车辆状态 -->
<view class="form-item">
<text class="form-label required">车辆状态</text>
<picker
:value="carIndex"
:range="carOptions"
range-key="label"
@change="handleCarChange"
>
<view class="picker-value">
{{ carOptions[carIndex]?.label || '请选择' }}
<text class="picker-arrow">▼</text>
</view>
</picker>
</view>
<!-- 婚姻状态 -->
<view class="form-item">
<text class="form-label required">婚姻状态</text>
<picker
:value="marriageIndex"
:range="marriageOptions"
range-key="label"
@change="handleMarriageChange"
>
<view class="picker-value">
{{ marriageOptions[marriageIndex]?.label || '请选择' }}
<text class="picker-arrow">▼</text>
</view>
</picker>
</view>
<!-- 期望结婚时间 -->
<view class="form-item">
<text class="form-label">期望结婚时间</text>
<picker
:value="expectMarryIndex"
:range="expectMarryOptions"
range-key="label"
@change="handleExpectMarryChange"
>
<view class="picker-value">
{{ expectMarryOptions[expectMarryIndex]?.label || '请选择' }}
<text class="picker-arrow">▼</text>
</view>
</picker>
</view>
</view>
<!-- 步骤3: 自我介绍 -->
<view v-show="currentStep === 2" class="form-section">
<view class="section-title">自我介绍</view>
<view class="form-item textarea-item">
<text class="form-label required">介绍一下相亲者</text>
<textarea
v-model="formData.introduction"
placeholder="请介绍相亲者的性格、爱好、优点等让对方更了解TA"
maxlength="500"
:show-count="true"
/>
<text class="char-count">{{ formData.introduction.length }}/500</text>
</view>
</view>
<!-- 步骤4: 择偶要求 -->
<view v-show="currentStep === 3" class="form-section">
<view class="section-title">择偶要求</view>
<!-- 年龄范围 -->
<view class="form-item">
<text class="form-label">年龄范围</text>
<view class="range-picker">
<picker
:value="requirementAgeMinIndex"
:range="ageRangeOptions"
@change="handleAgeMinChange"
>
<view class="picker-value small">{{ formData.requirement.ageMin || '不限' }}</view>
</picker>
<text class="range-separator">至</text>
<picker
:value="requirementAgeMaxIndex"
:range="ageRangeOptions"
@change="handleAgeMaxChange"
>
<view class="picker-value small">{{ formData.requirement.ageMax || '不限' }}</view>
</picker>
<text class="range-unit">岁</text>
</view>
</view>
<!-- 身高范围 -->
<view class="form-item">
<text class="form-label">身高范围</text>
<view class="range-picker">
<picker
:value="requirementHeightMinIndex"
:range="heightRangeOptions"
@change="handleHeightMinChange"
>
<view class="picker-value small">{{ formData.requirement.heightMin || '不限' }}</view>
</picker>
<text class="range-separator">至</text>
<picker
:value="requirementHeightMaxIndex"
:range="heightRangeOptions"
@change="handleHeightMaxChange"
>
<view class="picker-value small">{{ formData.requirement.heightMax || '不限' }}</view>
</picker>
<text class="range-unit">cm</text>
</view>
</view>
<!-- 学历要求 -->
<view class="form-item">
<text class="form-label">学历要求</text>
<view class="checkbox-group">
<view
v-for="edu in educationOptions"
:key="edu.value"
class="checkbox-item"
:class="{ active: formData.requirement.education.includes(edu.value) }"
@click="toggleEducationRequirement(edu.value)"
>
{{ edu.label }}
</view>
</view>
</view>
<!-- 意向城市1 -->
<view class="form-item">
<text class="form-label">意向城市1</text>
<picker
mode="region"
:level="'city'"
:value="[formData.requirement.city1Province, formData.requirement.city1City]"
@change="handleCity1Change"
>
<view class="picker-value">
{{ city1Text || '请选择' }}
<text class="picker-arrow">▼</text>
</view>
</picker>
</view>
<!-- 意向城市2 -->
<view class="form-item">
<text class="form-label">意向城市2</text>
<picker
mode="region"
:level="'city'"
:value="[formData.requirement.city2Province, formData.requirement.city2City]"
@change="handleCity2Change"
>
<view class="picker-value">
{{ city2Text || '请选择' }}
<text class="picker-arrow">▼</text>
</view>
</picker>
</view>
<!-- 房产要求 -->
<view class="form-item">
<text class="form-label">房产要求</text>
<view class="checkbox-group">
<view
v-for="house in houseOptions"
:key="house.value"
class="checkbox-item"
:class="{ active: formData.requirement.houseStatus.includes(house.value) }"
@click="toggleHouseRequirement(house.value)"
>
{{ house.label }}
</view>
</view>
</view>
<!-- 车辆要求 -->
<view class="form-item">
<text class="form-label">车辆要求</text>
<view class="checkbox-group">
<view
v-for="car in carOptions"
:key="car.value"
class="checkbox-item"
:class="{ active: formData.requirement.carStatus.includes(car.value) }"
@click="toggleCarRequirement(car.value)"
>
{{ car.label }}
</view>
</view>
</view>
<!-- 婚姻状态要求 -->
<view class="form-item">
<text class="form-label">婚姻状态要求</text>
<view class="checkbox-group">
<view
v-for="marriage in marriageOptions"
:key="marriage.value"
class="checkbox-item"
:class="{ active: formData.requirement.marriageStatus.includes(marriage.value) }"
@click="toggleMarriageRequirement(marriage.value)"
>
{{ marriage.label }}
</view>
</view>
</view>
<!-- 期望月收入 -->
<view class="form-item">
<text class="form-label">期望月收入</text>
<picker
:value="requirementIncomeMinIndex"
:range="incomeRangeOptions"
@change="handleIncomeMinChange"
>
<view class="picker-value">
{{ getIncomeLabel(formData.requirement.monthlyIncomeMin) }}
<text class="picker-arrow">▼</text>
</view>
</picker>
</view>
</view>
<!-- 步骤5: 联系方式 -->
<view v-show="currentStep === 4" class="form-section">
<view class="section-title">联系方式</view>
<!-- 微信号 -->
<view class="form-item">
<text class="form-label required">微信号</text>
<input
v-model="formData.weChatNo"
placeholder="请输入微信号"
maxlength="30"
/>
</view>
<!-- 手机号验证 -->
<view class="form-item">
<text class="form-label">手机号验证</text>
<button
v-if="!phoneVerified"
class="verify-btn"
open-type="getPhoneNumber"
@getphonenumber="handleGetPhoneNumber"
>
点击验证手机号
</button>
<view v-else class="phone-verified">
<text class="phone-number">{{ formData.phone }}</text>
<text class="verified-tag">已验证</text>
</view>
</view>
</view>
</view>
<!-- 底部按钮 -->
<view class="bottom-buttons">
<button
v-if="currentStep > 0"
class="btn-prev"
@click="handlePrevStep"
>
上一步
</button>
<button
v-if="currentStep < steps.length - 1"
class="btn-next"
@click="handleNextStep"
>
下一步
</button>
<button
v-if="currentStep === steps.length - 1"
class="btn-submit"
:loading="submitting"
@click="handleSubmit"
>
提交资料
</button>
</view>
</view>
</template>
<script setup>
import { ref, reactive, computed, onMounted, watch } from 'vue'
import { useUserStore } from '@/store/user.js'
import { createOrUpdate, getMyProfile, uploadPhotos, deletePhoto, cleanupOrphanPhotos } from '@/api/profile.js'
import { generateNickname, getBirthYearRange } from '@/utils/format.js'
import config from '@/config/index.js'
const userStore = useUserStore()
// 从统一配置获取静态资源地址
const STATIC_URL = config.STATIC_BASE_URL
// 状态栏高度
const statusBarHeight = ref(20)
const headerHeight = ref(120)
// 获取系统信息
const getSystemInfo = () => {
uni.getSystemInfo({
success: (res) => {
statusBarHeight.value = res.statusBarHeight || 20
// 计算头部总高度:状态栏 + 导航栏(44px) + 步骤指示器(约100px)
headerHeight.value = statusBarHeight.value + 44 + 100
}
})
}
// 返回确认
const handleBack = () => {
uni.showModal({
title: '提示',
content: '确定要离开吗?已填写的内容将不会保存',
confirmText: '离开',
cancelText: '继续填写',
success: (res) => {
if (res.confirm) {
uni.navigateBack()
}
}
})
}
// 步骤配置
const steps = [
{ label: '基础信息' },
{ label: '详细信息' },
{ label: '介绍' },
{ label: '要求' },
{ label: '联系方式' }
]
const currentStep = ref(0)
const submitting = ref(false)
const phoneVerified = ref(false)
// 表单数据
const formData = reactive({
photos: [],
isPhotoPublic: true,
relationship: 1, // 默认选择"父亲"
surname: '',
nickname: '',
childGender: 1, // 默认选择"男"
birthYear: 0,
education: 1, // 默认选择"高中及以下"
workProvince: '',
workCity: '',
workDistrict: '',
homeProvince: '',
homeCity: '',
homeDistrict: '',
occupation: '',
monthlyIncome: 1, // 默认选择"5000以下"
height: 0,
weight: 0,
houseStatus: 5, // 默认选择"租房"
carStatus: 2, // 默认选择"无车"
marriageStatus: 1, // 默认选择"未婚"
expectMarryTime: 0,
introduction: '',
weChatNo: '',
phone: '', // 验证后的手机号
requirement: {
ageMin: 0,
ageMax: 0,
heightMin: 0,
heightMax: 0,
education: [],
city1Province: '',
city1City: '',
city2Province: '',
city2City: '',
monthlyIncomeMin: 0,
monthlyIncomeMax: 0,
houseStatus: [],
carStatus: [],
marriageStatus: []
}
})
// 选项配置
const relationshipOptions = [
{ value: 1, label: '父亲' },
{ value: 2, label: '母亲' },
{ value: 3, label: '本人' }
]
const educationOptions = [
{ value: 1, label: '高中' },
{ value: 2, label: '中专' },
{ value: 3, label: '大专' },
{ value: 4, label: '本科' },
{ value: 5, label: '研究生' },
{ value: 6, label: '博士及以上' }
]
const incomeOptions = [
{ value: 1, label: '5000以下' },
{ value: 2, label: '5000-10000' },
{ value: 3, label: '10000-20000' },
{ value: 4, label: '20000-50000' },
{ value: 5, label: '50000以上' }
]
const houseOptions = [
{ value: 1, label: '现居地已购房' },
{ value: 2, label: '家乡已购房' },
{ value: 3, label: '婚后购房' },
{ value: 4, label: '父母同住' },
{ value: 5, label: '租房' },
{ value: 6, label: '近期有购房计划' }
]
const carOptions = [
{ value: 1, label: '已购车' },
{ value: 2, label: '无车' },
{ value: 3, label: '近期购车' }
]
const marriageOptions = [
{ value: 1, label: '未婚' },
{ value: 2, label: '离异未育' },
{ value: 3, label: '离异已育' }
]
const expectMarryOptions = [
{ value: 1, label: '尽快结婚' },
{ value: 2, label: '一到两年内' },
{ value: 3, label: '孩子满意就结婚' }
]
// 出生年份选项 (Property 9: Birth Year Range)
const birthYearOptions = computed(() => getBirthYearRange())
// 身高选项 (140-220cm)
const heightOptions = computed(() => {
const options = []
for (let h = 140; h <= 220; h++) {
options.push(h)
}
return options
})
// 体重选项 (30-150kg)
const weightOptions = computed(() => {
const options = []
for (let w = 30; w <= 150; w++) {
options.push(w)
}
return options
})
// 年龄范围选项 (18-60)
const ageRangeOptions = computed(() => {
const options = ['不限']
for (let a = 18; a <= 60; a++) {
options.push(a)
}
return options
})
// 身高范围选项
const heightRangeOptions = computed(() => {
const options = ['不限']
for (let h = 140; h <= 220; h++) {
options.push(h)
}
return options
})
// 月收入范围选项
const incomeRangeOptions = ['不限', '5000以下', '5000-10000', '10000-20000', '20000-50000', '50000以上']
// 获取月收入标签
const getIncomeLabel = (value) => {
if (!value) return '不限'
const labels = {
1: '5000以下',
2: '5000-10000',
3: '10000-20000',
4: '20000-50000',
5: '50000以上'
}
return labels[value] || '不限'
}
// 选中索引计算
const relationshipIndex = computed(() => {
const idx = relationshipOptions.findIndex(o => o.value === formData.relationship)
return idx >= 0 ? idx : 0
})
const birthYearIndex = computed(() => {
const idx = birthYearOptions.value.indexOf(formData.birthYear)
return idx >= 0 ? idx : 0
})
const educationIndex = computed(() => {
const idx = educationOptions.findIndex(o => o.value === formData.education)
return idx >= 0 ? idx : 0
})
const incomeIndex = computed(() => {
const idx = incomeOptions.findIndex(o => o.value === formData.monthlyIncome)
return idx >= 0 ? idx : 0
})
const heightIndex = computed(() => {
const idx = heightOptions.value.indexOf(formData.height)
return idx >= 0 ? idx : 0
})
const weightIndex = computed(() => {
const idx = weightOptions.value.indexOf(formData.weight)
return idx >= 0 ? idx : 0
})
const houseIndex = computed(() => {
const idx = houseOptions.findIndex(o => o.value === formData.houseStatus)
return idx >= 0 ? idx : 0
})
const carIndex = computed(() => {
const idx = carOptions.findIndex(o => o.value === formData.carStatus)
return idx >= 0 ? idx : 0
})
const marriageIndex = computed(() => {
const idx = marriageOptions.findIndex(o => o.value === formData.marriageStatus)
return idx >= 0 ? idx : 0
})
const expectMarryIndex = computed(() => {
const idx = expectMarryOptions.findIndex(o => o.value === formData.expectMarryTime)
return idx >= 0 ? idx : 0
})
// 择偶要求索引
const requirementAgeMinIndex = computed(() => {
if (!formData.requirement.ageMin) return 0
return ageRangeOptions.value.indexOf(formData.requirement.ageMin)
})
const requirementAgeMaxIndex = computed(() => {
if (!formData.requirement.ageMax) return 0
return ageRangeOptions.value.indexOf(formData.requirement.ageMax)
})
const requirementHeightMinIndex = computed(() => {
if (!formData.requirement.heightMin) return 0
return heightRangeOptions.value.indexOf(formData.requirement.heightMin)
})
const requirementHeightMaxIndex = computed(() => {
if (!formData.requirement.heightMax) return 0
return heightRangeOptions.value.indexOf(formData.requirement.heightMax)
})
const requirementIncomeMinIndex = computed(() => {
if (!formData.requirement.monthlyIncomeMin) return 0
return formData.requirement.monthlyIncomeMin
})
// 地点文本
const workLocationText = computed(() => {
if (formData.workProvince && formData.workCity) {
return `${formData.workProvince} ${formData.workCity} ${formData.workDistrict || ''}`
}
return ''
})
const homeLocationText = computed(() => {
if (formData.homeProvince && formData.homeCity) {
return `${formData.homeProvince} ${formData.homeCity} ${formData.homeDistrict || ''}`
}
return ''
})
const city1Text = computed(() => {
if (formData.requirement.city1Province && formData.requirement.city1City) {
return `${formData.requirement.city1Province} ${formData.requirement.city1City}`
}
return ''
})
const city2Text = computed(() => {
if (formData.requirement.city2Province && formData.requirement.city2City) {
return `${formData.requirement.city2Province} ${formData.requirement.city2City}`
}
return ''
})
// 监听关系和姓氏变化,自动生成昵称 (Property 8: Nickname Auto-Generation)
watch(
() => [formData.relationship, formData.surname, formData.childGender],
([relationship, surname, gender]) => {
if (relationship && surname) {
formData.nickname = generateNickname(relationship, surname, gender || 1)
}
}
)
// 选择器变更处理
const handleRelationshipChange = (e) => {
formData.relationship = relationshipOptions[e.detail.value].value
}
const handleBirthYearChange = (e) => {
formData.birthYear = birthYearOptions.value[e.detail.value]
}
const handleEducationChange = (e) => {
formData.education = educationOptions[e.detail.value].value
}
const handleWorkLocationChange = (e) => {
const [province, city, district] = e.detail.value
formData.workProvince = province
formData.workCity = city
formData.workDistrict = district || ''
}
const handleHomeLocationChange = (e) => {
const [province, city, district] = e.detail.value
formData.homeProvince = province
formData.homeCity = city
formData.homeDistrict = district || ''
}
const handleIncomeChange = (e) => {
formData.monthlyIncome = incomeOptions[e.detail.value].value
}
const handleHeightChange = (e) => {
formData.height = heightOptions.value[e.detail.value]
}
const handleWeightChange = (e) => {
formData.weight = weightOptions.value[e.detail.value]
}
const handleHouseChange = (e) => {
formData.houseStatus = houseOptions[e.detail.value].value
}
const handleCarChange = (e) => {
formData.carStatus = carOptions[e.detail.value].value
}
const handleMarriageChange = (e) => {
formData.marriageStatus = marriageOptions[e.detail.value].value
}
const handleExpectMarryChange = (e) => {
formData.expectMarryTime = expectMarryOptions[e.detail.value].value
}
const handleSurnameBlur = () => {
// 姓氏输入完成后触发昵称生成
if (formData.relationship && formData.surname) {
formData.nickname = generateNickname(formData.relationship, formData.surname, formData.childGender || 1)
}
}
// 择偶要求处理
const handleAgeMinChange = (e) => {
const val = ageRangeOptions.value[e.detail.value]
formData.requirement.ageMin = val === '不限' ? 0 : val
}
const handleAgeMaxChange = (e) => {
const val = ageRangeOptions.value[e.detail.value]
formData.requirement.ageMax = val === '不限' ? 0 : val
}
const handleHeightMinChange = (e) => {
const val = heightRangeOptions.value[e.detail.value]
formData.requirement.heightMin = val === '不限' ? 0 : val
}
const handleHeightMaxChange = (e) => {
const val = heightRangeOptions.value[e.detail.value]
formData.requirement.heightMax = val === '不限' ? 0 : val
}
const handleIncomeMinChange = (e) => {
const idx = e.detail.value
formData.requirement.monthlyIncomeMin = idx === 0 ? 0 : idx
}
const handleCity1Change = (e) => {
const [province, city] = e.detail.value
formData.requirement.city1Province = province
formData.requirement.city1City = city
}
const handleCity2Change = (e) => {
const [province, city] = e.detail.value
formData.requirement.city2Province = province
formData.requirement.city2City = city
}
// 多选切换
const toggleEducationRequirement = (value) => {
const idx = formData.requirement.education.indexOf(value)
if (idx >= 0) {
formData.requirement.education.splice(idx, 1)
} else {
formData.requirement.education.push(value)
}
}
const toggleHouseRequirement = (value) => {
const idx = formData.requirement.houseStatus.indexOf(value)
if (idx >= 0) {
formData.requirement.houseStatus.splice(idx, 1)
} else {
formData.requirement.houseStatus.push(value)
}
}
const toggleCarRequirement = (value) => {
const idx = formData.requirement.carStatus.indexOf(value)
if (idx >= 0) {
formData.requirement.carStatus.splice(idx, 1)
} else {
formData.requirement.carStatus.push(value)
}
}
const toggleMarriageRequirement = (value) => {
const idx = formData.requirement.marriageStatus.indexOf(value)
if (idx >= 0) {
formData.requirement.marriageStatus.splice(idx, 1)
} else {
formData.requirement.marriageStatus.push(value)
}
}
// 照片处理 (Property 10: Photo Upload Limit)
const handleChoosePhoto = () => {
// 检查照片数量限制
if (formData.photos.length >= 5) {
uni.showToast({ title: '最多上传5张照片', icon: 'none' })
return
}
const remainingCount = 5 - formData.photos.length
uni.chooseImage({
count: remainingCount,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: async (res) => {
const tempFilePaths = res.tempFilePaths
// 再次检查总数不超过5张
if (formData.photos.length + tempFilePaths.length > 5) {
uni.showToast({ title: '最多上传5张照片', icon: 'none' })
return
}
uni.showLoading({ title: '上传中...' })
try {
const uploadRes = await uploadPhotos(tempFilePaths)
if (uploadRes && uploadRes.success && uploadRes.data?.photos) {
// 添加到照片列表
uploadRes.data.photos.forEach(photo => {
if (formData.photos.length < 5) {
formData.photos.push({
id: photo.id || null, // 新上传的照片可能没有 id
url: photo.url || photo.photoUrl
})
}
})
} else {
uni.showToast({ title: '上传失败', icon: 'none' })
}
} catch (error) {
console.error('上传照片错误:', error)
uni.showToast({ title: error.message || '上传失败', icon: 'none' })
} finally {
uni.hideLoading()
}
}
})
}
const handleDeletePhoto = async (index) => {
const photo = formData.photos[index]
if (photo.id) {
try {
await deletePhoto(photo.id)
} catch (error) {
console.error('删除照片失败:', error)
}
}
formData.photos.splice(index, 1)
}
// 手机号验证 - 处理微信获取手机号回调
const handleGetPhoneNumber = async (e) => {
if (e.detail.errMsg !== 'getPhoneNumber:ok') {
uni.showToast({ title: '取消授权', icon: 'none' })
return
}
// 获取到 code发送给后端解密
const code = e.detail.code
if (!code) {
uni.showToast({ title: '获取手机号失败', icon: 'none' })
return
}
uni.showLoading({ title: '验证中...' })
try {
const { decryptPhone } = await import('@/api/user.js')
const res = await decryptPhone(code)
if (res && res.code === 0 && res.data?.phone) {
formData.phone = res.data.phone
phoneVerified.value = true
uni.showToast({ title: '验证成功', icon: 'success' })
} else {
uni.showToast({ title: res?.message || '验证失败', icon: 'none' })
}
} catch (error) {
console.error('验证手机号失败:', error)
uni.showToast({ title: '验证失败', icon: 'none' })
} finally {
uni.hideLoading()
}
}
// 步骤验证
const validateStep = (step) => {
switch (step) {
case 0: // 基础信息
if (!formData.relationship) {
uni.showToast({ title: '请选择与相亲者的关系', icon: 'none' })
return false
}
if (!formData.surname) {
uni.showToast({ title: '请输入姓氏', icon: 'none' })
return false
}
if (!formData.childGender) {
uni.showToast({ title: '请选择相亲者性别', icon: 'none' })
return false
}
if (!formData.birthYear) {
uni.showToast({ title: '请选择出生年份', icon: 'none' })
return false
}
return true
case 1: // 详细信息
if (!formData.education) {
uni.showToast({ title: '请选择学历', icon: 'none' })
return false
}
if (!formData.workProvince || !formData.workCity) {
uni.showToast({ title: '请选择工作地点', icon: 'none' })
return false
}
if (!formData.occupation) {
uni.showToast({ title: '请输入职业', icon: 'none' })
return false
}
if (!formData.monthlyIncome) {
uni.showToast({ title: '请选择月收入', icon: 'none' })
return false
}
if (!formData.height) {
uni.showToast({ title: '请选择身高', icon: 'none' })
return false
}
if (!formData.houseStatus) {
uni.showToast({ title: '请选择房产状态', icon: 'none' })
return false
}
if (!formData.carStatus) {
uni.showToast({ title: '请选择车辆状态', icon: 'none' })
return false
}
if (!formData.marriageStatus) {
uni.showToast({ title: '请选择婚姻状态', icon: 'none' })
return false
}
return true
case 2: // 自我介绍
if (!formData.introduction || formData.introduction.trim().length < 10) {
uni.showToast({ title: '请输入至少10个字的介绍', icon: 'none' })
return false
}
return true
case 3: // 择偶要求
// 择偶要求为可选,不做强制验证
return true
case 4: // 联系方式
if (!formData.weChatNo) {
uni.showToast({ title: '请输入微信号', icon: 'none' })
return false
}
return true
default:
return true
}
}
// 步骤导航
const handlePrevStep = () => {
if (currentStep.value > 0) {
currentStep.value--
}
}
const handleNextStep = () => {
if (validateStep(currentStep.value)) {
if (currentStep.value < steps.length - 1) {
currentStep.value++
}
}
}
// 提交资料
const handleSubmit = async () => {
if (!validateStep(currentStep.value)) {
return
}
submitting.value = true
try {
const submitData = {
...formData,
photos: formData.photos.map(p => p.id).filter(Boolean)
}
const res = await createOrUpdate(submitData)
if (res && res.code === 0) {
// 更新用户状态
userStore.updateUserInfo({ isProfileCompleted: true })
uni.showToast({ title: '资料提交成功', icon: 'success' })
setTimeout(() => {
uni.navigateBack()
}, 1500)
} else {
uni.showToast({ title: res?.message || '提交失败', icon: 'none' })
}
} catch (error) {
console.error('提交资料失败:', error)
uni.showToast({ title: '提交失败,请重试', icon: 'none' })
} finally {
submitting.value = false
}
}
// 加载已有资料
const loadProfile = async () => {
try {
const res = await getMyProfile()
if (res && res.code === 0 && res.data) {
const profile = res.data
// 填充表单数据
formData.isPhotoPublic = profile.isPhotoPublic ?? true
formData.relationship = profile.relationship || 1
formData.surname = profile.surname || ''
formData.nickname = profile.nickname || ''
formData.childGender = profile.childGender || 1
formData.birthYear = profile.birthYear || 0
formData.education = profile.education || 1
formData.workProvince = profile.workProvince || ''
formData.workCity = profile.workCity || ''
formData.workDistrict = profile.workDistrict || ''
formData.homeProvince = profile.homeProvince || ''
formData.homeCity = profile.homeCity || ''
formData.homeDistrict = profile.homeDistrict || ''
formData.occupation = profile.occupation || ''
formData.monthlyIncome = profile.monthlyIncome || 1
formData.height = profile.height || 0
formData.weight = profile.weight || 0
formData.houseStatus = profile.houseStatus || 5
formData.carStatus = profile.carStatus || 2
formData.marriageStatus = profile.marriageStatus || 1
formData.expectMarryTime = profile.expectMarryTime || 0
formData.introduction = profile.introduction || ''
formData.weChatNo = profile.weChatNo || ''
// 照片 - 需要拼接完整URL使用统一配置的地址
if (profile.photos && profile.photos.length > 0) {
formData.photos = profile.photos.map(p => ({
id: p.id,
url: p.photoUrl.startsWith('http') ? p.photoUrl : `${STATIC_URL}${p.photoUrl}`
}))
}
// 择偶要求
if (profile.requirement) {
formData.requirement = {
ageMin: profile.requirement.ageMin || 0,
ageMax: profile.requirement.ageMax || 0,
heightMin: profile.requirement.heightMin || 0,
heightMax: profile.requirement.heightMax || 0,
education: profile.requirement.education || [],
city1Province: profile.requirement.city1Province || '',
city1City: profile.requirement.city1City || '',
city2Province: profile.requirement.city2Province || '',
city2City: profile.requirement.city2City || '',
monthlyIncomeMin: profile.requirement.monthlyIncomeMin || 0,
monthlyIncomeMax: profile.requirement.monthlyIncomeMax || 0,
houseStatus: profile.requirement.houseStatus || [],
carStatus: profile.requirement.carStatus || [],
marriageStatus: profile.requirement.marriageStatus || []
}
}
} else {
// 用户没有 Profile 记录,清理之前可能上传的孤立照片
try {
await cleanupOrphanPhotos()
} catch (e) {
console.log('清理孤立照片:', e.message)
}
}
} catch (error) {
console.error('加载资料失败:', error)
// 加载失败也尝试清理孤立照片
try {
await cleanupOrphanPhotos()
} catch (e) {
console.log('清理孤立照片:', e.message)
}
}
}
onMounted(() => {
getSystemInfo()
loadProfile()
})
</script>
<script>
// 页面生命周期需要在普通 script 中定义
export default {
onBackPress() {
uni.showModal({
title: '提示',
content: '确定要离开吗?已填写的内容将不会保存',
confirmText: '离开',
cancelText: '继续填写',
success: (res) => {
if (res.confirm) {
uni.navigateBack()
}
}
})
return true // 返回 true 阻止默认返回行为
}
}
</script>
<style lang="scss" scoped>
.profile-edit-page {
min-height: 100vh;
background-color: #f8f8f8;
padding-bottom: 140rpx;
}
// 固定头部区域
.fixed-header {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
}
// 自定义导航栏(水平渐变背景)
.custom-navbar {
position: relative;
z-index: 1;
background: linear-gradient(90deg, #FFDEE0 0%, #FF939C 100%);
.navbar-content {
height: 44px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24rpx;
.navbar-back {
width: 80rpx;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
.back-icon {
font-size: 64rpx;
color: #333;
font-weight: 300;
}
}
.navbar-title {
font-size: 34rpx;
font-weight: 600;
color: #333;
}
.navbar-placeholder {
width: 80rpx;
}
}
}
// 占位区域
.header-placeholder {
width: 100%;
}
// 步骤指示器(白色背景)
.step-indicator {
position: relative;
z-index: 1;
display: flex;
justify-content: space-between;
padding: 30rpx 40rpx;
background-color: #fff;
.step-item {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
.step-dot {
width: 48rpx;
height: 48rpx;
border-radius: 50%;
background-color: #e0e0e0;
color: #999;
font-size: 24rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 8rpx;
}
.step-label {
font-size: 22rpx;
color: #666;
}
&.active {
.step-dot {
background: #FF6B6B;
color: #fff;
}
.step-label {
color: #FF6B6B;
font-weight: 500;
}
}
&.completed {
.step-dot {
background-color: #4cd964;
color: #fff;
}
.step-label {
color: #4cd964;
}
}
}
}
// 表单区域
.step-content {
padding: 20rpx;
position: relative;
z-index: 1;
}
.form-section {
background-color: #fff;
border-radius: 16rpx;
padding: 30rpx;
.section-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
margin-bottom: 30rpx;
padding-bottom: 20rpx;
border-bottom: 1rpx solid #f0f0f0;
}
}
.form-item {
margin-bottom: 30rpx;
.form-label {
display: block;
font-size: 28rpx;
color: #333;
margin-bottom: 16rpx;
&.required::before {
content: '*';
color: #ff6b6b;
margin-right: 4rpx;
}
}
input {
width: 100%;
height: 80rpx;
padding: 0 20rpx;
background-color: #f8f8f8;
border-radius: 12rpx;
font-size: 28rpx;
box-sizing: border-box;
&[disabled] {
color: #999;
background-color: #f0f0f0;
}
}
.form-tip {
font-size: 24rpx;
color: #999;
margin-top: 8rpx;
}
&.textarea-item {
textarea {
width: 100%;
height: 240rpx;
padding: 20rpx;
background-color: #f8f8f8;
border-radius: 12rpx;
font-size: 28rpx;
box-sizing: border-box;
}
.char-count {
display: block;
text-align: right;
font-size: 24rpx;
color: #999;
margin-top: 8rpx;
}
}
}
// 选择器
.picker-value {
display: flex;
align-items: center;
justify-content: space-between;
height: 80rpx;
padding: 0 20rpx;
background-color: #f8f8f8;
border-radius: 12rpx;
font-size: 28rpx;
color: #333;
&.small {
width: 160rpx;
justify-content: center;
}
.picker-arrow {
font-size: 20rpx;
color: #999;
}
}
// 单选组
.radio-group {
display: flex;
gap: 30rpx;
.radio-item {
display: flex;
align-items: center;
padding: 16rpx 30rpx;
background-color: #f8f8f8;
border-radius: 12rpx;
.radio-dot {
width: 32rpx;
height: 32rpx;
border-radius: 50%;
border: 2rpx solid #ddd;
margin-right: 12rpx;
}
text {
font-size: 28rpx;
color: #333;
}
&.active {
background-color: #fff0f0;
.radio-dot {
border-color: #ff6b6b;
background-color: #ff6b6b;
position: relative;
&::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 12rpx;
height: 12rpx;
border-radius: 50%;
background-color: #fff;
}
}
text {
color: #ff6b6b;
}
}
}
}
// 多选组
.checkbox-group {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
.checkbox-item {
padding: 12rpx 24rpx;
background-color: #f8f8f8;
border-radius: 8rpx;
font-size: 26rpx;
color: #666;
&.active {
background-color: #fff0f0;
color: #ff6b6b;
border: 1rpx solid #ff6b6b;
}
}
}
// 范围选择器
.range-picker {
display: flex;
align-items: center;
.range-separator {
margin: 0 16rpx;
color: #999;
}
.range-unit {
margin-left: 12rpx;
color: #666;
font-size: 26rpx;
}
}
// 照片上传
.photo-upload-section {
margin-bottom: 30rpx;
.photo-label {
font-size: 28rpx;
color: #333;
margin-bottom: 16rpx;
}
.photo-grid {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
.photo-item {
position: relative;
width: 200rpx;
height: 200rpx;
border-radius: 12rpx;
overflow: hidden;
.photo-image {
width: 100%;
height: 100%;
}
.photo-delete {
position: absolute;
top: 8rpx;
right: 8rpx;
width: 40rpx;
height: 40rpx;
background-color: rgba(0, 0, 0, 0.5);
border-radius: 50%;
color: #fff;
font-size: 28rpx;
display: flex;
align-items: center;
justify-content: center;
}
}
.photo-add {
width: 200rpx;
height: 200rpx;
background-color: #f8f8f8;
border-radius: 12rpx;
border: 2rpx dashed #ddd;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.add-icon {
font-size: 48rpx;
color: #999;
margin-bottom: 8rpx;
}
.add-text {
font-size: 24rpx;
color: #999;
}
}
}
}
// 验证按钮
.verify-btn {
width: 100%;
height: 80rpx;
line-height: 80rpx;
background-color: #f8f8f8;
color: #333;
font-size: 28rpx;
border-radius: 12rpx;
border: none;
&::after {
border: none;
}
&[disabled] {
background-color: #f0f0f0;
color: #ccc;
}
}
// 已验证手机号显示
.phone-verified {
display: flex;
align-items: center;
justify-content: space-between;
height: 80rpx;
padding: 0 20rpx;
background-color: #f0fff0;
border-radius: 12rpx;
.phone-number {
font-size: 28rpx;
color: #333;
}
.verified-tag {
font-size: 24rpx;
color: #4cd964;
padding: 4rpx 12rpx;
background-color: #e8f8e8;
border-radius: 8rpx;
}
}
// 底部按钮
.bottom-buttons {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 999;
display: flex;
gap: 20rpx;
padding: 20rpx 30rpx;
background-color: #fff;
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.05);
button {
flex: 1;
height: 88rpx;
line-height: 88rpx;
border-radius: 44rpx;
font-size: 30rpx;
border: none;
&::after {
border: none;
}
}
.btn-prev {
background-color: #f5f5f5;
color: #666;
}
.btn-next, .btn-submit {
background: linear-gradient(135deg, #ff6b6b 0%, #ff5252 100%);
color: #fff;
}
}
</style>
<!-- 非 scoped 样式,用于 placeholder-class -->
<style lang="scss">
.input-placeholder {
color: #999 !important;
font-size: 28rpx !important;
}
</style>