xiangqinxiaochengxu/miniprogram/pages/profile-edit/profile-edit.js

534 lines
17 KiB
JavaScript
Raw Normal View History

2026-04-17 10:49:14 +08:00
const request = require('../../utils/request')
2026-04-17 19:26:32 +08:00
const { getApiBaseUrl } = require('../../utils/constants')
function addCacheVersion(url, version) {
if (!url) {
return ''
}
const joiner = url.includes('?') ? '&' : '?'
return `${url}${joiner}v=${encodeURIComponent(version)}`
}
2026-04-17 21:28:56 +08:00
function isLocalDevUrl(url) {
return /^https?:\/\/(127\.0\.0\.1|localhost|0\.0\.0\.0)(:\d+)?\//i.test(url)
}
function upgradeToHttpsIfNeeded(url) {
if (/^http:\/\//i.test(url) && !isLocalDevUrl(url)) {
return url.replace(/^http:\/\//i, 'https://')
}
return url
}
function resolveImageUrl(url) {
if (!url) {
return ''
}
if (/^https?:\/\//i.test(url)) {
return upgradeToHttpsIfNeeded(url)
}
const baseUrl = getApiBaseUrl().replace(/\/api\/v1\/?$/, '')
const resolved = url.startsWith('/') ? `${baseUrl}${url}` : `${baseUrl}/${url}`
return upgradeToHttpsIfNeeded(resolved)
2026-04-17 19:26:32 +08:00
}
2026-04-17 21:28:56 +08:00
function normalizeAvatarState(userInfo) {
const avatarUrl = resolveImageUrl(userInfo.avatar_url || '')
const avatarBlurUrl = resolveImageUrl(userInfo.avatar_blur_url || '')
return {
...userInfo,
avatar_url: avatarUrl,
avatar_blur_url: avatarBlurUrl
}
2026-04-17 19:26:32 +08:00
}
2026-04-17 10:49:14 +08:00
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
}
2026-04-17 19:26:32 +08:00
const MAX_AVATAR_SIZE = 5 * 1024 * 1024
function cloneProfileCache() {
return wx.getStorageSync('profile_page_cache') || null
}
function saveProfileCache(userInfo) {
2026-04-17 21:28:56 +08:00
wx.setStorageSync('profile_page_cache', normalizeAvatarState(userInfo))
2026-04-17 19:26:32 +08:00
}
2026-04-17 10:49:14 +08:00
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: '',
2026-04-17 19:26:32 +08:00
avatarUrl: '',
avatarVersion: '',
2026-04-17 10:49:14 +08:00
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 || '',
2026-04-17 19:26:32 +08:00
phone: data.phone || '',
2026-04-17 21:28:56 +08:00
avatar_url: resolveImageUrl(data.avatar_url || ''),
avatar_blur_url: resolveImageUrl(data.avatar_blur_url || '')
2026-04-17 10:49:14 +08:00
}
2026-04-17 19:26:32 +08:00
const avatarVersion = Date.now().toString()
this.setData({
form,
avatarUrl: addCacheVersion(form.avatar_url, avatarVersion),
avatarVersion
})
2026-04-17 10:49:14 +08:00
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
},
2026-04-17 19:26:32 +08:00
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' })
}
}
})
},
uploadAvatar(tempFilePath) {
wx.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 || ''
2026-04-17 21:28:56 +08:00
const rawAvatarBlurUrl = body.data.avatar_blur_url || ''
const avatarUrl = resolveImageUrl(rawAvatarUrl)
const avatarBlurUrl = resolveImageUrl(rawAvatarBlurUrl)
2026-04-17 19:26:32 +08:00
const avatarVersion = String(Date.now())
2026-04-17 21:28:56 +08:00
const form = { ...this.data.form, avatar_url: avatarUrl, avatar_blur_url: avatarBlurUrl }
2026-04-17 19:26:32 +08:00
const cachedProfile = cloneProfileCache() || {}
2026-04-17 21:28:56 +08:00
const nextProfile = normalizeAvatarState({
2026-04-17 19:26:32 +08:00
...cachedProfile,
...form,
avatar_url: avatarUrl,
2026-04-17 21:28:56 +08:00
avatar_blur_url: avatarBlurUrl,
2026-04-17 19:26:32 +08:00
nicknameText: form.nickname || cachedProfile.nicknameText || '未设置昵称',
auditStatusText: cachedProfile.auditStatusText || '待审核'
2026-04-17 21:28:56 +08:00
})
2026-04-17 19:26:32 +08:00
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' })
}
})
},
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
},
2026-04-17 10:49:14 +08:00
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
})
2026-04-17 19:26:32 +08:00
const nextForm = { ...this.data.form, ...payload }
const avatarVersion = this.data.avatarVersion || Date.now().toString()
const cachedProfile = cloneProfileCache() || {}
2026-04-17 21:28:56 +08:00
const nextProfile = normalizeAvatarState({
2026-04-17 19:26:32 +08:00
...cachedProfile,
...nextForm,
avatar_url: nextForm.avatar_url || cachedProfile.avatar_url || '',
avatar_blur_url: nextForm.avatar_blur_url || cachedProfile.avatar_blur_url || ''
2026-04-17 21:28:56 +08:00
})
2026-04-17 19:26:32 +08:00
saveProfileCache(nextProfile)
2026-04-17 10:49:14 +08:00
this.setData({
2026-04-17 19:26:32 +08:00
form: nextForm,
2026-04-17 21:28:56 +08:00
avatarUrl: addCacheVersion(nextProfile.avatar_url || '', avatarVersion),
2026-04-17 19:26:32 +08:00
avatarVersion
2026-04-17 10:49:14 +08:00
})
2026-04-17 19:26:32 +08:00
const pages = getCurrentPages()
const prevPage = pages[pages.length - 2]
if (prevPage && typeof prevPage.applyUserInfo === 'function') {
prevPage.applyUserInfo(nextProfile)
}
this.syncPickerTexts(nextForm)
2026-04-17 10:49:14 +08:00
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' })
2026-04-17 19:26:32 +08:00
const pages = getCurrentPages()
const prevPage = pages[pages.length - 2]
if (prevPage && typeof prevPage.loadProfile === 'function') {
prevPage.loadProfile()
}
2026-04-17 10:49:14 +08:00
setTimeout(() => {
2026-04-17 19:26:32 +08:00
wx.navigateBack({ delta: 1 })
2026-04-17 10:49:14 +08:00
}, 300)
} catch (err) {
console.error('Submit audit failed:', err)
wx.showToast({ title: err.message || '提交审核失败', icon: 'none' })
}
}
})