# 前端任务: 「人工复审队列」加一个 tab 显示评价举报

@mmg —— **此文件已是第 3 版,前两版你说看不懂,这版尽量直白。如果还有不清楚的当面问我。**

---

## 一句话需求

「内容安全 → 人工复审队列」页面 **顶部加一个 tab 叫"评价举报"**,切到这个 tab 就调下面的接口,数据展示出来,操作按钮按下面的规则做。**只改这一个页面,不要新建别的菜单。**

---

## 改之前 vs 改之后

### 现在的页面

```
┌─────────────────────────────────────────┐
│ 人工复审队列                             │
├─────────────────────────────────────────┤
│ [出行人姓名] [用户昵称] [用户头像]       │  ← 这是当前已有的 tab
├─────────────────────────────────────────┤
│ 列表(调 /admin/wx-security/manual-review/page) │
└─────────────────────────────────────────┘
```

### 改成这样

```
┌─────────────────────────────────────────────────────┐
│ 人工复审队列                                         │
├─────────────────────────────────────────────────────┤
│ [出行人姓名] [用户昵称] [用户头像] [评价举报]        │  ← 加一个新 tab
├─────────────────────────────────────────────────────┤
│ 切到"评价举报"时:                                    │
│  - 列表调 /admin/review/report-queue/page           │
│  - 操作按钮换成 隐藏/恢复/警告作者/忽略             │
└─────────────────────────────────────────────────────┘
```

前 3 个 tab 你不用动,保持现状。**只加第 4 个 tab + 这个 tab 内的逻辑。**

---

## tab=评价举报 时调什么接口

### 1. 加载列表

```js
// GET 请求
axios.get('/admin/review/report-queue/page', {
  params: {
    page: 1,
    pageSize: 20,
    reportStatus: 'PENDING_REVIEW',  // 可选: NORMAL / PENDING_REVIEW / HIDDEN
    keyword: '违规'                   // 可选: 模糊搜评价内容/作者昵称
  }
})
```

### 响应实例(测试服真数据,reviewId=2048001001001001005 的那条举报)

```json
{
  "code": 200,
  "data": {
    "total": 1,
    "page": 1,
    "pageSize": 20,
    "records": [
      {
        "reviewId": "2048001001001001005",
        "content": "出发前一直犹豫值不值得去……(评价正文)",
        "userNickname": "罗子昂",
        "userId": "2031234567890123456",
        "productName": "粉色风蚀古堡",
        "status": "PUBLISHED",
        "reportCount": 1,
        "reportStatus": "NORMAL",
        "hiddenAt": null,
        "hiddenReason": null,
        "createTime": "2026-05-05 14:23:11",
        "reasonStats": { "SPAM": 1 },
        "reporters": [
          {
            "reportId": "2052007674911895553",
            "reporterUserId": "2031999888777666555",
            "reporterOpenid": "oK****n8",
            "reason": "SPAM",
            "detail": null,
            "status": "PENDING",
            "createTime": "2026-05-06 20:25:42"
          }
        ]
      }
    ]
  }
}
```

**注意**: `reviewId / userId / reporterUserId / reportId` 都是字符串(后端用 `@JsonFormat(shape=STRING)` 防 JS 精度丢失),前端**不要 parseInt**,直接当字符串用。

---

### 2. 表格列建议

| 列 | 字段 | 说明 |
|---|---|---|
| 评价内容 | `content` | 超过 100 字省略号,鼠标 hover tooltip 看全文 |
| 作者 | `userNickname` | 链接到用户详情(可选,用 `userId`) |
| 关联产品 | `productName` | |
| 举报数 | `reportCount` | |
| 状态 | `reportStatus` | 见下面"状态枚举"中文化 |
| 举报原因 | `reasonStats` | 例如 `广告 ×3 / 辱骂 ×1`,通过字典翻译(下面有字典接口) |
| 隐藏时间 | `hiddenAt` | `reportStatus=HIDDEN` 时显示,否则空 |
| 创建时间 | `createTime` | 评价发布时间 |
| 操作 | (按钮) | 见下面"操作按钮规则" |

### 状态枚举(中文化)

| `reportStatus` | 中文 | tag 颜色建议 |
|---|---|---|
| `NORMAL` | 正常 | 灰 / 默认 |
| `PENDING_REVIEW` | 待复审 | 橙 / warning |
| `HIDDEN` | 已隐藏 | 红 / error |

---

### 3. 操作按钮规则

按当前行的 `reportStatus` 显示不同按钮:

| `reportStatus` | 显示这些按钮 |
|---|---|
| `NORMAL` | `警告作者` `忽略全部` |
| `PENDING_REVIEW` | **`隐藏评价`(主)** `警告作者` `忽略全部` |
| `HIDDEN` | **`恢复评价`(主)** |

#### 各按钮对应的接口

