534 lines
17 KiB
JavaScript
534 lines
17 KiB
JavaScript
const request = require('../../utils/request')
|
||
const { getApiBaseUrl } = require('../../utils/constants')
|
||
|
||
function addCacheVersion(url, version) {
|
||
if (!url) {
|
||
return ''
|
||
}
|
||
const joiner = url.includes('?') ? '&' : '?'
|
||
return `${url}${joiner}v=${encodeURIComponent(version)}`
|
||
}
|
||
|
||
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)
|
||
}
|
||
|
||
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
|
||
}
|
||
}
|
||
|
||
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: '',
|
||
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
|
||
})
|
||
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' })
|
||
}
|
||
}
|
||
})
|
||
},
|
||
|
||
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 || ''
|
||
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' })
|
||
}
|
||
})
|
||
},
|
||
|
||
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)
|
||
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' })
|
||
}
|
||
}
|
||
}) |