245 lines
7.2 KiB
Markdown
245 lines
7.2 KiB
Markdown
# 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`
|
||
|
||
```javascript
|
||
// 在文件末尾追加(不覆盖现有内容):
|
||
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):
|
||
```vue
|
||
<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 组件一致性
|
||
- [ ] 我的页面菜单项接入
|