Jelajahi Sumber

Merge branch 'development' of http://8.152.195.41:3000/alien/group_web_merchant into development

sgc 2 bulan lalu
induk
melakukan
043b10982b

+ 37 - 13
src/api/modules/newLoginApi.ts

@@ -178,25 +178,49 @@ export const getAppealDetail = (params: any) => {
   return httpLogin.get(`/alienStore/storeCommentAppeal/getAppealDetail`, params);
 };
 
-//申诉删除
-
-export const addAppealNew = (params: any) => {
+// 申诉删除(与商家端一致:FormData 含 appealReason、storeId、commentId、file_0/file_1...)
+export const addAppealNew = (params: FormData | Record<string, unknown>) => {
   return httpLogin.post(`/alienStore/storeCommentAppeal/addAppealNew`, params);
 };
 
-//发表评论
-export const saveComment = (params: any) => {
-  const formData = new FormData();
-  Object.keys(params).forEach(key => {
-    if (params[key] !== undefined && params[key] !== null) {
-      formData.append(key, params[key]);
-    }
-  });
-  return httpLogin.post(`/alienStore/storeComment/saveComment`, formData, {
-    "Content-Type": "multipart/form-data"
+/** 商家端 commonComment/addComment:发表评论/回复(评价回复 sourceType=1,动态评论 sourceType=2) */
+export const addComment = (data: {
+  sourceType: number; // 1-评价的评论 2-动态等
+  sourceId: string | number; // 评价ID或动态ID
+  userId: string;
+  parentId: number; // 0-根评论
+  content: string;
+  commentType: number; // 2-商户评论
+  merchantId: string;
+  isAnonymous?: number;
+}) => {
+  return httpLogin.post(`/alienStore/commonComment/addComment`, {
+    ...data,
+    isAnonymous: data.isAnonymous ?? 0
   });
 };
 
+/**
+ * 发表评论/回复(兼容旧参数,内部转为 addComment)
+ * - 评价回复:businessType=1,replyId=评价ID → sourceType=1, sourceId=replyId, parentId=0
+ * - 动态评论:businessType=2,businessId=动态ID,replyId 有值为回复评论ID → sourceType=2, sourceId=businessId, parentId=replyId||0
+ */
+export const saveComment = (params: any) => {
+  const businessType = Number(params.businessType ?? 0);
+  const isRating = businessType === 1;
+  const payload = {
+    sourceType: isRating ? 1 : 2,
+    sourceId: isRating ? params.replyId : params.businessId,
+    userId: String(params.userId ?? params.phoneId ?? ""),
+    parentId: isRating ? 0 : Number(params.replyId || 0),
+    content: String(params.commentContent ?? params.content ?? ""),
+    commentType: 2,
+    merchantId: String(params.merchantId ?? params.storeId ?? params.businessId ?? ""),
+    isAnonymous: 0
+  };
+  return addComment(payload);
+};
+
 //评论列表
 export const commentList = (params: any) => {
   return httpLogin.get(`/alienStore/storeComment/getList`, params);

+ 255 - 127
src/views/dynamicManagement/reviewAppeal.vue

@@ -74,7 +74,7 @@
               </div>
             </div>
             <div class="review-time">
-              {{ review.createdTime.replace(/-/g, "/") }}
+              {{ (review.createdTime || "").replace(/-/g, "/") }}
             </div>
           </div>
 
@@ -86,15 +86,32 @@
             <div class="sjhf">商家回复: {{ itm.commentContent }}</div>
           </div>
 
-          <div v-if="review.images && review.images.length > 0" class="review-images">
-            <el-image
-              v-for="(img, index) in review.images"
-              :key="index"
-              :src="img"
-              :preview-src-list="review.images"
-              fit="cover"
-              class="review-image"
-            />
+          <div v-if="review.media && review.media.length > 0" class="review-media">
+            <template v-for="(m, index) in review.media" :key="index">
+              <el-image
+                v-if="m.type === 'image'"
+                :src="m.url"
+                :preview-src-list="review.images"
+                :initial-index="getMediaImageIndex(review.media, Number(index))"
+                fit="cover"
+                class="media-item review-image"
+              />
+              <div v-else class="media-item media-video-wrap" @click="openVideoPreview(m.url)">
+                <video
+                  :src="m.url"
+                  class="video-thumb"
+                  preload="metadata"
+                  muted
+                  playsinline
+                  @loadeddata="onVideoLoadedData($event)"
+                />
+                <div class="video-mask">
+                  <el-icon class="video-play-icon">
+                    <VideoPlay />
+                  </el-icon>
+                </div>
+              </div>
+            </template>
           </div>
 
           <div class="review-footer">
@@ -188,6 +205,19 @@
         <el-button type="primary" @click="submitReply"> 提交回复 </el-button>
       </template>
     </el-dialog>
+
+    <!-- 视频预览弹窗 -->
+    <el-dialog
+      v-model="videoPreviewVisible"
+      title="视频预览"
+      width="80%"
+      max-width="720px"
+      destroy-on-close
+      align-center
+      @closed="closeVideoPreview"
+    >
+      <video v-if="videoPreviewUrl" :src="videoPreviewUrl" controls autoplay style="width: 100%; max-height: 70vh" />
+    </el-dialog>
   </div>
 </template>
 
@@ -195,7 +225,7 @@
 import { ref, reactive, onMounted } from "vue";
 import { useRouter } from "vue-router";
 import { ElMessage } from "element-plus";
-import { User, Plus } from "@element-plus/icons-vue";
+import { User, Plus, VideoPlay } from "@element-plus/icons-vue";
 import type { FormInstance, FormRules, UploadUserFile } from "element-plus";
 import { getList, addAppealNew, saveComment, uploadImg, getRatingCount } from "@/api/modules/newLoginApi";
 import { localGet } from "@/utils";
@@ -216,13 +246,13 @@ const statistics = reactive({
   abnormalRate: "0%"
 });
 
-// 标签页计数
+// 标签页计数(由 getRatingCount 接口填充)
 const tabCounts = reactive({
-  all: 22,
-  pending: 2,
-  bad: 5,
-  good: 10,
-  neutral: 7
+  all: 0,
+  pending: 0,
+  bad: 0,
+  good: 0,
+  neutral: 0
 });
 
 // 当前激活的标签
@@ -251,9 +281,34 @@ const appealFormData = reactive({
   fileList: [] as UploadUserFile[]
 });
 const hasStoreReply = (review: any) => {
-  console.log(review);
-  return review.storeComment && review.storeComment.length > 0;
+  return review?.storeComment && review.storeComment.length > 0;
+};
+
+/** 在 media 列表中,当前项在「仅图片」列表中的下标,用于 el-image initial-index */
+const getMediaImageIndex = (media: { url: string; type: string }[], currentIndex: number): number => {
+  let idx = 0;
+  for (let i = 0; i < currentIndex; i++) if (media[i].type === "image") idx++;
+  return idx;
+};
+
+const videoPreviewVisible = ref(false);
+const videoPreviewUrl = ref("");
+const openVideoPreview = (url: string) => {
+  videoPreviewUrl.value = url;
+  videoPreviewVisible.value = true;
+};
+const closeVideoPreview = () => {
+  videoPreviewUrl.value = "";
+  videoPreviewVisible.value = false;
+};
+const onVideoLoadedData = (e: Event) => {
+  const video = e.target as HTMLVideoElement;
+  if (video) {
+    video.currentTime = 0;
+    video.pause();
+  }
 };
+
 const appealFormRules = reactive<FormRules>({
   reason: [{ required: true, message: "请输入申诉原因", trigger: "blur" }],
   images: [{ required: true, message: "请上传申诉凭证", trigger: "blur" }]
@@ -289,13 +344,15 @@ const handleTimeFilterChange = () => {
   loadReviewList(activeTab.value, true);
 };
 
-// 加载统计数据(只在初始化时调用一次)
+// 与商家端一致:兼容 code 0 / 200,从 getRatingCount 取 totalCount、goodCount、midCount、badCount、pending、replyRate
 const loadStatistics = async () => {
   try {
-    // 从 getRatingCount 获取各评论等级数量
-    const ratingCountRes = await getRatingCount({ businessId: localGet("createdId"), businessType: 1 });
-    const ratingCount = Number(ratingCountRes?.code) === 200 ? ratingCountRes.data : null;
-    // 从 getRatingCount 返回的 data 取值(totalCount / goodCount / midCount / badCount / pending)
+    const ratingCountRes: any = await getRatingCount({
+      businessId: localGet("createdId"),
+      businessType: 1
+    });
+    const isOk = ratingCountRes?.code === 200 || ratingCountRes?.code === 0;
+    const ratingCount = isOk ? (ratingCountRes.data ?? ratingCountRes) : null;
     const rc = ratingCount as Record<string, number | undefined> | null;
     tabCounts.all = rc?.totalCount ?? 0;
     tabCounts.good = rc?.goodCount ?? 0;
@@ -303,79 +360,118 @@ const loadStatistics = async () => {
     tabCounts.bad = rc?.badCount ?? 0;
     tabCounts.pending = rc?.pending ?? 0;
 
-    // 更新统计数据
     statistics.totalReviews = tabCounts.all;
-    statistics.badTextReviews = tabCounts.bad; // 新增差评数
-    statistics.badImageReviews = tabCounts.good; // 新增好评数
-    statistics.neutralReviews = tabCounts.neutral; // 中评数
-
-    // 计算评论回复率 = (已回复评论数 ÷ 总评论数) × 100%
-    const totalCount = tabCounts.all;
-    const repliedCount = totalCount - tabCounts.pending; // 已回复 = 总数 - 未回复
-    if (totalCount > 0) {
-      const rate = (repliedCount / totalCount) * 100;
-      statistics.abnormalRate = rate.toFixed(2) + "%";
+    statistics.badTextReviews = tabCounts.bad;
+    statistics.badImageReviews = tabCounts.good;
+    statistics.neutralReviews = tabCounts.neutral;
+
+    if (rc?.replyRate != null) {
+      statistics.abnormalRate = Number(rc.replyRate).toFixed(2) + "%";
     } else {
-      statistics.abnormalRate = "0%";
+      const totalCount = tabCounts.all;
+      const repliedCount = rc?.repliedCount ?? Math.max(0, totalCount - tabCounts.pending);
+      if (totalCount > 0) {
+        statistics.abnormalRate = ((repliedCount / totalCount) * 100).toFixed(2) + "%";
+      } else {
+        statistics.abnormalRate = "0%";
+      }
     }
   } catch (error) {
     console.error("获取统计数据失败", error);
   }
 };
 
-// 加载评论列表
+const VIDEO_EXT = [".mp4", ".mov", ".avi", ".wmv", ".flv", ".mkv", ".webm", ".m4v"];
+const isVideoUrl = (url: string) => {
+  const lower = (url || "").toLowerCase();
+  return VIDEO_EXT.some(ext => lower.includes(ext));
+};
+
+// 与商家端一致:将 commonRating/getList 返回的单条转为列表展示格式;imageUrls 中区分图片与视频,视频用首帧展示
+function transformRatingData(item: any): any {
+  if (!item || typeof item !== "object" || item.id == null) return null;
+  let urls: string[] = [];
+  if (item.imageUrls) {
+    const raw =
+      typeof item.imageUrls === "string" && item.imageUrls.trim()
+        ? item.imageUrls.split(",").filter((u: string) => u?.trim())
+        : Array.isArray(item.imageUrls)
+          ? item.imageUrls
+          : [];
+    urls = raw.map((u: string) => (u && u.trim()) || "").filter(Boolean);
+  }
+  const media = urls.map(url => ({ url, type: isVideoUrl(url) ? ("video" as const) : ("image" as const) }));
+  const images = media.filter(m => m.type === "image").map(m => m.url);
+  let storeComment: { commentContent: string; createdTime?: string }[] = [];
+  if (item.childCommonComments) {
+    const list = Array.isArray(item.childCommonComments)
+      ? item.childCommonComments
+      : Object.values(item.childCommonComments || {});
+    storeComment = list
+      .filter((c: any) => c && Number(c.commentType) === 2)
+      .map((c: any) => ({
+        commentContent: c.content || c.commentContent || "",
+        createdTime: c.createdTime || ""
+      }));
+  }
+  return {
+    id: item.id,
+    commentContent: item.content ?? item.commentContent ?? "",
+    score: item.score ?? 0,
+    images,
+    media,
+    userAvatar: item.userImage ?? item.userAvatar ?? "",
+    userName: item.userName ?? (item.isAnonymous === 1 ? "匿名用户" : "用户"),
+    createdTime: item.createdTime ?? "",
+    isAnonymous: item.isAnonymous ?? 0,
+    storeComment,
+    appealStatus: item.appealStatus !== undefined && item.appealStatus !== null ? item.appealStatus : null,
+    appealFlag: item.appealFlag !== undefined ? item.appealFlag : item.appealStatus != null ? 1 : 0
+  };
+}
+
+// 加载评论列表(与商家端 getRatingList 参数一致:pageNum、pageSize、businessType、businessId、userId、searchScore、days、replyStatus)
 const loadReviewList = async (commentLevel?: string | number, resetPage = false) => {
   try {
-    // 如果需要重置页码
-    if (resetPage) {
-      pageNum.value = 1;
-    }
+    if (resetPage) pageNum.value = 1;
 
-    // 如果没有传入参数,使用当前激活的标签页
     const level = commentLevel !== undefined ? commentLevel : activeTab.value;
-
-    // 判断是否是待回复差评
     const isPending = level === "pending";
 
-    // 构建请求参数
     const params: Record<string, any> = {
       pageNum: pageNum.value,
       pageSize: pageSize.value,
-      businessType: 1, // 1-店铺评价
+      businessType: 1,
       businessId: localGet("createdId"),
       userId: userStore.userInfo?.userId || userStore.userInfo?.id || ""
+      // replyStatus: 2
     };
 
-    // 评分筛选:0-全部 1-好评(≥4.5) 2-中评(3.0-4.0) 3-差评(0.5-2.5)
     const levelNum = isPending ? 3 : Number(level);
-    if (levelNum !== 0) {
-      params.searchScore = levelNum;
-    }
-
-    if (timeFilter.value) {
-      params.days = timeFilter.value;
-    }
-
-    // 待回复差评:回复状态 2-未回复
-    if (isPending) {
-      params.replyStatus = 2;
-    }
+    if (levelNum !== 0) params.searchScore = levelNum;
+    if (timeFilter.value) params.days = timeFilter.value;
+    if (isPending) params.replyStatus = 2;
 
     const res: any = await getList(params);
 
-    if (res.code === 200) {
-      reviewList.value = res.data.records || [];
-      total.value = res.data.total || 0;
+    const isOk = res?.code === 200 || res?.code === 0;
+    const data = res?.data ?? res;
+    const records = data?.records ?? (Array.isArray(data) ? data : []);
+    const totalCount = data?.total ?? res?.total ?? 0;
+
+    if (isOk && (records.length > 0 || totalCount >= 0)) {
+      reviewList.value = records.map((item: any) => transformRatingData(item)).filter(Boolean);
+      total.value = totalCount;
     } else {
       reviewList.value = [];
       total.value = 0;
-      ElMessage.error(res.msg || "获取评论列表失败");
+      if (!isOk && res?.msg) ElMessage.error(res.msg);
     }
   } catch (error: any) {
     console.error("获取评论列表失败", error);
     reviewList.value = [];
     total.value = 0;
-    ElMessage.error(error?.msg || "获取评论列表失败");
+    ElMessage.error(error?.message || error?.msg || "获取评论列表失败");
   }
 };
 
@@ -400,7 +496,6 @@ const delReviewAppeal = (review: any) => {
 //回复评论
 // 打开回复对话框
 const openReplayDialog = (review: any) => {
-  console.log("打开回复对话框:", review);
   currentReplyReview.value = review;
   replyDialogVisible.value = true;
 };
@@ -415,43 +510,41 @@ const closeReplyDialog = () => {
   currentReplyReview.value = null;
 };
 
-// 提交回复
+// 提交回复(与商家端 saveComment 参数一致:businessId=店铺ID、replyId=评论ID、commentContent、storeId、userId、phoneId、businessType)
 const submitReply = async () => {
-  if (!replyFormRef.value) return;
+  if (!replyFormRef.value || !currentReplyReview.value) return;
 
   await replyFormRef.value.validate(async (valid: boolean) => {
-    if (valid) {
-      try {
-        // 获取当前用户的手机号,并在前面拼接 "store_"
-        const phone = userStore.userInfo?.phone || "";
-        const phoneId = phone.startsWith("store_") ? phone : `store_${phone}`;
-
-        // 调用 saveComment 接口
-        const params = {
-          businessId: currentReplyReview.value.id, // 评论ID
-          businessType: "1", // 业务类型:1表示订单评价的回复
-          userId: userStore.userInfo?.userId || userStore.userInfo?.id || "",
-          storeId: userStore.userInfo?.storeId || localGet("createdId") || "",
-          commentContent: replyFormData.content, // 回复内容
-          phoneId: phoneId, // 店铺phoneId
-          replyId: currentReplyReview.value.id // 被回复的评论ID
-        };
-
-        console.log("提交回复参数:", params);
-
-        const res: any = await saveComment(params);
-
-        if (res.code === 200) {
-          ElMessage.success("回复提交成功");
-          closeReplyDialog();
-          loadReviewList();
-        } else {
-          ElMessage.error(res.message || "回复提交失败");
-        }
-      } catch (error) {
-        console.error("回复提交失败:", error);
-        ElMessage.error("回复提交失败");
+    if (!valid) return;
+    try {
+      const phone = userStore.userInfo?.phone || "";
+      const phoneId = phone.startsWith("store_") ? phone : `store_${phone}`;
+      const storeId = userStore.userInfo?.storeId || localGet("createdId") || "";
+
+      const params = {
+        businessId: storeId,
+        businessType: "1",
+        userId: String(userStore.userInfo?.userId ?? userStore.userInfo?.id ?? ""),
+        storeId,
+        commentContent: replyFormData.content,
+        phoneId,
+        replyId: currentReplyReview.value.id
+      };
+
+      const res: any = await saveComment(params);
+      const isOk = res?.code === 200 || res?.code === 0;
+
+      if (isOk) {
+        ElMessage.success("回复提交成功");
+        closeReplyDialog();
+        loadReviewList();
+        loadStatistics();
+      } else {
+        ElMessage.error(res?.message || res?.msg || "回复提交失败");
       }
+    } catch (error: any) {
+      console.error("回复提交失败:", error);
+      ElMessage.error(error?.message || "回复提交失败");
     }
   });
 };
@@ -468,38 +561,44 @@ const closeAppealDialog = () => {
   });
 };
 
-// 提交申诉
+// 提交申诉(与商家端 addAppealNew 一致:FormData 含 appealReason、storeId、commentId、file_0/file_1...,响应 result: 0成功 1失败 2已存在 3敏感词 4超长)
 const submitAppeal = async () => {
   if (!appealFormRef.value) return;
 
   await appealFormRef.value.validate(async (valid: boolean) => {
-    if (valid) {
-      try {
-        // 使用 FormData 发送图片
-        const formData = new FormData();
-        formData.append("appealReason", appealFormData.reason);
-        formData.append("storeId", localGet("geeker-user").userInfo.storeId);
-        formData.append("commentId", currentReviewId.value);
-
-        // 添加图片文件,使用 file_0, file_1, file_2 等格式
-        appealFormData.files.forEach((file, index) => {
-          formData.append(`file_${index}`, file);
-        });
-
-        const res: any = await addAppealNew(formData);
-        if (res.code === 200) {
-          ElMessage.success("申诉提交成功");
-          // 更新评价的申诉状态,使按钮置灰
-          const review = reviewList.value.find((r: any) => r.id === currentReviewId.value);
-          if (review) {
-            review.isAppealed = true;
-          }
-        }
-
-        closeAppealDialog();
-      } catch (error) {
-        ElMessage.error("申诉提交失败");
+    if (!valid) return;
+    try {
+      const geekerUser = localGet("geeker-user");
+      const storeId = geekerUser?.userInfo?.storeId ?? localGet("createdId") ?? "";
+
+      const formData = new FormData();
+      formData.append("appealReason", appealFormData.reason);
+      formData.append("storeId", String(storeId));
+      formData.append("commentId", String(currentReviewId.value));
+      appealFormData.files.forEach((file: File, index: number) => {
+        formData.append(`file_${index}`, file);
+      });
+
+      const res: any = await addAppealNew(formData);
+      const result = res?.data?.result ?? res?.result;
+
+      if (result === 0) {
+        ElMessage.success("申诉提交成功");
+        const review = reviewList.value.find((r: any) => String(r.id) === String(currentReviewId.value));
+        if (review) review.appealFlag = 1;
+        loadStatistics();
+      } else if (result === 2) {
+        ElMessage.warning("申诉已存在");
+      } else if (result === 3) {
+        ElMessage.warning("申诉理由包含敏感词,请修改后重试");
+      } else if (result === 4) {
+        ElMessage.warning("申诉理由超过300字限制");
+      } else {
+        ElMessage.error(res?.msg || "申诉提交失败");
       }
+      closeAppealDialog();
+    } catch (error: any) {
+      ElMessage.error(error?.message || "申诉提交失败");
     }
   });
 };
@@ -656,15 +755,44 @@ onMounted(() => {
           font-size: 14px;
           color: #606266;
         }
-        .review-images {
+        .review-media {
           display: flex;
+          flex-wrap: wrap;
           gap: 8px;
           margin-bottom: 12px;
-          .review-image {
+          .media-item {
+            flex-shrink: 0;
             width: 80px;
             height: 80px;
+            overflow: hidden;
             border-radius: 4px;
           }
+          .review-image {
+            object-fit: cover;
+          }
+          .media-video-wrap {
+            position: relative;
+            cursor: pointer;
+            background: #000000;
+            .video-thumb {
+              display: block;
+              width: 100%;
+              height: 100%;
+              object-fit: cover;
+            }
+            .video-mask {
+              position: absolute;
+              inset: 0;
+              display: flex;
+              align-items: center;
+              justify-content: center;
+              background: rgb(0 0 0 / 35%);
+              .video-play-icon {
+                font-size: 28px;
+                color: #ffffff;
+              }
+            }
+          }
         }
         .review-footer {
           display: flex;

+ 155 - 36
src/views/dynamicManagement/reviewAppealDetail.vue

@@ -15,7 +15,13 @@
         <div class="status-text">
           {{ getStatusText(detailData.appealStatus) }}
         </div>
-        <div class="status-desc">您反馈的评价内容及账号行为正处于审核阶段,请您耐心等待</div>
+        <div class="status-desc" v-if="detailData.appealStatus === 0">您反馈的评价内容及账号行为正处于审核阶段,请您耐心等待</div>
+        <div class="status-desc" v-else-if="detailData.appealStatus === 1">
+          您反馈的评价内容及账号行为经审核,暂未发现违规行为。建议您可以通过商家的回复功能对具体情况进行解释和沟通,以便于更好的解决问题。
+        </div>
+        <div class="status-desc" v-else-if="detailData.appealStatus === 2">
+          您反馈的评价内容及账号行为经审核,已发现违规行为。现已根据规定将此条评论进行删除,平台也会持续收集违规行为,一经发现,将按照规定处理。
+        </div>
       </div>
     </div>
 
@@ -38,7 +44,7 @@
         <div class="card-label">顾客评价</div>
         <div class="review-header">
           <div class="user-info">
-            <el-avatar :src="detailData.userAvatar" :size="40">
+            <el-avatar :src="detailData.userImage" :size="40">
               <el-icon><User /></el-icon>
             </el-avatar>
             <div class="user-details">
@@ -56,15 +62,32 @@
           {{ detailData.commentContent }}
         </div>
 
-        <div v-if="detailData.commentImages && detailData.commentImages.length > 0" class="review-images">
-          <el-image
-            v-for="(img, index) in detailData.commentImages"
-            :key="index"
-            :src="img"
-            :preview-src-list="detailData.commentImages"
-            fit="cover"
-            class="review-image"
-          />
+        <div v-if="commentMedia.length > 0" class="review-media">
+          <template v-for="(m, idx) in commentMedia" :key="'c-' + idx">
+            <el-image
+              v-if="m.type === 'image'"
+              :src="m.url"
+              :preview-src-list="commentMediaImages"
+              :initial-index="getMediaImageIndex(commentMedia, idx)"
+              fit="cover"
+              class="media-item review-image"
+            />
+            <div v-else class="media-item media-video-wrap" @click="openVideoPreview(m.url)">
+              <video
+                :src="m.url"
+                class="video-thumb"
+                preload="metadata"
+                muted
+                playsinline
+                @loadeddata="onVideoLoadedData($event)"
+              />
+              <div class="video-mask">
+                <el-icon class="video-play-icon">
+                  <VideoPlay />
+                </el-icon>
+              </div>
+            </div>
+          </template>
         </div>
       </div>
 
@@ -85,27 +108,57 @@
         </div>
         <div class="info-item">
           <span class="info-label">申诉图片</span>
-          <div v-if="detailData.imgList && detailData.imgList.length > 0" class="appeal-images">
-            <el-image
-              v-for="(img, index) in detailData.imgList"
-              :key="index"
-              :src="img"
-              :preview-src-list="detailData.imgList"
-              fit="cover"
-              class="appeal-image"
-            />
+          <div v-if="appealMedia.length > 0" class="appeal-media" style="width: 200px">
+            <template v-for="(m, idx) in appealMedia" :key="'a-' + idx">
+              <el-image
+                v-if="m.type === 'image'"
+                :src="m.url"
+                :preview-src-list="appealMediaImages"
+                :initial-index="getMediaImageIndex(appealMedia, idx)"
+                fit="cover"
+                class="media-item appeal-image"
+              />
+              <div v-else class="media-item media-video-wrap" @click="openVideoPreview(m.url)">
+                <video
+                  :src="m.url"
+                  class="video-thumb"
+                  preload="metadata"
+                  muted
+                  playsinline
+                  @loadeddata="onVideoLoadedData($event)"
+                />
+                <div class="video-mask">
+                  <el-icon class="video-play-icon">
+                    <VideoPlay />
+                  </el-icon>
+                </div>
+              </div>
+            </template>
           </div>
           <span v-else class="info-value">--</span>
         </div>
       </div>
     </div>
+
+    <!-- 视频预览弹窗 -->
+    <el-dialog
+      v-model="videoPreviewVisible"
+      title="视频预览"
+      width="80%"
+      max-width="720px"
+      destroy-on-close
+      align-center
+      @closed="closeVideoPreview"
+    >
+      <video v-if="videoPreviewUrl" :src="videoPreviewUrl" controls autoplay style="width: 100%; max-height: 70vh" />
+    </el-dialog>
   </div>
 </template>
 
 <script setup lang="ts" name="reviewAppealDetail">
-import { ref, reactive, onMounted } from "vue";
+import { ref, reactive, computed, onMounted } from "vue";
 import { useRouter, useRoute } from "vue-router";
-import { Clock, User } from "@element-plus/icons-vue";
+import { Clock, User, VideoPlay } from "@element-plus/icons-vue";
 import { ElMessage } from "element-plus";
 import { getAppealDetail } from "@/api/modules/newLoginApi";
 import { de } from "element-plus/es/locale";
@@ -126,9 +179,53 @@ const detailData = reactive({
   appealTime: "",
   appealReason: "",
   storePhone: "",
-  imgList: [] as string[]
+  imgList: [] as string[],
+  userImage: "",
+  commentImgList: [] as string[]
 });
 
+const VIDEO_EXT = [".mp4", ".mov", ".avi", ".wmv", ".flv", ".mkv", ".webm", ".m4v"];
+const isVideoUrl = (url: string) => {
+  const lower = (url || "").toLowerCase();
+  return VIDEO_EXT.some(ext => lower.includes(ext));
+};
+interface MediaItem {
+  url: string;
+  type: "image" | "video";
+}
+const getMediaFromUrls = (urls: string[]): MediaItem[] => {
+  if (!urls?.length) return [];
+  return urls.filter(Boolean).map(url => ({ url, type: isVideoUrl(url) ? "video" : "image" }));
+};
+const getMediaImageIndex = (media: MediaItem[], currentIndex: number): number => {
+  let idx = 0;
+  for (let i = 0; i < currentIndex; i++) if (media[i].type === "image") idx++;
+  return idx;
+};
+
+const commentMedia = computed<MediaItem[]>(() => getMediaFromUrls(detailData.commentImgList || []));
+const commentMediaImages = computed(() => commentMedia.value.filter(m => m.type === "image").map(m => m.url));
+const appealMedia = computed<MediaItem[]>(() => getMediaFromUrls(detailData.imgList || []));
+const appealMediaImages = computed(() => appealMedia.value.filter(m => m.type === "image").map(m => m.url));
+
+const videoPreviewVisible = ref(false);
+const videoPreviewUrl = ref("");
+const openVideoPreview = (url: string) => {
+  videoPreviewUrl.value = url;
+  videoPreviewVisible.value = true;
+};
+const closeVideoPreview = () => {
+  videoPreviewUrl.value = "";
+  videoPreviewVisible.value = false;
+};
+const onVideoLoadedData = (e: Event) => {
+  const video = e.target as HTMLVideoElement;
+  if (video) {
+    video.currentTime = 0;
+    video.pause();
+  }
+};
+
 // 处理进度步骤
 const progressSteps = ref<Array<{ content: string; timestamp: string }>>([
   {
@@ -186,7 +283,9 @@ const loadAppealDetail = async () => {
         appealTime: data.appealTime || data.createdTime || "",
         appealReason: data.appealReason || "",
         storePhone: data.storePhone || "",
-        imgList: data.imgList || []
+        imgList: data.imgList || [],
+        userImage: data.userImage || "",
+        commentImgList: data.commentImgList || []
       });
 
       // 更新处理进度
@@ -313,15 +412,45 @@ onMounted(() => {
         line-height: 1.6;
         color: #606266;
       }
-      .review-images {
+      .review-media,
+      .appeal-media {
         display: flex;
         flex-wrap: wrap;
         gap: 8px;
-        .review-image {
+        .media-item {
+          flex-shrink: 0;
           width: 100px;
           height: 100px;
+          overflow: hidden;
           border-radius: 4px;
         }
+        .review-image,
+        .appeal-image {
+          object-fit: cover;
+        }
+        .media-video-wrap {
+          position: relative;
+          cursor: pointer;
+          background: #000000;
+          .video-thumb {
+            display: block;
+            width: 100%;
+            height: 100%;
+            object-fit: cover;
+          }
+          .video-mask {
+            position: absolute;
+            inset: 0;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            background: rgb(0 0 0 / 35%);
+            .video-play-icon {
+              font-size: 28px;
+              color: #ffffff;
+            }
+          }
+        }
       }
     }
 
@@ -344,16 +473,6 @@ onMounted(() => {
           font-size: 14px;
           color: #606266;
         }
-        .appeal-images {
-          display: flex;
-          flex-wrap: wrap;
-          gap: 8px;
-          .appeal-image {
-            width: 100px;
-            height: 100px;
-            border-radius: 4px;
-          }
-        }
       }
     }
   }

+ 337 - 142
src/views/dynamicManagement/reviewAppealHistory.vue

@@ -1,293 +1,488 @@
 <template>
   <div class="review-appeal-history-container">
-    <!-- 返回按钮 -->
     <div class="page-header">
       <el-button @click="goBack"> 返回 </el-button>
       <h2 class="page-title">申诉历史</h2>
     </div>
 
-    <!-- 标签页 -->
     <div class="tabs-section">
-      <el-tabs v-model="activeTab" @tab-click="handleTabClick">
+      <el-tabs v-model="activeTab" @tab-change="handleTabClick($event)">
         <el-tab-pane label="全部" name="all" />
         <el-tab-pane label="审核中" name="pending" />
-        <el-tab-pane label="驳回" name="rejected" />
+        <el-tab-pane label="驳回" name="rejected" />
         <el-tab-pane label="已通过" name="approved" />
       </el-tabs>
     </div>
 
-    <!-- 申诉历史列表 -->
     <div v-if="appealHistoryList.length > 0" class="appeal-history-list">
-      <div v-for="item in appealHistoryList" :key="item.id" class="appeal-history-card">
-        <div class="appeal-header">
-          <div class="user-info">
-            <el-avatar :src="item.userAvatar" :size="40">
-              <el-icon><User /></el-icon>
-            </el-avatar>
-            <div class="user-details">
-              <div class="user-name">
-                {{ item.isAnonymous == 1 || !item.userName ? "匿名用户" : item.userName }}
-              </div>
-              <div class="appeal-time">
-                {{ item.createdTime }}
-              </div>
-            </div>
-          </div>
-          <el-tag :type="getAppealStatusType(item.status)" size="large">
+      <div v-for="item in appealHistoryList" :key="item.id" class="appeal-card">
+        <!-- 标题行:顾客评价 + 状态标签 -->
+        <div class="card-title-row">
+          <span class="card-title">顾客评价</span>
+          <el-tag :class="['status-tag', getAppealStatusTagClass(item.appealStatus)]" size="default">
             {{ getAppealStatusText(item.appealStatus) }}
           </el-tag>
         </div>
 
-        <div class="appeal-content">
-          {{ item.reviewContent }}
+        <!-- 用户信息:头像、昵称、评价时间 -->
+        <div class="card-user">
+          <el-avatar :src="getUserAvatar(item)" :size="40" class="user-avatar">
+            <el-icon><User /></el-icon>
+          </el-avatar>
+          <div class="user-meta">
+            <div class="user-name">
+              {{ item.isAnonymous == 1 || !item.userName ? "匿名用户" : item.userName }}
+            </div>
+            <div class="review-time">
+              {{ formatTime(item.createdTime) }}
+            </div>
+          </div>
         </div>
 
-        <div class="appeal-footer">
-          <div class="appeal-info">
-            <div class="info-item">
-              <span class="info-label">申诉状态:</span>
-              <span>{{ item.appealReason }}</span>
-            </div>
-            <div class="info-item">
-              <span class="info-label">申诉编号:</span>
-              <span>{{ item.appealNo || "--" }}</span>
+        <!-- 评价内容 -->
+        <div class="card-content">
+          {{ getCommentContent(item) }}
+        </div>
+
+        <!-- 评价图片/视频(视频用首帧 + 播放图标,可预览) -->
+        <div v-if="getCommentMedia(item).length > 0" class="card-media">
+          <template v-for="(m, idx) in getCommentMedia(item)" :key="idx">
+            <el-image
+              v-if="m.type === 'image'"
+              :src="m.url"
+              :preview-src-list="getCommentMediaImages(item)"
+              :initial-index="getMediaImageIndex(getCommentMedia(item), idx)"
+              fit="cover"
+              class="media-item comment-img"
+            />
+            <div v-else class="media-item media-video-wrap" @click="openVideoPreview(m.url)">
+              <video
+                :src="m.url"
+                class="video-thumb"
+                preload="metadata"
+                muted
+                playsinline
+                @loadeddata="onVideoLoadedData($event)"
+              />
+              <div class="video-mask">
+                <el-icon class="video-play-icon">
+                  <VideoPlay />
+                </el-icon>
+              </div>
             </div>
+          </template>
+        </div>
+        <!-- 申诉时间 + 状态 + 查看详情 -->
+        <div class="card-appeal">
+          <div class="appeal-time-row">
+            <span class="appeal-time-label">申诉时间</span>
+            <span class="appeal-time-value">{{ formatTime(item.appealTime || item.createdTime) }}</span>
+          </div>
+          <div class="appeal-status-row">
+            <span class="appeal-status-text">
+              <el-icon v-if="item.appealStatus === 0" class="status-icon status-pending"><Clock /></el-icon>
+              <el-icon v-else-if="item.appealStatus === 2" class="status-icon status-approved"><CircleCheck /></el-icon>
+              <el-icon v-else class="status-icon status-rejected"><CircleClose /></el-icon>
+              {{ getAppealStatusDesc(item.appealStatus) }}
+            </span>
+            <el-button type="primary" link class="btn-detail" @click="viewDetail(item)"> 查看详情 </el-button>
           </div>
-          <el-button type="primary" @click="viewDetail(item)"> 查看详情 </el-button>
         </div>
       </div>
     </div>
 
-    <!-- 空状态 -->
     <el-empty v-else description="暂无申诉记录" />
 
-    <!-- 分页 -->
-    <div v-if="appealHistoryList.length > 0" class="pagination-section">
+    <div v-if="pagination.total > 0" class="pagination-section">
       <el-pagination
         v-model:current-page="pagination.page"
         v-model:page-size="pagination.pageSize"
-        :page-sizes="[10, 20, 30, 50, 100]"
+        :page-sizes="[10, 20, 30, 50]"
         :total="pagination.total"
-        layout="total, sizes, prev, pager, next, jumper"
+        layout="prev, pager, next"
+        background
         @size-change="handleSizeChange"
         @current-change="handleCurrentChange"
       />
     </div>
+
+    <!-- 视频预览弹窗 -->
+    <el-dialog
+      v-model="videoPreviewVisible"
+      title="视频预览"
+      width="80%"
+      max-width="720px"
+      destroy-on-close
+      align-center
+      @closed="closeVideoPreview"
+    >
+      <video v-if="videoPreviewUrl" :src="videoPreviewUrl" controls autoplay style="width: 100%; max-height: 70vh" />
+    </el-dialog>
   </div>
 </template>
 
 <script setup lang="ts" name="reviewAppealHistory">
 import { ref, reactive, onMounted } from "vue";
 import { useRouter } from "vue-router";
-import { ArrowLeft, User } from "@element-plus/icons-vue";
+import { User, Clock, CircleCheck, CircleClose, VideoPlay } from "@element-plus/icons-vue";
 import { ElMessage } from "element-plus";
 import { getAppealHistory } from "@/api/modules/newLoginApi";
 import { localGet } from "@/utils";
 
 const router = useRouter();
 
-// 当前激活的标签
 const activeTab = ref("all");
-
-// 申诉历史列表
 const appealHistoryList = ref<any[]>([]);
-
-// 分页
 const pagination = reactive({
   page: 1,
-  pageSize: 50,
+  pageSize: 10,
   total: 0
 });
 
-// 返回
-const goBack = () => {
-  router.back();
-};
+const goBack = () => router.back();
 
-// 标签页切换
-const handleTabClick = () => {
+const handleTabClick = (val: string | number) => {
+  activeTab.value = typeof val === "number" ? String(val) : val;
   pagination.page = 1;
   loadAppealHistory();
 };
 
-// 分页大小改变
 const handleSizeChange = (val: number) => {
   pagination.pageSize = val;
   pagination.page = 1;
   loadAppealHistory();
 };
 
-// 当前页改变
 const handleCurrentChange = (val: number) => {
   pagination.page = val;
   loadAppealHistory();
 };
 
-// 加载申诉历史
 const loadAppealHistory = async () => {
   try {
-    // 根据标签页设置 appealStatus
-    let appealStatus: string | number = "";
-    if (activeTab.value === "pending") {
-      appealStatus = 0; // 审核中
-    } else if (activeTab.value === "rejected") {
-      appealStatus = 1; // 已驳回
-    } else if (activeTab.value === "approved") {
-      appealStatus = 2; // 已通过
-    } else {
-      appealStatus = ""; // 全部:空字符串
-    }
-
+    let appealStatus =
+      activeTab.value === "all" ? "" : activeTab.value === "pending" ? 0 : activeTab.value === "rejected" ? 1 : 2;
     const params: any = {
       storeId: localGet("createdId") || "",
-      appealStatus: appealStatus,
+      appealStatus,
       pageNum: pagination.page,
       pageSize: pagination.pageSize
     };
 
-    console.log("请求参数:", params);
     const res: any = await getAppealHistory(params);
-    console.log("接口返回:", res);
+    const isOk = res?.code === 200 || res?.code === 0;
+    const data = res?.data ?? res;
 
-    if (res.code === 200 || res.code == 200) {
-      appealHistoryList.value = res.data?.records || res.data || [];
-      pagination.total = res.data?.total || 0;
+    if (isOk) {
+      appealHistoryList.value = data?.records ?? data ?? [];
+      pagination.total = data?.total ?? 0;
     } else {
       appealHistoryList.value = [];
       pagination.total = 0;
-      ElMessage.error(res.msg || "获取申诉历史失败");
+      if (res?.msg) ElMessage.error(res.msg);
     }
   } catch (error: any) {
-    console.error("获取申诉历史失败", error);
     appealHistoryList.value = [];
     pagination.total = 0;
-    ElMessage.error(error?.msg || "获取申诉历史失败");
+    ElMessage.error(error?.message || "获取申诉历史失败");
   }
 };
 
-// 查看详情
 const viewDetail = (item: any) => {
-  router.push({
-    path: "/dynamicManagement/reviewAppealDetail",
-    query: { id: item.id }
-  });
+  router.push({ path: "/dynamicManagement/reviewAppealDetail", query: { id: item.id } });
 };
 
-// 获取申诉状态类型
-const getAppealStatusType = (status: number): "success" | "warning" | "info" | "danger" => {
-  const typeMap: Record<number, "success" | "warning" | "info" | "danger"> = {
-    0: "warning", // 审核中
-    1: "danger", // 已驳回
-    2: "success" // 已通过
+const formatTime = (t: string) => {
+  if (!t) return "--";
+  return String(t).replace(/-/g, "/");
+};
+
+const getUserAvatar = (item: any) => {
+  return item.userImage ?? item.userAvatar ?? "";
+};
+
+const getCommentContent = (item: any) => {
+  return item.commentContent ?? item.reviewContent ?? item.content ?? "--";
+};
+
+const VIDEO_EXT = [".mp4", ".mov", ".avi", ".wmv", ".flv", ".mkv", ".webm", ".m4v"];
+const isVideoUrl = (url: string) => {
+  const lower = (url || "").toLowerCase();
+  return VIDEO_EXT.some(ext => lower.includes(ext));
+};
+
+interface MediaItem {
+  url: string;
+  type: "image" | "video";
+}
+const getCommentMedia = (item: any): MediaItem[] => {
+  const urls: string[] = [];
+  const list = item.commentImgId;
+  if (Array.isArray(list)) urls.push(...list.filter(Boolean));
+  else if (typeof list === "string" && list.trim())
+    urls.push(
+      ...list
+        .split(",")
+        .map((s: string) => s.trim())
+        .filter(Boolean)
+    );
+  const videoList = item.commentVideoList ?? item.videoUrls ?? item.videoList;
+  if (Array.isArray(videoList)) urls.push(...videoList.filter(Boolean));
+  else if (typeof videoList === "string" && videoList.trim())
+    urls.push(
+      ...videoList
+        .split(",")
+        .map((s: string) => s.trim())
+        .filter(Boolean)
+    );
+  return urls.map(url => ({ url, type: isVideoUrl(url) ? "video" : "image" }));
+};
+
+const getCommentMediaImages = (item: any): string[] => {
+  return getCommentMedia(item)
+    .filter(m => m.type === "image")
+    .map(m => m.url);
+};
+
+const getMediaImageIndex = (media: MediaItem[], currentIndex: number): number => {
+  let idx = 0;
+  for (let i = 0; i < currentIndex; i++) if (media[i].type === "image") idx++;
+  return idx;
+};
+
+const videoPreviewVisible = ref(false);
+const videoPreviewUrl = ref("");
+const openVideoPreview = (url: string) => {
+  videoPreviewUrl.value = url;
+  videoPreviewVisible.value = true;
+};
+const closeVideoPreview = () => {
+  videoPreviewUrl.value = "";
+  videoPreviewVisible.value = false;
+};
+const onVideoLoadedData = (e: Event) => {
+  const video = e.target as HTMLVideoElement;
+  if (video) {
+    video.currentTime = 0;
+    video.pause();
+  }
+};
+
+// appealStatus: 0=审核中 1=被驳回 2=已通过
+const getAppealStatusTagClass = (status: number) => {
+  const map: Record<number, string> = {
+    0: "tag-pending",
+    1: "tag-rejected",
+    2: "tag-approved"
   };
-  return typeMap[status] || "info";
+  return map[status] ?? "";
 };
 
-// 获取申诉状态文本
 const getAppealStatusText = (status: number) => {
-  const textMap: Record<number, string> = {
+  const map: Record<number, string> = {
     0: "审核中",
-    1: "已驳回",
+    1: "驳回",
     2: "已通过"
   };
-  return textMap[status] || "";
+  return map[status] ?? "";
 };
 
-// 初始化
-onMounted(() => {
-  loadAppealHistory();
-});
+const getAppealStatusDesc = (status: number) => {
+  const map: Record<number, string> = {
+    0: "等待平台审核",
+    1: "被驳回",
+    2: "已通过"
+  };
+  return map[status] ?? "";
+};
+
+onMounted(() => loadAppealHistory());
 </script>
 
 <style lang="scss" scoped>
 .review-appeal-history-container {
   min-height: calc(100vh - 120px);
   padding: 20px;
-  background: #ffffff;
+  background: #f5f7fa;
   .page-header {
     display: flex;
     gap: 16px;
     align-items: center;
-    margin-bottom: 24px;
+    margin-bottom: 20px;
     .page-title {
-      width: 100%;
+      flex: 1;
       margin: 0;
-      font-size: 20px;
+      font-size: 18px;
       font-weight: 600;
       color: #303133;
       text-align: center;
     }
   }
   .tabs-section {
-    margin-bottom: 24px;
+    margin-bottom: 16px;
   }
   .appeal-history-list {
     display: flex;
     flex-direction: column;
     gap: 16px;
     margin-bottom: 24px;
-    .appeal-history-card {
-      padding: 20px;
-      border: 1px solid #e4e7ed;
-      border-radius: 8px;
-      transition: all 0.3s;
-      &:hover {
-        box-shadow: 0 2px 12px rgb(0 0 0 / 10%);
-      }
-      .appeal-header {
+    .appeal-card {
+      padding: 16px;
+      overflow: hidden;
+      background: #ffffff;
+      border-radius: 12px;
+      box-shadow: 0 2px 12px rgb(0 0 0 / 6%);
+      .card-title-row {
         display: flex;
+        align-items: center;
         justify-content: space-between;
+        padding-bottom: 12px;
         margin-bottom: 16px;
-        .user-info {
-          display: flex;
-          gap: 12px;
-          align-items: center;
-          .user-details {
-            .user-name {
-              margin-bottom: 4px;
-              font-size: 15px;
-              font-weight: 600;
-              color: #303133;
-            }
-            .appeal-time {
-              font-size: 13px;
-              color: #909399;
-            }
+        border-bottom: 1px solid #ebeef5;
+        .card-title {
+          font-size: 15px;
+          font-weight: 600;
+          color: #303133;
+        }
+        .status-tag {
+          padding: 4px 10px;
+          font-size: 12px;
+          color: #ffffff;
+          border: none;
+          border-radius: 6px;
+          &.tag-pending {
+            background: #409eff;
+          }
+          &.tag-approved {
+            background: #16aa68;
+          }
+          &.tag-rejected {
+            background: #f4420a;
           }
         }
       }
-      .appeal-content {
-        margin-bottom: 16px;
+      .card-user {
+        display: flex;
+        gap: 12px;
+        align-items: center;
+        margin-bottom: 12px;
+        .user-avatar {
+          flex-shrink: 0;
+        }
+        .user-meta {
+          .user-name {
+            margin-bottom: 4px;
+            font-size: 14px;
+            font-weight: 600;
+            color: #303133;
+          }
+          .review-time {
+            font-size: 12px;
+            color: #909399;
+          }
+        }
+      }
+      .card-content {
+        margin-bottom: 12px;
         font-size: 14px;
         line-height: 1.6;
         color: #606266;
       }
-      .appeal-footer {
+      .card-media {
         display: flex;
-        align-items: flex-end;
-        justify-content: space-between;
-        padding-top: 16px;
-        border-top: 1px solid #e4e7ed;
-        .appeal-info {
-          flex: 1;
-          .info-item {
-            margin-bottom: 8px;
-            font-size: 14px;
-            &:last-child {
-              margin-bottom: 0;
+        flex-wrap: wrap;
+        gap: 8px;
+        margin-bottom: 16px;
+        .media-item {
+          flex-shrink: 0;
+          width: 80px;
+          height: 80px;
+          overflow: hidden;
+          border-radius: 8px;
+        }
+        .comment-img {
+          object-fit: cover;
+        }
+        .media-video-wrap {
+          position: relative;
+          cursor: pointer;
+          background: #000000;
+          .video-thumb {
+            display: block;
+            width: 100%;
+            height: 100%;
+            object-fit: cover;
+          }
+          .video-mask {
+            position: absolute;
+            inset: 0;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            background: rgb(0 0 0 / 35%);
+            .video-play-icon {
+              font-size: 28px;
+              color: #ffffff;
             }
-            .info-label {
-              color: #909399;
+          }
+        }
+      }
+      .card-appeal {
+        padding: 12px 16px 16px;
+        padding-top: 12px;
+        margin: 0 -16px -16px;
+        background: linear-gradient(to top, rgb(86 125 244 / 3%), transparent);
+        border-top: 1px solid #ebeef5;
+        .appeal-time-row {
+          display: flex;
+          align-items: center;
+          justify-content: space-between;
+          margin-bottom: 10px;
+          font-size: 13px;
+          .appeal-time-label {
+            color: #606266;
+          }
+          .appeal-time-value {
+            color: #909399;
+          }
+        }
+        .appeal-status-row {
+          display: flex;
+          flex-wrap: wrap;
+          gap: 8px;
+          align-items: center;
+          justify-content: space-between;
+          .appeal-status-text {
+            display: inline-flex;
+            gap: 6px;
+            align-items: center;
+            font-size: 13px;
+            color: #606266;
+            .status-icon {
+              font-size: 16px;
+              &.status-pending {
+                color: #fa913d;
+              }
+              &.status-approved {
+                color: #16aa68;
+              }
+              &.status-rejected {
+                color: #f4420a;
+              }
             }
           }
+          .btn-detail {
+            padding: 0;
+            font-size: 14px;
+          }
         }
       }
     }
   }
   .pagination-section {
     display: flex;
-    justify-content: center;
-    padding: 20px 0;
+    justify-content: flex-end;
+    padding: 16px 0;
   }
 }
 </style>