7.2 KiB
7.2 KiB
Plan C: 客户端 uni-app 集成
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task.
父计划: docs/superpowers/plans/2026-06-17-moderation-report-feedback-system.md
Goal: 在 uni-app 客户端集成举报与反馈入口,使用户可对藏品/用户/描述词举报,提交反馈,查看自己的工单
Architecture:
- 8 个 API 方法追加到
frontend/utils/api.js(不新建文件) - 通用
ReportModal.vue弹窗(uView 2.x) - 我的页面菜单追加"我的举报"/"我的反馈"
- 复用 evidence 上传(
type=report/type=feedback,spec §9.1)
Tech Stack: uni-app + uView 2.x + Vue3
仓库: /Users/liulujian/Documents/code/TopFansByGithub/frontend
前置: Plan A 阶段 A.9 Gateway 路由已注册
阶段 C.1:API 方法追加
Task C.1.1: 在 api.js 末尾追加 8 个方法
Files:
- Modify:
frontend/utils/api.js
// 在文件末尾追加(不覆盖现有内容):
export function getReportCategoriesApi() {
return request.get('/api/v1/moderation/report-categories')
}
export function submitReportApi(payload) {
return request.post('/api/v1/moderation/reports', payload)
}
export function getMyReportsApi(params) {
return request.get('/api/v1/moderation/reports', { params })
}
export function getMyReportDetailApi(id) {
return request.get(`/api/v1/moderation/reports/${id}`)
}
export function getFeedbackCategoriesApi() {
return request.get('/api/v1/moderation/feedback-categories')
}
export function submitFeedbackApi(payload) {
return request.post('/api/v1/moderation/feedbacks', payload)
}
export function getMyFeedbacksApi(params) {
return request.get('/api/v1/moderation/feedbacks', { params })
}
export function getMyFeedbackDetailApi(id) {
return request.get(`/api/v1/moderation/feedbacks/${id}`)
}
阶段 C.2:ReportModal 通用举报弹窗
Task C.2.1: 创建组件
Files:
- Create:
frontend/components/ReportModal.vue
关键实现(uView 2.x):
<template>
<u-popup v-model="show" mode="bottom" border-radius="20" :mask-close-able="false">
<view class="report-modal">
<view class="header">
<text class="title">举报{{ targetTypeLabel }}</text>
<u-icon name="close" @click="show = false" />
</view>
<u-form :model="form" ref="formRef" label-width="80">
<u-form-item label="举报分类" prop="category_code">
<u-radio-group v-model="form.category_code" placement="column">
<u-radio v-for="cat in categories" :key="cat.code" :name="cat.code">
{{ cat.name }}
</u-radio>
</u-radio-group>
</u-form-item>
<u-form-item label="详细描述" prop="description">
<u-textarea v-model="form.description" :maxlength="500" :count="true" placeholder="请描述违规情况" />
</u-form-item>
<u-form-item label="证据图">
<u-upload :file-list="form.evidence_urls" @afterRead="afterRead" @delete="deletePic" :max-count="5" />
</u-form-item>
<u-form-item label="匿名举报">
<u-switch v-model="form.is_anonymous" />
</u-form-item>
<u-button type="primary" @click="onSubmit" :loading="submitting">提交举报</u-button>
</u-form>
</view>
</u-popup>
</template>
<script setup>
import { ref, watch, onMounted } from 'vue'
import { getReportCategoriesApi, submitReportApi } from '@/utils/api'
const props = defineProps({
modelValue: Boolean,
targetType: { type: String, required: true, validator: v => ['asset', 'user_profile'].includes(v) },
targetId: { type: Number, required: true },
targetName: { type: String, default: '' },
})
const emit = defineEmits(['update:modelValue', 'success'])
const show = ref(props.modelValue)
watch(() => props.modelValue, v => { show.value = v })
watch(show, v => { if (v !== props.modelValue) emit('update:modelValue', v) })
const categories = ref([])
const formRef = ref()
const submitting = ref(false)
const form = ref({
category_code: '',
description: '',
evidence_keys: [],
evidence_urls: [],
is_anonymous: false,
})
const targetTypeLabel = computed(() => props.targetType === 'asset' ? '藏品' : '用户')
onMounted(async () => {
const resp = await getReportCategoriesApi()
categories.value = resp.data || []
})
const afterRead = async (file) => {
// 1. 调 OSS 签名(type=report)
const sig = await getOSSUploadSignatureApi('report')
// 2. 直传 OSS
await uploadToOSS(sig, file)
// 3. 收集 key
form.value.evidence_keys.push(sig.dir + file.name)
form.value.evidence_urls.push({ url: sig.host + '/' + sig.dir + file.name })
}
const deletePic = (index) => {
form.value.evidence_urls.splice(index, 1)
form.value.evidence_keys.splice(index, 1)
}
const onSubmit = async () => {
if (!form.value.category_code) {
uni.showToast({ title: '请选择举报分类', icon: 'none' })
return
}
submitting.value = true
try {
const resp = await submitReportApi({
target_type: props.targetType,
target_id: props.targetId,
category_code: form.value.category_code,
description: form.value.description,
is_anonymous: form.value.is_anonymous,
evidence_keys: form.value.evidence_keys,
})
// 双分支 toast(spec §阶段 5.2)
if (resp.data.target_hidden) {
uni.showToast({ title: '已自动隐藏,等待审核', icon: 'none' })
} else {
uni.showToast({ title: '举报已提交', icon: 'success' })
}
emit('success', resp.data)
show.value = false
} catch (e) {
// 错误码 50004/50011/50012 等
uni.showToast({ title: e.message || '提交失败', icon: 'none' })
} finally {
submitting.value = false
}
}
</script>
阶段 C.3:接入到各入口
Task C.3.1: 藏品详情页
Files:
- Modify:
frontend/components/NftDetailModal.vue(或具体藏品详情组件)
在 "..." 菜单中加"举报"项,点击弹出 ReportModal。
Task C.3.2: 用户主页
Files:
- Modify:
frontend/pages/user/userProfile.vue
粉丝身份下的 "..." 菜单加"举报用户"。
Task C.3.3: 描述词展示组件
(如有)在 "..." 菜单加"举报"项。
阶段 C.4:意见反馈页
Task C.4.1: 创建意见反馈页
Files:
- Create:
frontend/pages/profile/feedback.vue
表单:分类下拉 + 标题 + 内容 + 联系方式 + 截图(最多 5 张)+ 匿名开关 + 提交。
Task C.4.2: 我的举报列表
Files:
- Create:
frontend/pages/profile/myReports.vue
调用 getMyReportsApi 渲染列表,点击详情跳转 myReportDetail。
Task C.4.3: 我的反馈列表
Files:
- Create:
frontend/pages/profile/myFeedbacks.vue
同模式。
Task C.4.4: 我的页面菜单追加
Files:
- Modify:
frontend/pages/profile/profile.vue
加"我的举报"和"我的反馈"菜单项。
自审检查清单(Plan C)
- 8 个 API 方法追加完整
- ReportModal 双分支 toast 正确(target_hidden true/false)
- 错误码处理(50004/50011/50012 等)
- evidence 上传用
type=report/type=feedback - uView 2.x UI 组件一致性
- 我的页面菜单项接入