/** * 头像与照片页 U08(Phase 04 P04-T23) * * 重构要点(遵循 AGENTS.md 不变量 + Phase 04 验收条件): * - 数据:profileService.getPhotos(每次从服务端获取,不在本地缓存图片 URL —— 验收"图片URL非永久公开") * - 上传:profileService.requestUploadToken + wx.uploadFile(EXIF 由服务端清理,前端不处理) * - 删除:profileService.deletePhoto * - 主头像:profileService.setMainPhoto(第一张或用户指定) * - 数量限制:BUSINESS_LIMITS.MAX_PROFILE_PHOTOS(第 12 条,不硬编码) * - 敏感数据:本页不涉及身份证/手机号(第 8 条) * - 下一步条件:至少 1 张主头像 */ const { appStore } = require('../../stores'); const { profileService } = require('../../services'); const { ROUTE, BUSINESS_LIMITS } = require('../../constants'); Page({ data: { statusBarHeight: 20, navBarHeight: 44, loading: false, saving: false, avatarPhotos: [], lifestylePhotos: [], maxLifestyle: BUSINESS_LIMITS.MAX_PROFILE_PHOTOS - 1, // 页面说明 guidelines: [ '请使用本人近期真实照片', '禁止网图、二维码、广告、联系方式图片', '建议真人生活照,避免过度美颜', ], }, onLoad() { this._initSystemInfo(); this._applyPhotoLimit(); this.loadPhotos(); }, onShow() { // 服务端配置(appStore.businessConfig)可能在冷启动后才拉取完成,onShow 再取一次 this._applyPhotoLimit(); }, /** * 照片上限读运行时配置(服务端下发,经 appStore.businessConfig 覆盖本地默认), * 前端不写死 6(PROFILE-001)。生活照槽位 = 总上限 - 1(头像占 1 张)。 */ _applyPhotoLimit() { const maxTotal = (appStore.state.businessConfig && appStore.state.businessConfig.MAX_PROFILE_PHOTOS) || BUSINESS_LIMITS.MAX_PROFILE_PHOTOS; this.setData({ maxLifestyle: Math.max(0, maxTotal - 1) }); }, _initSystemInfo() { try { const sysInfo = wx.getSystemInfoSync(); this.setData({ statusBarHeight: sysInfo.statusBarHeight || 20, navBarHeight: 44, }); } catch (e) { /* 使用默认值 */ } }, async loadPhotos() { this.setData({ loading: true }); try { const result = await profileService.getPhotos(); const photos = (result && result.photos) || []; const avatarPhotos = photos.filter((p) => p.photoType === 'avatar'); const lifestylePhotos = photos.filter((p) => p.photoType === 'lifestyle'); this.setData({ avatarPhotos, lifestylePhotos, loading: false }); } catch (err) { this.setData({ loading: false }); appStore.toastError(err, '加载照片失败'); } }, // ============ 上传 ============ /** 主头像区添加 */ onAddAvatar() { this._chooseAndUpload('avatar'); }, /** 生活照区添加 */ onAddLifestyle() { const remaining = this.data.maxLifestyle - this.data.lifestylePhotos.length; if (remaining <= 0) { appStore.toast(`最多上传 ${this.data.maxLifestyle + 1} 张照片`); return; } this._chooseAndUpload('lifestyle'); }, _chooseAndUpload(photoType) { wx.chooseMedia({ count: 1, mediaType: ['image'], sourceType: ['album', 'camera'], sizeType: ['compressed'], success: (res) => { const tempFile = res.tempFiles[0]; if (!tempFile) return; this._uploadPhoto(tempFile.tempFilePath, photoType); }, fail: () => { /* 用户取消 */ }, }); }, async _uploadPhoto(filePath, photoType) { appStore.showLoading('上传中…'); try { // 1. 获取上传凭证(EXIF 由服务端清理);带上 photoType 使 fileKey 路径与类型一致 const fileName = `photo_${Date.now()}.jpg`; const tokenRes = await profileService.requestUploadToken({ fileName, contentType: 'image/jpeg', photoType }); // 2. 上传到对象存储 await new Promise((resolve, reject) => { wx.uploadFile({ url: tokenRes.uploadUrl, filePath, name: 'file', formData: { 'Content-Type': 'image/jpeg' }, success: (res) => { if (res.statusCode >= 200 && res.statusCode < 300) resolve(res); else reject(new Error('上传失败')); }, fail: reject, }); }); // 3. 直传成功后必须调 confirm 落库,否则对象成孤儿、照片不展示(修复"照片直传不落库") const confirmRes = await profileService.confirmPhoto({ fileKey: tokenRes.fileKey, photoType }); // 4. 若上传的是头像区,或当前无主头像,则设为主头像(用真实 photoId) if (photoType === 'avatar' || this.data.avatarPhotos.length === 0) { await profileService.setMainPhoto(confirmRes.photoId); } appStore.hideLoading(); appStore.toastSuccess('上传成功'); this.loadPhotos(); } catch (err) { appStore.hideLoading(); appStore.toastError(err, '上传失败'); } }, // ============ 删除 ============ async onRemoveAvatar(e) { const { index } = e.detail; const photo = this.data.avatarPhotos[index]; if (!photo) return; this._removePhoto(photo.photoId); }, async onRemoveLifestyle(e) { const { index } = e.detail; const photo = this.data.lifestylePhotos[index]; if (!photo) return; this._removePhoto(photo.photoId); }, async _removePhoto(photoId) { const confirm = await appStore.confirm('确定删除这张照片?', '删除后不可恢复'); if (!confirm) return; appStore.showLoading('删除中…'); try { await profileService.deletePhoto(photoId); appStore.hideLoading(); appStore.toastSuccess('已删除'); this.loadPhotos(); } catch (err) { appStore.hideLoading(); appStore.toastError(err, '删除失败'); } }, // ============ 预览 ============ onPreviewAvatar(e) { const { index } = e.detail; const photo = this.data.avatarPhotos[index]; if (photo) this._preview(photo.url); }, onPreviewLifestyle(e) { const { index } = e.detail; const photo = this.data.lifestylePhotos[index]; if (photo) this._preview(photo.url); }, _preview(currentUrl) { const urls = [ ...this.data.avatarPhotos.map((p) => p.url), ...this.data.lifestylePhotos.map((p) => p.url), ].filter(Boolean); if (urls.length === 0) return; wx.previewImage({ urls, current: currentUrl }); }, // ============ 导航 ============ onBack() { wx.navigateBack(); }, onNext() { if (this.data.avatarPhotos.length === 0) { appStore.toast('请至少上传 1 张主头像'); return; } wx.navigateTo({ url: ROUTE.ONBOARDING.PROFILE_INTERESTS }); }, });