fix:修复投诉与建议图片多文件上传bug

This commit is contained in:
2025-12-01 16:45:06 +08:00
parent 65a168daed
commit 8dd95a325b
7 changed files with 221 additions and 142 deletions
+17 -9
View File
@@ -1,18 +1,18 @@
import request from '../http' import request from '../http'
// 投诉反馈 // 新增反馈
// data 参数包含: type, content, phone, files (数组)
export const addUserFeedback = (data) => { export const addUserFeedback = (data) => {
console.log(data);
return request({ return request({
url: '/app/feedback/add', url: '/app/feedback/add',
method: 'post', method: 'post',
data, data,
hideLoading: true, // 手动控制loading,避免重复显示
}) })
} }
// 获取投诉记录列表 // 获取反馈列表
export const getFeedbackList = (params) => { export const getFeedbackList = (params) => {
// GET请求需要将参数拼接到URL上
let url = '/app/feedback/list'; let url = '/app/feedback/list';
if (params) { if (params) {
const queryString = Object.keys(params) const queryString = Object.keys(params)
@@ -23,12 +23,12 @@ export const getFeedbackList = (params) => {
} }
} }
return request({ return request({
url: url, url,
method: 'get', method: 'get',
}) })
} }
// 获取投诉详情 // 获取反馈详情(含基础信息)
export const getFeedbackDetail = (id) => { export const getFeedbackDetail = (id) => {
return request({ return request({
url: `/app/feedback/${id}`, url: `/app/feedback/${id}`,
@@ -36,10 +36,18 @@ export const getFeedbackDetail = (id) => {
}) })
} }
// 提交投诉回复 // 获取反馈对话消息
export const submitFeedbackReply = (data) => { export const getFeedbackMessages = (id) => {
return request({ return request({
url: '/app/feedback/reply', url: `/app/feedback/${id}/messages`,
method: 'get',
})
}
// 用户追加消息
export const sendFeedbackMessage = (id, data) => {
return request({
url: `/app/feedback/${id}/message`,
method: 'post', method: 'post',
data, data,
}) })
+3 -3
View File
@@ -1,7 +1,7 @@
// export const URL = "https://my.gxfs123.com/api" //正式服务器-弃用 // export const URL = "https://my.gxfs123.com/api" //正式服务器-弃用
// export const URL = "https://manager.fdzpower.com/api" //正式服务器 export const URL = "https://manager.fdzpower.com/api" //正式服务器
export const URL = "https://fansdev.gxfs123.com/api" //测试服务器 // export const URL = "https://fansdev.gxfs123.com/api" //测试服务器
// export const URL = "http://192.168.5.149:8080" //本地调试 // export const URL = "http://192.168.5.30:8080" //本地调试
// export const URL = "http://127.0.0.1:8080" //本地调试 // export const URL = "http://127.0.0.1:8080" //本地调试
export const appid = "wx2165f0be356ae7a9" //小程序appid export const appid = "wx2165f0be356ae7a9" //小程序appid
+2 -1
View File
@@ -335,6 +335,7 @@ export default {
}, },
feedback: { feedback: {
uploading: 'Uploading...',
title: 'Feedback', title: 'Feedback',
placeholder: 'Describe the issue', placeholder: 'Describe the issue',
submit: 'Submit', submit: 'Submit',
@@ -350,7 +351,7 @@ export default {
pleaseSelectType: 'Select type', pleaseSelectType: 'Select type',
pleaseDescribe: 'Describe issue', pleaseDescribe: 'Describe issue',
pleaseContact: 'Leave contact', pleaseContact: 'Leave contact',
imageUploadFailed: 'Upload failed', imageUploadFailed: 'Image upload failed, please try again',
deviceFault: 'Device Fault', deviceFault: 'Device Fault',
chargingIssue: 'Charging', chargingIssue: 'Charging',
usageSuggestion: 'Suggestion', usageSuggestion: 'Suggestion',
+2 -1
View File
@@ -335,6 +335,7 @@ export default {
}, },
feedback: { feedback: {
uploading: '上传中...',
title: '投诉与建议', title: '投诉与建议',
placeholder: '请详细描述您遇到的问题,以便我们更好地为您解决', placeholder: '请详细描述您遇到的问题,以便我们更好地为您解决',
submit: '提交反馈', submit: '提交反馈',
@@ -350,7 +351,7 @@ export default {
pleaseSelectType: '请选择问题类型', pleaseSelectType: '请选择问题类型',
pleaseDescribe: '请描述您的问题', pleaseDescribe: '请描述您的问题',
pleaseContact: '请留下联系方式', pleaseContact: '请留下联系方式',
imageUploadFailed: '图片上传失败', imageUploadFailed: '图片上传失败,请重试',
deviceFault: '设备故障', deviceFault: '设备故障',
chargingIssue: '收费问题', chargingIssue: '收费问题',
usageSuggestion: '使用建议', usageSuggestion: '使用建议',
+118 -47
View File
@@ -12,11 +12,22 @@
</view> </view>
</view> </view>
<!-- 标题 -->
<view class="title-section">
<text class="title-text">{{ detail.title || '-' }}</text>
</view>
<!-- 问题描述 --> <!-- 问题描述 -->
<view class="content-section"> <view class="content-section">
<view class="content-text">{{ detail.content || '-' }}</view> <view class="content-text">{{ detail.content || '-' }}</view>
</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-section" v-if="getImageList(detail).length > 0">
<view class="image-list"> <view class="image-list">
@@ -39,14 +50,14 @@
<view class="divider-line"></view> <view class="divider-line"></view>
</view> </view>
<view class="reply-list"> <view class="reply-list">
<view class="reply-item" v-for="(reply, index) in allReplies" :key="index" <view class="reply-item" v-for="(reply, index) in allReplies" :key="reply.id || index"
:class="{ 'reply-platform': reply.isPlatform, 'reply-user': !reply.isPlatform }"> :class="{ 'reply-platform': reply.isPlatform, 'reply-user': !reply.isPlatform }">
<view class="reply-avatar" :class="{ 'avatar-platform': reply.isPlatform, 'avatar-user': !reply.isPlatform }"> <view class="reply-avatar" :class="{ 'avatar-platform': reply.isPlatform, 'avatar-user': !reply.isPlatform }">
<text class="avatar-text">{{ reply.isPlatform ? '平台' : '我' }}</text> <text class="avatar-text">{{ reply.isPlatform ? '平台' : '我' }}</text>
</view> </view>
<view class="reply-content-wrapper"> <view class="reply-content-wrapper">
<view class="reply-name-time"> <view class="reply-name-time">
<text class="reply-name">{{ reply.isPlatform ? $t('feedback.platform') : $t('feedback.me') }}</text> <text class="reply-name">{{ reply.senderName || (reply.isPlatform ? $t('feedback.platform') : $t('feedback.me')) }}</text>
<text class="reply-time">{{ formatTime(reply.createTime) }}</text> <text class="reply-time">{{ formatTime(reply.createTime) }}</text>
</view> </view>
<view class="reply-content">{{ reply.content || '-' }}</view> <view class="reply-content">{{ reply.content || '-' }}</view>
@@ -56,7 +67,7 @@
</view> </view>
<!-- 底部输入框 --> <!-- 底部输入框 -->
<view class="bottom-input-bar" v-if="detail.status === 'processing'"> <view class="bottom-input-bar" v-if="canSendMessage">
<view class="input-wrapper"> <view class="input-wrapper">
<textarea class="reply-input" v-model="replyContent" :placeholder="$t('feedback.replyPlaceholder')" <textarea class="reply-input" v-model="replyContent" :placeholder="$t('feedback.replyPlaceholder')"
maxlength="500" :auto-height="true"></textarea> maxlength="500" :auto-height="true"></textarea>
@@ -71,14 +82,16 @@
<script setup> <script setup>
import { import {
ref, ref,
onMounted onMounted,
computed
} from 'vue'; } from 'vue';
import { import {
onLoad onLoad
} from '@dcloudio/uni-app'; } from '@dcloudio/uni-app';
import { import {
getFeedbackDetail, getFeedbackDetail,
submitFeedbackReply getFeedbackMessages,
sendFeedbackMessage
} from '../../config/api/feedback.js'; } from '../../config/api/feedback.js';
import { import {
useI18n useI18n
@@ -97,12 +110,15 @@
// 初始化状态 // 初始化状态
const detail = ref({}); const detail = ref({});
const replyList = ref([]);
const userReplyList = ref([]);
const allReplies = ref([]); const allReplies = ref([]);
const replyContent = ref(''); const replyContent = ref('');
const feedbackId = ref(''); const feedbackId = ref('');
const canSendMessage = computed(() => {
const status = detail.value?.status
return ['pending', 'in_progress'].includes(status)
})
// 页面加载 // 页面加载
onLoad(async (options) => { onLoad(async (options) => {
if (options.id) { if (options.id) {
@@ -119,33 +135,49 @@
} }
}); });
// 加载详情 const normalizeMessages = (messages = []) => {
const loadDetail = async () => { return (messages || [])
try { .map(message => ({
uni.showLoading({ ...message,
title: $t('common.loading') 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); const res = await getFeedbackDetail(feedbackId.value);
if (res.code === 200 && res.data) { if (res.code === 200 && res.data) {
detail.value = res.data; detail.value = res.data;
await loadMessages(res.data.messages);
// 分离平台回复和用户回复
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 { } else {
uni.showToast({ uni.showToast({
title: res.msg || $t('feedback.getDetailFailed'), title: res.msg || $t('feedback.getDetailFailed'),
@@ -165,7 +197,9 @@
uni.navigateBack(); uni.navigateBack();
}, 1500); }, 1500);
} finally { } finally {
uni.hideLoading(); if (shouldShowLoading) {
uni.hideLoading();
}
} }
}; };
@@ -184,8 +218,7 @@
title: $t('common.submitting') title: $t('common.submitting')
}); });
const res = await submitFeedbackReply({ const res = await sendFeedbackMessage(feedbackId.value, {
feedbackId: feedbackId.value,
content: replyContent.value.trim() content: replyContent.value.trim()
}); });
@@ -195,8 +228,10 @@
icon: 'success' icon: 'success'
}); });
replyContent.value = ''; replyContent.value = '';
// 重新加载详情 // 重新加载详情和对话
await loadDetail(); await loadDetail({
showLoading: false
});
} else { } else {
uni.showToast({ uni.showToast({
title: res.msg || $t('feedback.replyFailed'), title: res.msg || $t('feedback.replyFailed'),
@@ -217,9 +252,9 @@
// 获取状态文本 // 获取状态文本
const getStatusText = (status) => { const getStatusText = (status) => {
const statusMap = { const statusMap = {
'processing': $t('feedback.processing'), 'pending': $t('feedback.pending'),
'completed': $t('feedback.completed'), 'in_progress': $t('feedback.processing'),
'pending': $t('feedback.pending') 'resolved': $t('feedback.completed')
}; };
return statusMap[status] || $t('feedback.pending'); return statusMap[status] || $t('feedback.pending');
}; };
@@ -227,9 +262,9 @@
// 获取状态样式类 // 获取状态样式类
const getStatusClass = (status) => { const getStatusClass = (status) => {
const classMap = { const classMap = {
'processing': 'status-processing', 'pending': 'status-pending',
'completed': 'status-completed', 'in_progress': 'status-processing',
'pending': 'status-pending' 'resolved': 'status-completed'
}; };
return classMap[status] || 'status-pending'; return classMap[status] || 'status-pending';
}; };
@@ -270,14 +305,17 @@
// 获取图片列表(支持字符串或数组) // 获取图片列表(支持字符串或数组)
const getImageList = (item) => { const getImageList = (item) => {
if (!item || !item.picturePath) return []; if (!item) return [];
if (Array.isArray(item.picturePath)) return item.picturePath; const pictureSource = item.pictureUrls ?? item.picturePath;
if (typeof item.picturePath === 'string') { if (!pictureSource) return [];
// 如果是逗号分隔的字符串,拆分为数组 if (Array.isArray(pictureSource)) {
if (item.picturePath.includes(',')) { return pictureSource.filter(img => !!img);
return item.picturePath.split(',').filter(img => img.trim()); }
if (typeof pictureSource === 'string') {
if (pictureSource.includes(',')) {
return pictureSource.split(',').map(img => img.trim()).filter(img => img);
} }
return [item.picturePath]; return pictureSource.trim() ? [pictureSource.trim()] : [];
} }
return []; return [];
}; };
@@ -362,6 +400,17 @@
} }
} }
// 标题
.title-section {
margin-bottom: 16rpx;
.title-text {
font-size: 32rpx;
font-weight: 600;
color: #111;
}
}
// 问题描述 // 问题描述
.content-section { .content-section {
margin-bottom: 20rpx; margin-bottom: 20rpx;
@@ -374,6 +423,28 @@
} }
} }
// 联系方式
.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 { .image-section {
margin-bottom: 20rpx; margin-bottom: 20rpx;
+63 -71
View File
@@ -71,15 +71,11 @@
onLoad onLoad
} from "@dcloudio/uni-app" } from "@dcloudio/uni-app"
import { import {
URL, addUserFeedback
appid } from '../../config/api/feedback'
} from '../../config/url'
import { import {
uploadOssResource uploadOssResource
} from '../../config/api/user' } from '../../config/api/user'
import {
addUserFeedback
} from '../../config/api/feedback'
import { import {
useI18n useI18n
} from '@/utils/i18n.js' } from '@/utils/i18n.js'
@@ -113,7 +109,6 @@
const description = ref('') const description = ref('')
const images = ref([]) const images = ref([])
const contact = ref('') const contact = ref('')
const apiUrl = URL
// 方法 // 方法
const selectType = (index) => { const selectType = (index) => {
@@ -124,29 +119,10 @@
const chooseImage = () => { const chooseImage = () => {
uni.chooseImage({ uni.chooseImage({
count: 3 - images.value.length, count: 3 - images.value.length,
success: async (res) => { success: (res) => {
console.log(res); // 直接保存本地路径,不上传
const toUpload = res.tempFilePaths || [] const toUpload = res.tempFilePaths || []
for (const localPath of toUpload) { images.value.push(...toUpload)
// 先追加本地预览,再上传并替换为远程URL
images.value.push(localPath)
try {
const remoteUrl = await uploadOssResource(localPath)
const idx = images.value.indexOf(localPath)
if (idx !== -1) {
images.value.splice(idx, 1, remoteUrl)
}
} catch (e) {
const idx = images.value.indexOf(localPath)
if (idx !== -1) {
images.value.splice(idx, 1)
}
uni.showToast({
title: '图片上传失败',
icon: 'none'
})
}
}
} }
}) })
} }
@@ -187,52 +163,68 @@
paramsType.value = 'suggestion' paramsType.value = 'suggestion'
} }
// 构建反馈数据 try {
const feedbackData = { // 显示上传进度
type: paramsType.value, uni.showLoading({
content: description.value, title: $t('feedback.uploading') || '上传中...',
phone: contact.value, mask: true
picturePath: images.value[0] })
}
uni.request({ // 先逐步上传所有文件到OSS
url: `${apiUrl}/app/feedback/add`, const files = []
method: 'POST', if (images.value.length > 0) {
data: feedbackData, for (let i = 0; i < images.value.length; i++) {
header: { const filePath = images.value[i]
'Content-Type': 'application/json', try {
'appid': appid, const remoteUrl = await uploadOssResource(filePath)
'Authorization': 'Bearer ' + uni.getStorageSync('token'), files.push(remoteUrl)
'Clientid': uni.getStorageSync('client_id') } catch (err) {
}, console.error(`文件 ${i + 1} 上传失败:`, err)
dataType: 'json', uni.hideLoading()
success: (res) => { uni.showToast({
// 兼容后端返回 { code: 200 } 或 HTTP 200 情况 title: $t('feedback.imageUploadFailed'),
if ((res.statusCode === 200) && ((res.data && res.data.code === 200) || res.data === icon: 'none'
true || res.data?.success === true)) { })
uni.showToast({ return
title: $t('feedback.submitSuccess'), }
icon: 'success'
})
setTimeout(() => {
uni.navigateBack();
}, 1500);
return
} }
}
// 构建反馈数据
const feedbackData = {
type: paramsType.value,
content: description.value,
phone: contact.value,
files: files
}
// 调用API提交反馈(使用 hideLoading 避免重复显示loading
const res = await addUserFeedback(feedbackData)
uni.hideLoading()
// 处理响应
if (res && (res.code === 200 || res === true || res?.success === true)) {
uni.showToast({ uni.showToast({
title: (res.data && (res.data.msg || res.data.message)) || $t( title: $t('feedback.submitSuccess'),
'feedback.submitFailed'), icon: 'success'
icon: 'none'
}) })
}, setTimeout(() => {
fail: (err) => { uni.navigateBack();
console.error('feedback request failed:', err) }, 1500);
} else {
uni.showToast({ uni.showToast({
title: $t('error.networkError'), title: (res && (res.msg || res.message)) || $t('feedback.submitFailed'),
icon: 'none' icon: 'none'
}) })
} }
}) } catch (err) {
console.error('feedback submit failed:', err)
uni.hideLoading()
uni.showToast({
title: $t('error.networkError') || '网络错误,请重试',
icon: 'none'
})
}
} }
</script> </script>
@@ -365,8 +357,8 @@
flex-wrap: wrap; flex-wrap: wrap;
.upload-item { .upload-item {
width: 200rpx; width: 180rpx;
height: 200rpx; height: 180rpx;
margin-right: 20rpx; margin-right: 20rpx;
margin-bottom: 20rpx; margin-bottom: 20rpx;
position: relative; position: relative;
@@ -394,8 +386,8 @@
} }
.upload-btn { .upload-btn {
width: 200rpx; width: 180rpx;
height: 200rpx; height: 180rpx;
background: #f5f5f5; background: #f5f5f5;
border-radius: 10rpx; border-radius: 10rpx;
display: flex; display: flex;
+16 -10
View File
@@ -104,17 +104,23 @@
}, },
status: '' status: ''
}, },
{
get text() {
return $t('feedback.pending')
},
status: 'pending'
},
{ {
get text() { get text() {
return $t('feedback.processing') return $t('feedback.processing')
}, },
status: 'processing' status: 'in_progress'
}, },
{ {
get text() { get text() {
return $t('feedback.completed') return $t('feedback.completed')
}, },
status: 'completed' status: 'resolved'
} }
]); ]);
@@ -145,8 +151,8 @@
loading.value = true; loading.value = true;
const status = statusTabs[currentTab.value].status; const status = statusTabs[currentTab.value].status;
const params = { const params = {
page: currentPage.value, pageNum: currentPage.value,
size: pageSize.value pageSize: pageSize.value
}; };
if (status) { if (status) {
params.status = status; params.status = status;
@@ -205,9 +211,9 @@
// 获取状态文本 // 获取状态文本
const getStatusText = (status) => { const getStatusText = (status) => {
const statusMap = { const statusMap = {
'processing': $t('feedback.processing'), 'pending': $t('feedback.pending'),
'completed': $t('feedback.completed'), 'in_progress': $t('feedback.processing'),
'pending': $t('feedback.pending') 'resolved': $t('feedback.completed')
}; };
return statusMap[status] || $t('feedback.pending'); return statusMap[status] || $t('feedback.pending');
}; };
@@ -215,9 +221,9 @@
// 获取状态样式类 // 获取状态样式类
const getStatusClass = (status) => { const getStatusClass = (status) => {
const classMap = { const classMap = {
'processing': 'chip-processing', 'pending': 'chip-pending',
'completed': 'chip-completed', 'in_progress': 'chip-processing',
'pending': 'chip-pending' 'resolved': 'chip-completed'
}; };
return classMap[status] || 'chip-pending'; return classMap[status] || 'chip-pending';
}; };