|
|
@@ -0,0 +1,286 @@
|
|
|
+<template>
|
|
|
+ <div class="feedback-detail">
|
|
|
+ <div class="page-header">
|
|
|
+ <el-button @click="goBack"> 返回 </el-button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-if="loading" class="loading-wrap">
|
|
|
+ <el-icon class="is-loading" :size="32">
|
|
|
+ <Loading />
|
|
|
+ </el-icon>
|
|
|
+ <span>加载中...</span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <template v-else-if="feedbackInfo.id">
|
|
|
+ <!-- 反馈内容卡片 -->
|
|
|
+ <div class="detail-card">
|
|
|
+ <div class="card-type">
|
|
|
+ {{ getTypeName(feedbackInfo.feedbackType) }}
|
|
|
+ </div>
|
|
|
+ <div class="card-meta">反馈时间:{{ feedbackInfo.feedbackTime }}</div>
|
|
|
+ <div class="card-meta" v-if="feedbackInfo.contactWay">联系方式:{{ feedbackInfo.contactWay }}</div>
|
|
|
+ <div class="card-content">
|
|
|
+ {{ feedbackInfo.content }}
|
|
|
+ </div>
|
|
|
+ <!-- 附件 -->
|
|
|
+ <div v-if="allAttachments.length > 0" class="card-attach">
|
|
|
+ <div class="attach-title">附件</div>
|
|
|
+ <div class="attach-grid">
|
|
|
+ <el-image
|
|
|
+ v-for="(url, idx) in feedbackInfo.imgUrlList"
|
|
|
+ :key="'img-' + idx"
|
|
|
+ :src="url"
|
|
|
+ fit="cover"
|
|
|
+ class="attach-img"
|
|
|
+ :preview-src-list="feedbackInfo.imgUrlList || []"
|
|
|
+ :initial-index="idx"
|
|
|
+ />
|
|
|
+ <div v-for="(url, idx) in feedbackInfo.videoUrlList" :key="'vid-' + idx" class="attach-video">
|
|
|
+ <video :src="url" controls class="video-player" />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 回复区域 -->
|
|
|
+ <div v-if="replyList.length > 0" class="reply-wrapper">
|
|
|
+ <div
|
|
|
+ v-for="(reply, index) in replyList"
|
|
|
+ :key="'reply-' + index"
|
|
|
+ class="reply-section"
|
|
|
+ :class="reply.isPlatform ? 'platform' : 'user'"
|
|
|
+ >
|
|
|
+ <div class="section-title">
|
|
|
+ {{ reply.isPlatform ? "平台回复我" : "我的回复" }}
|
|
|
+ </div>
|
|
|
+ <div class="reply-card">
|
|
|
+ <div class="reply-content">
|
|
|
+ {{ reply.content }}
|
|
|
+ </div>
|
|
|
+ <div class="reply-time">
|
|
|
+ {{ reply.displayTime }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 回复输入 -->
|
|
|
+ <div v-if="hasPlatformReply" class="reply-bar">
|
|
|
+ <el-input v-model="replyContent" type="textarea" :rows="3" placeholder="回复平台" maxlength="500" show-word-limit />
|
|
|
+ <el-button type="primary" :disabled="!replyContent.trim()" @click="sendReply"> 发送 </el-button>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <div v-else-if="!loading" class="empty-tip">暂无数据</div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+import { ref, computed, onMounted } from "vue";
|
|
|
+import { useRoute, useRouter } from "vue-router";
|
|
|
+import { Loading } from "@element-plus/icons-vue";
|
|
|
+import { ElMessage } from "element-plus";
|
|
|
+import { getFeedbackDetail, userReply, getTypeName } from "@/api/modules/feedback";
|
|
|
+// 获取缓存的geeker-user
|
|
|
+import { localGet } from "@/utils/index";
|
|
|
+const route = useRoute();
|
|
|
+const router = useRouter();
|
|
|
+
|
|
|
+const feedbackId = ref<string>("");
|
|
|
+const loading = ref(true);
|
|
|
+const replyContent = ref("");
|
|
|
+
|
|
|
+const feedbackInfo = ref<{
|
|
|
+ id?: number;
|
|
|
+ feedbackType?: number;
|
|
|
+ content?: string;
|
|
|
+ feedbackTime?: string;
|
|
|
+ contactWay?: string;
|
|
|
+ imgUrlList?: string[];
|
|
|
+ videoUrlList?: string[];
|
|
|
+ platformReplies?: Array<{ content?: string; feedbackTime?: string; staffId?: number }>;
|
|
|
+}>({});
|
|
|
+
|
|
|
+const allAttachments = computed(() => {
|
|
|
+ const imgs = feedbackInfo.value.imgUrlList || [];
|
|
|
+ const videos = feedbackInfo.value.videoUrlList || [];
|
|
|
+ return [...imgs, ...videos];
|
|
|
+});
|
|
|
+
|
|
|
+const replyList = computed(() => {
|
|
|
+ const replies = feedbackInfo.value.platformReplies || [];
|
|
|
+ return replies.map(item => ({
|
|
|
+ ...item,
|
|
|
+ isPlatform: item.staffId != null,
|
|
|
+ displayTime: item.feedbackTime
|
|
|
+ }));
|
|
|
+});
|
|
|
+
|
|
|
+const hasPlatformReply = computed(() => {
|
|
|
+ const replies = feedbackInfo.value.platformReplies || [];
|
|
|
+ return replies.some(item => item.staffId != null);
|
|
|
+});
|
|
|
+
|
|
|
+async function fetchDetail(id: string) {
|
|
|
+ loading.value = true;
|
|
|
+ try {
|
|
|
+ const res: any = await getFeedbackDetail({ feedbackId: id });
|
|
|
+ if (res?.code === 200 || res?.code === "200") {
|
|
|
+ feedbackInfo.value = res?.data || res || {};
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ ElMessage.error("获取详情失败");
|
|
|
+ } finally {
|
|
|
+ loading.value = false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function goBack() {
|
|
|
+ router.back();
|
|
|
+}
|
|
|
+
|
|
|
+async function sendReply() {
|
|
|
+ if (!replyContent.value.trim()) return;
|
|
|
+ try {
|
|
|
+ const res: any = await userReply({
|
|
|
+ feedbackId: feedbackId.value,
|
|
|
+ content: replyContent.value.trim(),
|
|
|
+ userId: localGet("geeker-user")?.userInfo?.id
|
|
|
+ });
|
|
|
+ if (res?.code === 200 || res?.code === "200") {
|
|
|
+ ElMessage.success("回复成功");
|
|
|
+ replyContent.value = "";
|
|
|
+ await fetchDetail(feedbackId.value);
|
|
|
+ } else {
|
|
|
+ ElMessage.error(res?.msg || "回复失败");
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ ElMessage.error("回复失败");
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ const id = route.query.id || route.query.feedbackId;
|
|
|
+ if (id) {
|
|
|
+ feedbackId.value = String(id);
|
|
|
+ fetchDetail(feedbackId.value);
|
|
|
+ }
|
|
|
+});
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped lang="scss">
|
|
|
+.feedback-detail {
|
|
|
+ min-height: 100vh;
|
|
|
+ padding: 20px;
|
|
|
+ background: #f4f6fb;
|
|
|
+}
|
|
|
+.page-header {
|
|
|
+ margin-bottom: 20px;
|
|
|
+}
|
|
|
+.loading-wrap {
|
|
|
+ display: flex;
|
|
|
+ gap: 12px;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ padding: 60px;
|
|
|
+ color: var(--el-text-color-secondary);
|
|
|
+}
|
|
|
+.detail-card {
|
|
|
+ padding: 24px;
|
|
|
+ margin-bottom: 16px;
|
|
|
+ background: #ffffff;
|
|
|
+ border-radius: 12px;
|
|
|
+ .card-type {
|
|
|
+ margin-bottom: 8px;
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #333333;
|
|
|
+ }
|
|
|
+ .card-meta {
|
|
|
+ margin-bottom: 4px;
|
|
|
+ font-size: 13px;
|
|
|
+ color: #999999;
|
|
|
+ }
|
|
|
+ .card-content {
|
|
|
+ margin-top: 12px;
|
|
|
+ font-size: 14px;
|
|
|
+ line-height: 1.8;
|
|
|
+ color: #333333;
|
|
|
+ }
|
|
|
+ .card-attach {
|
|
|
+ margin-top: 24px;
|
|
|
+ .attach-title {
|
|
|
+ margin-bottom: 12px;
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 600;
|
|
|
+ }
|
|
|
+ .attach-grid {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 12px;
|
|
|
+ }
|
|
|
+ .attach-img {
|
|
|
+ width: 100px;
|
|
|
+ height: 100px;
|
|
|
+ border-radius: 8px;
|
|
|
+ }
|
|
|
+ .attach-video {
|
|
|
+ width: 200px;
|
|
|
+ .video-player {
|
|
|
+ width: 100%;
|
|
|
+ border-radius: 8px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+.reply-wrapper {
|
|
|
+ padding: 24px;
|
|
|
+ margin-bottom: 16px;
|
|
|
+ background: #ffffff;
|
|
|
+ border-radius: 12px;
|
|
|
+}
|
|
|
+.reply-section {
|
|
|
+ margin-bottom: 20px;
|
|
|
+ &:last-child {
|
|
|
+ margin-bottom: 0;
|
|
|
+ }
|
|
|
+ .section-title {
|
|
|
+ margin-bottom: 8px;
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 600;
|
|
|
+ }
|
|
|
+ &.user .reply-card {
|
|
|
+ background: rgb(108 143 248 / 15%);
|
|
|
+ }
|
|
|
+ &.platform .reply-card {
|
|
|
+ background: #f5f6fa;
|
|
|
+ }
|
|
|
+}
|
|
|
+.reply-card {
|
|
|
+ padding: 16px;
|
|
|
+ border-radius: 8px;
|
|
|
+ .reply-content {
|
|
|
+ margin-bottom: 8px;
|
|
|
+ font-size: 14px;
|
|
|
+ line-height: 1.6;
|
|
|
+ color: #333333;
|
|
|
+ }
|
|
|
+ .reply-time {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #999999;
|
|
|
+ }
|
|
|
+}
|
|
|
+.reply-bar {
|
|
|
+ padding: 16px;
|
|
|
+ background: #ffffff;
|
|
|
+ border-radius: 12px;
|
|
|
+ .el-textarea {
|
|
|
+ margin-bottom: 12px;
|
|
|
+ }
|
|
|
+}
|
|
|
+.empty-tip {
|
|
|
+ padding: 60px;
|
|
|
+ color: var(--el-text-color-secondary);
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+</style>
|