chore: establish V1.1 recovery baseline
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
/**
|
||||
* 头像与照片页 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.loadPhotos();
|
||||
},
|
||||
|
||||
_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(`最多上传 ${BUSINESS_LIMITS.MAX_PROFILE_PHOTOS} 张照片`);
|
||||
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 });
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"navigationBarTitleText": "头像与照片",
|
||||
"navigationStyle": "custom",
|
||||
"usingComponents": {
|
||||
"ui-nav-bar": "/components/nav-bar/index",
|
||||
"ui-section-header": "/components/section-header/index",
|
||||
"ui-photo-uploader": "/components/photo-uploader/index",
|
||||
"ui-button": "/components/button/index",
|
||||
"ui-loading": "/components/loading/index"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<!-- 头像与照片页 U08 -->
|
||||
<view class="pp-page">
|
||||
<ui-nav-bar
|
||||
title="头像与照片"
|
||||
status-bar-height="{{statusBarHeight}}"
|
||||
nav-bar-height="{{navBarHeight}}"
|
||||
bind:back="onBack"
|
||||
/>
|
||||
|
||||
<view wx:if="{{loading}}" class="pp-loading">
|
||||
<ui-loading mode="spinner" text="加载照片…" />
|
||||
</view>
|
||||
|
||||
<view wx:else class="pp-body">
|
||||
<!-- 主头像 -->
|
||||
<view class="pp-section card-base">
|
||||
<ui-section-header title="主头像" subtitle="必须上传 1 张清晰正面照" />
|
||||
<view class="pp-uploader">
|
||||
<ui-photo-uploader
|
||||
photos="{{avatarPhotos}}"
|
||||
max="{{1}}"
|
||||
bind:add="onAddAvatar"
|
||||
bind:remove="onRemoveAvatar"
|
||||
bind:tap="onPreviewAvatar"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 生活照 -->
|
||||
<view class="pp-section card-base">
|
||||
<ui-section-header title="生活照片" subtitle="最多 {{maxLifestyle}} 张(可选)" />
|
||||
<view class="pp-uploader">
|
||||
<ui-photo-uploader
|
||||
photos="{{lifestylePhotos}}"
|
||||
max="{{maxLifestyle}}"
|
||||
bind:add="onAddLifestyle"
|
||||
bind:remove="onRemoveLifestyle"
|
||||
bind:tap="onPreviewLifestyle"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 上传须知 -->
|
||||
<view class="pp-section card-base">
|
||||
<ui-section-header title="上传须知" />
|
||||
<view class="pp-guidelines">
|
||||
<view wx:for="{{guidelines}}" wx:key="*this" class="pp-guideline">
|
||||
<view class="pp-guideline__dot"></view>
|
||||
<text class="pp-guideline__text">{{item}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="pp-actions">
|
||||
<ui-button variant="primary" size="lg" block bind:tap="onNext">下一步</ui-button>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* 头像与照片页 U08
|
||||
*/
|
||||
|
||||
.pp-page {
|
||||
min-height: 100vh;
|
||||
background: var(--color-bg-page);
|
||||
padding-bottom: calc(160rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.pp-loading {
|
||||
padding-top: 200rpx;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.pp-body {
|
||||
padding: var(--spacing-2) var(--spacing-page-x);
|
||||
}
|
||||
|
||||
.pp-section {
|
||||
margin-bottom: var(--spacing-3);
|
||||
}
|
||||
|
||||
.pp-uploader {
|
||||
margin-top: var(--spacing-2);
|
||||
}
|
||||
|
||||
/* ============ 上传须知 ============ */
|
||||
.pp-guidelines {
|
||||
margin-top: var(--spacing-2);
|
||||
}
|
||||
|
||||
.pp-guideline {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--spacing-2);
|
||||
padding: var(--spacing-1) 0;
|
||||
}
|
||||
|
||||
.pp-guideline__dot {
|
||||
width: 10rpx;
|
||||
height: 10rpx;
|
||||
margin-top: 14rpx;
|
||||
border-radius: 50%;
|
||||
background: var(--color-text-placeholder);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.pp-guideline__text {
|
||||
flex: 1;
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-text-tertiary);
|
||||
line-height: var(--line-height-relaxed);
|
||||
}
|
||||
|
||||
/* ============ 底部 ============ */
|
||||
.pp-actions {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: var(--spacing-2) var(--spacing-page-x);
|
||||
padding-bottom: calc(var(--spacing-2) + env(safe-area-inset-bottom));
|
||||
background: var(--color-bg-card);
|
||||
border-top: var(--border-width) solid var(--color-divider);
|
||||
z-index: var(--z-sticky);
|
||||
}
|
||||
Reference in New Issue
Block a user