418 lines
9.0 KiB
Vue
418 lines
9.0 KiB
Vue
<template>
|
||
<view class="feedback-container">
|
||
<view class="intro-card">
|
||
<view class="intro-icon">
|
||
<uni-icons type="compose" size="28" color="#ffffff"></uni-icons>
|
||
</view>
|
||
<view class="intro-content">
|
||
<text class="intro-title">意见反馈</text>
|
||
<text class="intro-desc">请描述遇到的问题或功能建议。</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="form-card">
|
||
<view class="form-item">
|
||
<text class="label">反馈类型</text>
|
||
<picker mode="selector" :range="feedbackTypes" :value="feedbackTypeIndex" @change="handleTypeChange">
|
||
<view class="picker-row">
|
||
<text>{{ currentType }}</text>
|
||
<uni-icons type="right" size="16" color="#c0c4cc"></uni-icons>
|
||
</view>
|
||
</picker>
|
||
</view>
|
||
|
||
<view class="form-item">
|
||
<text class="label required">反馈内容</text>
|
||
<textarea
|
||
v-model="content"
|
||
class="content-input"
|
||
maxlength="500"
|
||
placeholder="请尽量说明操作步骤、看到的问题或希望增加的功能"
|
||
placeholder-class="placeholder"
|
||
/>
|
||
<text class="count-text">{{ content.length }}/500</text>
|
||
</view>
|
||
|
||
<view class="form-item">
|
||
<text class="label">联系方式</text>
|
||
<input
|
||
v-model="contact"
|
||
class="contact-input"
|
||
maxlength="100"
|
||
placeholder="选填,便于客服进一步沟通"
|
||
placeholder-class="placeholder"
|
||
/>
|
||
</view>
|
||
|
||
<view class="form-item">
|
||
<view class="label-row">
|
||
<text class="label">截图</text>
|
||
<text class="tip-text">选填,最多 3 张</text>
|
||
</view>
|
||
<view class="image-list">
|
||
<view v-for="(image, index) in imageList" :key="image" class="image-item">
|
||
<image :src="image" mode="aspectFill"></image>
|
||
<view class="remove-image" @click="removeImage(index)">
|
||
<uni-icons type="closeempty" size="18" color="#ffffff"></uni-icons>
|
||
</view>
|
||
</view>
|
||
<view v-if="imageList.length < 3" class="add-image" @click="chooseImages">
|
||
<uni-icons type="plusempty" size="28" color="#909399"></uni-icons>
|
||
<text>添加截图</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="submit-section">
|
||
<button class="submit-btn" :loading="submitting" :disabled="submitting" @click="handleSubmit">
|
||
{{ submitting ? '提交中...' : '提交反馈' }}
|
||
</button>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { computed, ref } from 'vue'
|
||
import appConfig from '@/config'
|
||
import { submitFeedback, uploadFeedbackImage } from '@/api/system/feedback'
|
||
|
||
const feedbackTypes = ['功能建议', '问题反馈', '数据异常', '其他']
|
||
const feedbackTypeIndex = ref(0)
|
||
const content = ref('')
|
||
const contact = ref('')
|
||
const imageList = ref([])
|
||
const submitting = ref(false)
|
||
const serviceWechat = '17753252359'
|
||
|
||
const currentType = computed(() => feedbackTypes[feedbackTypeIndex.value])
|
||
|
||
function handleTypeChange(e) {
|
||
feedbackTypeIndex.value = Number(e.detail.value)
|
||
}
|
||
|
||
function chooseImages() {
|
||
const restCount = 3 - imageList.value.length
|
||
if (restCount <= 0 || submitting.value) return
|
||
|
||
uni.chooseImage({
|
||
count: restCount,
|
||
sizeType: ['compressed'],
|
||
success: (res) => {
|
||
imageList.value = imageList.value.concat(res.tempFilePaths || []).slice(0, 3)
|
||
}
|
||
})
|
||
}
|
||
|
||
function removeImage(index) {
|
||
if (submitting.value) return
|
||
imageList.value.splice(index, 1)
|
||
}
|
||
|
||
async function handleSubmit() {
|
||
if (submitting.value) return
|
||
|
||
const feedbackContent = content.value.trim()
|
||
if (!feedbackContent) {
|
||
uni.showToast({
|
||
title: '请先填写反馈内容',
|
||
icon: 'none'
|
||
})
|
||
return
|
||
}
|
||
|
||
submitting.value = true
|
||
uni.showLoading({ title: imageList.value.length ? '上传截图中...' : '提交中...' })
|
||
|
||
try {
|
||
const imageUrls = await uploadImages(imageList.value)
|
||
uni.showLoading({ title: '提交中...' })
|
||
|
||
await submitFeedback({
|
||
feedbackType: currentType.value,
|
||
content: feedbackContent,
|
||
contact: contact.value.trim(),
|
||
images: imageUrls.join(','),
|
||
source: getFeedbackSource()
|
||
})
|
||
|
||
content.value = ''
|
||
contact.value = ''
|
||
imageList.value = []
|
||
feedbackTypeIndex.value = 0
|
||
|
||
uni.hideLoading()
|
||
uni.showModal({
|
||
title: '提交成功',
|
||
content: '您的反馈已提交,我们会尽快处理。',
|
||
confirmText: '知道了',
|
||
showCancel: false
|
||
})
|
||
} catch (error) {
|
||
uni.hideLoading()
|
||
if (error !== '500') {
|
||
uni.showToast({
|
||
title: '提交失败,请稍后重试',
|
||
icon: 'none'
|
||
})
|
||
}
|
||
} finally {
|
||
submitting.value = false
|
||
}
|
||
}
|
||
|
||
async function uploadImages(paths) {
|
||
const urls = []
|
||
for (const path of paths) {
|
||
const uploadRes = await uploadFeedbackImage(path)
|
||
urls.push(normalizeUploadUrl(uploadRes))
|
||
}
|
||
return urls
|
||
}
|
||
|
||
function normalizeUploadUrl(uploadRes) {
|
||
const data = uploadRes?.data || {}
|
||
const url = data.url || data.fileName || uploadRes?.url || uploadRes?.fileName || uploadRes?.imgUrl
|
||
if (!url) {
|
||
throw new Error('上传截图失败')
|
||
}
|
||
let finalUrl
|
||
if (/^https?:\/\//.test(url)) {
|
||
finalUrl = url
|
||
} else {
|
||
finalUrl = `${appConfig.baseUrl}${url.startsWith('/') ? '' : '/'}${url}`
|
||
}
|
||
// 去除 API 网关前缀,文件访问 URL 不应包含 /prod-api
|
||
return finalUrl.replace('/prod-api/', '/')
|
||
}
|
||
|
||
function getFeedbackSource() {
|
||
// #ifdef MP-WEIXIN
|
||
return '小程序'
|
||
// #endif
|
||
// #ifdef H5
|
||
return 'H5'
|
||
// #endif
|
||
return 'App'
|
||
}
|
||
|
||
function copyServiceWechat() {
|
||
uni.setClipboardData({
|
||
data: serviceWechat,
|
||
success: () => {
|
||
uni.showToast({
|
||
title: '客服微信已复制',
|
||
icon: 'none'
|
||
})
|
||
}
|
||
})
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
page {
|
||
display: flex;
|
||
flex-direction: column;
|
||
box-sizing: border-box;
|
||
background-color: #f5f7fa;
|
||
min-height: 100%;
|
||
}
|
||
|
||
.feedback-container {
|
||
padding: 24rpx;
|
||
}
|
||
|
||
.intro-card {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 28rpx;
|
||
border-radius: 24rpx;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
box-shadow: 0 8rpx 32rpx rgba(102, 126, 234, 0.22);
|
||
margin-bottom: 24rpx;
|
||
}
|
||
|
||
.intro-icon {
|
||
width: 72rpx;
|
||
height: 72rpx;
|
||
border-radius: 20rpx;
|
||
background: rgba(255, 255, 255, 0.18);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-right: 20rpx;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.intro-content {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.intro-title {
|
||
color: #ffffff;
|
||
font-size: 34rpx;
|
||
font-weight: 700;
|
||
margin-bottom: 8rpx;
|
||
}
|
||
|
||
.intro-desc {
|
||
color: rgba(255, 255, 255, 0.86);
|
||
font-size: 24rpx;
|
||
line-height: 36rpx;
|
||
}
|
||
|
||
.form-card {
|
||
background: linear-gradient(135deg, #ffffff 0%, #f8f9fb 100%);
|
||
border-radius: 24rpx;
|
||
padding: 8rpx 28rpx 28rpx;
|
||
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.08);
|
||
}
|
||
|
||
.form-item {
|
||
padding-top: 28rpx;
|
||
}
|
||
|
||
.label-row {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.label {
|
||
display: block;
|
||
color: #303133;
|
||
font-size: 28rpx;
|
||
font-weight: 600;
|
||
margin-bottom: 16rpx;
|
||
}
|
||
|
||
.required::after {
|
||
content: '*';
|
||
color: #f56c6c;
|
||
margin-left: 4rpx;
|
||
}
|
||
|
||
.tip-text,
|
||
.count-text {
|
||
color: #909399;
|
||
font-size: 22rpx;
|
||
}
|
||
|
||
.count-text {
|
||
display: block;
|
||
text-align: right;
|
||
margin-top: 8rpx;
|
||
}
|
||
|
||
.picker-row,
|
||
.contact-input,
|
||
.content-input {
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
background: #f7f8fb;
|
||
border: 1rpx solid #e8edf3;
|
||
border-radius: 18rpx;
|
||
color: #303133;
|
||
font-size: 26rpx;
|
||
}
|
||
|
||
.picker-row {
|
||
min-height: 88rpx;
|
||
padding: 0 22rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.contact-input {
|
||
height: 88rpx;
|
||
padding: 0 22rpx;
|
||
}
|
||
|
||
.content-input {
|
||
min-height: 220rpx;
|
||
padding: 22rpx;
|
||
line-height: 38rpx;
|
||
}
|
||
|
||
.placeholder {
|
||
color: #c0c4cc;
|
||
}
|
||
|
||
.image-list {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 16rpx;
|
||
}
|
||
|
||
.image-item,
|
||
.add-image {
|
||
width: 152rpx;
|
||
height: 152rpx;
|
||
border-radius: 18rpx;
|
||
overflow: hidden;
|
||
position: relative;
|
||
}
|
||
|
||
.image-item image {
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
.remove-image {
|
||
position: absolute;
|
||
top: 0;
|
||
right: 0;
|
||
width: 42rpx;
|
||
height: 42rpx;
|
||
border-bottom-left-radius: 18rpx;
|
||
background: rgba(0, 0, 0, 0.48);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.add-image {
|
||
border: 2rpx dashed #dcdfe6;
|
||
background: #f7f8fb;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: #909399;
|
||
font-size: 22rpx;
|
||
}
|
||
|
||
.submit-section {
|
||
padding: 32rpx 0 16rpx;
|
||
}
|
||
|
||
.submit-btn,
|
||
.service-btn {
|
||
height: 88rpx;
|
||
border-radius: 44rpx;
|
||
font-size: 28rpx;
|
||
margin: 0 0 20rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
|
||
&::after {
|
||
border: none;
|
||
}
|
||
}
|
||
|
||
.submit-btn {
|
||
color: #ffffff;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
box-shadow: 0 8rpx 24rpx rgba(102, 126, 234, 0.28);
|
||
}
|
||
|
||
.service-btn {
|
||
color: #667eea;
|
||
background: #ffffff;
|
||
border: 1rpx solid rgba(102, 126, 234, 0.28);
|
||
}
|
||
</style>
|