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
+141
View File
@@ -0,0 +1,141 @@
/**
* 申诉提交页(重构:用 appealService.submitAppeal + ui-nav-bar + ui-textarea)
*
* 用户针对封禁/暂停处置提交申诉。类型由入口决定(ban_appeal / suspend_appeal)。
*
* 不变量(AGENTS.md):
* - 第 8 条:敏感数据默认拒绝——本页只接收申诉原因 + 证据截图 + 联系方式,
* 不接受身份证号等敏感字段;联系方式为可选。
* - 第 4 条:Block 优先级最高——申诉不会自动解封或解除拉黑,由运营人工处置。
* 幂等:appealService.submitAppeal 内部以 appealType+relatedActionId+秒级时间生成 Idempotency-Key。
*/
const { appStore } = require('../../stores');
const { appealService } = require('../../services');
const TYPE_LABELS = {
ban_appeal: '封禁申诉',
suspend_appeal: '暂停申诉',
};
const MIN_REASON_LEN = 10;
const MAX_EVIDENCE = 5;
Page({
data: {
appealType: '',
actionId: '',
typeLabel: '申诉',
reason: '',
reasonError: '',
evidenceFiles: [],
contactMethod: '',
submitting: false,
submitted: false,
statusBarHeight: 20,
navBarHeight: 44,
},
onLoad(options) {
try {
const info = (wx.getWindowInfo && wx.getWindowInfo()) || wx.getSystemInfoSync();
this.setData({
statusBarHeight: info.statusBarHeight || 20,
navBarHeight: info.platform === 'android' ? 48 : 44,
});
} catch (e) { /* 使用默认值 */ }
const appealType = options.type ? decodeURIComponent(options.type) : '';
const actionId = options.actionId ? decodeURIComponent(options.actionId) : '';
this.setData({
appealType,
actionId,
typeLabel: TYPE_LABELS[appealType] || '申诉',
});
},
onReasonInput(e) {
this.setData({ reason: e.detail.value, reasonError: '' });
},
onContactInput(e) {
this.setData({ contactMethod: e.detail.value });
},
// ============ 证据上传 ============
chooseEvidence() {
const remain = MAX_EVIDENCE - this.data.evidenceFiles.length;
if (remain <= 0) {
appStore.toast('最多上传 5 张证据');
return;
}
wx.chooseMedia({
count: remain,
mediaType: ['image'],
sourceType: ['album', 'camera'],
success: (res) => {
const files = res.tempFiles.map((f) => f.tempFilePath);
this.setData({ evidenceFiles: [...this.data.evidenceFiles, ...files] });
},
});
},
previewImage(e) {
wx.previewImage({
current: e.currentTarget.dataset.url,
urls: this.data.evidenceFiles,
});
},
removeImage(e) {
const idx = e.currentTarget.dataset.index;
const files = this.data.evidenceFiles;
files.splice(idx, 1);
this.setData({ evidenceFiles: files });
},
// ============ 提交 ============
async submitAppeal() {
const { appealType, actionId, reason, contactMethod } = this.data;
const trimmedReason = (reason || '').trim();
if (!appealType) {
appStore.toast('缺少申诉类型');
return;
}
if (!trimmedReason) {
this.setData({ reasonError: '请填写申诉原因' });
return;
}
if (trimmedReason.length < MIN_REASON_LEN) {
this.setData({ reasonError: `申诉原因请至少 ${MIN_REASON_LEN} 个字` });
return;
}
this.setData({ submitting: true });
try {
await appealService.submitAppeal({
appealType,
relatedActionId: actionId || undefined,
reason: trimmedReason,
// 证据上传至 OSS 后回填 fileId;当前阶段先以本地路径占位,由后端证据服务接入后补全
evidenceFileIds: [],
contactMethod: (contactMethod || '').trim() || undefined,
});
this.setData({ submitted: true, submitting: false });
appStore.toastSuccess('申诉已提交');
} catch (err) {
appStore.toastError(err);
this.setData({ submitting: false });
}
},
goBack() {
wx.navigateBack();
},
onBack() {
wx.navigateBack();
},
});
+11
View File
@@ -0,0 +1,11 @@
{
"navigationStyle": "custom",
"navigationBarTitleText": "提交申诉",
"backgroundColor": "#FFF8F7",
"backgroundTextStyle": "dark",
"usingComponents": {
"ui-nav-bar": "/components/nav-bar/index",
"ui-section-header": "/components/section-header/index",
"ui-textarea": "/components/textarea/index"
}
}
+82
View File
@@ -0,0 +1,82 @@
<!-- 申诉提交页(重构:appealService + ui-nav-bar + ui-textarea) -->
<view class="page">
<ui-nav-bar
title="{{typeLabel}}"
show-back="{{true}}"
status-bar-height="{{statusBarHeight}}"
nav-bar-height="{{navBarHeight}}"
bind:back="onBack"
/>
<!-- 提交成功态 -->
<view wx:if="{{submitted}}" class="body" style="padding-top: {{statusBarHeight + navBarHeight}}px;">
<view class="success-wrap">
<view class="success-icon">✓</view>
<text class="success-title">申诉已提交</text>
<text class="success-hint">工作人员将在 1-3 个工作日内审核您的申诉结果,请耐心等待。</text>
<button class="btn-primary" bindtap="goBack">返回</button>
</view>
</view>
<!-- 表单态 -->
<view wx:else class="body" style="padding-top: {{statusBarHeight + navBarHeight}}px;">
<view class="intro-card">
<text class="intro-title">{{typeLabel}}</text>
<text class="intro-subtitle">请如实描述您的情况,工作人员将认真审核</text>
</view>
<!-- 申诉原因 -->
<view class="card">
<ui-section-header title="申诉原因" subtitle="必填,至少10字" />
<ui-textarea
value="{{reason}}"
placeholder="请详细说明您认为处置不当的原因,以及您希望恢复账号的理由..."
maxlength="500"
error="{{reasonError}}"
bind:input="onReasonInput"
/>
</view>
<!-- 补充证据 -->
<view class="card">
<ui-section-header title="补充证据" subtitle="选填,最多5张" />
<view class="evidence-list">
<view
class="evidence-item"
wx:for="{{evidenceFiles}}"
wx:key="index"
data-url="{{item}}"
bindtap="previewImage"
>
<image src="{{item}}" mode="aspectFill" />
<view class="remove-btn" catchtap="removeImage" data-index="{{index}}">✕</view>
</view>
<view class="evidence-add" bindtap="chooseEvidence" wx:if="{{evidenceFiles.length < 5}}">
<text class="add-icon">+</text>
<text class="add-text">添加证据</text>
</view>
</view>
</view>
<!-- 联系方式 -->
<view class="card">
<ui-section-header title="联系方式" subtitle="选填" />
<input
class="contact-input"
placeholder="请输入手机号或邮箱,便于我们联系您"
value="{{contactMethod}}"
bindinput="onContactInput"
/>
</view>
<button class="btn-submit" bindtap="submitAppeal" loading="{{submitting}}" disabled="{{!reason}}">
提交申诉
</button>
<text class="hint">提交后工作人员将在 1-3 个工作日内审核,请勿重复提交</text>
<!-- 公益免费标识 -->
<view class="free-badge">
<text>本平台为公益服务,申诉受理永久免费</text>
</view>
</view>
</view>
+209
View File
@@ -0,0 +1,209 @@
/* 申诉提交页 — Design Tokens */
.page {
min-height: 100vh;
background: #FFF8F7;
}
.body {
padding: 24rpx;
box-sizing: border-box;
}
/* 介绍卡片 */
.intro-card {
background: #fff;
border-radius: 20rpx;
padding: 32rpx;
margin-bottom: 24rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
border-left: 8rpx solid #E96B6B;
}
.intro-title {
display: block;
font-size: 34rpx;
font-weight: bold;
color: #1A1A1A;
margin-bottom: 12rpx;
}
.intro-subtitle {
display: block;
font-size: 26rpx;
color: #999;
line-height: 1.5;
}
/* 卡片 */
.card {
background: #fff;
border-radius: 20rpx;
padding: 24rpx 32rpx;
margin-bottom: 24rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
}
/* 证据 */
.evidence-list {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
margin-top: 16rpx;
}
.evidence-item {
position: relative;
width: 160rpx;
height: 160rpx;
border-radius: 12rpx;
overflow: hidden;
}
.evidence-item image {
width: 100%;
height: 100%;
}
.remove-btn {
position: absolute;
top: 4rpx;
right: 4rpx;
width: 36rpx;
height: 36rpx;
line-height: 32rpx;
text-align: center;
background: rgba(0, 0, 0, 0.5);
color: #fff;
border-radius: 50%;
font-size: 22rpx;
}
.evidence-add {
width: 160rpx;
height: 160rpx;
border-radius: 12rpx;
background: #F7F8FA;
border: 2rpx dashed #D0D0D0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #999;
}
.add-icon {
font-size: 48rpx;
color: #B0B0B0;
line-height: 1;
}
.add-text {
font-size: 22rpx;
margin-top: 8rpx;
}
/* 联系方式输入 */
.contact-input {
margin-top: 16rpx;
background: #F7F8FA;
border-radius: 12rpx;
padding: 24rpx;
font-size: 28rpx;
min-height: 40rpx;
}
/* 提交按钮 */
.btn-submit {
width: 100%;
height: 88rpx;
line-height: 88rpx;
border-radius: 44rpx;
background: #E96B6B;
color: #fff;
font-size: 32rpx;
border: none;
margin-top: 8rpx;
}
.btn-submit::after {
border: none;
}
.btn-submit[disabled] {
opacity: 0.5;
}
.btn-primary {
width: 80%;
height: 88rpx;
line-height: 88rpx;
border-radius: 44rpx;
background: #E96B6B;
color: #fff;
font-size: 32rpx;
border: none;
margin-top: 48rpx;
}
.btn-primary::after {
border: none;
}
.hint {
display: block;
text-align: center;
font-size: 22rpx;
color: #B0B0B0;
margin-top: 20rpx;
padding: 0 24rpx;
}
/* 成功态 */
.success-wrap {
display: flex;
flex-direction: column;
align-items: center;
padding: 120rpx 48rpx;
}
.success-icon {
width: 120rpx;
height: 120rpx;
line-height: 120rpx;
text-align: center;
border-radius: 50%;
background: #E8F5E9;
color: #1A6B3E;
font-size: 64rpx;
margin-bottom: 32rpx;
}
.success-title {
font-size: 36rpx;
font-weight: bold;
color: #1A1A1A;
margin-bottom: 16rpx;
}
.success-hint {
font-size: 26rpx;
color: #999;
text-align: center;
line-height: 1.6;
}
/* 公益免费标识 */
.free-badge {
text-align: center;
padding: 24rpx;
margin-top: 16rpx;
}
.free-badge text {
display: inline-block;
font-size: 22rpx;
color: #1A6B3E;
background: #E8F5E9;
padding: 8rpx 24rpx;
border-radius: 24rpx;
}