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' }) } } })