hookehuyr

feat(反馈): 新增意见反馈列表页面及功能

添加反馈列表页面,包含以下功能:
1. 展示用户反馈历史记录
2. 支持图片预览和分页加载
3. 添加底部反馈按钮跳转
4. 更新API接口和组件类型定义
5. 优化反馈提交后的返回逻辑
......@@ -22,6 +22,7 @@ declare module 'vue' {
NutFormItem: typeof import('@nutui/nutui-taro')['FormItem']
NutImagePreview: typeof import('@nutui/nutui-taro')['ImagePreview']
NutInput: typeof import('@nutui/nutui-taro')['Input']
NutLoading: typeof import('@nutui/nutui-taro')['Loading']
NutMenu: typeof import('@nutui/nutui-taro')['Menu']
NutMenuItem: typeof import('@nutui/nutui-taro')['MenuItem']
NutOverlay: typeof import('@nutui/nutui-taro')['Overlay']
......
......@@ -12,6 +12,7 @@ const Api = {
GET_BRANDS_MODELS: '/srv/?a=common&t=get_brands_models',
GET_VEHICLE_BRANDS: '/srv/?a=common&t=get_vehicle_brands',
SUBMIT_FEEDBACK: '/srv/?a=feedback&t=add',
GET_FEEDBACK_LIST: '/srv/?a=feedback&t=list',
GET_FAVORITE_LIST: '/srv/?a=favorite&t=list',
TOGGLE_FAVORITE_ADD: '/srv/?a=favorite&t=add',
TOGGLE_FAVORITE_DEL: '/srv/?a=favorite&t=del',
......@@ -56,6 +57,15 @@ export const toggleFavoriteAddAPI = (params) => fn(fetch.post(Api.TOGGLE_FAVORIT
export const toggleFavoriteDelAPI = (params) => fn(fetch.post(Api.TOGGLE_FAVORITE_DEL, params));
/**
* @description: 获取反馈列表
* @param {*} params
* @param {number} params.page - 页码,从0开始
* @param {number} params.limit - 每页数量
* @returns
*/
export const getFeedbackListAPI = (params) => fn(fetch.get(Api.GET_FEEDBACK_LIST, params));
/**
* @description: 提交反馈
* @param {*} params
* @param {string} params.category - 反馈类型(1=功能建议,3=界面设计,5=车辆信息, 7=其他)
......
......@@ -25,6 +25,7 @@ export default {
'pages/myOrders/index',
'pages/myAuthCar/index',
'pages/feedBack/index',
'pages/feedBackList/index',
'pages/helpCenter/index',
'pages/search/index',
'pages/recommendCarList/index',
......
<!--
* @Date: 2022-09-19 14:11:06
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-07-15 13:14:50
* @LastEditTime: 2025-07-17 12:22:47
* @FilePath: /jgdl/src/pages/feedBack/index.vue
* @Description: 意见反馈页面
-->
......@@ -329,6 +329,11 @@ const closeSuccessModal = () => {
feedbackText.value = ''
contactInfo.value = ''
uploadedImages.value = []
// 返回上一页
Taro.navigateBack({
delta: 1
})
}
</script>
......
export default {
navigationBarTitleText: '意见反馈列表',
usingComponents: {
},
}
/*
* @Date: 2022-09-19 14:11:06
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-07-17 11:23:10
* @FilePath: /jgdl/src/pages/feedBackList/index.less
* @Description: 意见反馈列表页面样式
*/
.feedback-list-page {
position: relative;
height: 100vh;
background-color: #f5f5f5;
.scroll-view {
padding: 20rpx;
box-sizing: border-box;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
.feedback-list {
.feedback-card {
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
border: 1rpx solid #f0f0f0;
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
.feedback-time {
font-size: 24rpx;
color: #999;
}
.feedback-type {
background: #fb923c;
color: #fff;
padding: 8rpx 16rpx;
border-radius: 20rpx;
font-size: 22rpx;
font-weight: 500;
}
}
.feedback-content {
margin-bottom: 16rpx;
.content-text {
font-size: 28rpx;
color: #333;
line-height: 1.6;
word-break: break-all;
}
}
.feedback-images {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
margin-bottom: 16rpx;
.image-item {
width: 120rpx;
height: 120rpx;
border-radius: 8rpx;
overflow: hidden;
border: 1rpx solid #eee;
.feedback-image {
width: 100%;
height: 100%;
}
}
}
.contact-info {
background: #f8f9fa;
padding: 16rpx;
border-radius: 8rpx;
margin-bottom: 16rpx;
border-left: 4rpx solid #fb923c;
.contact-label {
font-size: 24rpx;
color: #666;
}
.contact-text {
font-size: 26rpx;
color: #333;
font-weight: 500;
}
}
.reply-section {
background: #f0f8ff;
padding: 16rpx;
border-radius: 8rpx;
border-left: 4rpx solid #fb923c;
.reply-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12rpx;
.reply-label {
font-size: 24rpx;
color: #fb923c;
font-weight: 500;
}
.reply-time {
font-size: 22rpx;
color: #999;
}
}
.reply-content {
.reply-text {
font-size: 26rpx;
color: #333;
line-height: 1.6;
word-break: break-all;
}
}
}
}
}
// 空状态样式
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 40rpx;
text-align: center;
.empty-icon {
font-size: 120rpx;
margin-bottom: 24rpx;
opacity: 0.6;
}
.empty-text {
font-size: 32rpx;
color: #666;
margin-bottom: 12rpx;
font-weight: 500;
}
.empty-desc {
font-size: 26rpx;
color: #999;
line-height: 1.5;
}
}
// 加载状态样式
.loading-container {
display: flex;
align-items: center;
justify-content: center;
padding: 40rpx;
gap: 16rpx;
.loading-text {
font-size: 26rpx;
color: #666;
}
}
// 没有更多数据样式
.no-more-container {
display: flex;
align-items: center;
justify-content: center;
padding: 40rpx;
.no-more-text {
font-size: 24rpx;
color: #999;
position: relative;
&::before,
&::after {
content: '';
position: absolute;
top: 50%;
width: 60rpx;
height: 1rpx;
background: #ddd;
}
&::before {
left: -80rpx;
}
&::after {
right: -80rpx;
}
}
}
// 底部固定按钮
.fixed-bottom {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #fff;
padding: 20rpx 30rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
border-top: 1rpx solid #eee;
box-shadow: 0 -2rpx 8rpx rgba(0, 0, 0, 0.06);
z-index: 100;
.feedback-btn {
width: 100%;
height: 88rpx;
background: #fb923c;
color: #fff;
border: none;
border-radius: 44rpx;
font-size: 32rpx;
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4rpx 12rpx rgba(251, 146, 60, 0.3);
transition: all 0.3s ease;
&:active {
transform: translateY(2rpx);
box-shadow: 0 2rpx 8rpx rgba(251, 146, 60, 0.2);
}
}
}
}
\ No newline at end of file
<!--
* @Date: 2022-09-19 14:11:06
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-07-17 12:19:40
* @FilePath: /jgdl/src/pages/feedBackList/index.vue
* @Description: 意见反馈列表页面
-->
<template>
<view class="feedback-list-page">
<!-- 滚动视图 -->
<scroll-view
class="scroll-view"
:scroll-y="true"
:style="scrollStyle"
@scrolltolower="loadMore"
:lower-threshold="50"
>
<!-- 反馈列表 -->
<view class="feedback-list" v-if="feedbackList.length > 0">
<view
v-for="(item, index) in feedbackList"
:key="index"
class="feedback-card"
>
<!-- 卡片头部:时间和类型 -->
<view class="card-header">
<view class="feedback-time">{{ formatTime(item.created_at) }}</view>
<view class="feedback-type">{{ getCategoryName(item.category) }}</view>
</view>
<!-- 反馈内容 -->
<view class="feedback-content">
<text class="content-text">{{ item.note }}</text>
</view>
<!-- 图片展示 -->
<view class="feedback-images" v-if="item.images && item.images.length > 0">
<view
v-for="(image, imgIndex) in item.images"
:key="imgIndex"
class="image-item"
@click="previewImage(item.images, imgIndex)"
>
<image :src="image" class="feedback-image" mode="aspectFill" />
</view>
</view>
<!-- 联系方式 -->
<view class="contact-info" v-if="item.contact">
<text class="contact-label">联系方式:</text>
<text class="contact-text">{{ item.contact }}</text>
</view>
<!-- 回复内容 -->
<view class="reply-section" v-if="item.reply_content">
<view class="reply-header">
<text class="reply-label">回复内容:</text>
<text class="reply-time">{{ formatTime(item.reply_time) }}</text>
</view>
<view class="reply-content">
<text class="reply-text">{{ item.reply_content }}</text>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view class="empty-state" v-if="!loading && feedbackList.length === 0">
<view class="empty-icon">📝</view>
<view class="empty-text">暂无反馈记录</view>
<view class="empty-desc">您还没有提交过任何反馈</view>
</view>
<!-- 加载状态 -->
<view class="loading-container" v-if="loading">
<text class="loading-text">加载中...</text>
</view>
<!-- 没有更多数据 -->
<view class="no-more-container" v-if="!hasMore && feedbackList.length > 0">
<text class="no-more-text">没有更多数据了</text>
</view>
</scroll-view>
<!-- 底部固定按钮 -->
<view class="fixed-bottom">
<button class="feedback-btn" @click="goToFeedback">
我要反馈
</button>
</view>
<!-- 图片预览 -->
<nut-image-preview
v-model:show="previewVisible"
:images="previewImages"
:init-no="previewIndex"
@close="closePreview"
/>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import Taro from '@tarojs/taro'
import { Toast } from '@nutui/nutui-taro'
// import { getFeedbackListAPI } from '@/api/other'
import './index.less'
// 反馈类型映射
const categoryMap = {
'1': '功能建议',
'3': '界面设计',
'5': '车辆信息',
'7': '其他问题'
}
// 响应式数据
const loading = ref(false)
const refreshing = ref(false)
const hasMore = ref(true)
const currentPage = ref(0)
const pageSize = ref(10)
const feedbackList = ref([])
// 图片预览相关
const previewVisible = ref(false)
const previewImages = ref([])
const previewIndex = ref(0)
// 计算滚动视图样式
const scrollStyle = computed(() => {
const systemInfo = Taro.getSystemInfoSync()
const windowHeight = systemInfo.windowHeight
// 动态计算底部区域高度:padding + 按钮高度 + 边框 + 安全区域
// 转换rpx到px: rpx值 / 750 * 屏幕宽度
const rpxToPx = systemInfo.screenWidth / 750
const topPadding = 20 * rpxToPx // 顶部padding
const bottomPadding = 20 * rpxToPx // 底部基础padding
const buttonHeight = 88 * rpxToPx // 按钮高度
const borderHeight = 1 * rpxToPx // 边框高度
// 获取安全区域底部高度
const safeAreaInsetBottom = systemInfo.safeArea
? Math.max(0, systemInfo.screenHeight - systemInfo.safeArea.bottom)
: 0
// 总的底部高度 = 顶部padding + 底部padding + 按钮高度 + 边框高度 + 安全区域
const totalBottomHeight = topPadding + bottomPadding + buttonHeight + borderHeight + safeAreaInsetBottom
const scrollHeight = Math.max(200, windowHeight - totalBottomHeight)
return `height: ${scrollHeight}px;`
})
/**
* 获取反馈类型名称
*/
const getCategoryName = (category) => {
return categoryMap[category] || '未知类型'
}
/**
* 格式化时间
*/
const formatTime = (timestamp) => {
if (!timestamp) return ''
const date = new Date(timestamp * 1000)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}`
}
/**
* 获取反馈列表数据 - 使用Mock数据
*/
const getFeedbackList = async (isRefresh = false) => {
if (loading.value) return
loading.value = true
try {
// Mock数据
const mockData = [
{
id: 1,
category: '1',
note: '希望能增加夜间模式功能,这样在晚上使用时眼睛会更舒服一些。',
images: ['https://picsum.photos/200/200?random=1', 'https://picsum.photos/200/200?random=2'],
contact: '138****8888',
created_at: Math.floor(Date.now() / 1000) - 86400,
reply_content: '感谢您的建议,我们已将夜间模式功能加入开发计划,预计下个版本上线。',
reply_time: Math.floor(Date.now() / 1000) - 3600
},
{
id: 2,
category: '3',
note: '首页的搜索框位置有点偏上,建议调整到更容易点击的位置。',
images: [],
contact: 'wechat_user123',
created_at: Math.floor(Date.now() / 1000) - 172800,
reply_content: null,
reply_time: null
},
{
id: 3,
category: '5',
note: '发现有些车辆信息显示不完整,价格和配置信息缺失。',
images: ['https://picsum.photos/200/200?random=3'],
contact: '159****6666',
created_at: Math.floor(Date.now() / 1000) - 259200,
reply_content: '我们已经修复了车辆信息显示问题,感谢您的反馈!',
reply_time: Math.floor(Date.now() / 1000) - 86400
},
{
id: 4,
category: '7',
note: '应用启动速度有点慢,希望能优化一下性能。',
images: [],
contact: null,
created_at: Math.floor(Date.now() / 1000) - 345600,
reply_content: null,
reply_time: null
}
]
// 模拟网络延迟
await new Promise(resolve => setTimeout(resolve, 1000))
const startIndex = isRefresh ? 0 : currentPage.value * pageSize.value
const endIndex = startIndex + pageSize.value
const newData = mockData.slice(startIndex, endIndex)
if (isRefresh) {
feedbackList.value = newData
currentPage.value = 0
} else {
feedbackList.value.push(...newData)
}
// 判断是否还有更多数据
hasMore.value = endIndex < mockData.length
if (!isRefresh) {
currentPage.value++
}
} catch (error) {
console.error('获取反馈列表失败:', error)
Toast.fail(error.message || '获取数据失败')
} finally {
loading.value = false
refreshing.value = false
}
}
/**
* 下拉刷新
*/
const onRefresh = () => {
refreshing.value = true
getFeedbackList(true)
}
/**
* 加载更多
*/
const loadMore = () => {
if (!hasMore.value || loading.value) return
getFeedbackList()
}
/**
* 预览图片
*/
const previewImage = (images, index) => {
previewImages.value = images.map(src => ({ src }))
previewIndex.value = index
previewVisible.value = true
}
/**
* 关闭图片预览
*/
const closePreview = () => {
previewVisible.value = false
}
/**
* 跳转到反馈页面
*/
const goToFeedback = () => {
Taro.navigateTo({
url: '/pages/feedBack/index'
})
}
// 页面加载时获取数据
onMounted(() => {
getFeedbackList(true)
})
</script>
<script>
export default {
name: 'FeedbackListPage'
}
</script>
<!--
* @Date: 2022-09-19 14:11:06
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-07-17 10:31:45
* @LastEditTime: 2025-07-17 11:31:47
* @FilePath: /jgdl/src/pages/myAuthCar/index.vue
* @Description: 我的认证车页面
-->
......@@ -42,7 +42,7 @@
<text>{{ getVerificationStatusText(item.verification_status) }}</text>
</view>
</view>
<text class="text-sm text-gray-500 mt-1 block">{{ item.manufacture_year }}年|续航{{ item.range_km }}km|最高时速{{ item.max_speed_kmh }}km/h</text>
<text class="text-sm text-gray-500 mt-1 block">续航{{ item.range_km }}km | 最高时速{{ item.max_speed_kmh }}km/h</text>
<!-- 认证失败原因 -->
<view v-if="item.verification_status === 7 && item.verification_reason" class="verification-reason mt-1">
<text class="text-xs text-red-500">审核结果:{{ item.verification_reason }}</text>
......