topfans/docs/superpowers/plans/2026-06-17-plan-c-uniapp-client.md
2026-06-22 17:19:48 +08:00

7.2 KiB
Raw Blame History

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=feedbackspec §9.1

Tech Stack: uni-app + uView 2.x + Vue3

仓库: /Users/liulujian/Documents/code/TopFansByGithub/frontend

前置: Plan A 阶段 A.9 Gateway 路由已注册


阶段 C.1API 方法追加

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.2ReportModal 通用举报弹窗

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,
    })
    // 双分支 toastspec §阶段 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 组件一致性
  • 我的页面菜单项接入