chore: establish V1.1 recovery baseline

This commit is contained in:
2026-09-09 22:05:00 +08:00
commit b46b48eb33
947 changed files with 155471 additions and 0 deletions
@@ -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);
}