chore: establish V1.1 recovery baseline
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* PhotoUploader 照片上传器(规范 §22, §115 PHOTO_STATUS)
|
||||
*
|
||||
* 多图上传 + 审核状态展示 + 排序 + 删除。
|
||||
* 不变量(AGENTS.md 第 8 条):本组件不接收身份证号/手机号等敏感字段,
|
||||
* 仅处理生活照 URL(已上传至 OSS 后由 Profile.photos 引用)。
|
||||
* 上传凭证由调用方通过 profileService.requestUploadToken 获取后注入。
|
||||
*
|
||||
* 用法:
|
||||
* <ui-photo-uploader photos="{{photos}}" max="{{6}}"
|
||||
* bind:add="onAddPhoto" bind:remove="onRemovePhoto" bind:tap="onPreviewPhoto" />
|
||||
*/
|
||||
Component({
|
||||
options: { addGlobalClass: true },
|
||||
properties: {
|
||||
photos: { type: Array, value: [] },
|
||||
max: { type: Number, value: 6 },
|
||||
readonly: { type: Boolean, value: false },
|
||||
// 是否显示审核状态徽标
|
||||
showReviewStatus: { type: Boolean, value: true },
|
||||
},
|
||||
data: {
|
||||
photoStatusKindMap: {
|
||||
local: 'warning',
|
||||
uploading: 'info',
|
||||
uploaded: 'success',
|
||||
failed: 'danger',
|
||||
review_pending: 'warning',
|
||||
},
|
||||
photoStatusLabelMap: {
|
||||
local: '本地',
|
||||
uploading: '上传中',
|
||||
uploaded: '已上传',
|
||||
failed: '失败',
|
||||
review_pending: '审核中',
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onAdd() {
|
||||
if (this.data.readonly) return;
|
||||
if (this.data.photos.length >= this.data.max) return;
|
||||
this.triggerEvent('add', {});
|
||||
},
|
||||
onRemove(e) {
|
||||
if (this.data.readonly) return;
|
||||
const { index } = e.currentTarget.dataset;
|
||||
this.triggerEvent('remove', { index });
|
||||
},
|
||||
onTap(e) {
|
||||
const { index } = e.currentTarget.dataset;
|
||||
this.triggerEvent('tap', { index });
|
||||
},
|
||||
onRetry(e) {
|
||||
const { index } = e.currentTarget.dataset;
|
||||
this.triggerEvent('retry', { index });
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {
|
||||
"ui-status-badge": "../status-badge/index"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<view class="ui-photo">
|
||||
<view class="ui-photo__list">
|
||||
<view
|
||||
wx:for="{{photos}}"
|
||||
wx:key="index"
|
||||
class="ui-photo__item"
|
||||
data-index="{{index}}"
|
||||
bindtap="onTap"
|
||||
>
|
||||
<image class="ui-photo__img" src="{{item.url}}" mode="aspectFill" />
|
||||
<view wx:if="{{item.status && item.status !== 'uploaded'}}" class="ui-photo__status">
|
||||
<ui-status-badge
|
||||
kind="{{photoStatusKindMap[item.status]}}"
|
||||
label="{{photoStatusLabelMap[item.status]}}"
|
||||
/>
|
||||
</view>
|
||||
<view
|
||||
wx:if="{{!readonly && item.status !== 'uploading'}}"
|
||||
class="ui-photo__remove"
|
||||
catchtap="onRemove"
|
||||
data-index="{{index}}"
|
||||
>×</view>
|
||||
<view
|
||||
wx:if="{{item.status === 'failed'}}"
|
||||
class="ui-photo__retry"
|
||||
catchtap="onRetry"
|
||||
data-index="{{index}}"
|
||||
>重试</view>
|
||||
<view wx:if="{{item.status === 'uploading'}}" class="ui-photo__mask">
|
||||
<view class="ui-photo__spinner"></view>
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
wx:if="{{!readonly && photos.length < max}}"
|
||||
class="ui-photo__add"
|
||||
bindtap="onAdd"
|
||||
>
|
||||
<view class="ui-photo__add-icon">+</view>
|
||||
<text class="ui-photo__add-text">{{photos.length}}/{{max}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="ui-photo__hint">
|
||||
<text>最多 {{max}} 张,建议真人生活照,避免过度美颜</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,99 @@
|
||||
.ui-photo__list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-2);
|
||||
}
|
||||
.ui-photo__item,
|
||||
.ui-photo__add {
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
border-radius: var(--radius-md);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.ui-photo__item {
|
||||
background: var(--color-bg-disabled);
|
||||
}
|
||||
.ui-photo__img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.ui-photo__status {
|
||||
position: absolute;
|
||||
top: 8rpx;
|
||||
left: 8rpx;
|
||||
}
|
||||
.ui-photo__remove {
|
||||
position: absolute;
|
||||
top: 8rpx;
|
||||
right: 8rpx;
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
color: #FFFFFF;
|
||||
font-size: 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
line-height: 1;
|
||||
}
|
||||
.ui-photo__retry {
|
||||
position: absolute;
|
||||
bottom: 8rpx;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
padding: 4rpx 16rpx;
|
||||
border-radius: var(--radius-full);
|
||||
background: var(--color-danger);
|
||||
color: #FFFFFF;
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
.ui-photo__mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.ui-photo__spinner {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
border: 3rpx solid rgba(255, 255, 255, 0.5);
|
||||
border-top-color: #FFFFFF;
|
||||
border-radius: 50%;
|
||||
animation: ui-photo-spin 0.8s linear infinite;
|
||||
}
|
||||
@keyframes ui-photo-spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
.ui-photo__add {
|
||||
background: var(--color-bg-disabled);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 2rpx dashed var(--color-text-disabled);
|
||||
}
|
||||
.ui-photo__add-icon {
|
||||
font-size: var(--font-size-2xl);
|
||||
color: var(--color-text-tertiary);
|
||||
line-height: 1;
|
||||
}
|
||||
.ui-photo__add-text {
|
||||
margin-top: var(--spacing-1);
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--color-text-tertiary);
|
||||
}
|
||||
.ui-photo__hint {
|
||||
margin-top: var(--spacing-2);
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--color-text-tertiary);
|
||||
line-height: var(--line-height-normal);
|
||||
}
|
||||
Reference in New Issue
Block a user