2026-09-09 22:05:00 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 头像与照片页 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();
|
2026-09-21 02:05:15 +08:00
|
|
|
|
this._applyPhotoLimit();
|
2026-09-09 22:05:00 +08:00
|
|
|
|
this.loadPhotos();
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-09-21 02:05:15 +08:00
|
|
|
|
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) });
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-09-09 22:05:00 +08:00
|
|
|
|
_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) {
|
2026-09-21 02:05:15 +08:00
|
|
|
|
appStore.toast(`最多上传 ${this.data.maxLifestyle + 1} 张照片`);
|
2026-09-09 22:05:00 +08:00
|
|
|
|
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 由服务端清理)
|
|
|
|
|
|
const fileName = `photo_${Date.now()}.jpg`;
|
|
|
|
|
|
const tokenRes = await profileService.requestUploadToken({ fileName, contentType: 'image/jpeg' });
|
|
|
|
|
|
|
|
|
|
|
|
// 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. 若上传的是头像区,或当前无主头像,则设为主头像
|
|
|
|
|
|
if (photoType === 'avatar' || this.data.avatarPhotos.length === 0) {
|
|
|
|
|
|
await profileService.setMainPhoto(tokenRes.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 });
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|