Files
intc-vue-h5/src/pages_mine/pages/feedback/index.vue
2026-07-14 13:40:33 +08:00

418 lines
9.0 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-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>