feat: 申诉常见理由快捷选择 + 证据截图上传

- 后端: 新增 appeal_reason_type, appeal_evidence_urls 字段
- 后端: 新建 upload_routes.py 图片上传接口
- 前端: history 页面添加快捷理由选择器 + 截图上传
- 前端: admin-review 页面展示证据图片 + 点击预览
- 新增 SQL 更新脚本 update_appeal_fields.sql

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
刘正航
2026-04-21 23:26:25 +08:00
parent 50440e84fb
commit f7d0601c4e
12 changed files with 344 additions and 13 deletions

View File

@@ -1,4 +1,4 @@
const { request } = require('../../utils/request')
const { request, uploadFile } = require('../../utils/request')
const STATUS_OPTIONS = [
{ value: '', label: '全部状态' },
@@ -26,6 +26,14 @@ const APPEAL_STATUS_TEXT = {
rejected: '已驳回'
}
const REASON_TYPE_OPTIONS = [
{ value: '', label: '请选择申诉理由类型' },
{ value: '正常活动文案', label: '正常活动文案' },
{ value: '正常社区通知', label: '正常社区通知' },
{ value: '私信沟通内容', label: '私信沟通内容' },
{ value: '其他', label: '其他(需手动填写)' }
]
Page({
data: {
loading: false,
@@ -35,7 +43,12 @@ Page({
visibilityOptions: VISIBILITY_OPTIONS,
visibilityIndex: 0,
appealPostId: null,
appealReason: ''
appealReasonType: '',
appealReasonTypeOptions: REASON_TYPE_OPTIONS,
appealReasonTypeIndex: 0,
appealReason: '',
appealEvidenceUrls: [],
appealEvidenceFiles: []
},
formatPercent(value, digits = 2) {
@@ -84,33 +97,112 @@ Page({
startAppeal(e) {
const postId = Number(e.currentTarget.dataset.id)
this.setData({ appealPostId: postId, appealReason: '' })
this.setData({
appealPostId: postId,
appealReasonType: '',
appealReasonTypeIndex: 0,
appealReason: '',
appealEvidenceUrls: [],
appealEvidenceFiles: []
})
},
onReasonTypeChange(e) {
const idx = Number(e.detail.value || 0)
const reasonType = this.data.appealReasonTypeOptions[idx].value
this.setData({ appealReasonTypeIndex: idx, appealReasonType: reasonType })
// 如果选择"其他",清空快捷理由,让用户手动输入
if (reasonType === '其他') {
this.setData({ appealReasonType: '' })
}
},
onAppealInput(e) {
this.setData({ appealReason: e.detail.value || '' })
},
chooseEvidence() {
wx.chooseMedia({
count: 3,
mediaType: ['image'],
sourceType: ['album', 'camera'],
success: (res) => {
const files = res.tempFiles.map((f) => f.tempFilePath)
this.setData({
appealEvidenceFiles: [...this.data.appealEvidenceFiles, ...files].slice(0, 3)
})
}
})
},
removeEvidence(e) {
const idx = Number(e.currentTarget.dataset.index || 0)
const files = this.data.appealEvidenceFiles.filter((_, i) => i !== idx)
const urls = this.data.appealEvidenceUrls.filter((_, i) => i !== idx)
this.setData({ appealEvidenceFiles: files, appealEvidenceUrls: urls })
},
async uploadAllEvidence() {
const files = this.data.appealEvidenceFiles
if (!files.length) return []
const urls = []
for (const filePath of files) {
try {
const result = await uploadFile(filePath)
urls.push(result.url)
} catch (err) {
console.error('上传失败', filePath, err)
}
}
return urls
},
cancelAppeal() {
this.setData({ appealPostId: null, appealReason: '' })
this.setData({
appealPostId: null,
appealReasonType: '',
appealReasonTypeIndex: 0,
appealReason: '',
appealEvidenceUrls: [],
appealEvidenceFiles: []
})
},
async submitAppeal() {
const postId = this.data.appealPostId
if (!postId) return
const reasonType = (this.data.appealReasonType || '').trim()
const reason = (this.data.appealReason || '').trim()
if (reason.length < 2) {
// 如果没有选择快捷理由类型,必须手动填写理由
if (!reasonType && reason.length < 2) {
wx.showToast({ title: '申诉理由至少 2 个字符', icon: 'none' })
return
}
// 上传证据图片
const evidenceUrls = await this.uploadAllEvidence()
await request({
url: `/content/posts/${postId}/appeal`,
method: 'POST',
data: { reason }
data: {
reason_type: reasonType,
reason,
evidence_urls: evidenceUrls
}
})
wx.showToast({ title: '申诉提交成功', icon: 'success' })
this.setData({ appealPostId: null, appealReason: '' })
this.setData({
appealPostId: null,
appealReasonType: '',
appealReasonTypeIndex: 0,
appealReason: '',
appealEvidenceUrls: [],
appealEvidenceFiles: []
})
this.fetchList()
},