|
|
@@ -40,7 +40,25 @@
|
|
|
<el-table-column prop="description" label="描述" min-width="220" show-overflow-tooltip />
|
|
|
<el-table-column label="截图" width="120">
|
|
|
<template #default="{ row }">
|
|
|
- <el-image v-if="parseScreens(row.screenshots).length" :src="formatMedia(parseScreens(row.screenshots)[0])" :preview-src-list="parseScreens(row.screenshots).map(formatMedia)" fit="cover" style="width:64px;height:64px;border-radius:6px;" />
|
|
|
+ <template v-if="getScreenshotUrls(row).length">
|
|
|
+ <el-image
|
|
|
+ :src="getScreenshotUrls(row)[0]"
|
|
|
+ :preview-src-list="getScreenshotUrls(row)"
|
|
|
+ fit="cover"
|
|
|
+ style="width:64px;height:64px;border-radius:6px;cursor:pointer;"
|
|
|
+ :lazy="true"
|
|
|
+ :hide-on-click-modal="true"
|
|
|
+ :data-report-id="row.reportId"
|
|
|
+ @error="(e) => handleImageError(e, row)"
|
|
|
+ >
|
|
|
+ <template #error>
|
|
|
+ <div class="img-error" :title="`URL: ${getScreenshotUrls(row)[0]}`">
|
|
|
+ <el-icon><Picture /></el-icon>
|
|
|
+ <span>加载失败</span>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-image>
|
|
|
+ </template>
|
|
|
<div v-else class="img-placeholder">无</div>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
@@ -80,7 +98,23 @@
|
|
|
<div v-else>
|
|
|
<div class="dynamic-content">{{ dynamicDialog.data?.content || '(无内容)' }}</div>
|
|
|
<div class="media-list">
|
|
|
- <el-image v-for="(m, idx) in dynamicDialog.medias" :key="idx" :src="formatMedia(m)" :preview-src-list="dynamicDialog.medias.map(formatMedia)" fit="cover" style="width:96px;height:96px;border-radius:6px;margin-right:8px;margin-bottom:8px;" />
|
|
|
+ <el-image
|
|
|
+ v-for="(m, idx) in dynamicDialog.medias"
|
|
|
+ :key="idx"
|
|
|
+ :src="m"
|
|
|
+ :preview-src-list="dynamicDialog.medias"
|
|
|
+ fit="cover"
|
|
|
+ style="width:96px;height:96px;border-radius:6px;margin-right:8px;margin-bottom:8px;cursor:pointer;"
|
|
|
+ :lazy="true"
|
|
|
+ @error="handleImageError"
|
|
|
+ >
|
|
|
+ <template #error>
|
|
|
+ <div class="img-error" style="width:96px;height:96px;">
|
|
|
+ <el-icon><Picture /></el-icon>
|
|
|
+ <span>加载失败</span>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-image>
|
|
|
</div>
|
|
|
</div>
|
|
|
</el-dialog>
|
|
|
@@ -132,6 +166,7 @@
|
|
|
<script setup>
|
|
|
import { ref, reactive, onMounted } from 'vue'
|
|
|
import { ElMessage } from 'element-plus'
|
|
|
+import { Picture } from '@element-plus/icons-vue'
|
|
|
import request from '@/utils/request'
|
|
|
import { API_ENDPOINTS, API_BASE_URL } from '@/config/api'
|
|
|
|
|
|
@@ -154,11 +189,42 @@ const statusType = (s) => ({0:'danger',1:'warning',2:'success',3:'info'}[s] || '
|
|
|
const typeText = (t) => ({ spam:'垃圾广告', porn:'色情低俗', violence:'暴力违法', attack:'人身攻击', fake:'虚假信息', plagiarism:'抄袭侵权', other:'其他' }[t] || t || '其他')
|
|
|
const typeTagType = (t) => ({ spam:'warning', porn:'danger', violence:'danger', attack:'danger', fake:'warning', plagiarism:'warning', other:'info' }[t] || 'info')
|
|
|
|
|
|
-const BASE = import.meta.env.DEV ? 'http://localhost:8083' : API_BASE_URL || ''
|
|
|
+// 获取基础URL(用于图片资源)
|
|
|
+const getBaseUrl = () => {
|
|
|
+ // 开发环境:图片资源通过8083端口访问
|
|
|
+ if (import.meta.env.DEV) {
|
|
|
+ return 'http://localhost:8083'
|
|
|
+ }
|
|
|
+ // 生产环境:使用配置的API基础URL
|
|
|
+ return API_BASE_URL || ''
|
|
|
+}
|
|
|
+
|
|
|
+// 格式化媒体URL(与DynamicDetail.vue保持一致)
|
|
|
const formatMedia = (u) => {
|
|
|
if (!u) return ''
|
|
|
- const url = String(u).trim()
|
|
|
- return /^https?:\/\//i.test(url) ? url : `${BASE}${url}`
|
|
|
+
|
|
|
+ let url = String(u).trim()
|
|
|
+ if (!url) return ''
|
|
|
+
|
|
|
+ // 去除引号
|
|
|
+ if ((url.startsWith('"') && url.endsWith('"')) || (url.startsWith("'") && url.endsWith("'"))) {
|
|
|
+ url = url.substring(1, url.length - 1)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果已经是完整的URL,直接返回
|
|
|
+ if (url.startsWith('http')) {
|
|
|
+ return url
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取BASE URL并拼接
|
|
|
+ const baseUrl = getBaseUrl()
|
|
|
+
|
|
|
+ // 确保url以/开头
|
|
|
+ if (!url.startsWith('/')) {
|
|
|
+ url = '/' + url
|
|
|
+ }
|
|
|
+
|
|
|
+ return `${baseUrl}${url}`
|
|
|
}
|
|
|
|
|
|
// 从后端临时塞入的 handleResult 前缀里解析昵称(@nick:李娜 ...)
|
|
|
@@ -169,18 +235,86 @@ const extractNick = (row) => {
|
|
|
return `用户${row?.reporterId ?? ''}`
|
|
|
}
|
|
|
|
|
|
+// 解析截图数据(与DynamicDetail.vue保持一致)
|
|
|
const parseScreens = (raw) => {
|
|
|
if (!raw) return []
|
|
|
+
|
|
|
+ let urls = []
|
|
|
+ const trimmed = String(raw).trim()
|
|
|
+
|
|
|
+ // 尝试解析JSON数组格式
|
|
|
try {
|
|
|
- const v = typeof raw === 'string' ? raw.trim() : raw
|
|
|
- if (typeof v === 'string' && v.startsWith('[')) {
|
|
|
- return JSON.parse(v)
|
|
|
- }
|
|
|
- if (typeof v === 'string') {
|
|
|
- return v.split(',').map(s => s.trim()).filter(Boolean)
|
|
|
+ if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
|
|
|
+ const arr = JSON.parse(trimmed)
|
|
|
+ if (Array.isArray(arr)) {
|
|
|
+ urls = arr
|
|
|
+ }
|
|
|
}
|
|
|
- return Array.isArray(v) ? v : []
|
|
|
- } catch { return [] }
|
|
|
+ } catch (e) {
|
|
|
+ // JSON解析失败,忽略错误,继续尝试其他格式
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果不是JSON数组,尝试按逗号分隔
|
|
|
+ if (!urls.length) {
|
|
|
+ urls = trimmed.split(',').map((item) => item.trim()).filter(Boolean)
|
|
|
+ }
|
|
|
+
|
|
|
+ return urls
|
|
|
+}
|
|
|
+
|
|
|
+// 获取格式化后的截图URL列表
|
|
|
+const getScreenshotUrls = (row) => {
|
|
|
+ if (!row?.screenshots) {
|
|
|
+ return []
|
|
|
+ }
|
|
|
+
|
|
|
+ const screens = parseScreens(row.screenshots)
|
|
|
+ if (!screens || screens.length === 0) {
|
|
|
+ return []
|
|
|
+ }
|
|
|
+
|
|
|
+ const urls = screens.map(formatMedia).filter(Boolean)
|
|
|
+
|
|
|
+ // 详细调试日志
|
|
|
+ if (urls.length > 0) {
|
|
|
+ console.log('📸 举报ID:', row.reportId, {
|
|
|
+ '原始数据': row.screenshots,
|
|
|
+ '解析后数组': screens,
|
|
|
+ '格式化后URLs': urls,
|
|
|
+ 'BASE URL': getBaseUrl()
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ console.warn('⚠️ 举报ID:', row.reportId, '解析后无有效URL,原始数据:', row.screenshots)
|
|
|
+ }
|
|
|
+
|
|
|
+ return urls
|
|
|
+}
|
|
|
+
|
|
|
+// 图片加载错误处理
|
|
|
+const handleImageError = (e, row) => {
|
|
|
+ const img = e.target || e
|
|
|
+ const src = img?.src || img?.currentSrc || 'unknown'
|
|
|
+ const reportId = row?.reportId || img?.dataset?.reportId || 'unknown'
|
|
|
+ const urls = row ? getScreenshotUrls(row) : []
|
|
|
+
|
|
|
+ console.error('❌ 图片加载失败:', {
|
|
|
+ reportId,
|
|
|
+ src,
|
|
|
+ '原始screenshots': row?.screenshots,
|
|
|
+ '解析后URLs': urls,
|
|
|
+ 'BASE URL': getBaseUrl(),
|
|
|
+ error: e
|
|
|
+ })
|
|
|
+
|
|
|
+ // 检查URL格式
|
|
|
+ if (src && !src.startsWith('http')) {
|
|
|
+ console.warn('⚠️ 图片URL格式可能不正确:', src)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查URL是否可访问
|
|
|
+ if (src && src.startsWith('http')) {
|
|
|
+ console.warn('⚠️ 图片URL格式正确但加载失败,可能是文件不存在或CORS问题:', src)
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
const loadList = async () => {
|
|
|
@@ -192,6 +326,17 @@ const loadList = async () => {
|
|
|
if (res.code === 200) {
|
|
|
list.value = res.data.list || []
|
|
|
total.value = res.data.total || list.value.length
|
|
|
+
|
|
|
+ // 调试日志:检查截图数据
|
|
|
+ console.log('📋 举报列表加载成功,共', list.value.length, '条')
|
|
|
+ list.value.forEach((row, idx) => {
|
|
|
+ if (row.screenshots) {
|
|
|
+ const urls = getScreenshotUrls(row)
|
|
|
+ console.log(` [${idx + 1}] 举报ID: ${row.reportId}, 原始screenshots:`, row.screenshots, '解析后URLs:', urls)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ console.error('加载举报列表失败:', res.msg || '未知错误')
|
|
|
}
|
|
|
} catch (e) {
|
|
|
console.error('加载举报失败', e)
|
|
|
@@ -219,30 +364,30 @@ const openDynamic = async (row) => {
|
|
|
const dynamicId = row?.dynamicId
|
|
|
if (!dynamicId) {
|
|
|
dynamicDialog.data = { content: '未获取到动态ID' }
|
|
|
- dynamicDialog.medias = parseScreens(row?.screenshots)
|
|
|
+ dynamicDialog.medias = getScreenshotUrls(row)
|
|
|
dynamicDialog.visible = true
|
|
|
return
|
|
|
}
|
|
|
- dynamicDialog.visible = true
|
|
|
- dynamicDialog.loading = true
|
|
|
- dynamicDialog.data = null
|
|
|
- dynamicDialog.medias = []
|
|
|
- try {
|
|
|
- const res = await request.get(`${API_ENDPOINTS.DYNAMIC_DETAIL}/${dynamicId}`)
|
|
|
- if (res.code === 200 && res.data) {
|
|
|
- dynamicDialog.data = res.data
|
|
|
- const medias = parseMediaUrls(res.data?.mediaUrls)
|
|
|
- dynamicDialog.medias = medias.length ? medias : parseScreens(row?.screenshots)
|
|
|
- } else {
|
|
|
- dynamicDialog.data = { content: res.msg || '动态不存在或已被删除' }
|
|
|
- dynamicDialog.medias = parseScreens(row?.screenshots)
|
|
|
- }
|
|
|
- } catch (e) {
|
|
|
- console.error('获取动态详情失败', e)
|
|
|
- dynamicDialog.data = { content: e?.message ? `加载失败:${e.message}` : '加载失败' }
|
|
|
- dynamicDialog.medias = parseScreens(row?.screenshots)
|
|
|
- }
|
|
|
- finally { dynamicDialog.loading = false }
|
|
|
+ dynamicDialog.visible = true
|
|
|
+ dynamicDialog.loading = true
|
|
|
+ dynamicDialog.data = null
|
|
|
+ dynamicDialog.medias = []
|
|
|
+ try {
|
|
|
+ const res = await request.get(`${API_ENDPOINTS.DYNAMIC_DETAIL}/${dynamicId}`)
|
|
|
+ if (res.code === 200 && res.data) {
|
|
|
+ dynamicDialog.data = res.data
|
|
|
+ const medias = parseMediaUrls(res.data?.mediaUrls)
|
|
|
+ dynamicDialog.medias = medias.length ? medias.map(formatMedia) : getScreenshotUrls(row)
|
|
|
+ } else {
|
|
|
+ dynamicDialog.data = { content: res.msg || '动态不存在或已被删除' }
|
|
|
+ dynamicDialog.medias = getScreenshotUrls(row)
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ console.error('获取动态详情失败', e)
|
|
|
+ dynamicDialog.data = { content: e?.message ? `加载失败:${e.message}` : '加载失败' }
|
|
|
+ dynamicDialog.medias = getScreenshotUrls(row)
|
|
|
+ }
|
|
|
+ finally { dynamicDialog.loading = false }
|
|
|
}
|
|
|
|
|
|
// 处理举报
|
|
|
@@ -312,6 +457,8 @@ const openUnban = (row) => {
|
|
|
.table-card { margin-top: 20px; }
|
|
|
.pagination-container { display: flex; justify-content: flex-end; margin-top: 20px; }
|
|
|
.img-placeholder { width:64px;height:64px;border-radius:6px;background:#f5f7fa;color:#909399;display:flex;align-items:center;justify-content:center;font-size:12px; }
|
|
|
+.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; }
|
|
|
+.img-error .el-icon { font-size:20px;margin-bottom:2px; }
|
|
|
.dynamic-content { margin-bottom: 10px; color:#333; }
|
|
|
.media-list { display:flex; flex-wrap: wrap; }
|
|
|
.loading { color:#909399; }
|