646 lines
14 KiB
Vue
646 lines
14 KiB
Vue
<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="title-section">
|
||
<text class="title-text">{{ detail.title || '-' }}</text>
|
||
</view>
|
||
|
||
<!-- 问题描述 -->
|
||
<view class="content-section">
|
||
<view class="content-text">{{ detail.content || '-' }}</view>
|
||
</view>
|
||
|
||
<!-- 联系电话 -->
|
||
<view class="contact-section" v-if="detail.phone">
|
||
<text class="contact-label">{{ $t('feedback.contactPhone') }}</text>
|
||
<text class="contact-value">{{ detail.phone }}</text>
|
||
</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="reply.id || 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.senderName || (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="canSendMessage">
|
||
<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,
|
||
computed
|
||
} from 'vue';
|
||
import {
|
||
onLoad
|
||
} from '@dcloudio/uni-app';
|
||
import {
|
||
getFeedbackDetail,
|
||
getFeedbackMessages,
|
||
sendFeedbackMessage
|
||
} 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 allReplies = ref([]);
|
||
const replyContent = ref('');
|
||
const feedbackId = ref('');
|
||
|
||
const canSendMessage = computed(() => {
|
||
const status = detail.value?.status
|
||
return ['pending', 'in_progress'].includes(status)
|
||
})
|
||
|
||
// 页面加载
|
||
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 normalizeMessages = (messages = []) => {
|
||
return (messages || [])
|
||
.map(message => ({
|
||
...message,
|
||
isPlatform: message.senderType === 'staff' || message.senderType === 'platform' || message.isPlatform === true
|
||
}))
|
||
.sort((a, b) => {
|
||
const timeA = new Date(a.createTime || 0).getTime();
|
||
const timeB = new Date(b.createTime || 0).getTime();
|
||
return timeA - timeB;
|
||
});
|
||
}
|
||
|
||
const loadMessages = async (initialMessages) => {
|
||
try {
|
||
if (Array.isArray(initialMessages)) {
|
||
allReplies.value = normalizeMessages(initialMessages);
|
||
return;
|
||
}
|
||
if (!feedbackId.value) return;
|
||
const res = await getFeedbackMessages(feedbackId.value);
|
||
if (res.code === 200) {
|
||
allReplies.value = normalizeMessages(res.data || []);
|
||
}
|
||
} catch (error) {
|
||
console.error('获取对话消息失败:', error);
|
||
}
|
||
}
|
||
|
||
// 加载详情
|
||
const loadDetail = async (options = {}) => {
|
||
const shouldShowLoading = options.showLoading !== false;
|
||
try {
|
||
if (shouldShowLoading) {
|
||
uni.showLoading({
|
||
title: $t('common.loading')
|
||
});
|
||
}
|
||
|
||
const res = await getFeedbackDetail(feedbackId.value);
|
||
if (res.code === 200 && res.data) {
|
||
detail.value = res.data;
|
||
await loadMessages(res.data.messages);
|
||
} 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 {
|
||
if (shouldShowLoading) {
|
||
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 sendFeedbackMessage(feedbackId.value, {
|
||
content: replyContent.value.trim()
|
||
});
|
||
|
||
if (res.code === 200) {
|
||
uni.showToast({
|
||
title: $t('feedback.replySuccess'),
|
||
icon: 'success'
|
||
});
|
||
replyContent.value = '';
|
||
// 重新加载详情和对话
|
||
await loadDetail({
|
||
showLoading: false
|
||
});
|
||
} 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 = {
|
||
'pending': $t('feedback.pending'),
|
||
'in_progress': $t('feedback.processing'),
|
||
'resolved': $t('feedback.completed')
|
||
};
|
||
return statusMap[status] || $t('feedback.pending');
|
||
};
|
||
|
||
// 获取状态样式类
|
||
const getStatusClass = (status) => {
|
||
const classMap = {
|
||
'pending': 'status-pending',
|
||
'in_progress': 'status-processing',
|
||
'resolved': 'status-completed'
|
||
};
|
||
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) return [];
|
||
const pictureSource = item.pictureUrls ?? item.picturePath;
|
||
if (!pictureSource) return [];
|
||
if (Array.isArray(pictureSource)) {
|
||
return pictureSource.filter(img => !!img);
|
||
}
|
||
if (typeof pictureSource === 'string') {
|
||
if (pictureSource.includes(',')) {
|
||
return pictureSource.split(',').map(img => img.trim()).filter(img => img);
|
||
}
|
||
return pictureSource.trim() ? [pictureSource.trim()] : [];
|
||
}
|
||
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;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 标题
|
||
.title-section {
|
||
margin-bottom: 16rpx;
|
||
|
||
.title-text {
|
||
font-size: 32rpx;
|
||
font-weight: 600;
|
||
color: #111;
|
||
}
|
||
}
|
||
|
||
// 问题描述
|
||
.content-section {
|
||
margin-bottom: 20rpx;
|
||
|
||
.content-text {
|
||
font-size: 28rpx;
|
||
color: #333;
|
||
line-height: 1.8;
|
||
word-break: break-all;
|
||
}
|
||
}
|
||
|
||
// 联系方式
|
||
.contact-section {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 20rpx;
|
||
padding: 16rpx;
|
||
background: #fafafa;
|
||
border-radius: 12rpx;
|
||
|
||
.contact-label {
|
||
font-size: 24rpx;
|
||
color: #999;
|
||
margin-right: 16rpx;
|
||
}
|
||
|
||
.contact-value {
|
||
font-size: 28rpx;
|
||
color: #333;
|
||
font-weight: 500;
|
||
}
|
||
}
|
||
|
||
// 图片区域
|
||
.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>
|