| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488 |
- <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-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="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-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="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="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>
- </div>
- </div>
- </div>
- <el-empty v-else description="暂无申诉记录" />
- <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]"
- :total="pagination.total"
- 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 { 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: 10,
- total: 0
- });
- const goBack = () => router.back();
- 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 {
- let appealStatus =
- activeTab.value === "all" ? "" : activeTab.value === "pending" ? 0 : activeTab.value === "rejected" ? 1 : 2;
- const params: any = {
- storeId: localGet("createdId") || "",
- appealStatus,
- pageNum: pagination.page,
- pageSize: pagination.pageSize
- };
- const res: any = await getAppealHistory(params);
- const isOk = res?.code === 200 || res?.code === 0;
- const data = res?.data ?? res;
- if (isOk) {
- appealHistoryList.value = data?.records ?? data ?? [];
- pagination.total = data?.total ?? 0;
- } else {
- appealHistoryList.value = [];
- pagination.total = 0;
- if (res?.msg) ElMessage.error(res.msg);
- }
- } catch (error: any) {
- appealHistoryList.value = [];
- pagination.total = 0;
- ElMessage.error(error?.message || "获取申诉历史失败");
- }
- };
- const viewDetail = (item: any) => {
- router.push({ path: "/dynamicManagement/reviewAppealDetail", query: { id: item.id } });
- };
- 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 map[status] ?? "";
- };
- const getAppealStatusText = (status: number) => {
- const map: Record<number, string> = {
- 0: "审核中",
- 1: "被驳回",
- 2: "已通过"
- };
- return map[status] ?? "";
- };
- 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: #f5f7fa;
- .page-header {
- display: flex;
- gap: 16px;
- align-items: center;
- margin-bottom: 20px;
- .page-title {
- flex: 1;
- margin: 0;
- font-size: 18px;
- font-weight: 600;
- color: #303133;
- text-align: center;
- }
- }
- .tabs-section {
- margin-bottom: 16px;
- }
- .appeal-history-list {
- display: flex;
- flex-direction: column;
- gap: 16px;
- margin-bottom: 24px;
- .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;
- 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;
- }
- }
- }
- .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;
- }
- .card-media {
- display: flex;
- 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;
- }
- }
- }
- }
- .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: flex-end;
- padding: 16px 0;
- }
- }
- </style>
|