reviewAppealDetail.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479
  1. <template>
  2. <div class="review-appeal-detail-container">
  3. <!-- 返回按钮 -->
  4. <div class="page-header">
  5. <el-button type="primary" @click="goBack"> 返回 </el-button>
  6. </div>
  7. <!-- 标题和状态 -->
  8. <div class="detail-header">
  9. <h2 class="page-title">申诉详情</h2>
  10. <div class="status-section">
  11. <el-icon class="status-icon" :size="48">
  12. <Clock />
  13. </el-icon>
  14. <div class="status-text">
  15. {{ getStatusText(detailData.appealStatus) }}
  16. </div>
  17. <div class="status-desc" v-if="detailData.appealStatus === 0">您反馈的评价内容及账号行为正处于审核阶段,请您耐心等待</div>
  18. <div class="status-desc" v-else-if="detailData.appealStatus === 1">
  19. 您反馈的评价内容及账号行为经审核,暂未发现违规行为。建议您可以通过商家的回复功能对具体情况进行解释和沟通,以便于更好的解决问题。
  20. </div>
  21. <div class="status-desc" v-else-if="detailData.appealStatus === 2">
  22. 您反馈的评价内容及账号行为经审核,已发现违规行为。现已根据规定将此条评论进行删除,平台也会持续收集违规行为,一经发现,将按照规定处理。
  23. </div>
  24. </div>
  25. </div>
  26. <!-- 处理进度 -->
  27. <div class="progress-section">
  28. <h3 class="section-title">处理进度</h3>
  29. <el-timeline>
  30. <el-timeline-item v-for="(step, index) in progressSteps" :key="index" :timestamp="step.timestamp" placement="top">
  31. {{ step.content }}
  32. </el-timeline-item>
  33. </el-timeline>
  34. </div>
  35. <!-- 申诉详情 -->
  36. <div class="appeal-detail-section">
  37. <h3 class="section-title">申诉详情</h3>
  38. <!-- 顾客评价 -->
  39. <div class="review-card">
  40. <div class="card-label">顾客评价</div>
  41. <div class="review-header">
  42. <div class="user-info">
  43. <el-avatar :src="detailData.userImage" :size="40">
  44. <el-icon><User /></el-icon>
  45. </el-avatar>
  46. <div class="user-details">
  47. <div class="user-name">
  48. {{ detailData.isAnonymous == 1 || !detailData.userName ? "匿名用户" : detailData.userName }}
  49. </div>
  50. </div>
  51. </div>
  52. <div class="review-time">
  53. {{ detailData.commentTime }}
  54. </div>
  55. </div>
  56. <div class="review-content">
  57. {{ detailData.commentContent }}
  58. </div>
  59. <div v-if="commentMedia.length > 0" class="review-media">
  60. <template v-for="(m, idx) in commentMedia" :key="'c-' + idx">
  61. <el-image
  62. v-if="m.type === 'image'"
  63. :src="m.url"
  64. :preview-src-list="commentMediaImages"
  65. :initial-index="getMediaImageIndex(commentMedia, idx)"
  66. fit="cover"
  67. class="media-item review-image"
  68. />
  69. <div v-else class="media-item media-video-wrap" @click="openVideoPreview(m.url)">
  70. <video
  71. :src="m.url"
  72. class="video-thumb"
  73. preload="metadata"
  74. muted
  75. playsinline
  76. @loadeddata="onVideoLoadedData($event)"
  77. />
  78. <div class="video-mask">
  79. <el-icon class="video-play-icon">
  80. <VideoPlay />
  81. </el-icon>
  82. </div>
  83. </div>
  84. </template>
  85. </div>
  86. </div>
  87. <!-- 申诉信息 -->
  88. <div class="appeal-info-card">
  89. <div class="card-label">申诉信息</div>
  90. <div class="info-item">
  91. <span class="info-label">申诉账号</span>
  92. <span class="info-value">{{ detailData.storePhone }}</span>
  93. </div>
  94. <div class="info-item">
  95. <span class="info-label">申诉时间</span>
  96. <span class="info-value">{{ detailData.appealTime }}</span>
  97. </div>
  98. <div class="info-item">
  99. <span class="info-label">申诉原因</span>
  100. <span class="info-value">{{ detailData.appealReason }}</span>
  101. </div>
  102. <div class="info-item">
  103. <span class="info-label">申诉图片</span>
  104. <div v-if="appealMedia.length > 0" class="appeal-media" style="width: 200px">
  105. <template v-for="(m, idx) in appealMedia" :key="'a-' + idx">
  106. <el-image
  107. v-if="m.type === 'image'"
  108. :src="m.url"
  109. :preview-src-list="appealMediaImages"
  110. :initial-index="getMediaImageIndex(appealMedia, idx)"
  111. fit="cover"
  112. class="media-item appeal-image"
  113. />
  114. <div v-else class="media-item media-video-wrap" @click="openVideoPreview(m.url)">
  115. <video
  116. :src="m.url"
  117. class="video-thumb"
  118. preload="metadata"
  119. muted
  120. playsinline
  121. @loadeddata="onVideoLoadedData($event)"
  122. />
  123. <div class="video-mask">
  124. <el-icon class="video-play-icon">
  125. <VideoPlay />
  126. </el-icon>
  127. </div>
  128. </div>
  129. </template>
  130. </div>
  131. <span v-else class="info-value">--</span>
  132. </div>
  133. </div>
  134. </div>
  135. <!-- 视频预览弹窗 -->
  136. <el-dialog
  137. v-model="videoPreviewVisible"
  138. title="视频预览"
  139. width="80%"
  140. max-width="720px"
  141. destroy-on-close
  142. align-center
  143. @closed="closeVideoPreview"
  144. >
  145. <video v-if="videoPreviewUrl" :src="videoPreviewUrl" controls autoplay style="width: 100%; max-height: 70vh" />
  146. </el-dialog>
  147. </div>
  148. </template>
  149. <script setup lang="ts" name="reviewAppealDetail">
  150. import { ref, reactive, computed, onMounted } from "vue";
  151. import { useRouter, useRoute } from "vue-router";
  152. import { Clock, User, VideoPlay } from "@element-plus/icons-vue";
  153. import { ElMessage } from "element-plus";
  154. import { getAppealDetail } from "@/api/modules/newLoginApi";
  155. const router = useRouter();
  156. const route = useRoute();
  157. // 详情数据
  158. const detailData = reactive({
  159. appealStatus: 0, // 0-待审核, 1-已驳回, 2-已通过
  160. userName: "",
  161. userAvatar: "",
  162. isAnonymous: 0,
  163. commentTime: "",
  164. commentContent: "",
  165. commentImages: [] as string[],
  166. appealAccount: "",
  167. appealTime: "",
  168. appealReason: "",
  169. storePhone: "",
  170. imgList: [] as string[],
  171. userImage: "",
  172. commentImgList: [] as string[]
  173. });
  174. const VIDEO_EXT = [".mp4", ".mov", ".avi", ".wmv", ".flv", ".mkv", ".webm", ".m4v"];
  175. const isVideoUrl = (url: string) => {
  176. const lower = (url || "").toLowerCase();
  177. return VIDEO_EXT.some(ext => lower.includes(ext));
  178. };
  179. interface MediaItem {
  180. url: string;
  181. type: "image" | "video";
  182. }
  183. const getMediaFromUrls = (urls: string[]): MediaItem[] => {
  184. if (!urls?.length) return [];
  185. return urls.filter(Boolean).map(url => ({ url, type: isVideoUrl(url) ? "video" : "image" }));
  186. };
  187. const getMediaImageIndex = (media: MediaItem[], currentIndex: number): number => {
  188. let idx = 0;
  189. for (let i = 0; i < currentIndex; i++) if (media[i].type === "image") idx++;
  190. return idx;
  191. };
  192. const commentMedia = computed<MediaItem[]>(() => getMediaFromUrls(detailData.commentImgList || []));
  193. const commentMediaImages = computed(() => commentMedia.value.filter(m => m.type === "image").map(m => m.url));
  194. const appealMedia = computed<MediaItem[]>(() => getMediaFromUrls(detailData.imgList || []));
  195. const appealMediaImages = computed(() => appealMedia.value.filter(m => m.type === "image").map(m => m.url));
  196. const videoPreviewVisible = ref(false);
  197. const videoPreviewUrl = ref("");
  198. const openVideoPreview = (url: string) => {
  199. videoPreviewUrl.value = url;
  200. videoPreviewVisible.value = true;
  201. };
  202. const closeVideoPreview = () => {
  203. videoPreviewUrl.value = "";
  204. videoPreviewVisible.value = false;
  205. };
  206. const onVideoLoadedData = (e: Event) => {
  207. const video = e.target as HTMLVideoElement;
  208. if (video) {
  209. video.currentTime = 0;
  210. video.pause();
  211. }
  212. };
  213. // 处理进度步骤
  214. const progressSteps = ref<Array<{ content: string; timestamp: string }>>([
  215. {
  216. content: "商家提交申诉",
  217. timestamp: "2025/06/10 12:00:00"
  218. },
  219. {
  220. content: "通过系统初审,已为您安排专人审核",
  221. timestamp: "2025/06/10 12:00:00"
  222. },
  223. {
  224. content: "申诉成功",
  225. timestamp: "2025/06/10 12:00:00"
  226. }
  227. ]);
  228. // 返回
  229. const goBack = () => {
  230. router.back();
  231. };
  232. // 获取状态文本
  233. const getStatusText = (status: number) => {
  234. const statusMap: Record<number, string> = {
  235. 0: "待审核",
  236. 1: "已驳回",
  237. 2: "已通过"
  238. };
  239. return statusMap[status] || "待审核";
  240. };
  241. // 加载申诉详情
  242. const loadAppealDetail = async () => {
  243. try {
  244. const appealId = route.query.id;
  245. if (!appealId) {
  246. ElMessage.error("缺少申诉ID");
  247. return;
  248. }
  249. const res: any = await getAppealDetail({ id: appealId });
  250. console.log("申诉详情:", res);
  251. if (res.code === 200 || res.code == 200) {
  252. const data = res.data;
  253. Object.assign(detailData, {
  254. appealStatus: data.appealStatus ?? 0,
  255. userName: data.userName || "",
  256. userAvatar: data.userAvatar || "",
  257. isAnonymous: data.isAnonymous ?? 0,
  258. commentTime: data.commentTime || data.createdTime || "",
  259. commentContent: data.commentContent || "",
  260. commentImages: data.commentImages || [],
  261. appealAccount: data.appealAccount || data.phone || "",
  262. appealTime: data.appealTime || data.createdTime || "",
  263. appealReason: data.appealReason || "",
  264. storePhone: data.storePhone || "",
  265. imgList: data.imgList || [],
  266. userImage: data.userImage || "",
  267. commentImgList: data.commentImgList || []
  268. });
  269. // 更新处理进度
  270. if (data.progressSteps && Array.isArray(data.progressSteps)) {
  271. progressSteps.value = data.progressSteps;
  272. }
  273. } else {
  274. ElMessage.error(res.msg || "获取申诉详情失败");
  275. }
  276. } catch (error: any) {
  277. console.error("获取申诉详情失败", error);
  278. ElMessage.error(error?.msg || "获取申诉详情失败");
  279. }
  280. };
  281. // 初始化
  282. onMounted(() => {
  283. loadAppealDetail();
  284. });
  285. </script>
  286. <style lang="scss" scoped>
  287. .review-appeal-detail-container {
  288. min-height: calc(100vh - 120px);
  289. padding: 20px;
  290. background: #f5f7fa;
  291. .page-header {
  292. margin-bottom: 20px;
  293. }
  294. // 标题和状态
  295. .detail-header {
  296. padding: 24px;
  297. margin-bottom: 20px;
  298. text-align: center;
  299. background: #ffffff;
  300. border-radius: 8px;
  301. .page-title {
  302. margin: 0 0 24px;
  303. font-size: 20px;
  304. font-weight: 600;
  305. color: #303133;
  306. }
  307. .status-section {
  308. .status-icon {
  309. margin-bottom: 12px;
  310. color: #e6a23c;
  311. }
  312. .status-text {
  313. margin-bottom: 8px;
  314. font-size: 18px;
  315. font-weight: 600;
  316. color: #303133;
  317. }
  318. .status-desc {
  319. font-size: 14px;
  320. color: #909399;
  321. }
  322. }
  323. }
  324. // 处理进度
  325. .progress-section {
  326. padding: 24px;
  327. margin-bottom: 20px;
  328. background: #ffffff;
  329. border-radius: 8px;
  330. .section-title {
  331. margin: 0 0 20px;
  332. font-size: 16px;
  333. font-weight: 600;
  334. color: #303133;
  335. }
  336. }
  337. // 申诉详情
  338. .appeal-detail-section {
  339. padding: 24px;
  340. background: #ffffff;
  341. border-radius: 8px;
  342. .section-title {
  343. margin: 0 0 20px;
  344. font-size: 16px;
  345. font-weight: 600;
  346. color: #303133;
  347. }
  348. .card-label {
  349. margin-bottom: 16px;
  350. font-size: 14px;
  351. font-weight: 600;
  352. color: #606266;
  353. }
  354. // 顾客评价卡片
  355. .review-card {
  356. padding: 16px;
  357. margin-bottom: 24px;
  358. background: #f5f7fa;
  359. border-radius: 8px;
  360. .review-header {
  361. display: flex;
  362. justify-content: space-between;
  363. margin-bottom: 12px;
  364. .user-info {
  365. display: flex;
  366. gap: 12px;
  367. align-items: center;
  368. .user-details {
  369. .user-name {
  370. font-size: 14px;
  371. font-weight: 600;
  372. color: #303133;
  373. }
  374. }
  375. }
  376. .review-time {
  377. font-size: 13px;
  378. color: #909399;
  379. }
  380. }
  381. .review-content {
  382. margin-bottom: 12px;
  383. font-size: 14px;
  384. line-height: 1.6;
  385. color: #606266;
  386. }
  387. .review-media,
  388. .appeal-media {
  389. display: flex;
  390. flex-wrap: wrap;
  391. gap: 8px;
  392. .media-item {
  393. flex-shrink: 0;
  394. width: 100px;
  395. height: 100px;
  396. overflow: hidden;
  397. border-radius: 4px;
  398. }
  399. .review-image,
  400. .appeal-image {
  401. object-fit: cover;
  402. }
  403. .media-video-wrap {
  404. position: relative;
  405. cursor: pointer;
  406. background: #000000;
  407. .video-thumb {
  408. display: block;
  409. width: 100%;
  410. height: 100%;
  411. object-fit: cover;
  412. }
  413. .video-mask {
  414. position: absolute;
  415. inset: 0;
  416. display: flex;
  417. align-items: center;
  418. justify-content: center;
  419. background: rgb(0 0 0 / 35%);
  420. .video-play-icon {
  421. font-size: 28px;
  422. color: #ffffff;
  423. }
  424. }
  425. }
  426. }
  427. }
  428. // 申诉信息卡片
  429. .appeal-info-card {
  430. .info-item {
  431. display: flex;
  432. margin-bottom: 16px;
  433. &:last-child {
  434. margin-bottom: 0;
  435. }
  436. .info-label {
  437. flex-shrink: 0;
  438. width: 100px;
  439. font-size: 14px;
  440. color: #909399;
  441. }
  442. .info-value {
  443. flex: 1;
  444. font-size: 14px;
  445. color: #606266;
  446. }
  447. }
  448. }
  449. }
  450. }
  451. </style>