```js
// 隐藏评价(弹个框让 admin 输 reason)
axios.post(`/admin/review/report-queue/${reviewId}/hide`, {
  reason: '违规广告'   // 必填
})

// 恢复评价
axios.post(`/admin/review/report-queue/${reviewId}/restore`)

// 警告作者(后端目前只落日志,以后接站内信 SDK 再说)
axios.post(`/admin/review/report-queue/${reviewId}/warn-author`)

// 忽略全部 = 把当前行 reporters[] 数组里每个 reportId 循环调忽略接口
for (const r of row.reporters) {
  await axios.post(`/admin/review/report-queue/report/${r.reportId}/ignore`)
}
```

**所有操作完成后请刷新当前页列表。**

---

### 4. 展开行(可选,推荐有)

点击行旁边的展开图标,展开看 `reporters[]` 数组,每个举报人一行:

| 举报人 | 原因 | 详情 | 状态 | 时间 | 操作 |
|---|---|---|---|---|---|
| `oK****n8` (脱敏 openid) | 垃圾营销 | (用户填的 detail,可能为空) | `PENDING` | createTime | `[忽略本条]` |

**`[忽略本条]` 按钮**:

```js
axios.post(`/admin/review/report-queue/report/${reportId}/ignore`)
```

---

### 5. 字典翻译(reasonStats 里的 key)

`reasonStats` 的 key 是英文 value(SPAM/INSULT/...),展示时翻译成中文 label:

```js
// 拿字典(整个项目通用接口)
axios.get('/admin/system/dict-data/by-type', {
  params: { dictType: 'review_report_reason' }
})
```

返回(测试服实测):

```json
{
  "code": 200,
  "data": [
    { "value": "SPAM",         "label": "广告/营销" },
    { "value": "INSULT",       "label": "辱骂/人身攻击" },
    { "value": "PORN",         "label": "色情低俗" },
    { "value": "POLITICS",     "label": "政治敏感" },
    { "value": "FAKE",         "label": "虚假评价" },
    { "value": "LEAK_PRIVACY", "label": "泄露他人隐私" },
    { "value": "OTHER",        "label": "其他" }
  ]
}
```

**不要硬编码这个映射**, 进页面拉一次缓存到 vuex/pinia 就行。

---

### 6. 顶部筛选区(REVIEW tab 时)

页面顶部本来已经有"建议 / 场景 / 关键词"3 个筛选,在 REVIEW tab 时:

| 原字段 | 改成 |
|---|---|
| 建议(下拉) | 改成 `reportStatus` 下拉(全部 / 正常 / 待复审 / 已隐藏),传 `reportStatus` 参数 |
| 场景 | **隐藏**(评价举报无 scene) |
| 关键词 | 保留,传 `keyword` 参数 |

切回前 3 个 tab 时恢复原本的筛选字段。

---

## 你不用关心的(后端已搞定的)

- 用户从小程序点举报(`POST /mp/review/report`)的数据已经 100% 落到后端 DB,实测 `reviewId=2048001001001001005` 已经有 1 条 SPAM 举报数据,你接入后能直接看到。
- 网关路由、JWT 认证、字典初始化 全部就绪。
- 排序后端默认 `reportCount DESC, createTime DESC`,**前端不要 sort 重排**。
- `reporterOpenid` 已经脱敏(`oK****n8`),**不要再 mask**。

---

## 验收清单(全部对了再说做完)

- [ ] 「人工复审队列」页面顶部 4 个 tab,默认还是"出行人姓名"
- [ ] 点"评价举报" tab,列表能加载,看到 reviewId=2048001001001001005 的那条数据
- [ ] 评价内容/作者昵称/产品名 都是中文显示
- [ ] `reasonStats` 的 SPAM 翻译成 "广告/营销"
- [ ] 状态显示"正常"(因为目前 reportCount=1 < 3)
- [ ] 操作按钮显示"警告作者 / 忽略全部"(NORMAL 状态)
- [ ] 展开行能看到 1 条 reporters,openid `oK****n8`,原因 "广告/营销"
- [ ] 切回"出行人姓名" tab,原列表还能正常加载(没把老 tab 弄坏)

---

## 工作量

预计 1.5 天。

后端 0 改动。**所有接口都已部署到测试服,你可以直接联调。**

---

## 联调环境

- 测试服域名: `https://api.test.1814.love:9443`
- admin 账号: `admin / Admin@123456`
- 已有测试数据: `reviewId=2048001001001001005`(粉色风蚀古堡产品的一条评价,被 SPAM 举报 1 次)
- 想再造数据: 用小程序登录,POST `/mp/review/report` body `{"reviewId":"...","reason":"INSULT"}`

---

## 历史 changelog 注意

之前的 2 版让你"新建独立菜单 评价举报处理",**那个方向作废**。最终方向就是这版: **现有页面加 tab**。
