Files
uni-fans-score/pages/feedback/detail.vue
T

575 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="feedback-detail-container">
<!-- 投诉详情卡片 -->
<view class="detail-card">
<!-- 第一行问题类型和状态 -->
<view class="card-header">
<view class="type-badge" :class="getTypeClass(detail.type)">
{{ getTypeText(detail.type) }}
</view>
<view class="status-badge" :class="getStatusClass(detail.status)">
{{ getStatusText(detail.status) }}
</view>
</view>
<!-- 问题描述 -->
<view class="content-section">
<view class="content-text">{{ detail.content || '-' }}</view>
</view>
<!-- 上传图片 -->
<view class="image-section" v-if="getImageList(detail).length > 0">
<view class="image-list">
<image v-for="(img, index) in getImageList(detail)" :key="index" :src="img" mode="aspectFill" class="detail-image"
@click="previewImage(img, index)"></image>
</view>
</view>
<!-- 提交时间 -->
<view class="time-section">
<text class="time-text">{{ formatTime(detail.createTime) }}</text>
</view>
</view>
<!-- 回复列表合并平台和用户回复按时间排序 -->
<view class="reply-section" v-if="allReplies.length > 0">
<view class="section-divider">
<view class="divider-line"></view>
<text class="divider-text">{{ $t('feedback.replyHistory') }}</text>
<view class="divider-line"></view>
</view>
<view class="reply-list">
<view class="reply-item" v-for="(reply, index) in allReplies" :key="index"
:class="{ 'reply-platform': reply.isPlatform, 'reply-user': !reply.isPlatform }">
<view class="reply-avatar" :class="{ 'avatar-platform': reply.isPlatform, 'avatar-user': !reply.isPlatform }">
<text class="avatar-text">{{ reply.isPlatform ? '平台' : '我' }}</text>
</view>
<view class="reply-content-wrapper">
<view class="reply-name-time">
<text class="reply-name">{{ reply.isPlatform ? $t('feedback.platform') : $t('feedback.me') }}</text>
<text class="reply-time">{{ formatTime(reply.createTime) }}</text>
</view>
<view class="reply-content">{{ reply.content || '-' }}</view>
</view>
</view>
</view>
</view>
<!-- 底部输入框 -->
<view class="bottom-input-bar" v-if="detail.status === 'processing'">
<view class="input-wrapper">
<textarea class="reply-input" v-model="replyContent" :placeholder="$t('feedback.replyPlaceholder')"
maxlength="500" :auto-height="true"></textarea>
</view>
<view class="submit-btn" @click="submitReply" :class="{ disabled: !replyContent.trim() }">
{{ $t('feedback.submitReply') }}
</view>
</view>
</view>
</template>
<script setup>
import {
ref,
onMounted
} from 'vue';
import {
onLoad
} from '@dcloudio/uni-app';
import {
getFeedbackDetail,
submitFeedbackReply
} from '../../config/api/feedback.js';
import {
useI18n
} from '@/utils/i18n.js'
const {
t: $t
} = useI18n()
// 设置页面标题
onMounted(() => {
uni.setNavigationBarTitle({
title: $t('feedback.detail')
})
})
// 初始化状态
const detail = ref({});
const replyList = ref([]);
const userReplyList = ref([]);
const allReplies = ref([]);
const replyContent = ref('');
const feedbackId = ref('');
// 页面加载
onLoad(async (options) => {
if (options.id) {
feedbackId.value = options.id;
await loadDetail();
} else {
uni.showToast({
title: $t('feedback.idRequired'),
icon: 'none'
});
setTimeout(() => {
uni.navigateBack();
}, 1500);
}
});
// 加载详情
const loadDetail = async () => {
try {
uni.showLoading({
title: $t('common.loading')
});
const res = await getFeedbackDetail(feedbackId.value);
if (res.code === 200 && res.data) {
detail.value = res.data;
// 分离平台回复和用户回复
const replies = res.data.replies || res.data.replyList || [];
replyList.value = replies.filter(reply => reply.replyType === 'platform' || reply.isPlatform);
userReplyList.value = replies.filter(reply => reply.replyType === 'user' || !reply.isPlatform);
// 合并所有回复并按时间排序
allReplies.value = replies
.map(reply => ({
...reply,
isPlatform: reply.replyType === 'platform' || reply.isPlatform === true
}))
.sort((a, b) => {
const timeA = new Date(a.createTime || 0).getTime();
const timeB = new Date(b.createTime || 0).getTime();
return timeA - timeB;
});
} else {
uni.showToast({
title: res.msg || $t('feedback.getDetailFailed'),
icon: 'none'
});
setTimeout(() => {
uni.navigateBack();
}, 1500);
}
} catch (error) {
console.error('获取投诉详情失败:', error);
uni.showToast({
title: $t('feedback.getDetailFailed'),
icon: 'none'
});
setTimeout(() => {
uni.navigateBack();
}, 1500);
} finally {
uni.hideLoading();
}
};
// 提交回复
const submitReply = async () => {
if (!replyContent.value.trim()) {
uni.showToast({
title: $t('feedback.pleaseEnterReply'),
icon: 'none'
});
return;
}
try {
uni.showLoading({
title: $t('common.submitting')
});
const res = await submitFeedbackReply({
feedbackId: feedbackId.value,
content: replyContent.value.trim()
});
if (res.code === 200) {
uni.showToast({
title: $t('feedback.replySuccess'),
icon: 'success'
});
replyContent.value = '';
// 重新加载详情
await loadDetail();
} else {
uni.showToast({
title: res.msg || $t('feedback.replyFailed'),
icon: 'none'
});
}
} catch (error) {
console.error('提交回复失败:', error);
uni.showToast({
title: $t('feedback.replyFailed'),
icon: 'none'
});
} finally {
uni.hideLoading();
}
};
// 获取状态文本
const getStatusText = (status) => {
const statusMap = {
'processing': $t('feedback.processing'),
'completed': $t('feedback.completed'),
'pending': $t('feedback.pending')
};
return statusMap[status] || $t('feedback.pending');
};
// 获取状态样式类
const getStatusClass = (status) => {
const classMap = {
'processing': 'status-processing',
'completed': 'status-completed',
'pending': 'status-pending'
};
return classMap[status] || 'status-pending';
};
// 获取类型文本
const getTypeText = (type) => {
const typeMap = {
'complain': $t('feedback.complain'),
'suggestion': $t('feedback.suggestion')
};
return typeMap[type] || type || '-';
};
// 获取类型样式类
const getTypeClass = (type) => {
const classMap = {
'complain': 'type-complain',
'suggestion': 'type-suggestion'
};
return classMap[type] || 'type-default';
};
// 格式化时间
const formatTime = (timeStr) => {
if (!timeStr) return '-';
try {
const date = new Date(timeStr);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hour = String(date.getHours()).padStart(2, '0');
const minute = String(date.getMinutes()).padStart(2, '0');
return `${year}-${month}-${day} ${hour}:${minute}`;
} catch (e) {
return timeStr;
}
};
// 获取图片列表(支持字符串或数组)
const getImageList = (item) => {
if (!item || !item.picturePath) return [];
if (Array.isArray(item.picturePath)) return item.picturePath;
if (typeof item.picturePath === 'string') {
// 如果是逗号分隔的字符串,拆分为数组
if (item.picturePath.includes(',')) {
return item.picturePath.split(',').filter(img => img.trim());
}
return [item.picturePath];
}
return [];
};
// 预览图片
const previewImage = (url, index) => {
const imageList = getImageList(detail.value);
uni.previewImage({
urls: imageList,
current: index !== undefined ? index : 0
});
};
</script>
<style lang="scss" scoped>
.feedback-detail-container {
min-height: 100vh;
background: #f5f5f5;
padding: 20rpx;
padding-bottom: 200rpx;
box-sizing: border-box;
// 详情卡片
.detail-card {
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
// 第一行:问题类型和状态
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24rpx;
padding-bottom: 20rpx;
border-bottom: 1rpx solid #f0f0f0;
.type-badge {
padding: 6rpx 16rpx;
border-radius: 6rpx;
font-size: 24rpx;
font-weight: 500;
&.type-complain {
background: #fff3e0;
color: #ff9800;
}
&.type-suggestion {
background: #e8f5e9;
color: #4caf50;
}
&.type-default {
background: #f5f5f5;
color: #666;
}
}
.status-badge {
padding: 6rpx 16rpx;
border-radius: 6rpx;
font-size: 24rpx;
font-weight: 500;
&.status-processing {
background: #fff3e0;
color: #ff9800;
}
&.status-completed {
background: #e8f5e9;
color: #4caf50;
}
&.status-pending {
background: #f5f5f5;
color: #999;
}
}
}
// 问题描述
.content-section {
margin-bottom: 20rpx;
.content-text {
font-size: 28rpx;
color: #333;
line-height: 1.8;
word-break: break-all;
}
}
// 图片区域
.image-section {
margin-bottom: 20rpx;
.image-list {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
.detail-image {
width: 200rpx;
height: 200rpx;
border-radius: 8rpx;
background: #f5f5f5;
}
}
}
// 提交时间
.time-section {
padding-top: 16rpx;
border-top: 1rpx solid #f0f0f0;
.time-text {
font-size: 24rpx;
color: #999;
}
}
}
// 回复区域
.reply-section {
margin-bottom: 30rpx;
.section-divider {
display: flex;
align-items: center;
margin: 30rpx 0 20rpx 0;
.divider-line {
flex: 1;
height: 1rpx;
background: #e0e0e0;
}
.divider-text {
padding: 0 20rpx;
font-size: 24rpx;
color: #999;
}
}
.reply-list {
.reply-item {
display: flex;
margin-bottom: 24rpx;
&.reply-platform {
.reply-content-wrapper {
background: #f5f5f5;
}
}
&.reply-user {
flex-direction: row-reverse;
.reply-content-wrapper {
background: #07c160;
color: #fff;
margin-right: 16rpx;
margin-left: 0;
.reply-name-time {
.reply-name,
.reply-time {
color: rgba(255, 255, 255, 0.9);
}
}
.reply-content {
color: #fff;
}
}
}
.reply-avatar {
width: 56rpx;
height: 56rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
&.avatar-platform {
background: #ff9800;
}
&.avatar-user {
background: #07c160;
}
.avatar-text {
font-size: 22rpx;
color: #fff;
font-weight: 500;
}
}
.reply-content-wrapper {
flex: 1;
background: #f5f5f5;
border-radius: 12rpx;
padding: 16rpx 20rpx;
margin-left: 16rpx;
max-width: calc(100% - 72rpx);
.reply-name-time {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8rpx;
.reply-name {
font-size: 24rpx;
color: #666;
font-weight: 500;
}
.reply-time {
font-size: 22rpx;
color: #999;
}
}
.reply-content {
font-size: 28rpx;
color: #333;
line-height: 1.6;
word-break: break-all;
}
}
}
}
}
// 底部输入栏
.bottom-input-bar {
position: fixed;
left: 0;
right: 0;
bottom: 0;
padding: 20rpx 30rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
background: #fff;
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.04);
z-index: 10;
display: flex;
align-items: flex-end;
gap: 20rpx;
.input-wrapper {
flex: 1;
background: #f5f5f5;
border-radius: 20rpx;
padding: 16rpx 20rpx;
max-height: 200rpx;
.reply-input {
width: 100%;
font-size: 28rpx;
color: #333;
min-height: 40rpx;
max-height: 200rpx;
}
}
.submit-btn {
padding: 16rpx 40rpx;
background: #07c160;
color: #fff;
border-radius: 20rpx;
font-size: 28rpx;
font-weight: 500;
white-space: nowrap;
&.disabled {
background: #ccc;
opacity: 0.6;
}
&:active:not(.disabled) {
opacity: 0.8;
}
}
}
}
</style>