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:
@@ -6,6 +6,7 @@ from app.routes.admin_routes import admin_bp
|
|||||||
from app.routes.auth_routes import auth_bp
|
from app.routes.auth_routes import auth_bp
|
||||||
from app.routes.content_routes import content_bp
|
from app.routes.content_routes import content_bp
|
||||||
from app.routes.spam_routes import spam_bp
|
from app.routes.spam_routes import spam_bp
|
||||||
|
from app.routes.upload_routes import upload_bp
|
||||||
from app.routes.user_routes import user_bp
|
from app.routes.user_routes import user_bp
|
||||||
|
|
||||||
|
|
||||||
@@ -22,6 +23,7 @@ def create_app() -> Flask:
|
|||||||
app.register_blueprint(spam_bp, url_prefix="/api/spam")
|
app.register_blueprint(spam_bp, url_prefix="/api/spam")
|
||||||
app.register_blueprint(content_bp, url_prefix="/api/content")
|
app.register_blueprint(content_bp, url_prefix="/api/content")
|
||||||
app.register_blueprint(admin_bp, url_prefix="/api/admin")
|
app.register_blueprint(admin_bp, url_prefix="/api/admin")
|
||||||
|
app.register_blueprint(upload_bp, url_prefix="/api/upload")
|
||||||
|
|
||||||
@app.get("/api/health")
|
@app.get("/api/health")
|
||||||
def health_check():
|
def health_check():
|
||||||
|
|||||||
@@ -35,3 +35,4 @@ class Config:
|
|||||||
|
|
||||||
SPAM_DATASET_PATH = str(BASE_DIR / "seed" / "spam_samples_seed.json")
|
SPAM_DATASET_PATH = str(BASE_DIR / "seed" / "spam_samples_seed.json")
|
||||||
NB_MODEL_PATH = str(BASE_DIR / "models" / "spam_nb_model.joblib")
|
NB_MODEL_PATH = str(BASE_DIR / "models" / "spam_nb_model.joblib")
|
||||||
|
UPLOAD_FOLDER = str(BASE_DIR / "uploads")
|
||||||
|
|||||||
@@ -141,7 +141,9 @@ class ContentPost(db.Model):
|
|||||||
manual_review_at = db.Column(db.DateTime, nullable=True)
|
manual_review_at = db.Column(db.DateTime, nullable=True)
|
||||||
|
|
||||||
appeal_status = db.Column(db.String(16), nullable=False, default="none") # none | pending | approved | rejected
|
appeal_status = db.Column(db.String(16), nullable=False, default="none") # none | pending | approved | rejected
|
||||||
|
appeal_reason_type = db.Column(db.String(32), default="") # 快捷理由类型
|
||||||
appeal_reason = db.Column(db.String(255), default="")
|
appeal_reason = db.Column(db.String(255), default="")
|
||||||
|
appeal_evidence_urls = db.Column(db.JSON, default=list) # 证据图片 URL 列表
|
||||||
appeal_admin_note = db.Column(db.String(255), default="")
|
appeal_admin_note = db.Column(db.String(255), default="")
|
||||||
appeal_submitted_at = db.Column(db.DateTime, nullable=True)
|
appeal_submitted_at = db.Column(db.DateTime, nullable=True)
|
||||||
appeal_processed_at = db.Column(db.DateTime, nullable=True)
|
appeal_processed_at = db.Column(db.DateTime, nullable=True)
|
||||||
@@ -170,7 +172,9 @@ class ContentPost(db.Model):
|
|||||||
"manual_review_note": self.manual_review_note,
|
"manual_review_note": self.manual_review_note,
|
||||||
"manual_review_at": self.manual_review_at.isoformat() if self.manual_review_at else None,
|
"manual_review_at": self.manual_review_at.isoformat() if self.manual_review_at else None,
|
||||||
"appeal_status": self.appeal_status,
|
"appeal_status": self.appeal_status,
|
||||||
|
"appeal_reason_type": self.appeal_reason_type,
|
||||||
"appeal_reason": self.appeal_reason,
|
"appeal_reason": self.appeal_reason,
|
||||||
|
"appeal_evidence_urls": self.appeal_evidence_urls or [],
|
||||||
"appeal_admin_note": self.appeal_admin_note,
|
"appeal_admin_note": self.appeal_admin_note,
|
||||||
"appeal_submitted_at": self.appeal_submitted_at.isoformat() if self.appeal_submitted_at else None,
|
"appeal_submitted_at": self.appeal_submitted_at.isoformat() if self.appeal_submitted_at else None,
|
||||||
"appeal_processed_at": self.appeal_processed_at.isoformat() if self.appeal_processed_at else None,
|
"appeal_processed_at": self.appeal_processed_at.isoformat() if self.appeal_processed_at else None,
|
||||||
|
|||||||
@@ -295,14 +295,20 @@ def submit_appeal(post_id: int):
|
|||||||
return fail("仅被拦截的信息可申诉", 400)
|
return fail("仅被拦截的信息可申诉", 400)
|
||||||
|
|
||||||
payload = request.get_json(silent=True) or {}
|
payload = request.get_json(silent=True) or {}
|
||||||
|
reason_type = (payload.get("reason_type") or "").strip()
|
||||||
reason = (payload.get("reason") or "").strip()
|
reason = (payload.get("reason") or "").strip()
|
||||||
if len(reason) < 2:
|
evidence_urls = payload.get("evidence_urls") or []
|
||||||
|
|
||||||
|
# 如果选择了快捷理由,reason 可为空;否则至少 2 字符
|
||||||
|
if not reason_type and len(reason) < 2:
|
||||||
return fail("申诉理由至少2个字符", 400)
|
return fail("申诉理由至少2个字符", 400)
|
||||||
if post.appeal_status == "pending":
|
if post.appeal_status == "pending":
|
||||||
return fail("该记录已在申诉处理中", 400)
|
return fail("该记录已在申诉处理中", 400)
|
||||||
|
|
||||||
post.appeal_status = "pending"
|
post.appeal_status = "pending"
|
||||||
|
post.appeal_reason_type = reason_type
|
||||||
post.appeal_reason = reason
|
post.appeal_reason = reason
|
||||||
|
post.appeal_evidence_urls = evidence_urls
|
||||||
post.appeal_submitted_at = datetime.utcnow()
|
post.appeal_submitted_at = datetime.utcnow()
|
||||||
post.appeal_admin_note = ""
|
post.appeal_admin_note = ""
|
||||||
post.appeal_processed_at = None
|
post.appeal_processed_at = None
|
||||||
|
|||||||
68
backend/app/routes/upload_routes.py
Normal file
68
backend/app/routes/upload_routes.py
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import os
|
||||||
|
import uuid
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from flask import Blueprint, current_app, request, send_from_directory
|
||||||
|
from flask_jwt_extended import jwt_required
|
||||||
|
from werkzeug.utils import secure_filename
|
||||||
|
|
||||||
|
from app.utils.auth import current_user
|
||||||
|
from app.utils.response import fail, ok
|
||||||
|
|
||||||
|
|
||||||
|
upload_bp = Blueprint("upload", __name__)
|
||||||
|
|
||||||
|
ALLOWED_EXTENSIONS = {"png", "jpg", "jpeg", "gif", "webg"}
|
||||||
|
MAX_FILE_SIZE = 5 * 1024 * 1024 # 5MB
|
||||||
|
|
||||||
|
|
||||||
|
def allowed_file(filename: str) -> bool:
|
||||||
|
ext = (filename.rsplit(".", 1)[-1] if "." in filename else "").lower()
|
||||||
|
return ext in ALLOWED_EXTENSIONS
|
||||||
|
|
||||||
|
|
||||||
|
def generate_filename(original: str) -> str:
|
||||||
|
ext = original.rsplit(".", 1)[-1] if "." in original else "jpg"
|
||||||
|
timestamp = datetime.utcnow().strftime("%Y%m%d%H%M%S")
|
||||||
|
unique = uuid.uuid4().hex[:8]
|
||||||
|
return f"{timestamp}_{unique}.{ext.lower()}"
|
||||||
|
|
||||||
|
|
||||||
|
@upload_bp.post("/image")
|
||||||
|
@jwt_required()
|
||||||
|
def upload_image():
|
||||||
|
user = current_user()
|
||||||
|
if not user:
|
||||||
|
return fail("用户不存在", 404)
|
||||||
|
|
||||||
|
if "file" not in request.files:
|
||||||
|
return fail("未上传文件", 400)
|
||||||
|
|
||||||
|
file = request.files["file"]
|
||||||
|
if not file.filename:
|
||||||
|
return fail("文件名无效", 400)
|
||||||
|
|
||||||
|
if not allowed_file(file.filename):
|
||||||
|
return fail("仅支持 png/jpg/jpeg/gif/webg 格式", 400)
|
||||||
|
|
||||||
|
upload_folder = current_app.config.get("UPLOAD_FOLDER")
|
||||||
|
if not upload_folder:
|
||||||
|
return fail("上传目录未配置", 500)
|
||||||
|
|
||||||
|
os.makedirs(upload_folder, exist_ok=True)
|
||||||
|
|
||||||
|
filename = generate_filename(secure_filename(file.filename))
|
||||||
|
filepath = os.path.join(upload_folder, filename)
|
||||||
|
file.save(filepath)
|
||||||
|
|
||||||
|
url = f"/api/upload/images/{filename}"
|
||||||
|
return ok({"url": url, "filename": filename}, "上传成功")
|
||||||
|
|
||||||
|
|
||||||
|
@upload_bp.get("/images/<filename>")
|
||||||
|
def get_image(filename: str):
|
||||||
|
upload_folder = current_app.config.get("UPLOAD_FOLDER")
|
||||||
|
if not upload_folder:
|
||||||
|
return fail("上传目录未配置", 500)
|
||||||
|
|
||||||
|
return send_from_directory(upload_folder, filename)
|
||||||
17
backend/sql/update_appeal_fields.sql
Normal file
17
backend/sql/update_appeal_fields.sql
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
-- 申诉功能增强:新增理由类型和证据图片字段
|
||||||
|
-- 执行方式:mysql -u root -p database_name < update_appeal_fields.sql
|
||||||
|
|
||||||
|
-- 新增申诉理由类型字段
|
||||||
|
ALTER TABLE content_posts
|
||||||
|
ADD COLUMN appeal_reason_type VARCHAR(32) DEFAULT '' COMMENT '快捷申诉理由类型'
|
||||||
|
AFTER appeal_status;
|
||||||
|
|
||||||
|
-- 新增证据图片URL列表字段
|
||||||
|
ALTER TABLE content_posts
|
||||||
|
ADD COLUMN appeal_evidence_urls JSON DEFAULT NULL COMMENT '证据图片URL列表'
|
||||||
|
AFTER appeal_reason;
|
||||||
|
|
||||||
|
-- 如果 MySQL 版本不支持 JSON 类型,使用 TEXT 替代
|
||||||
|
-- ALTER TABLE content_posts
|
||||||
|
-- ADD COLUMN appeal_evidence_urls TEXT DEFAULT NULL COMMENT '证据图片URL列表(JSON)'
|
||||||
|
-- AFTER appeal_reason;
|
||||||
@@ -399,6 +399,67 @@ button.btn::after {
|
|||||||
color: #ffdce1;
|
color: #ffdce1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.evidence-grid {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12rpx;
|
||||||
|
margin-top: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.evidence-item {
|
||||||
|
position: relative;
|
||||||
|
width: 120rpx;
|
||||||
|
height: 120rpx;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1rpx solid rgba(139, 177, 223, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.evidence-thumb {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.evidence-clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.evidence-clickable:active {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.evidence-remove {
|
||||||
|
position: absolute;
|
||||||
|
top: -4rpx;
|
||||||
|
right: -4rpx;
|
||||||
|
width: 32rpx;
|
||||||
|
height: 32rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(255, 91, 111, 0.9);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 24rpx;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.evidence-add {
|
||||||
|
width: 120rpx;
|
||||||
|
height: 120rpx;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
border: 1rpx dashed rgba(139, 177, 223, 0.5);
|
||||||
|
background: rgba(17, 35, 58, 0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.evidence-add-icon {
|
||||||
|
font-size: 48rpx;
|
||||||
|
color: var(--sub);
|
||||||
|
}
|
||||||
|
|
||||||
.chip-group {
|
.chip-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|||||||
@@ -110,10 +110,15 @@ Page({
|
|||||||
},
|
},
|
||||||
|
|
||||||
normalizeAppeals(rows = []) {
|
normalizeAppeals(rows = []) {
|
||||||
|
const baseURL = getApp().globalData.baseURL || 'http://127.0.0.1:5000/api'
|
||||||
|
const serverBase = baseURL.replace('/api', '')
|
||||||
return rows.map((item) => ({
|
return rows.map((item) => ({
|
||||||
...item,
|
...item,
|
||||||
created_text: (item.created_at || '').replace('T', ' ').slice(0, 19),
|
created_text: (item.created_at || '').replace('T', ' ').slice(0, 19),
|
||||||
appeal_status_text: APPEAL_STATUS_TEXT[item.appeal_status] || item.appeal_status
|
appeal_status_text: APPEAL_STATUS_TEXT[item.appeal_status] || item.appeal_status,
|
||||||
|
appeal_evidence_urls: (item.appeal_evidence_urls || []).map((url) =>
|
||||||
|
url.startsWith('http') ? url : `${serverBase}${url}`
|
||||||
|
)
|
||||||
}))
|
}))
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -155,8 +160,10 @@ Page({
|
|||||||
].join('&')
|
].join('&')
|
||||||
|
|
||||||
const data = await request({ url: `/admin/appeals?${query}` })
|
const data = await request({ url: `/admin/appeals?${query}` })
|
||||||
|
const normalizedAppeals = this.normalizeAppeals(data.items || [])
|
||||||
|
console.log('normalized appeals:', normalizedAppeals)
|
||||||
this.setData({
|
this.setData({
|
||||||
appeals: this.normalizeAppeals(data.items || []),
|
appeals: normalizedAppeals,
|
||||||
appealPager: buildPager(data.total || 0, appealPager.page, appealPager.pageSize)
|
appealPager: buildPager(data.total || 0, appealPager.page, appealPager.pageSize)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -294,5 +301,15 @@ Page({
|
|||||||
wx.showToast({ title: '申诉处理完成', icon: 'success' })
|
wx.showToast({ title: '申诉处理完成', icon: 'success' })
|
||||||
this.setData({ [`appealNoteMap.${id}`]: '' })
|
this.setData({ [`appealNoteMap.${id}`]: '' })
|
||||||
await Promise.all([this.fetchIntercepts(), this.fetchAppeals()])
|
await Promise.all([this.fetchIntercepts(), this.fetchAppeals()])
|
||||||
|
},
|
||||||
|
|
||||||
|
previewEvidence(e) {
|
||||||
|
const url = e.currentTarget.dataset.url
|
||||||
|
const item = this.data.appeals.find((a) => a.appeal_evidence_urls && a.appeal_evidence_urls.includes(url))
|
||||||
|
const urls = item ? item.appeal_evidence_urls : [url]
|
||||||
|
wx.previewImage({
|
||||||
|
current: url,
|
||||||
|
urls
|
||||||
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -100,9 +100,19 @@
|
|||||||
<view class="list-item" wx:for="{{appeals}}" wx:key="id">
|
<view class="list-item" wx:for="{{appeals}}" wx:key="id">
|
||||||
<view class="item-title">{{item.text}}</view>
|
<view class="item-title">{{item.text}}</view>
|
||||||
<view class="item-sub">申诉人:{{item.nickname || item.username}} · 当前状态:{{item.appeal_status_text}}</view>
|
<view class="item-sub">申诉人:{{item.nickname || item.username}} · 当前状态:{{item.appeal_status_text}}</view>
|
||||||
|
<view class="item-sub">申诉理由类型:{{item.appeal_reason_type || '未选择'}}</view>
|
||||||
<view class="item-sub">申诉理由:{{item.appeal_reason || '未填写'}}</view>
|
<view class="item-sub">申诉理由:{{item.appeal_reason || '未填写'}}</view>
|
||||||
<view class="item-sub">时间:{{item.created_text}}</view>
|
<view class="item-sub">时间:{{item.created_text}}</view>
|
||||||
|
|
||||||
|
<view class="field" wx:if="{{item.appeal_evidence_urls && item.appeal_evidence_urls.length}}">
|
||||||
|
<text class="field-label">证据截图</text>
|
||||||
|
<view class="evidence-grid">
|
||||||
|
<view class="evidence-item" wx:for="{{item.appeal_evidence_urls}}" wx:for-item="evidenceUrl" wx:key="*this">
|
||||||
|
<image class="evidence-thumb evidence-clickable" src="{{evidenceUrl}}" mode="aspectFill" data-url="{{evidenceUrl}}" bindtap="previewEvidence" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
<textarea class="textarea note-textarea" placeholder="可填写申诉处理备注" value="{{appealNoteMap[item.id] || ''}}" data-id="{{item.id}}" bindinput="onAppealNoteInput" />
|
<textarea class="textarea note-textarea" placeholder="可填写申诉处理备注" value="{{appealNoteMap[item.id] || ''}}" data-id="{{item.id}}" bindinput="onAppealNoteInput" />
|
||||||
|
|
||||||
<view class="btn-row">
|
<view class="btn-row">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const { request } = require('../../utils/request')
|
const { request, uploadFile } = require('../../utils/request')
|
||||||
|
|
||||||
const STATUS_OPTIONS = [
|
const STATUS_OPTIONS = [
|
||||||
{ value: '', label: '全部状态' },
|
{ value: '', label: '全部状态' },
|
||||||
@@ -26,6 +26,14 @@ const APPEAL_STATUS_TEXT = {
|
|||||||
rejected: '已驳回'
|
rejected: '已驳回'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const REASON_TYPE_OPTIONS = [
|
||||||
|
{ value: '', label: '请选择申诉理由类型' },
|
||||||
|
{ value: '正常活动文案', label: '正常活动文案' },
|
||||||
|
{ value: '正常社区通知', label: '正常社区通知' },
|
||||||
|
{ value: '私信沟通内容', label: '私信沟通内容' },
|
||||||
|
{ value: '其他', label: '其他(需手动填写)' }
|
||||||
|
]
|
||||||
|
|
||||||
Page({
|
Page({
|
||||||
data: {
|
data: {
|
||||||
loading: false,
|
loading: false,
|
||||||
@@ -35,7 +43,12 @@ Page({
|
|||||||
visibilityOptions: VISIBILITY_OPTIONS,
|
visibilityOptions: VISIBILITY_OPTIONS,
|
||||||
visibilityIndex: 0,
|
visibilityIndex: 0,
|
||||||
appealPostId: null,
|
appealPostId: null,
|
||||||
appealReason: ''
|
appealReasonType: '',
|
||||||
|
appealReasonTypeOptions: REASON_TYPE_OPTIONS,
|
||||||
|
appealReasonTypeIndex: 0,
|
||||||
|
appealReason: '',
|
||||||
|
appealEvidenceUrls: [],
|
||||||
|
appealEvidenceFiles: []
|
||||||
},
|
},
|
||||||
|
|
||||||
formatPercent(value, digits = 2) {
|
formatPercent(value, digits = 2) {
|
||||||
@@ -84,33 +97,112 @@ Page({
|
|||||||
|
|
||||||
startAppeal(e) {
|
startAppeal(e) {
|
||||||
const postId = Number(e.currentTarget.dataset.id)
|
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) {
|
onAppealInput(e) {
|
||||||
this.setData({ appealReason: e.detail.value || '' })
|
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() {
|
cancelAppeal() {
|
||||||
this.setData({ appealPostId: null, appealReason: '' })
|
this.setData({
|
||||||
|
appealPostId: null,
|
||||||
|
appealReasonType: '',
|
||||||
|
appealReasonTypeIndex: 0,
|
||||||
|
appealReason: '',
|
||||||
|
appealEvidenceUrls: [],
|
||||||
|
appealEvidenceFiles: []
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
async submitAppeal() {
|
async submitAppeal() {
|
||||||
const postId = this.data.appealPostId
|
const postId = this.data.appealPostId
|
||||||
if (!postId) return
|
if (!postId) return
|
||||||
|
|
||||||
|
const reasonType = (this.data.appealReasonType || '').trim()
|
||||||
const reason = (this.data.appealReason || '').trim()
|
const reason = (this.data.appealReason || '').trim()
|
||||||
if (reason.length < 2) {
|
|
||||||
|
// 如果没有选择快捷理由类型,必须手动填写理由
|
||||||
|
if (!reasonType && reason.length < 2) {
|
||||||
wx.showToast({ title: '申诉理由至少 2 个字符', icon: 'none' })
|
wx.showToast({ title: '申诉理由至少 2 个字符', icon: 'none' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 上传证据图片
|
||||||
|
const evidenceUrls = await this.uploadAllEvidence()
|
||||||
|
|
||||||
await request({
|
await request({
|
||||||
url: `/content/posts/${postId}/appeal`,
|
url: `/content/posts/${postId}/appeal`,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: { reason }
|
data: {
|
||||||
|
reason_type: reasonType,
|
||||||
|
reason,
|
||||||
|
evidence_urls: evidenceUrls
|
||||||
|
}
|
||||||
})
|
})
|
||||||
wx.showToast({ title: '申诉提交成功', icon: 'success' })
|
wx.showToast({ title: '申诉提交成功', icon: 'success' })
|
||||||
this.setData({ appealPostId: null, appealReason: '' })
|
this.setData({
|
||||||
|
appealPostId: null,
|
||||||
|
appealReasonType: '',
|
||||||
|
appealReasonTypeIndex: 0,
|
||||||
|
appealReason: '',
|
||||||
|
appealEvidenceUrls: [],
|
||||||
|
appealEvidenceFiles: []
|
||||||
|
})
|
||||||
this.fetchList()
|
this.fetchList()
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -61,7 +61,25 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view wx:if="{{appealPostId === item.id}}">
|
<view wx:if="{{appealPostId === item.id}}">
|
||||||
<textarea class="textarea" placeholder="请输入申诉理由(至少 2 个字符)" value="{{appealReason}}" bindinput="onAppealInput" />
|
<view class="field">
|
||||||
|
<text class="field-label">申诉理由类型</text>
|
||||||
|
<picker mode="selector" range="{{appealReasonTypeOptions}}" range-key="label" value="{{appealReasonTypeIndex}}" bindchange="onReasonTypeChange">
|
||||||
|
<view class="picker-value">{{appealReasonTypeOptions[appealReasonTypeIndex].label}}</view>
|
||||||
|
</picker>
|
||||||
|
</view>
|
||||||
|
<textarea class="textarea" placeholder="可补充申诉理由(选择快捷理由后可省略)" value="{{appealReason}}" bindinput="onAppealInput" />
|
||||||
|
<view class="field">
|
||||||
|
<text class="field-label">证据截图(最多3张)</text>
|
||||||
|
<view class="evidence-grid">
|
||||||
|
<view class="evidence-item" wx:for="{{appealEvidenceFiles}}" wx:key="index">
|
||||||
|
<image class="evidence-thumb" src="{{item}}" mode="aspectFill" />
|
||||||
|
<view class="evidence-remove" data-index="{{index}}" bindtap="removeEvidence">×</view>
|
||||||
|
</view>
|
||||||
|
<view class="evidence-add" wx:if="{{appealEvidenceFiles.length < 3}}" bindtap="chooseEvidence">
|
||||||
|
<text class="evidence-add-icon">+</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
<view class="btn-row">
|
<view class="btn-row">
|
||||||
<button class="btn btn-primary" bindtap="submitAppeal">提交申诉</button>
|
<button class="btn btn-primary" bindtap="submitAppeal">提交申诉</button>
|
||||||
<button class="btn btn-ghost" bindtap="cancelAppeal">取消</button>
|
<button class="btn btn-ghost" bindtap="cancelAppeal">取消</button>
|
||||||
|
|||||||
@@ -4,6 +4,11 @@ function getBaseURL() {
|
|||||||
return app.globalData.baseURL || 'http://127.0.0.1:5000/api'
|
return app.globalData.baseURL || 'http://127.0.0.1:5000/api'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getServerBase() {
|
||||||
|
const baseURL = getBaseURL()
|
||||||
|
return baseURL.replace('/api', '')
|
||||||
|
}
|
||||||
|
|
||||||
function getToken() {
|
function getToken() {
|
||||||
return app.globalData.token || wx.getStorageSync('token') || ''
|
return app.globalData.token || wx.getStorageSync('token') || ''
|
||||||
}
|
}
|
||||||
@@ -53,6 +58,36 @@ function request({ url, method = 'GET', data = {}, header = {} }) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
function uploadFile(filePath) {
|
||||||
request
|
return new Promise((resolve, reject) => {
|
||||||
|
const token = getToken()
|
||||||
|
wx.uploadFile({
|
||||||
|
url: `${getServerBase()}/api/upload/image`,
|
||||||
|
filePath,
|
||||||
|
name: 'file',
|
||||||
|
header: {
|
||||||
|
Authorization: token ? `Bearer ${token}` : ''
|
||||||
|
},
|
||||||
|
success: (res) => {
|
||||||
|
const data = JSON.parse(res.data || '{}')
|
||||||
|
if (data.code === 0) {
|
||||||
|
resolve(data.data)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const message = data.message || '上传失败'
|
||||||
|
wx.showToast({ title: message, icon: 'none' })
|
||||||
|
reject(new Error(message))
|
||||||
|
},
|
||||||
|
fail: (err) => {
|
||||||
|
const msg = (err && err.errMsg) || '上传失败'
|
||||||
|
wx.showToast({ title: msg, icon: 'none' })
|
||||||
|
reject(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
request,
|
||||||
|
uploadFile
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user