You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

623 lines
15 KiB

2 months ago
<template>
<view class="page">
<view class="title">
<view class="title-sreach">
<view class="back-btn" @tap="back" :style="{'top': menuButtonInfo.top +'px'}">
<uni-icons type="left" size="28"></uni-icons>
</view>
<view class="title-name" :style="{'padding-top': menuButtonInfo.top +'px'}">
投诉吐槽
</view>
</view>
</view>
<view class="content">
19 hours ago
<view class="service-phone-card" @tap="callPlatformService">
<view>
<view class="service-phone-label">平台客服电话</view>
<view class="service-phone-value">{{servicePhoneLoading ? '正在获取客服热线...' : '遇到问题可联系平台客服'}}</view>
</view>
<view class="service-phone-action">联系客服</view>
</view>
2 months ago
<view class="list-head">
<text>我的反馈</text>
<text class="list-tip">按处理状态和时间排序</text>
</view>
<scroll-view scroll-y class="feedback-scroll" @scrolltolower="loadMore">
<view class="feedback-item" v-for="(item,index) in feedbackList" :key="index">
<view class="item-head">
<text class="item-type">{{item.feedbackType || '投诉吐槽'}}</text>
<text :class="item.status == 1 ? 'status done' : 'status'">{{item.status == 1 ? '已回复' : '待回复'}}</text>
</view>
<view class="item-content">{{item.problemContent}}</view>
<view class="item-pictures" v-if="getPictureList(item.problemPicture).length">
<image v-for="(pic,picIndex) in getPictureList(item.problemPicture)" :key="picIndex"
:src="pic" mode="aspectFill" @tap="previewItemImage(item.problemPicture, picIndex)"></image>
</view>
<view class="item-time">{{formatTime(item.createTime)}}</view>
<view class="reply-box" v-if="item.handleOpinion">
<view class="reply-title">平台回复</view>
<view class="reply-content">{{item.handleOpinion}}</view>
<view class="item-time" v-if="item.handleTime">{{formatTime(item.handleTime)}}</view>
</view>
</view>
<view class="empty" v-if="!loading && feedbackList.length == 0">还没有提交过投诉吐槽</view>
<view class="load-more" v-if="feedbackList.length > 0">{{hasMore ? '上拉加载更多' : '没有更多了'}}</view>
</scroll-view>
</view>
<view class="bottom-feedback-bar">
<view class="feedback-entry-btn" @tap="openFeedbackPopup">我要吐槽</view>
</view>
<uni-popup ref="feedbackPopup" type="center" background-color="transparent">
<view class="feedback-popup" @touchmove.stop>
<view class="popup-head">
<view>
<view class="card-title">我要吐槽</view>
<view class="popup-subtitle">遇到问题或有建议都可以告诉我们</view>
</view>
<view class="popup-close" @tap="closeFeedbackPopup">×</view>
</view>
<picker :range="typeOptions" @change="changeType">
<view class="type-picker">{{form.feedbackType || '请选择类型'}} <text></text></view>
</picker>
<textarea class="feedback-textarea" maxlength="300" v-model="form.problemContent"
placeholder="遇到了什么问题?写下来我们会尽快处理"></textarea>
<input class="phone-input" v-model="form.contactPhone" placeholder="联系电话(选填,方便我们联系你)" />
<view class="upload-row">
<view class="upload-btn" @tap="chooseImage">
<uni-icons type="camera" size="28" color="#777"></uni-icons>
</view>
<view class="upload-preview" v-for="(pic,index) in pictureList" :key="index">
<image :src="pic" mode="aspectFill" @tap="previewImage(index)"></image>
<text class="remove-img" @tap.stop="removeImage(index)">×</text>
</view>
</view>
<view class="submit-btn" @tap="submitFeedback">提交</view>
</view>
</uni-popup>
<common-loading />
</view>
</template>
<script>
export default {
data() {
return {
menuButtonInfo: {},
typeOptions: ['投诉', '吐槽', '建议', '其他'],
pictureList: [],
loading: false,
19 hours ago
servicePhone: '',
servicePhoneLoading: false,
2 months ago
totalPages: 0,
feedbackList: [],
form: {
feedbackType: '',
problemContent: '',
contactPhone: '',
problemPicture: ''
},
searchForm: {
pageNum: 1,
pageSize: 10,
userId: uni.getStorageSync('id')
}
}
},
onLoad() {
this.menuButtonInfo = uni.getMenuButtonBoundingClientRect ? uni.getMenuButtonBoundingClientRect() : {}
this.form.contactPhone = uni.getStorageSync('mobile') || uni.getStorageSync('phone') || ''
19 hours ago
this.getPlatformServicePhone()
2 months ago
this.getFeedbackList()
},
computed: {
hasMore() {
return this.searchForm.pageNum < this.totalPages
}
},
methods: {
openFeedbackPopup() {
this.$refs.feedbackPopup.open()
},
closeFeedbackPopup() {
this.$refs.feedbackPopup.close()
},
changeType(e) {
this.form.feedbackType = this.typeOptions[e.detail.value]
},
getArea() {
try {
19 hours ago
const area = uni.getStorageSync('area')
if (!area) return {}
if (typeof area === 'object') return area
return JSON.parse(area)
2 months ago
} catch (e) {
return {}
}
},
19 hours ago
getCurrentRegionId() {
const area = this.getArea()
return area && area.id ? area.id : ''
},
makeCall(phone) {
if (!phone) {
this.tui.toast('暂无联系电话')
return
}
uni.makePhoneCall({
phoneNumber: phone
})
},
getPlatformServicePhone(callAfterFetch = false) {
if (this.servicePhone) {
if (callAfterFetch) {
this.makeCall(this.servicePhone)
}
return
}
if (this.servicePhoneLoading) return
const regionId = this.getCurrentRegionId()
if (!regionId) {
if (callAfterFetch) {
this.tui.toast('请选择区域')
}
return
}
this.servicePhoneLoading = true
this.tui.request('/mall/admin/seckillGroup/customerPhone/get', 'GET', {
regionId: regionId
}, false, true).then((res) => {
this.servicePhoneLoading = false
if (res.code == 200 && res.result) {
this.servicePhone = res.result
if (callAfterFetch) {
this.makeCall(res.result)
}
} else if (callAfterFetch) {
this.tui.toast(res.message || '获取客服电话失败')
}
}).catch(() => {
this.servicePhoneLoading = false
if (callAfterFetch) {
this.tui.toast('获取客服电话失败')
}
})
},
callPlatformService() {
this.getPlatformServicePhone(true)
},
2 months ago
getFeedbackList() {
if (this.loading) return
this.loading = true
this.tui.request("/app/problemFeedback/getProblemFeedbackList", "POST", this.searchForm, false, false).then((res) => {
this.loading = false
if (res.code == 200) {
const records = (res.result && res.result.records) || []
this.totalPages = (res.result && res.result.pages) || 0
if (this.searchForm.pageNum == 1) {
this.feedbackList = records
} else {
this.feedbackList = this.feedbackList.concat(records)
}
} else {
this.tui.toast(res.message)
}
uni.hideLoading()
}).catch(() => {
this.loading = false
uni.hideLoading()
})
},
loadMore() {
if (!this.hasMore || this.loading) return
this.searchForm.pageNum += 1
this.getFeedbackList()
},
submitFeedback() {
const content = (this.form.problemContent || '').trim()
if (!content) {
this.tui.toast('请输入投诉吐槽内容')
return
}
const area = this.getArea()
const data = {
problemContent: content,
problemPicture: this.pictureList.join(','),
userId: uni.getStorageSync('id'),
userName: uni.getStorageSync('nickName') || uni.getStorageSync('nickname') || uni.getStorageSync('userName'),
contactPhone: this.form.contactPhone,
feedbackType: this.form.feedbackType || '投诉',
regionId: area.id || '',
regionName: area.title || uni.getStorageSync('schoolName') || '',
status: 0
}
this.tui.request("/app/problemFeedback/saveOrUpdateProblemFeedback", "POST", data, false, false).then((res) => {
if (res.code == 200) {
this.tui.toast('提交成功')
this.resetForm()
this.closeFeedbackPopup()
this.searchForm.pageNum = 1
this.getFeedbackList()
} else {
this.tui.toast(res.message)
}
uni.hideLoading()
}).catch(() => {
uni.hideLoading()
})
},
resetForm() {
this.form.feedbackType = ''
this.form.problemContent = ''
this.form.problemPicture = ''
this.pictureList = []
},
chooseImage() {
if (this.pictureList.length >= 3) {
this.tui.toast('最多上传3张图片')
return
}
uni.chooseMedia({
count: 3 - this.pictureList.length,
mediaType: ['image'],
sourceType: ['camera', 'album'],
success: (res) => {
for (let i = 0; i < res.tempFiles.length; i++) {
this.uploadFile(res.tempFiles[i].tempFilePath)
}
}
})
},
async uploadFile(path) {
let hiverToken = uni.getStorageSync("hiver_token")
await uni.uploadFile({
url: this.tui.interfaceUrl() + '/upload/file',
filePath: path,
name: 'file',
header: {
"content-type": "multipart/form-data",
'accessToken': hiverToken
},
success: (uploadFileRes) => {
let pathData = JSON.parse(uploadFileRes.data)
if (pathData.result) {
this.pictureList.push(pathData.result)
}
},
fail: (err) => {
uni.showToast({
title: JSON.stringify(err),
icon: 'none'
})
}
})
},
removeImage(index) {
this.pictureList.splice(index, 1)
},
getPictureList(picture) {
if (!picture) return []
if (Array.isArray(picture)) return picture.filter(Boolean)
return String(picture).split(',').map(item => item.trim()).filter(Boolean)
},
previewImage(index) {
uni.previewImage({
urls: this.pictureList,
current: this.pictureList[index]
})
},
previewItemImage(picture, index) {
const urls = this.getPictureList(picture)
uni.previewImage({
urls,
current: urls[index]
})
},
formatTime(value) {
if (!value) return ''
const date = new Date(value)
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}`
},
back() {
uni.navigateBack()
}
}
}
</script>
<style lang="scss">
page {
width: 100%;
height: 100%;
background: #F5F8F5;
color: #00231C;
}
.page {
min-height: 100vh;
}
.title {
height: 200rpx;
background: #F5F8F5;
}
.title-sreach {
position: relative;
display: flex;
height: 200rpx;
}
.back-btn {
position: absolute;
left: 24rpx;
bottom: 0;
}
.title-name {
flex: 1;
text-align: center;
font-size: 36rpx;
font-weight: 900;
}
.content {
padding: 0 24rpx 180rpx;
}
19 hours ago
.service-phone-card {
display: flex;
align-items: center;
justify-content: space-between;
margin: 0 4rpx 24rpx;
padding: 24rpx 10rpx;
border-radius: 28rpx;
background: linear-gradient(135deg, #FFFFFF 0%, #ECFFF7 100%);
box-shadow: 0 12rpx 40rpx rgba(30, 80, 60, .06);
}
.service-phone-label {
color: #60706C;
font-size: 24rpx;
font-weight: 700;
}
.service-phone-value {
margin-top: 8rpx;
color: #00231C;
font-size: 26rpx;
font-weight: 700;
}
.service-phone-action {
min-width: 108rpx;
height: 56rpx;
padding: 0 24rpx;
border-radius: 999rpx;
background: #34C8A0;
color: #fff;
font-size: 24rpx;
font-weight: 900;
line-height: 56rpx;
text-align: center;
}
2 months ago
.feedback-popup,
.feedback-item {
border-radius: 28rpx;
background: #fff;
box-shadow: 0 12rpx 40rpx rgba(30, 80, 60, .06);
}
.feedback-popup {
width: 660rpx;
max-height: 86vh;
padding: 30rpx;
box-sizing: border-box;
}
.popup-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
}
.popup-subtitle {
margin-top: 8rpx;
color: #8A9199;
font-size: 24rpx;
font-weight: 400;
}
.popup-close {
width: 56rpx;
height: 56rpx;
border-radius: 50%;
background: #F7F8F8;
color: #8A9199;
font-size: 42rpx;
line-height: 50rpx;
text-align: center;
}
.card-title,
.list-head {
font-size: 32rpx;
font-weight: 900;
}
.type-picker,
.phone-input {
height: 82rpx;
margin-top: 22rpx;
padding: 0 24rpx;
border-radius: 20rpx;
background: #F7F8F8;
font-size: 28rpx;
line-height: 82rpx;
}
.feedback-textarea {
width: 100%;
height: 210rpx;
margin-top: 22rpx;
padding: 24rpx;
border-radius: 20rpx;
background: #F7F8F8;
box-sizing: border-box;
font-size: 28rpx;
}
.upload-row,
.item-pictures {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
margin-top: 22rpx;
}
.upload-btn,
.upload-preview image,
.item-pictures image {
width: 150rpx;
height: 150rpx;
border-radius: 18rpx;
background: #F7F8F8;
}
.upload-btn {
text-align: center;
line-height: 150rpx;
}
.upload-preview {
position: relative;
}
.remove-img {
position: absolute;
top: -12rpx;
right: -12rpx;
width: 36rpx;
height: 36rpx;
border-radius: 50%;
background: #FF5A4F;
color: #fff;
text-align: center;
line-height: 36rpx;
}
.submit-btn {
height: 86rpx;
margin-top: 28rpx;
border-radius: 999rpx;
background: #34C8A0;
color: #fff;
font-size: 30rpx;
font-weight: 900;
line-height: 86rpx;
text-align: center;
}
.list-head {
display: flex;
justify-content: space-between;
margin: 0 4rpx 18rpx;
}
.list-tip,
.item-time {
color: #8A9199;
font-size: 22rpx;
font-weight: 400;
}
.feedback-scroll {
19 hours ago
height: calc(100vh - 476rpx);
2 months ago
min-height: 520rpx;
}
.feedback-item {
margin-bottom: 20rpx;
padding: 24rpx;
}
.item-head {
display: flex;
align-items: center;
justify-content: space-between;
}
.item-type {
font-size: 28rpx;
font-weight: 900;
}
.status {
padding: 6rpx 14rpx;
border-radius: 999rpx;
background: #FFF0E6;
color: #FF7A3D;
font-size: 22rpx;
}
.status.done {
background: #E8FFF5;
color: #34C8A0;
}
.item-content,
.reply-content {
margin-top: 16rpx;
font-size: 28rpx;
line-height: 44rpx;
word-break: break-all;
}
.reply-box {
margin-top: 20rpx;
padding: 18rpx;
border-radius: 18rpx;
background: #F7F8F8;
}
.reply-title {
color: #34C8A0;
font-weight: 900;
}
.empty,
.load-more {
padding: 38rpx 0;
color: #8A9199;
text-align: center;
}
.bottom-feedback-bar {
position: fixed;
right: 0;
bottom: 0;
left: 0;
z-index: 9;
padding: 20rpx 28rpx calc(20rpx + env(safe-area-inset-bottom));
background: rgba(245, 248, 245, .96);
box-sizing: border-box;
}
.feedback-entry-btn {
height: 88rpx;
border-radius: 999rpx;
background: #34C8A0;
color: #fff;
font-size: 32rpx;
font-weight: 900;
line-height: 88rpx;
text-align: center;
box-shadow: 0 14rpx 30rpx rgba(52, 200, 160, .24);
}
</style>