ReportList.vue 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680
  1. <template>
  2. <div class="report-page">
  3. <h2 class="page-title">举报管理</h2>
  4. <el-card shadow="never" class="toolbar-card">
  5. <el-space wrap>
  6. <el-select v-model="filters.status" placeholder="举报状态" clearable style="width: 140px" @change="loadList">
  7. <el-option label="全部" :value="undefined" />
  8. <el-option label="待处理" :value="0" />
  9. <el-option label="处理中" :value="1" />
  10. <el-option label="已处理" :value="2" />
  11. <el-option label="已驳回" :value="3" />
  12. </el-select>
  13. <el-input v-model="filters.dynamicId" placeholder="动态ID" clearable style="width: 160px" @keyup.enter.native="loadList" />
  14. <el-button icon="Search" type="primary" @click="loadList">查询</el-button>
  15. <el-button icon="Refresh" @click="resetAndReload">重置</el-button>
  16. </el-space>
  17. </el-card>
  18. <el-card shadow="never" class="table-card">
  19. <el-table v-loading="loading" :data="list" stripe>
  20. <el-table-column type="index" label="序号" width="60" />
  21. <el-table-column prop="reportId" label="举报ID" width="90" />
  22. <el-table-column prop="dynamicId" label="动态ID" width="90" />
  23. <el-table-column label="举报人" width="120">
  24. <template #default="{ row }">
  25. {{ row.reporterName || (`用户${row.reporterId ?? ''}`) }}
  26. </template>
  27. </el-table-column>
  28. <el-table-column label="被举报人" width="120">
  29. <template #default="{ row }">
  30. {{ row.reportedName || '-' }}
  31. </template>
  32. </el-table-column>
  33. <el-table-column label="类型" width="110">
  34. <template #default="{ row }">
  35. <el-tag size="small" :type="typeTagType(row.reportType)">{{ typeText(row.reportType) }}</el-tag>
  36. </template>
  37. </el-table-column>
  38. <el-table-column prop="description" label="描述" min-width="220" show-overflow-tooltip />
  39. <el-table-column label="截图" width="120">
  40. <template #default="{ row }">
  41. <template v-if="getScreenshotUrls(row).length">
  42. <el-image
  43. :src="getScreenshotUrls(row)[0]"
  44. :preview-src-list="getScreenshotUrls(row)"
  45. fit="cover"
  46. style="width:64px;height:64px;border-radius:6px;cursor:pointer;"
  47. :lazy="true"
  48. :hide-on-click-modal="true"
  49. :data-report-id="row.reportId"
  50. @error="(e) => handleImageError(e, row)"
  51. >
  52. <template #error>
  53. <div class="img-error" :title="`URL: ${getScreenshotUrls(row)[0]}`">
  54. <el-icon><Picture /></el-icon>
  55. <span>加载失败</span>
  56. </div>
  57. </template>
  58. </el-image>
  59. </template>
  60. <div v-else class="img-placeholder">无</div>
  61. </template>
  62. </el-table-column>
  63. <el-table-column prop="status" label="状态" width="100">
  64. <template #default="{ row }">
  65. <el-tag :type="statusType(row.status)" size="small">{{ statusText(row.status) }}</el-tag>
  66. </template>
  67. </el-table-column>
  68. <el-table-column prop="createdAt" label="举报时间" width="170" />
  69. <el-table-column label="操作" width="220" fixed="right">
  70. <template #default="{ row }">
  71. <el-button size="small" type="primary" link @click="openDynamic(row)">查看动态</el-button>
  72. <el-button size="small" type="warning" link @click="openHandle(row)">处理举报</el-button>
  73. <el-divider direction="vertical" />
  74. <el-button size="small" type="danger" link @click="openModerate(row, 3)">删除</el-button>
  75. <el-button v-if="row.status!==2" size="small" type="danger" link @click="openModerate(row, 2)">封禁</el-button>
  76. <el-button v-else size="small" type="success" link @click="openUnban(row)">取消封禁</el-button>
  77. </template>
  78. </el-table-column>
  79. </el-table>
  80. <div class="pagination-container">
  81. <el-pagination
  82. v-model:current-page="currentPage"
  83. v-model:page-size="pageSize"
  84. :total="total"
  85. layout="total, sizes, prev, pager, next"
  86. @size-change="loadList"
  87. @current-change="loadList"
  88. />
  89. </div>
  90. </el-card>
  91. <!-- 动态详情 -->
  92. <el-dialog v-model="dynamicDialog.visible" title="动态详情" width="800px">
  93. <div v-if="dynamicDialog.loading" class="loading">加载中...</div>
  94. <div v-else-if="dynamicDialog.data">
  95. <el-descriptions :column="2" border>
  96. <el-descriptions-item label="动态ID">{{ dynamicDialog.data.dynamicId || '-' }}</el-descriptions-item>
  97. <el-descriptions-item label="作者昵称">
  98. {{ dynamicDialog.data.userNickname || `用户${dynamicDialog.data.userId || ''}` }}
  99. </el-descriptions-item>
  100. <el-descriptions-item label="作者ID">{{ dynamicDialog.data.userId || '-' }}</el-descriptions-item>
  101. <el-descriptions-item label="发布时间">{{ dynamicDialog.data.createTime || '-' }}</el-descriptions-item>
  102. <el-descriptions-item label="点赞数">{{ dynamicDialog.data.likeCount || 0 }}</el-descriptions-item>
  103. <el-descriptions-item label="评论数">{{ dynamicDialog.data.commentCount || 0 }}</el-descriptions-item>
  104. <el-descriptions-item label="审核状态" :span="2">
  105. <el-tag :type="getAuditStatusType(dynamicDialog.data.auditStatus)" size="small">
  106. {{ getAuditStatusText(dynamicDialog.data.auditStatus) }}
  107. </el-tag>
  108. </el-descriptions-item>
  109. <el-descriptions-item label="动态内容" :span="2">
  110. <div class="dynamic-content-text">{{ dynamicDialog.data.content || '(无内容)' }}</div>
  111. </el-descriptions-item>
  112. </el-descriptions>
  113. <div v-if="dynamicDialog.medias && dynamicDialog.medias.length > 0" class="media-section">
  114. <div class="media-title">媒体文件({{ dynamicDialog.medias.length }}张)</div>
  115. <div class="media-list">
  116. <el-image
  117. v-for="(m, idx) in dynamicDialog.medias"
  118. :key="idx"
  119. :src="m"
  120. :preview-src-list="dynamicDialog.medias"
  121. fit="cover"
  122. style="width:96px;height:96px;border-radius:6px;margin-right:8px;margin-bottom:8px;cursor:pointer;"
  123. :lazy="true"
  124. @error="handleImageError"
  125. >
  126. <template #error>
  127. <div class="img-error" style="width:96px;height:96px;">
  128. <el-icon><Picture /></el-icon>
  129. <span>加载失败</span>
  130. </div>
  131. </template>
  132. </el-image>
  133. </div>
  134. </div>
  135. </div>
  136. <div v-else class="error-message">动态不存在或已被删除</div>
  137. </el-dialog>
  138. <!-- 处理举报 -->
  139. <el-dialog v-model="handleDialog.visible" title="处理举报" width="520px">
  140. <el-form :model="handleDialog.form" label-width="88px">
  141. <el-form-item label="举报ID">
  142. <span>{{ handleDialog.form.reportId }}</span>
  143. </el-form-item>
  144. <el-form-item label="当前状态">
  145. <el-tag :type="statusType(handleDialog.form.originStatus)">{{ statusText(handleDialog.form.originStatus) }}</el-tag>
  146. </el-form-item>
  147. <el-form-item label="处理结果" required>
  148. <el-select v-model="handleDialog.form.status" placeholder="请选择">
  149. <el-option :value="2" label="已处理" />
  150. <el-option :value="3" label="已驳回" />
  151. </el-select>
  152. </el-form-item>
  153. <el-form-item label="备注">
  154. <el-input v-model="handleDialog.form.handleResult" type="textarea" :rows="4" placeholder="填写处理说明" />
  155. </el-form-item>
  156. </el-form>
  157. <template #footer>
  158. <el-button @click="handleDialog.visible=false">取消</el-button>
  159. <el-button type="primary" :loading="handleDialog.loading" @click="submitHandle">确定</el-button>
  160. </template>
  161. </el-dialog>
  162. <!-- 管理操作(封禁/删除) -->
  163. <el-dialog v-model="moderateDialog.visible" :title="moderateDialog.action===2?'封禁动态':moderateDialog.action===3?'删除动态':'取消封禁'" width="720px">
  164. <div v-if="moderateDialog.loading" class="loading">加载中...</div>
  165. <div v-else>
  166. <el-descriptions :column="2" border style="margin-bottom: 20px;">
  167. <el-descriptions-item label="动态ID">{{ moderateDialog.form.dynamicId || '-' }}</el-descriptions-item>
  168. <el-descriptions-item label="作者昵称">
  169. {{ moderateDialog.form.userNickname || `用户${moderateDialog.form.userId || ''}` }}
  170. </el-descriptions-item>
  171. <el-descriptions-item label="作者ID">{{ moderateDialog.form.userId || '-' }}</el-descriptions-item>
  172. <el-descriptions-item label="发布时间">{{ moderateDialog.form.createTime || '-' }}</el-descriptions-item>
  173. <el-descriptions-item label="点赞数">{{ moderateDialog.form.likeCount || 0 }}</el-descriptions-item>
  174. <el-descriptions-item label="评论数">{{ moderateDialog.form.commentCount || 0 }}</el-descriptions-item>
  175. <el-descriptions-item label="审核状态" :span="2">
  176. <el-tag :type="getAuditStatusType(moderateDialog.form.auditStatus)" size="small">
  177. {{ getAuditStatusText(moderateDialog.form.auditStatus) }}
  178. </el-tag>
  179. </el-descriptions-item>
  180. <el-descriptions-item label="动态内容" :span="2">
  181. <div class="dynamic-content-text">{{ moderateDialog.form.content || '(无内容)' }}</div>
  182. </el-descriptions-item>
  183. </el-descriptions>
  184. <div v-if="moderateDialog.form.reportInfo" class="report-info-section">
  185. <el-divider content-position="left">相关举报信息</el-divider>
  186. <el-descriptions :column="1" border size="small">
  187. <el-descriptions-item label="举报类型">
  188. <el-tag size="small" :type="typeTagType(moderateDialog.form.reportInfo.reportType)">
  189. {{ typeText(moderateDialog.form.reportInfo.reportType) }}
  190. </el-tag>
  191. </el-descriptions-item>
  192. <el-descriptions-item label="举报描述">{{ moderateDialog.form.reportInfo.description || '-' }}</el-descriptions-item>
  193. <el-descriptions-item label="举报人">{{ moderateDialog.form.reportInfo.reporterName || `用户${moderateDialog.form.reportInfo.reporterId || ''}` }}</el-descriptions-item>
  194. <el-descriptions-item label="举报时间">{{ moderateDialog.form.reportInfo.createdAt || '-' }}</el-descriptions-item>
  195. </el-descriptions>
  196. </div>
  197. <el-form :model="moderateDialog.form" label-width="100px" style="margin-top: 20px;">
  198. <el-form-item label="处理原因" required>
  199. <el-input v-model="moderateDialog.form.reason" type="textarea" :rows="4" placeholder="请填写具体原因" />
  200. </el-form-item>
  201. </el-form>
  202. </div>
  203. <template #footer>
  204. <el-button @click="moderateDialog.visible=false">取消</el-button>
  205. <el-button type="primary" :loading="moderateDialog.loading" @click="submitModerate" :disabled="!moderateDialog.form.reason?.trim()">确定</el-button>
  206. </template>
  207. </el-dialog>
  208. </div>
  209. </template>
  210. <script setup>
  211. import { ref, reactive, onMounted } from 'vue'
  212. import { ElMessage } from 'element-plus'
  213. import { Picture } from '@element-plus/icons-vue'
  214. import request from '@/utils/request'
  215. import { API_ENDPOINTS, API_BASE_URL } from '@/config/api'
  216. const loading = ref(false)
  217. const currentPage = ref(1)
  218. const pageSize = ref(10)
  219. const total = ref(0)
  220. const list = ref([])
  221. const filters = reactive({ status: undefined, dynamicId: '' })
  222. const resetAndReload = () => {
  223. filters.status = undefined
  224. filters.dynamicId = ''
  225. loadList()
  226. }
  227. const statusText = (s) => ({0:'待处理',1:'处理中',2:'已处理',3:'已驳回'}[s] || '未知')
  228. const statusType = (s) => ({0:'danger',1:'warning',2:'success',3:'info'}[s] || '')
  229. const typeText = (t) => ({ spam:'垃圾广告', porn:'色情低俗', violence:'暴力违法', attack:'人身攻击', fake:'虚假信息', plagiarism:'抄袭侵权', other:'其他' }[t] || t || '其他')
  230. const typeTagType = (t) => ({ spam:'warning', porn:'danger', violence:'danger', attack:'danger', fake:'warning', plagiarism:'warning', other:'info' }[t] || 'info')
  231. const getAuditStatusText = (status) => {
  232. const texts = { 0: '待审核', 1: '已通过', 2: '未通过' }
  233. return texts[status] || '未知'
  234. }
  235. const getAuditStatusType = (status) => {
  236. const types = { 0: 'warning', 1: 'success', 2: 'danger' }
  237. return types[status] || ''
  238. }
  239. // 获取基础URL(用于图片资源)
  240. const getBaseUrl = () => {
  241. // 开发环境:图片资源通过8083端口访问
  242. if (import.meta.env.DEV) {
  243. return 'http://localhost:8083'
  244. }
  245. // 生产环境:使用配置的API基础URL
  246. return API_BASE_URL || ''
  247. }
  248. // 格式化媒体URL(与DynamicDetail.vue保持一致)
  249. const formatMedia = (u) => {
  250. if (!u) return ''
  251. let url = String(u).trim()
  252. if (!url) return ''
  253. // 去除引号
  254. if ((url.startsWith('"') && url.endsWith('"')) || (url.startsWith("'") && url.endsWith("'"))) {
  255. url = url.substring(1, url.length - 1)
  256. }
  257. // 如果已经是完整的URL,直接返回
  258. if (url.startsWith('http')) {
  259. return url
  260. }
  261. // 获取BASE URL并拼接
  262. const baseUrl = getBaseUrl()
  263. // 确保url以/开头
  264. if (!url.startsWith('/')) {
  265. url = '/' + url
  266. }
  267. return `${baseUrl}${url}`
  268. }
  269. // 从后端临时塞入的 handleResult 前缀里解析昵称(@nick:李娜 ...)
  270. const extractNick = (row) => {
  271. const r = row?.handleResult || ''
  272. const m = /^@nick:([^\s]+)\s?/.exec(r)
  273. if (m && m[1]) return m[1]
  274. return `用户${row?.reporterId ?? ''}`
  275. }
  276. // 解析截图数据(与DynamicDetail.vue保持一致)
  277. const parseScreens = (raw) => {
  278. if (!raw) return []
  279. let urls = []
  280. const trimmed = String(raw).trim()
  281. // 尝试解析JSON数组格式
  282. try {
  283. if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
  284. const arr = JSON.parse(trimmed)
  285. if (Array.isArray(arr)) {
  286. urls = arr
  287. }
  288. }
  289. } catch (e) {
  290. // JSON解析失败,忽略错误,继续尝试其他格式
  291. }
  292. // 如果不是JSON数组,尝试按逗号分隔
  293. if (!urls.length) {
  294. urls = trimmed.split(',').map((item) => item.trim()).filter(Boolean)
  295. }
  296. return urls
  297. }
  298. // 获取格式化后的截图URL列表
  299. const getScreenshotUrls = (row) => {
  300. if (!row?.screenshots) {
  301. return []
  302. }
  303. const screens = parseScreens(row.screenshots)
  304. if (!screens || screens.length === 0) {
  305. return []
  306. }
  307. const urls = screens.map(formatMedia).filter(Boolean)
  308. // 详细调试日志
  309. if (urls.length > 0) {
  310. console.log('📸 举报ID:', row.reportId, {
  311. '原始数据': row.screenshots,
  312. '解析后数组': screens,
  313. '格式化后URLs': urls,
  314. 'BASE URL': getBaseUrl()
  315. })
  316. } else {
  317. console.warn('⚠️ 举报ID:', row.reportId, '解析后无有效URL,原始数据:', row.screenshots)
  318. }
  319. return urls
  320. }
  321. // 图片加载错误处理
  322. const handleImageError = (e, row) => {
  323. const img = e.target || e
  324. const src = img?.src || img?.currentSrc || 'unknown'
  325. const reportId = row?.reportId || img?.dataset?.reportId || 'unknown'
  326. const urls = row ? getScreenshotUrls(row) : []
  327. console.error('❌ 图片加载失败:', {
  328. reportId,
  329. src,
  330. '原始screenshots': row?.screenshots,
  331. '解析后URLs': urls,
  332. 'BASE URL': getBaseUrl(),
  333. error: e
  334. })
  335. // 检查URL格式
  336. if (src && !src.startsWith('http')) {
  337. console.warn('⚠️ 图片URL格式可能不正确:', src)
  338. }
  339. // 检查URL是否可访问
  340. if (src && src.startsWith('http')) {
  341. console.warn('⚠️ 图片URL格式正确但加载失败,可能是文件不存在或CORS问题:', src)
  342. }
  343. }
  344. const loadList = async () => {
  345. loading.value = true
  346. try {
  347. const res = await request.get(API_ENDPOINTS.REPORT_LIST, {
  348. params: { page: currentPage.value, pageSize: pageSize.value, status: filters.status, dynamicId: filters.dynamicId || undefined }
  349. })
  350. if (res.code === 200) {
  351. list.value = res.data.list || []
  352. total.value = res.data.total || list.value.length
  353. // 调试日志:检查截图数据
  354. console.log('📋 举报列表加载成功,共', list.value.length, '条')
  355. list.value.forEach((row, idx) => {
  356. if (row.screenshots) {
  357. const urls = getScreenshotUrls(row)
  358. console.log(` [${idx + 1}] 举报ID: ${row.reportId}, 原始screenshots:`, row.screenshots, '解析后URLs:', urls)
  359. }
  360. })
  361. } else {
  362. console.error('加载举报列表失败:', res.msg || '未知错误')
  363. }
  364. } catch (e) {
  365. console.error('加载举报失败', e)
  366. } finally { loading.value = false }
  367. }
  368. // 动态详情
  369. const dynamicDialog = reactive({ visible: false, loading: false, data: null, medias: [] })
  370. const parseMediaUrls = (raw) => {
  371. if (!raw) return []
  372. try {
  373. const value = typeof raw === 'string' ? raw.trim() : raw
  374. if (typeof value === 'string' && value.startsWith('[')) {
  375. return JSON.parse(value)
  376. }
  377. if (typeof value === 'string') {
  378. return value.split(',').map((s) => s.trim().replace(/^['"]|['"]$/g, '')).filter(Boolean)
  379. }
  380. return Array.isArray(value) ? value : []
  381. } catch {
  382. return []
  383. }
  384. }
  385. const openDynamic = async (row) => {
  386. const reportId = row?.reportId
  387. if (!reportId) {
  388. dynamicDialog.data = { content: '未获取到举报ID' }
  389. dynamicDialog.medias = getScreenshotUrls(row)
  390. dynamicDialog.visible = true
  391. return
  392. }
  393. dynamicDialog.visible = true
  394. dynamicDialog.loading = true
  395. dynamicDialog.data = null
  396. dynamicDialog.medias = []
  397. try {
  398. // 通过 reportId 获取举报详情(包含动态详情)
  399. const reportRes = await request.get(`${API_ENDPOINTS.REPORT_DETAIL}/${reportId}`)
  400. if (reportRes.code !== 200 || !reportRes.data) {
  401. dynamicDialog.data = { content: reportRes.msg || '举报不存在' }
  402. dynamicDialog.medias = getScreenshotUrls(row)
  403. dynamicDialog.loading = false
  404. return
  405. }
  406. const report = reportRes.data
  407. const dynamic = report.dynamic
  408. if (!dynamic) {
  409. dynamicDialog.data = { content: '动态不存在或已被删除' }
  410. dynamicDialog.medias = getScreenshotUrls(row)
  411. dynamicDialog.loading = false
  412. return
  413. }
  414. // 直接使用举报详情接口返回的动态信息
  415. dynamicDialog.data = dynamic
  416. // 使用 screenshots 字段作为展示图片
  417. const screens = parseScreens(dynamic?.screenshots)
  418. dynamicDialog.medias = screens.length ? screens.map(formatMedia) : getScreenshotUrls(row)
  419. } catch (e) {
  420. console.error('获取动态详情失败', e)
  421. dynamicDialog.data = { content: e?.message ? `加载失败:${e.message}` : '加载失败' }
  422. dynamicDialog.medias = getScreenshotUrls(row)
  423. } finally {
  424. dynamicDialog.loading = false
  425. }
  426. }
  427. // 处理举报
  428. const handleDialog = reactive({ visible: false, loading: false, form: { reportId: null, originStatus: 0, status: 2, handleResult: '' } })
  429. const openHandle = (row) => {
  430. handleDialog.visible = true
  431. handleDialog.form.reportId = row.reportId
  432. handleDialog.form.originStatus = row.status
  433. handleDialog.form.status = 2
  434. handleDialog.form.handleResult = ''
  435. }
  436. const submitHandle = async () => {
  437. if (!handleDialog.form.reportId) return
  438. handleDialog.loading = true
  439. try {
  440. const res = await request.post(`${API_ENDPOINTS.REPORT_HANDLE}/${handleDialog.form.reportId}`, {
  441. status: handleDialog.form.status,
  442. handleResult: handleDialog.form.handleResult
  443. })
  444. if (res.code === 200) {
  445. ElMessage.success('处理成功')
  446. handleDialog.visible = false
  447. loadList()
  448. }
  449. } catch (e) { console.error('处理失败', e) }
  450. finally { handleDialog.loading = false }
  451. }
  452. onMounted(loadList)
  453. // 管理操作:封禁/删除
  454. const moderateDialog = reactive({
  455. visible: false,
  456. loading: false,
  457. action: 2,
  458. form: {
  459. dynamicId: null,
  460. userId: null,
  461. userNickname: null,
  462. content: null,
  463. createTime: null,
  464. likeCount: 0,
  465. commentCount: 0,
  466. auditStatus: null,
  467. reportInfo: null,
  468. reason: ''
  469. }
  470. })
  471. const openModerate = async (row, action) => {
  472. moderateDialog.visible = true
  473. moderateDialog.action = action
  474. moderateDialog.loading = true
  475. moderateDialog.form.reason = ''
  476. // 重置其他字段
  477. moderateDialog.form.dynamicId = null
  478. moderateDialog.form.userId = null
  479. moderateDialog.form.userNickname = null
  480. moderateDialog.form.content = null
  481. moderateDialog.form.createTime = null
  482. moderateDialog.form.likeCount = 0
  483. moderateDialog.form.commentCount = 0
  484. moderateDialog.form.auditStatus = null
  485. moderateDialog.form.reportInfo = null
  486. const reportId = row?.reportId
  487. if (!reportId) {
  488. moderateDialog.loading = false
  489. ElMessage.error('未获取到举报ID')
  490. return
  491. }
  492. try {
  493. // 先通过 reportId 获取举报详情
  494. const reportRes = await request.get(`${API_ENDPOINTS.REPORT_DETAIL}/${reportId}`)
  495. if (reportRes.code !== 200 || !reportRes.data) {
  496. ElMessage.error(reportRes.msg || '举报不存在')
  497. moderateDialog.loading = false
  498. return
  499. }
  500. const report = reportRes.data
  501. const dynamic = report.dynamic
  502. // 加载举报信息
  503. moderateDialog.form.reportInfo = {
  504. reportId: report.reportId,
  505. reportType: report.reportType,
  506. description: report.description,
  507. reporterId: report.reporterId,
  508. reporterName: report.reporterName || row.reporterName,
  509. createdAt: report.createdAt || row.createdAt
  510. }
  511. // 加载动态详情(从举报详情接口返回的 dynamic 字段中获取)
  512. if (dynamic) {
  513. moderateDialog.form.dynamicId = dynamic.dynamicId
  514. moderateDialog.form.userId = dynamic.userId
  515. moderateDialog.form.userNickname = dynamic.userNickname
  516. moderateDialog.form.content = dynamic.content
  517. moderateDialog.form.createTime = dynamic.createTime
  518. moderateDialog.form.likeCount = dynamic.likeCount || 0
  519. moderateDialog.form.commentCount = dynamic.commentCount || 0
  520. moderateDialog.form.auditStatus = dynamic.auditStatus
  521. }
  522. } catch (e) {
  523. console.error('加载举报详情失败', e)
  524. ElMessage.error('加载举报详情失败')
  525. } finally {
  526. moderateDialog.loading = false
  527. }
  528. }
  529. const submitModerate = async () => {
  530. if (!moderateDialog.form.reason.trim()) return ElMessage.error('请填写处理原因')
  531. moderateDialog.loading = true
  532. try {
  533. const res = await request.post('/api/dynamic/admin/moderate', null, {
  534. params: { dynamicId: moderateDialog.form.dynamicId, action: moderateDialog.action, reason: moderateDialog.form.reason }
  535. })
  536. if (res.code === 200) {
  537. ElMessage.success('操作成功')
  538. moderateDialog.visible = false
  539. loadList()
  540. }
  541. } catch (e) { console.error('操作失败', e) }
  542. finally { moderateDialog.loading = false }
  543. }
  544. const openUnban = async (row) => {
  545. moderateDialog.visible = true
  546. moderateDialog.action = 1
  547. moderateDialog.loading = true
  548. moderateDialog.form.dynamicId = row.dynamicId
  549. moderateDialog.form.reason = ''
  550. // 重置其他字段
  551. moderateDialog.form.userId = null
  552. moderateDialog.form.userNickname = null
  553. moderateDialog.form.content = null
  554. moderateDialog.form.createTime = null
  555. moderateDialog.form.likeCount = 0
  556. moderateDialog.form.commentCount = 0
  557. moderateDialog.form.auditStatus = null
  558. moderateDialog.form.reportInfo = null
  559. // 加载动态详情
  560. if (row.dynamicId) {
  561. try {
  562. const res = await request.get(`${API_ENDPOINTS.DYNAMIC_DETAIL}/${row.dynamicId}`)
  563. if (res.code === 200 && res.data) {
  564. moderateDialog.form.userId = res.data.userId
  565. moderateDialog.form.userNickname = res.data.userNickname
  566. moderateDialog.form.content = res.data.content
  567. moderateDialog.form.createTime = res.data.createTime
  568. moderateDialog.form.likeCount = res.data.likeCount || 0
  569. moderateDialog.form.commentCount = res.data.commentCount || 0
  570. moderateDialog.form.auditStatus = res.data.auditStatus
  571. }
  572. } catch (e) {
  573. console.error('加载动态详情失败', e)
  574. }
  575. }
  576. moderateDialog.loading = false
  577. }
  578. </script>
  579. <style scoped>
  580. .report-page { padding: 0; }
  581. .page-title { font-size: 24px; font-weight: bold; color: #333; margin: 0 0 20px 0; }
  582. .toolbar-card { margin-bottom: 20px; }
  583. .table-card { margin-top: 20px; }
  584. .pagination-container { display: flex; justify-content: flex-end; margin-top: 20px; }
  585. .img-placeholder { width:64px;height:64px;border-radius:6px;background:#f5f7fa;color:#909399;display:flex;align-items:center;justify-content:center;font-size:12px; }
  586. .img-error { width:64px;height:64px;border-radius:6px;background:#fef0f0;color:#f56c6c;display:flex;flex-direction:column;align-items:center;justify-content:center;font-size:10px; }
  587. .img-error .el-icon { font-size:20px;margin-bottom:2px; }
  588. .dynamic-content { margin-bottom: 10px; color:#333; }
  589. .dynamic-content-text {
  590. max-height: 200px;
  591. overflow-y: auto;
  592. padding: 8px;
  593. background: #f5f7fa;
  594. border-radius: 4px;
  595. line-height: 1.6;
  596. word-break: break-word;
  597. white-space: pre-wrap;
  598. }
  599. .media-section { margin-top: 20px; }
  600. .media-title {
  601. font-size: 14px;
  602. font-weight: 600;
  603. color: #606266;
  604. margin-bottom: 12px;
  605. }
  606. .media-list { display:flex; flex-wrap: wrap; }
  607. .loading {
  608. color:#909399;
  609. text-align: center;
  610. padding: 40px 0;
  611. }
  612. .error-message {
  613. color: #f56c6c;
  614. text-align: center;
  615. padding: 40px 0;
  616. }
  617. .report-info-section {
  618. margin-top: 20px;
  619. padding: 16px;
  620. background: #f9fafc;
  621. border-radius: 4px;
  622. }
  623. </style>