pet/pages/profile/feedback.vue

995 lines
20 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 page-container-with-bg">
<!-- 反馈表单卡片 -->
<view class="feedback-form-card">
<view class="card-header">
<text class="card-title">意见反馈</text>
<text class="card-desc">您的建议是我们改进的动力</text>
</view>
<view class="form-content">
<view class="form-group">
<text class="form-label">反馈类型 *</text>
<view class="type-options">
<view
class="type-option"
v-for="type in feedbackTypes"
:key="type.value"
:class="{ active: formData.type === type.value }"
@click="selectType(type.value)"
>
<view class="option-icon">{{ type.icon }}</view>
<text class="option-text">{{ type.label }}</text>
</view>
</view>
</view>
<view class="form-group">
<text class="form-label">问题描述 *</text>
<u-textarea
v-model="formData.content"
placeholder="请详细描述您遇到的问题或建议..."
:maxlength="500"
count
:custom-style="textareaStyle"
></u-textarea>
</view>
<view class="form-group">
<text class="form-label">联系方式</text>
<u-input
v-model="formData.contact"
placeholder="请留下您的联系方式(可选)"
border="none"
:custom-style="inputStyle"
></u-input>
</view>
<view class="form-group">
<text class="form-label">截图上传</text>
<view class="upload-area">
<view
class="upload-item"
v-for="(image, index) in formData.images"
:key="index"
@click="previewImage(index)"
>
<image class="upload-image" :src="image" mode="aspectFill" />
<view class="upload-delete" @click.stop="deleteImage(index)">
<text class="delete-icon">×</text>
</view>
</view>
<view
class="upload-add"
v-if="formData.images.length < 3"
@click="uploadImage"
>
<text class="add-icon">+</text>
<text class="add-text">添加截图</text>
</view>
</view>
</view>
<view class="submit-button">
<u-button
type="primary"
:custom-style="buttonStyle"
@click="submitFeedback"
:loading="submitting"
>
{{ submitting ? '提交中...' : '提交反馈' }}
</u-button>
</view>
</view>
</view>
<!-- 历史反馈卡片 -->
<view class="history-card">
<view class="card-header">
<text class="card-title">历史反馈</text>
<view class="header-action" @click="refreshHistory">
<text class="action-text">刷新</text>
</view>
</view>
<view class="history-list" v-if="historyList.length > 0">
<view
class="history-item"
v-for="item in historyList"
:key="item.id"
@click="viewFeedbackDetail(item)"
>
<view class="item-header">
<view class="item-type">
<text class="type-icon">{{ getTypeIcon(item.type) }}</text>
<text class="type-text">{{ getTypeText(item.type) }}</text>
</view>
<view class="item-status" :class="item.status">
<text class="status-text">{{ getStatusText(item.status) }}</text>
</view>
</view>
<view class="item-content">
<text class="content-text">{{ item.content }}</text>
</view>
<view class="item-footer">
<text class="submit-time">{{ formatTime(item.submitTime) }}</text>
<text class="item-arrow">→</text>
</view>
</view>
</view>
<view class="empty-history" v-else>
<view class="empty-icon">📝</view>
<view class="empty-text">暂无反馈记录</view>
</view>
</view>
<!-- 常见问题卡片 -->
<view class="faq-card">
<view class="card-header">
<text class="card-title">常见问题</text>
</view>
<view class="faq-list">
<view
class="faq-item"
v-for="faq in faqList"
:key="faq.id"
@click="toggleFaq(faq)"
>
<view class="faq-question">
<text class="question-text">{{ faq.question }}</text>
<text class="question-arrow" :class="{ expanded: faq.expanded }">▼</text>
</view>
<view class="faq-answer" v-if="faq.expanded">
<text class="answer-text">{{ faq.answer }}</text>
</view>
</view>
</view>
</view>
<!-- 联系我们卡片 -->
<view class="contact-card">
<view class="card-header">
<text class="card-title">联系我们</text>
</view>
<view class="contact-list">
<view class="contact-item" @click="copyContact('email')">
<view class="contact-info">
<text class="contact-icon">📧</text>
<text class="contact-text">邮箱support@petai.com</text>
</view>
<view class="contact-action">
<text class="action-text">复制</text>
</view>
</view>
<view class="contact-item" @click="copyContact('qq')">
<view class="contact-info">
<text class="contact-icon">💬</text>
<text class="contact-text">QQ群123456789</text>
</view>
<view class="contact-action">
<text class="action-text">复制</text>
</view>
</view>
<view class="contact-item" @click="copyContact('wechat')">
<view class="contact-info">
<text class="contact-icon">💚</text>
<text class="contact-text">微信PetAI_Support</text>
</view>
<view class="contact-action">
<text class="action-text">复制</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
import { reactive, ref, onMounted } from 'vue'
export default {
name: 'FeedbackPage',
setup() {
// 响应式数据
const formData = reactive({
type: '',
content: '',
contact: '',
images: []
})
const submitting = ref(false)
const historyList = ref([])
const faqList = ref([])
const feedbackTypes = ref([
{ value: 'bug', label: '功能异常', icon: '🐛' },
{ value: 'suggestion', label: '功能建议', icon: '💡' },
{ value: 'ui', label: '界面问题', icon: '🎨' },
{ value: 'other', label: '其他问题', icon: '❓' }
])
// 样式配置
const inputStyle = {
fontSize: '28rpx',
color: '#333333',
backgroundColor: 'rgba(255, 138, 128, 0.05)',
borderRadius: '12rpx',
padding: '20rpx'
}
const textareaStyle = {
fontSize: '26rpx',
color: '#333333',
backgroundColor: 'rgba(255, 138, 128, 0.05)',
borderRadius: '12rpx',
padding: '20rpx'
}
const buttonStyle = {
background: 'linear-gradient(135deg, #FF8A80, #FFB6C1)',
borderRadius: '24rpx',
height: '88rpx',
fontSize: '32rpx'
}
// 生命周期
onMounted(() => {
loadHistory()
loadFAQ()
loadUserContact()
})
// 方法定义
const loadHistory = () => {
try {
const savedHistory = uni.getStorageSync('feedbackHistory') || []
// 如果没有历史记录,创建一些模拟数据
if (savedHistory.length === 0) {
const mockHistory = [
{
id: 'fb_1',
type: 'bug',
content: '宠物记录页面偶尔会出现加载失败的情况',
contact: '',
status: 'resolved',
submitTime: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000).toISOString(),
response: '感谢您的反馈,该问题已在最新版本中修复。'
},
{
id: 'fb_2',
type: 'suggestion',
content: '希望能增加宠物体重变化趋势图功能',
contact: '',
status: 'processing',
submitTime: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(),
response: '您的建议很棒,我们正在评估技术可行性。'
}
]
uni.setStorageSync('feedbackHistory', mockHistory)
historyList.value = mockHistory
} else {
historyList.value = savedHistory
}
} catch (error) {
console.error('加载反馈历史失败:', error)
}
}
const loadFAQ = () => {
faqList.value = [
{
id: 'faq_1',
question: '如何添加新的宠物?',
answer: '在首页点击"添加宠物"按钮,填写宠物的基本信息和上传照片即可。',
expanded: false
},
{
id: 'faq_2',
question: '如何设置宠物提醒?',
answer: '进入宠物详情页,点击"添加提醒",选择提醒类型和时间即可设置。',
expanded: false
},
{
id: 'faq_3',
question: '家庭成员如何共享宠物信息?',
answer: '在"我的"页面进入家庭管理,邀请成员加入家庭即可共享宠物信息。',
expanded: false
},
{
id: 'faq_4',
question: '如何发布领养信息?',
answer: '在领养页面点击"发布领养",填写宠物信息和领养要求即可发布。',
expanded: false
}
]
}
const loadUserContact = () => {
try {
const userInfo = uni.getStorageSync('userInfo') || {}
if (userInfo.phone) {
formData.contact = userInfo.phone
}
} catch (error) {
console.error('加载用户联系方式失败:', error)
}
}
const selectType = (type) => {
formData.type = type
}
const uploadImage = () => {
uni.chooseImage({
count: 3 - formData.images.length,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
formData.images.push(...res.tempFilePaths)
},
fail: (err) => {
console.error('选择图片失败:', err)
uni.showToast({
title: '选择图片失败',
icon: 'none'
})
}
})
}
const deleteImage = (index) => {
formData.images.splice(index, 1)
}
const previewImage = (index) => {
uni.previewImage({
urls: formData.images,
current: index
})
}
const validateForm = () => {
if (!formData.type) {
uni.showToast({
title: '请选择反馈类型',
icon: 'none'
})
return false
}
if (!formData.content.trim()) {
uni.showToast({
title: '请填写问题描述',
icon: 'none'
})
return false
}
return true
}
const submitFeedback = async () => {
if (!validateForm()) {
return
}
submitting.value = true
try {
// 模拟网络请求延迟
await new Promise(resolve => setTimeout(resolve, 2000))
const feedback = {
id: 'fb_' + Date.now(),
type: formData.type,
content: formData.content.trim(),
contact: formData.contact.trim(),
images: formData.images,
status: 'pending',
submitTime: new Date().toISOString(),
response: ''
}
// 保存到历史记录
historyList.value.unshift(feedback)
uni.setStorageSync('feedbackHistory', historyList.value)
// 重置表单
Object.assign(formData, {
type: '',
content: '',
contact: formData.contact, // 保留联系方式
images: []
})
uni.showToast({
title: '反馈提交成功',
icon: 'success'
})
} catch (error) {
console.error('提交反馈失败:', error)
uni.showToast({
title: '提交失败',
icon: 'none'
})
} finally {
submitting.value = false
}
}
const refreshHistory = () => {
loadHistory()
uni.showToast({
title: '刷新成功',
icon: 'success'
})
}
const getTypeIcon = (type) => {
const typeMap = {
'bug': '🐛',
'suggestion': '💡',
'ui': '🎨',
'other': '❓'
}
return typeMap[type] || '❓'
}
const getTypeText = (type) => {
const typeMap = {
'bug': '功能异常',
'suggestion': '功能建议',
'ui': '界面问题',
'other': '其他问题'
}
return typeMap[type] || '其他问题'
}
const getStatusText = (status) => {
const statusMap = {
'pending': '待处理',
'processing': '处理中',
'resolved': '已解决',
'closed': '已关闭'
}
return statusMap[status] || '待处理'
}
const formatTime = (timeString) => {
if (!timeString) return ''
const date = new Date(timeString)
return date.toLocaleDateString('zh-CN')
}
const viewFeedbackDetail = (item) => {
let content = `类型:${getTypeText(item.type)}\n状态${getStatusText(item.status)}\n提交时间${formatTime(item.submitTime)}\n\n问题描述\n${item.content}`
if (item.response) {
content += `\n\n官方回复\n${item.response}`
}
uni.showModal({
title: '反馈详情',
content: content,
showCancel: false
})
}
const toggleFaq = (faq) => {
faq.expanded = !faq.expanded
}
const copyContact = (type) => {
let text = ''
switch (type) {
case 'email':
text = 'support@petai.com'
break
case 'qq':
text = '123456789'
break
case 'wechat':
text = 'PetAI_Support'
break
}
if (text) {
uni.setClipboardData({
data: text,
success: () => {
uni.showToast({
title: '已复制到剪贴板',
icon: 'success'
})
}
})
}
}
return {
formData,
submitting,
historyList,
faqList,
feedbackTypes,
inputStyle,
textareaStyle,
buttonStyle,
selectType,
uploadImage,
deleteImage,
previewImage,
submitFeedback,
refreshHistory,
getTypeIcon,
getTypeText,
getStatusText,
formatTime,
viewFeedbackDetail,
toggleFaq,
copyContact
}
}
}
</script>
<style lang="scss" scoped>
.feedback-container {
background: linear-gradient(135deg, #FF8A80 0%, #FFB6C1 25%, #FECFEF 50%, #F8BBD9 100%);
min-height: 100vh;
padding-bottom: 40rpx;
}
/* 通用卡片样式 */
.feedback-form-card,
.history-card,
.faq-card,
.contact-card {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20rpx);
margin: 0 30rpx 24rpx 30rpx;
border-radius: 24rpx;
padding: 32rpx;
box-shadow: 0 8rpx 32rpx rgba(255, 138, 128, 0.2);
border: 1rpx solid rgba(255, 255, 255, 0.3);
.card-header {
margin-bottom: 24rpx;
.card-title {
font-size: 32rpx;
font-weight: 600;
color: #333333;
margin-bottom: 8rpx;
}
.card-desc {
font-size: 24rpx;
color: #666666;
}
.header-action {
display: flex;
justify-content: flex-end;
.action-text {
font-size: 24rpx;
color: #FF8A80;
}
}
}
}
/* 反馈表单 */
.form-content {
.form-group {
margin-bottom: 32rpx;
&:last-child {
margin-bottom: 0;
}
.form-label {
display: block;
font-size: 28rpx;
font-weight: 500;
color: #333333;
margin-bottom: 16rpx;
}
}
.type-options {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16rpx;
.type-option {
display: flex;
flex-direction: column;
align-items: center;
padding: 24rpx 16rpx;
border: 2rpx solid rgba(255, 138, 128, 0.2);
border-radius: 16rpx;
transition: all 0.3s ease;
&.active {
border-color: #FF8A80;
background: rgba(255, 138, 128, 0.1);
.option-text {
color: #FF8A80;
font-weight: 600;
}
}
&:active {
transform: scale(0.98);
}
.option-icon {
font-size: 32rpx;
margin-bottom: 8rpx;
}
.option-text {
font-size: 24rpx;
color: #666666;
}
}
}
.upload-area {
display: flex;
gap: 16rpx;
.upload-item {
position: relative;
width: 120rpx;
height: 120rpx;
border-radius: 12rpx;
overflow: hidden;
.upload-image {
width: 100%;
height: 100%;
}
.upload-delete {
position: absolute;
top: 4rpx;
right: 4rpx;
width: 24rpx;
height: 24rpx;
background: rgba(0, 0, 0, 0.6);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
.delete-icon {
color: white;
font-size: 16rpx;
font-weight: bold;
}
}
}
.upload-add {
width: 120rpx;
height: 120rpx;
border: 2rpx dashed #FF8A80;
border-radius: 12rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: rgba(255, 138, 128, 0.05);
transition: all 0.3s ease;
&:active {
background: rgba(255, 138, 128, 0.1);
transform: scale(0.98);
}
.add-icon {
font-size: 32rpx;
color: #FF8A80;
margin-bottom: 4rpx;
}
.add-text {
font-size: 20rpx;
color: #FF8A80;
}
}
}
.submit-button {
margin-top: 40rpx;
}
}
/* 历史反馈 */
.history-card {
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
}
.history-list {
.history-item {
padding: 20rpx 0;
border-bottom: 1rpx solid rgba(255, 255, 255, 0.3);
transition: all 0.3s ease;
&:last-child {
border-bottom: none;
}
&:active {
background: rgba(255, 138, 128, 0.05);
border-radius: 16rpx;
}
.item-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12rpx;
.item-type {
display: flex;
align-items: center;
.type-icon {
font-size: 20rpx;
margin-right: 8rpx;
}
.type-text {
font-size: 24rpx;
color: #666666;
}
}
.item-status {
padding: 4rpx 12rpx;
border-radius: 12rpx;
font-size: 20rpx;
&.pending {
background: #FFF3E0;
color: #FF9800;
}
&.processing {
background: #E3F2FD;
color: #2196F3;
}
&.resolved {
background: #E8F5E8;
color: #4CAF50;
}
&.closed {
background: #F5F5F5;
color: #757575;
}
.status-text {
font-size: 20rpx;
}
}
}
.item-content {
margin-bottom: 12rpx;
.content-text {
font-size: 26rpx;
color: #333333;
line-height: 1.5;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
}
.item-footer {
display: flex;
justify-content: space-between;
align-items: center;
.submit-time {
font-size: 22rpx;
color: #999999;
}
.item-arrow {
font-size: 20rpx;
color: #FF8A80;
}
}
}
}
/* 空历史状态 */
.empty-history {
text-align: center;
padding: 60rpx 0;
.empty-icon {
font-size: 60rpx;
margin-bottom: 16rpx;
opacity: 0.5;
}
.empty-text {
font-size: 24rpx;
color: #999999;
}
}
/* 常见问题 */
.faq-list {
.faq-item {
border-bottom: 1rpx solid rgba(255, 255, 255, 0.3);
&:last-child {
border-bottom: none;
}
.faq-question {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 0;
transition: all 0.3s ease;
&:active {
background: rgba(255, 138, 128, 0.05);
border-radius: 12rpx;
}
.question-text {
flex: 1;
font-size: 28rpx;
color: #333333;
font-weight: 500;
}
.question-arrow {
font-size: 20rpx;
color: #FF8A80;
transition: transform 0.3s ease;
&.expanded {
transform: rotate(180deg);
}
}
}
.faq-answer {
padding: 0 0 20rpx 0;
animation: fadeIn 0.3s ease-out;
.answer-text {
font-size: 24rpx;
color: #666666;
line-height: 1.6;
background: rgba(255, 138, 128, 0.05);
padding: 16rpx;
border-radius: 12rpx;
}
}
}
}
/* 联系我们 */
.contact-list {
.contact-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 0;
border-bottom: 1rpx solid rgba(255, 255, 255, 0.3);
transition: all 0.3s ease;
&:last-child {
border-bottom: none;
}
&:active {
background: rgba(255, 138, 128, 0.05);
border-radius: 16rpx;
}
.contact-info {
display: flex;
align-items: center;
flex: 1;
.contact-icon {
font-size: 28rpx;
margin-right: 16rpx;
}
.contact-text {
font-size: 26rpx;
color: #333333;
}
}
.contact-action {
.action-text {
font-size: 24rpx;
color: #FF8A80;
}
}
}
}
/* 响应式设计 */
@media (max-width: 375px) {
.feedback-container {
.feedback-form-card,
.history-card,
.faq-card,
.contact-card {
margin: 0 20rpx 20rpx 20rpx;
padding: 24rpx;
}
.type-options {
gap: 12rpx;
.type-option {
padding: 20rpx 12rpx;
}
}
.upload-area {
gap: 12rpx;
.upload-item,
.upload-add {
width: 100rpx;
height: 100rpx;
}
}
}
}
/* 动画效果 */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10rpx);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.feedback-container > view {
animation: fadeIn 0.5s ease-out;
}
.feedback-container > view:nth-child(1) { animation-delay: 0.1s; }
.feedback-container > view:nth-child(2) { animation-delay: 0.2s; }
.feedback-container > view:nth-child(3) { animation-delay: 0.3s; }
.feedback-container > view:nth-child(4) { animation-delay: 0.4s; }
/* 交互反馈 */
.type-option:active,
.upload-add:active,
.history-item:active,
.faq-question:active,
.contact-item:active {
transform: scale(0.98);
}
</style>