xiangqinxiaochengxu/miniprogram/pages/profile-edit/profile-edit.js
2026-05-18 14:24:55 +08:00

588 lines
19 KiB
JavaScript
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.

const request = require('../../utils/request')
const { getApiBaseUrl } = require('../../utils/constants')
const { resolveImageUrl, resolveImageList } = require('../../utils/media')
function addCacheVersion(url, version) {
if (!url) {
return ''
}
const joiner = url.includes('?') ? '&' : '?'
return `${url}${joiner}v=${encodeURIComponent(version)}`
}
function normalizeAvatarState(userInfo) {
const avatarUrl = resolveImageUrl(userInfo.avatar_url || '')
const avatarBlurUrl = resolveImageUrl(userInfo.avatar_blur_url || '')
const profileImages = resolveImageList(userInfo.profile_images)
return {
...userInfo,
avatar_url: avatarUrl,
avatar_blur_url: avatarBlurUrl,
profile_images: profileImages
}
}
const AUDIT_REQUIRED_FIELDS = ['personality_tags', 'hobbies', 'wechat_id', 'phone']
const PROFILE_LIMITS = {
nickname: 50,
real_name: 20,
city: 30,
job_industry: 50,
job_company: 100,
prefer_city: 30,
self_intro: 500,
wechat_id: 64,
phone: 20,
tagItem: 20,
listItems: 20
}
const MAX_AVATAR_SIZE = 5 * 1024 * 1024
function cloneProfileCache() {
return wx.getStorageSync('profile_page_cache') || null
}
function saveProfileCache(userInfo) {
wx.setStorageSync('profile_page_cache', normalizeAvatarState(userInfo))
}
Page({
data: {
form: {
nickname: '',
real_name: '',
gender: null,
birth_year: null,
city: '',
height: null,
education: null,
job_industry: '',
job_company: '',
income_range: null,
prefer_city: '',
self_intro: '',
personality_tags: [],
hobbies: [],
wechat_id: '',
phone: ''
},
tagsText: '',
hobbiesText: '',
avatarUrl: '',
avatarVersion: '',
profileImages: [],
genderOptions: [
{ label: '男', value: 1 },
{ label: '女', value: 2 }
],
educationOptions: [
{ label: '高中', value: 1 },
{ label: '大专', value: 2 },
{ label: '本科', value: 3 },
{ label: '硕士', value: 4 },
{ label: '博士', value: 5 }
],
incomeOptions: [
{ label: '5k以下', value: 1 },
{ label: '5k-10k', value: 2 },
{ label: '10k-20k', value: 3 },
{ label: '20k以上', value: 4 }
],
genderText: '请选择',
educationText: '请选择',
incomeText: '请选择',
loading: false,
saving: false
},
onLoad() {
this.syncPickerTexts(this.data.form)
this.syncExtraTexts(this.data.form)
this.loadProfile()
},
syncExtraTexts(form) {
this.setData({
tagsText: (form.personality_tags || []).join(''),
hobbiesText: (form.hobbies || []).join('')
})
},
parseListText(text) {
return (text || '')
.split(/[,\n]/)
.map((item) => item.trim())
.filter(Boolean)
},
async loadProfile() {
this.setData({ loading: true })
try {
const data = await request({ url: '/users/me' })
const form = {
...this.data.form,
...data,
gender: data.gender == null ? null : Number(data.gender),
birth_year: data.birth_year == null ? null : Number(data.birth_year),
height: data.height == null ? null : Number(data.height),
education: data.education == null ? null : Number(data.education),
income_range: data.income_range == null ? null : Number(data.income_range),
personality_tags: data.personality_tags || [],
hobbies: data.hobbies || [],
wechat_id: data.wechat_id || '',
phone: data.phone || '',
avatar_url: resolveImageUrl(data.avatar_url || ''),
avatar_blur_url: resolveImageUrl(data.avatar_blur_url || '')
}
const avatarVersion = Date.now().toString()
this.setData({
form,
avatarUrl: addCacheVersion(form.avatar_url, avatarVersion),
avatarVersion,
profileImages: Array.isArray(form.profile_images) ? form.profile_images : []
})
this.syncPickerTexts(form)
this.syncExtraTexts(form)
} catch (err) {
console.error('Load profile edit data failed:', err)
} finally {
this.setData({ loading: false })
}
},
handleInput(e) {
const field = e.currentTarget.dataset.field
const value = e.detail.value
if (field === 'personality_tags') {
const tags = this.parseListText(value)
this.setData({
form: { ...this.data.form, personality_tags: tags },
tagsText: value
})
return
}
if (field === 'hobbies') {
const hobbies = this.parseListText(value)
this.setData({
form: { ...this.data.form, hobbies },
hobbiesText: value
})
return
}
this.setData({ [`form.${field}`]: value })
},
handlePickerChange(e) {
const field = e.currentTarget.dataset.field
const optionKeyMap = {
income_range: 'incomeOptions'
}
const options = this.data[optionKeyMap[field] || `${field}Options`]
const index = Number(e.detail.value)
if (!options || !options[index]) {
return
}
const form = {
...this.data.form,
[field]: options[index].value
}
this.setData({ form })
this.syncPickerTexts(form)
},
syncPickerTexts(form) {
this.setData({
genderText: this.getOptionLabel(this.data.genderOptions, form.gender, '请选择'),
educationText: this.getOptionLabel(this.data.educationOptions, form.education, '请选择'),
incomeText: this.getOptionLabel(this.data.incomeOptions, form.income_range, '请选择')
})
},
getOptionLabel(options, value, fallback) {
const normalizedValue = value === null || value === undefined || value === '' ? value : Number(value)
const matched = (options || []).find((item) => item.value === normalizedValue)
return matched ? matched.label : fallback
},
sanitizeText(value, maxLength) {
if (value === null || value === undefined) {
return ''
}
return String(value).trim().slice(0, maxLength)
},
sanitizeNumber(value) {
if (value === null || value === undefined || value === '') {
return null
}
const num = Number(value)
return Number.isFinite(num) ? num : null
},
buildProfilePayload() {
const payload = {
nickname: this.sanitizeText(this.data.form.nickname, PROFILE_LIMITS.nickname) || null,
real_name: this.sanitizeText(this.data.form.real_name, PROFILE_LIMITS.real_name) || null,
gender: this.sanitizeNumber(this.data.form.gender),
birth_year: this.sanitizeNumber(this.data.form.birth_year),
city: this.sanitizeText(this.data.form.city, PROFILE_LIMITS.city) || null,
height: this.sanitizeNumber(this.data.form.height),
education: this.sanitizeNumber(this.data.form.education),
job_industry: this.sanitizeText(this.data.form.job_industry, PROFILE_LIMITS.job_industry) || null,
job_company: this.sanitizeText(this.data.form.job_company, PROFILE_LIMITS.job_company) || null,
income_range: this.sanitizeNumber(this.data.form.income_range),
prefer_city: this.sanitizeText(this.data.form.prefer_city, PROFILE_LIMITS.prefer_city) || null,
self_intro: this.sanitizeText(this.data.form.self_intro, PROFILE_LIMITS.self_intro) || null,
personality_tags: Array.isArray(this.data.form.personality_tags)
? this.data.form.personality_tags.map((item) => this.sanitizeText(item, PROFILE_LIMITS.tagItem)).filter(Boolean).slice(0, PROFILE_LIMITS.listItems)
: [],
hobbies: Array.isArray(this.data.form.hobbies)
? this.data.form.hobbies.map((item) => this.sanitizeText(item, PROFILE_LIMITS.tagItem)).filter(Boolean).slice(0, PROFILE_LIMITS.listItems)
: [],
wechat_id: this.sanitizeText(this.data.form.wechat_id, PROFILE_LIMITS.wechat_id) || null,
phone: this.sanitizeText(this.data.form.phone, PROFILE_LIMITS.phone) || null
}
return payload
},
getValidationLabel(field) {
const labels = {
nickname: '昵称',
real_name: '真实姓名',
city: '所在城市',
job_industry: '行业',
job_company: '公司',
prefer_city: '期望城市',
self_intro: '个人介绍',
wechat_id: '微信号',
phone: '手机号',
personality_tags: '性格标签',
hobbies: '兴趣爱好',
gender: '性别',
birth_year: '出生年份',
height: '身高',
education: '学历',
income_range: '收入范围'
}
return labels[field] || field
},
chooseAvatar() {
wx.chooseMedia({
count: 1,
mediaType: ['image'],
sourceType: ['album', 'camera'],
success: (res) => {
const tempFilePath = res.tempFiles && res.tempFiles[0] && res.tempFiles[0].tempFilePath
if (tempFilePath) {
this.uploadAvatar(tempFilePath)
}
},
fail: (err) => {
if (err && !String(err.errMsg || '').includes('cancel')) {
wx.showToast({ title: '请选择头像图片', icon: 'none' })
}
}
})
},
chooseProfileImages() {
wx.chooseMedia({
count: 6,
mediaType: ['image'],
sourceType: ['album', 'camera'],
success: async (res) => {
const files = (res.tempFiles || []).slice(0, 6)
for (const item of files) {
if (item && item.tempFilePath) {
// eslint-disable-next-line no-await-in-loop
await this.uploadProfileImage(item.tempFilePath)
}
}
},
fail: (err) => {
if (err && !String(err.errMsg || '').includes('cancel')) {
wx.showToast({ title: '请选择资料图片', icon: 'none' })
}
}
})
},
uploadAvatar(tempFilePath) {
wx.getFileSystemManager().getFileInfo({
filePath: tempFilePath,
success: (info) => {
if (info.size > MAX_AVATAR_SIZE) {
wx.showToast({ title: '头像不能超过 5MB', icon: 'none' })
return
}
wx.uploadFile({
url: `${getApiBaseUrl()}/upload/avatar`,
filePath: tempFilePath,
name: 'file',
header: {
Authorization: wx.getStorageSync('token') ? `Bearer ${wx.getStorageSync('token')}` : ''
},
success: (res) => {
try {
const body = JSON.parse(res.data)
if (body.code !== 0) {
throw new Error(body.message || '上传失败')
}
const rawAvatarUrl = body.data.avatar_url || ''
const rawAvatarBlurUrl = body.data.avatar_blur_url || ''
const avatarUrl = resolveImageUrl(rawAvatarUrl)
const avatarBlurUrl = resolveImageUrl(rawAvatarBlurUrl)
const avatarVersion = String(Date.now())
const form = { ...this.data.form, avatar_url: avatarUrl, avatar_blur_url: avatarBlurUrl }
const cachedProfile = cloneProfileCache() || {}
const nextProfile = normalizeAvatarState({
...cachedProfile,
...form,
avatar_url: avatarUrl,
avatar_blur_url: avatarBlurUrl,
nicknameText: form.nickname || cachedProfile.nicknameText || '未设置昵称',
auditStatusText: cachedProfile.auditStatusText || '待审核'
})
saveProfileCache(nextProfile)
this.setData({ form, avatarUrl: addCacheVersion(avatarUrl, avatarVersion), avatarVersion })
const pages = getCurrentPages()
const prevPage = pages[pages.length - 2]
if (prevPage && typeof prevPage.applyUserInfo === 'function') {
prevPage.applyUserInfo(nextProfile)
}
wx.showToast({ title: '头像已更新', icon: 'success' })
} catch (err) {
wx.showToast({ title: err.message || '上传失败', icon: 'none' })
}
},
fail: () => {
wx.showToast({ title: '上传失败,请重试', icon: 'none' })
}
})
},
fail: () => {
wx.showToast({ title: '无法读取图片大小', icon: 'none' })
}
})
},
uploadProfileImage(tempFilePath) {
return new Promise((resolve) => {
wx.getFileSystemManager().getFileInfo({
filePath: tempFilePath,
success: (info) => {
if (info.size > MAX_AVATAR_SIZE) {
wx.showToast({ title: '资料图不能超过 5MB', icon: 'none' })
resolve(false)
return
}
wx.uploadFile({
url: `${getApiBaseUrl()}/upload/profile-image`,
filePath: tempFilePath,
name: 'file',
header: {
Authorization: wx.getStorageSync('token') ? `Bearer ${wx.getStorageSync('token')}` : ''
},
success: (res) => {
try {
const body = JSON.parse(res.data)
if (body.code !== 0) {
throw new Error(body.message || '上传失败')
}
const profileImages = resolveImageList(body.data.profile_images)
const form = { ...this.data.form, profile_images: profileImages }
this.setData({ form, profileImages })
resolve(true)
} catch (err) {
wx.showToast({ title: err.message || '上传失败', icon: 'none' })
resolve(false)
}
},
fail: () => {
wx.showToast({ title: '上传失败,请重试', icon: 'none' })
resolve(false)
}
})
},
fail: () => {
wx.showToast({ title: '无法读取图片大小', icon: 'none' })
resolve(false)
}
})
})
},
validateProfilePayload(payload) {
const errors = []
const checkTextLength = (field, maxLength) => {
const value = payload[field]
if (typeof value === 'string' && value.length > maxLength) {
errors.push(`${this.getValidationLabel(field)}不能超过${maxLength}个字符`)
}
}
checkTextLength('nickname', PROFILE_LIMITS.nickname)
checkTextLength('real_name', PROFILE_LIMITS.real_name)
checkTextLength('city', PROFILE_LIMITS.city)
checkTextLength('job_industry', PROFILE_LIMITS.job_industry)
checkTextLength('job_company', PROFILE_LIMITS.job_company)
checkTextLength('prefer_city', PROFILE_LIMITS.prefer_city)
checkTextLength('self_intro', PROFILE_LIMITS.self_intro)
checkTextLength('wechat_id', PROFILE_LIMITS.wechat_id)
checkTextLength('phone', PROFILE_LIMITS.phone)
;['personality_tags', 'hobbies'].forEach((field) => {
const list = payload[field]
const label = this.getValidationLabel(field)
if (!Array.isArray(list)) {
errors.push(`${label}格式不正确`)
return
}
if (list.length > PROFILE_LIMITS.listItems) {
errors.push(`${label}最多${PROFILE_LIMITS.listItems}`)
}
list.forEach((item) => {
if (item.length > PROFILE_LIMITS.tagItem) {
errors.push(`${label}单项不能超过${PROFILE_LIMITS.tagItem}个字符`)
}
})
})
;['gender', 'birth_year', 'height', 'education', 'income_range'].forEach((field) => {
const value = payload[field]
if (value !== null && value !== undefined && !Number.isFinite(value)) {
errors.push(`${this.getValidationLabel(field)}格式不正确`)
}
})
return errors
},
async saveProfile() {
this.setData({ saving: true })
try {
const payload = this.buildProfilePayload()
const validationErrors = this.validateProfilePayload(payload)
if (validationErrors.length > 0) {
wx.showModal({
title: '资料有误',
content: validationErrors.slice(0, 3).join(''),
showCancel: false
})
return false
}
await request({
url: '/users/me',
method: 'PUT',
data: payload
})
const nextForm = { ...this.data.form, ...payload }
const avatarVersion = this.data.avatarVersion || Date.now().toString()
const cachedProfile = cloneProfileCache() || {}
const nextProfile = normalizeAvatarState({
...cachedProfile,
...nextForm,
avatar_url: nextForm.avatar_url || cachedProfile.avatar_url || '',
avatar_blur_url: nextForm.avatar_blur_url || cachedProfile.avatar_blur_url || ''
})
saveProfileCache(nextProfile)
this.setData({
form: nextForm,
avatarUrl: addCacheVersion(nextProfile.avatar_url || '', avatarVersion),
avatarVersion
})
const pages = getCurrentPages()
const prevPage = pages[pages.length - 2]
if (prevPage && typeof prevPage.applyUserInfo === 'function') {
prevPage.applyUserInfo(nextProfile)
}
this.syncPickerTexts(nextForm)
if (wx.getStorageSync('pending_activity_bind')) {
wx.showToast({ title: '资料已保存,请返回活动页完成绑定', icon: 'none' })
return true
}
wx.showToast({ title: '保存成功', icon: 'success' })
return true
} catch (err) {
console.error('Save profile failed:', err)
wx.showToast({ title: err.message || '保存失败', icon: 'none' })
return false
} finally {
this.setData({ saving: false })
}
},
async submitAudit() {
const saveOk = await this.saveProfile()
if (!saveOk) {
return
}
const payload = this.buildProfilePayload()
const validationErrors = this.validateProfilePayload(payload)
if (validationErrors.length > 0) {
wx.showModal({
title: '资料有误',
content: validationErrors.slice(0, 3).join(''),
showCancel: false
})
return
}
const missingFields = AUDIT_REQUIRED_FIELDS.filter((field) => {
const value = payload[field]
if (Array.isArray(value)) {
return value.length === 0
}
if (value && typeof value === 'object') {
return Object.keys(value).length === 0
}
return value === null || value === undefined || value === ''
})
if (missingFields.length > 0) {
const labelMap = {
personality_tags: '性格标签',
hobbies: '兴趣爱好',
wechat_id: '微信号',
phone: '手机号'
}
wx.showModal({
title: '资料未完善',
content: `提交审核前请先补充:${missingFields.map((field) => labelMap[field] || field).join('、')}`,
showCancel: false
})
return
}
try {
await request({
url: '/users/submit-audit',
method: 'POST'
})
wx.showToast({ title: '已提交审核', icon: 'success' })
const pages = getCurrentPages()
const prevPage = pages[pages.length - 2]
if (prevPage && typeof prevPage.loadProfile === 'function') {
prevPage.loadProfile()
}
setTimeout(() => {
wx.navigateBack({ delta: 1 })
}, 300)
} catch (err) {
console.error('Submit audit failed:', err)
wx.showToast({ title: err.message || '提交审核失败', icon: 'none' })
}
}
})