hookehuyr

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

添加反馈列表页面,包含以下功能:
1. 展示用户反馈历史记录
2. 支持图片预览和分页加载
3. 添加底部反馈按钮跳转
4. 更新API接口和组件类型定义
5. 优化反馈提交后的返回逻辑
...@@ -22,6 +22,7 @@ declare module 'vue' { ...@@ -22,6 +22,7 @@ declare module 'vue' {
22 NutFormItem: typeof import('@nutui/nutui-taro')['FormItem'] 22 NutFormItem: typeof import('@nutui/nutui-taro')['FormItem']
23 NutImagePreview: typeof import('@nutui/nutui-taro')['ImagePreview'] 23 NutImagePreview: typeof import('@nutui/nutui-taro')['ImagePreview']
24 NutInput: typeof import('@nutui/nutui-taro')['Input'] 24 NutInput: typeof import('@nutui/nutui-taro')['Input']
25 + NutLoading: typeof import('@nutui/nutui-taro')['Loading']
25 NutMenu: typeof import('@nutui/nutui-taro')['Menu'] 26 NutMenu: typeof import('@nutui/nutui-taro')['Menu']
26 NutMenuItem: typeof import('@nutui/nutui-taro')['MenuItem'] 27 NutMenuItem: typeof import('@nutui/nutui-taro')['MenuItem']
27 NutOverlay: typeof import('@nutui/nutui-taro')['Overlay'] 28 NutOverlay: typeof import('@nutui/nutui-taro')['Overlay']
......
...@@ -12,6 +12,7 @@ const Api = { ...@@ -12,6 +12,7 @@ const Api = {
12 GET_BRANDS_MODELS: '/srv/?a=common&t=get_brands_models', 12 GET_BRANDS_MODELS: '/srv/?a=common&t=get_brands_models',
13 GET_VEHICLE_BRANDS: '/srv/?a=common&t=get_vehicle_brands', 13 GET_VEHICLE_BRANDS: '/srv/?a=common&t=get_vehicle_brands',
14 SUBMIT_FEEDBACK: '/srv/?a=feedback&t=add', 14 SUBMIT_FEEDBACK: '/srv/?a=feedback&t=add',
15 + GET_FEEDBACK_LIST: '/srv/?a=feedback&t=list',
15 GET_FAVORITE_LIST: '/srv/?a=favorite&t=list', 16 GET_FAVORITE_LIST: '/srv/?a=favorite&t=list',
16 TOGGLE_FAVORITE_ADD: '/srv/?a=favorite&t=add', 17 TOGGLE_FAVORITE_ADD: '/srv/?a=favorite&t=add',
17 TOGGLE_FAVORITE_DEL: '/srv/?a=favorite&t=del', 18 TOGGLE_FAVORITE_DEL: '/srv/?a=favorite&t=del',
...@@ -56,6 +57,15 @@ export const toggleFavoriteAddAPI = (params) => fn(fetch.post(Api.TOGGLE_FAVORIT ...@@ -56,6 +57,15 @@ export const toggleFavoriteAddAPI = (params) => fn(fetch.post(Api.TOGGLE_FAVORIT
56 export const toggleFavoriteDelAPI = (params) => fn(fetch.post(Api.TOGGLE_FAVORITE_DEL, params)); 57 export const toggleFavoriteDelAPI = (params) => fn(fetch.post(Api.TOGGLE_FAVORITE_DEL, params));
57 58
58 /** 59 /**
60 + * @description: 获取反馈列表
61 + * @param {*} params
62 + * @param {number} params.page - 页码,从0开始
63 + * @param {number} params.limit - 每页数量
64 + * @returns
65 + */
66 +export const getFeedbackListAPI = (params) => fn(fetch.get(Api.GET_FEEDBACK_LIST, params));
67 +
68 +/**
59 * @description: 提交反馈 69 * @description: 提交反馈
60 * @param {*} params 70 * @param {*} params
61 * @param {string} params.category - 反馈类型(1=功能建议,3=界面设计,5=车辆信息, 7=其他) 71 * @param {string} params.category - 反馈类型(1=功能建议,3=界面设计,5=车辆信息, 7=其他)
......
...@@ -25,6 +25,7 @@ export default { ...@@ -25,6 +25,7 @@ export default {
25 'pages/myOrders/index', 25 'pages/myOrders/index',
26 'pages/myAuthCar/index', 26 'pages/myAuthCar/index',
27 'pages/feedBack/index', 27 'pages/feedBack/index',
28 + 'pages/feedBackList/index',
28 'pages/helpCenter/index', 29 'pages/helpCenter/index',
29 'pages/search/index', 30 'pages/search/index',
30 'pages/recommendCarList/index', 31 'pages/recommendCarList/index',
......
1 <!-- 1 <!--
2 * @Date: 2022-09-19 14:11:06 2 * @Date: 2022-09-19 14:11:06
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2025-07-15 13:14:50 4 + * @LastEditTime: 2025-07-17 12:22:47
5 * @FilePath: /jgdl/src/pages/feedBack/index.vue 5 * @FilePath: /jgdl/src/pages/feedBack/index.vue
6 * @Description: 意见反馈页面 6 * @Description: 意见反馈页面
7 --> 7 -->
...@@ -329,6 +329,11 @@ const closeSuccessModal = () => { ...@@ -329,6 +329,11 @@ const closeSuccessModal = () => {
329 feedbackText.value = '' 329 feedbackText.value = ''
330 contactInfo.value = '' 330 contactInfo.value = ''
331 uploadedImages.value = [] 331 uploadedImages.value = []
332 +
333 + // 返回上一页
334 + Taro.navigateBack({
335 + delta: 1
336 + })
332 } 337 }
333 </script> 338 </script>
334 339
......
1 +export default {
2 + navigationBarTitleText: '意见反馈列表',
3 + usingComponents: {
4 + },
5 +}
1 +/*
2 + * @Date: 2022-09-19 14:11:06
3 + * @LastEditors: hookehuyr hookehuyr@gmail.com
4 + * @LastEditTime: 2025-07-17 11:23:10
5 + * @FilePath: /jgdl/src/pages/feedBackList/index.less
6 + * @Description: 意见反馈列表页面样式
7 + */
8 +
9 +.feedback-list-page {
10 + position: relative;
11 + height: 100vh;
12 + background-color: #f5f5f5;
13 +
14 + .scroll-view {
15 + padding: 20rpx;
16 + box-sizing: border-box;
17 + overflow-y: auto;
18 + -webkit-overflow-scrolling: touch;
19 + }
20 +
21 + .feedback-list {
22 + .feedback-card {
23 + background: #fff;
24 + border-radius: 16rpx;
25 + padding: 24rpx;
26 + margin-bottom: 20rpx;
27 + box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
28 + border: 1rpx solid #f0f0f0;
29 +
30 + .card-header {
31 + display: flex;
32 + justify-content: space-between;
33 + align-items: center;
34 + margin-bottom: 16rpx;
35 +
36 + .feedback-time {
37 + font-size: 24rpx;
38 + color: #999;
39 + }
40 +
41 + .feedback-type {
42 + background: #fb923c;
43 + color: #fff;
44 + padding: 8rpx 16rpx;
45 + border-radius: 20rpx;
46 + font-size: 22rpx;
47 + font-weight: 500;
48 + }
49 + }
50 +
51 + .feedback-content {
52 + margin-bottom: 16rpx;
53 +
54 + .content-text {
55 + font-size: 28rpx;
56 + color: #333;
57 + line-height: 1.6;
58 + word-break: break-all;
59 + }
60 + }
61 +
62 + .feedback-images {
63 + display: flex;
64 + flex-wrap: wrap;
65 + gap: 12rpx;
66 + margin-bottom: 16rpx;
67 +
68 + .image-item {
69 + width: 120rpx;
70 + height: 120rpx;
71 + border-radius: 8rpx;
72 + overflow: hidden;
73 + border: 1rpx solid #eee;
74 +
75 + .feedback-image {
76 + width: 100%;
77 + height: 100%;
78 + }
79 + }
80 + }
81 +
82 + .contact-info {
83 + background: #f8f9fa;
84 + padding: 16rpx;
85 + border-radius: 8rpx;
86 + margin-bottom: 16rpx;
87 + border-left: 4rpx solid #fb923c;
88 +
89 + .contact-label {
90 + font-size: 24rpx;
91 + color: #666;
92 + }
93 +
94 + .contact-text {
95 + font-size: 26rpx;
96 + color: #333;
97 + font-weight: 500;
98 + }
99 + }
100 +
101 + .reply-section {
102 + background: #f0f8ff;
103 + padding: 16rpx;
104 + border-radius: 8rpx;
105 + border-left: 4rpx solid #fb923c;
106 +
107 + .reply-header {
108 + display: flex;
109 + justify-content: space-between;
110 + align-items: center;
111 + margin-bottom: 12rpx;
112 +
113 + .reply-label {
114 + font-size: 24rpx;
115 + color: #fb923c;
116 + font-weight: 500;
117 + }
118 +
119 + .reply-time {
120 + font-size: 22rpx;
121 + color: #999;
122 + }
123 + }
124 +
125 + .reply-content {
126 + .reply-text {
127 + font-size: 26rpx;
128 + color: #333;
129 + line-height: 1.6;
130 + word-break: break-all;
131 + }
132 + }
133 + }
134 + }
135 + }
136 +
137 + // 空状态样式
138 + .empty-state {
139 + display: flex;
140 + flex-direction: column;
141 + align-items: center;
142 + justify-content: center;
143 + padding: 120rpx 40rpx;
144 + text-align: center;
145 +
146 + .empty-icon {
147 + font-size: 120rpx;
148 + margin-bottom: 24rpx;
149 + opacity: 0.6;
150 + }
151 +
152 + .empty-text {
153 + font-size: 32rpx;
154 + color: #666;
155 + margin-bottom: 12rpx;
156 + font-weight: 500;
157 + }
158 +
159 + .empty-desc {
160 + font-size: 26rpx;
161 + color: #999;
162 + line-height: 1.5;
163 + }
164 + }
165 +
166 + // 加载状态样式
167 + .loading-container {
168 + display: flex;
169 + align-items: center;
170 + justify-content: center;
171 + padding: 40rpx;
172 + gap: 16rpx;
173 +
174 + .loading-text {
175 + font-size: 26rpx;
176 + color: #666;
177 + }
178 + }
179 +
180 + // 没有更多数据样式
181 + .no-more-container {
182 + display: flex;
183 + align-items: center;
184 + justify-content: center;
185 + padding: 40rpx;
186 +
187 + .no-more-text {
188 + font-size: 24rpx;
189 + color: #999;
190 + position: relative;
191 +
192 + &::before,
193 + &::after {
194 + content: '';
195 + position: absolute;
196 + top: 50%;
197 + width: 60rpx;
198 + height: 1rpx;
199 + background: #ddd;
200 + }
201 +
202 + &::before {
203 + left: -80rpx;
204 + }
205 +
206 + &::after {
207 + right: -80rpx;
208 + }
209 + }
210 + }
211 +
212 + // 底部固定按钮
213 + .fixed-bottom {
214 + position: fixed;
215 + bottom: 0;
216 + left: 0;
217 + right: 0;
218 + background: #fff;
219 + padding: 20rpx 30rpx;
220 + padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
221 + border-top: 1rpx solid #eee;
222 + box-shadow: 0 -2rpx 8rpx rgba(0, 0, 0, 0.06);
223 + z-index: 100;
224 +
225 + .feedback-btn {
226 + width: 100%;
227 + height: 88rpx;
228 + background: #fb923c;
229 + color: #fff;
230 + border: none;
231 + border-radius: 44rpx;
232 + font-size: 32rpx;
233 + font-weight: 500;
234 + display: flex;
235 + align-items: center;
236 + justify-content: center;
237 + box-shadow: 0 4rpx 12rpx rgba(251, 146, 60, 0.3);
238 + transition: all 0.3s ease;
239 +
240 + &:active {
241 + transform: translateY(2rpx);
242 + box-shadow: 0 2rpx 8rpx rgba(251, 146, 60, 0.2);
243 + }
244 + }
245 + }
246 +}
...\ No newline at end of file ...\ No newline at end of file
1 +<!--
2 + * @Date: 2022-09-19 14:11:06
3 + * @LastEditors: hookehuyr hookehuyr@gmail.com
4 + * @LastEditTime: 2025-07-17 12:19:40
5 + * @FilePath: /jgdl/src/pages/feedBackList/index.vue
6 + * @Description: 意见反馈列表页面
7 +-->
8 +<template>
9 + <view class="feedback-list-page">
10 + <!-- 滚动视图 -->
11 + <scroll-view
12 + class="scroll-view"
13 + :scroll-y="true"
14 + :style="scrollStyle"
15 + @scrolltolower="loadMore"
16 + :lower-threshold="50"
17 + >
18 + <!-- 反馈列表 -->
19 + <view class="feedback-list" v-if="feedbackList.length > 0">
20 + <view
21 + v-for="(item, index) in feedbackList"
22 + :key="index"
23 + class="feedback-card"
24 + >
25 + <!-- 卡片头部:时间和类型 -->
26 + <view class="card-header">
27 + <view class="feedback-time">{{ formatTime(item.created_at) }}</view>
28 + <view class="feedback-type">{{ getCategoryName(item.category) }}</view>
29 + </view>
30 +
31 + <!-- 反馈内容 -->
32 + <view class="feedback-content">
33 + <text class="content-text">{{ item.note }}</text>
34 + </view>
35 +
36 + <!-- 图片展示 -->
37 + <view class="feedback-images" v-if="item.images && item.images.length > 0">
38 + <view
39 + v-for="(image, imgIndex) in item.images"
40 + :key="imgIndex"
41 + class="image-item"
42 + @click="previewImage(item.images, imgIndex)"
43 + >
44 + <image :src="image" class="feedback-image" mode="aspectFill" />
45 + </view>
46 + </view>
47 +
48 + <!-- 联系方式 -->
49 + <view class="contact-info" v-if="item.contact">
50 + <text class="contact-label">联系方式:</text>
51 + <text class="contact-text">{{ item.contact }}</text>
52 + </view>
53 +
54 + <!-- 回复内容 -->
55 + <view class="reply-section" v-if="item.reply_content">
56 + <view class="reply-header">
57 + <text class="reply-label">回复内容:</text>
58 + <text class="reply-time">{{ formatTime(item.reply_time) }}</text>
59 + </view>
60 + <view class="reply-content">
61 + <text class="reply-text">{{ item.reply_content }}</text>
62 + </view>
63 + </view>
64 + </view>
65 + </view>
66 +
67 + <!-- 空状态 -->
68 + <view class="empty-state" v-if="!loading && feedbackList.length === 0">
69 + <view class="empty-icon">📝</view>
70 + <view class="empty-text">暂无反馈记录</view>
71 + <view class="empty-desc">您还没有提交过任何反馈</view>
72 + </view>
73 +
74 + <!-- 加载状态 -->
75 + <view class="loading-container" v-if="loading">
76 + <text class="loading-text">加载中...</text>
77 + </view>
78 +
79 + <!-- 没有更多数据 -->
80 + <view class="no-more-container" v-if="!hasMore && feedbackList.length > 0">
81 + <text class="no-more-text">没有更多数据了</text>
82 + </view>
83 + </scroll-view>
84 +
85 + <!-- 底部固定按钮 -->
86 + <view class="fixed-bottom">
87 + <button class="feedback-btn" @click="goToFeedback">
88 + 我要反馈
89 + </button>
90 + </view>
91 +
92 + <!-- 图片预览 -->
93 + <nut-image-preview
94 + v-model:show="previewVisible"
95 + :images="previewImages"
96 + :init-no="previewIndex"
97 + @close="closePreview"
98 + />
99 + </view>
100 +</template>
101 +
102 +<script setup>
103 +import { ref, computed, onMounted } from 'vue'
104 +import Taro from '@tarojs/taro'
105 +import { Toast } from '@nutui/nutui-taro'
106 +// import { getFeedbackListAPI } from '@/api/other'
107 +import './index.less'
108 +
109 +// 反馈类型映射
110 +const categoryMap = {
111 + '1': '功能建议',
112 + '3': '界面设计',
113 + '5': '车辆信息',
114 + '7': '其他问题'
115 +}
116 +
117 +// 响应式数据
118 +const loading = ref(false)
119 +const refreshing = ref(false)
120 +const hasMore = ref(true)
121 +const currentPage = ref(0)
122 +const pageSize = ref(10)
123 +const feedbackList = ref([])
124 +
125 +// 图片预览相关
126 +const previewVisible = ref(false)
127 +const previewImages = ref([])
128 +const previewIndex = ref(0)
129 +
130 +// 计算滚动视图样式
131 +const scrollStyle = computed(() => {
132 + const systemInfo = Taro.getSystemInfoSync()
133 + const windowHeight = systemInfo.windowHeight
134 +
135 + // 动态计算底部区域高度:padding + 按钮高度 + 边框 + 安全区域
136 + // 转换rpx到px: rpx值 / 750 * 屏幕宽度
137 + const rpxToPx = systemInfo.screenWidth / 750
138 + const topPadding = 20 * rpxToPx // 顶部padding
139 + const bottomPadding = 20 * rpxToPx // 底部基础padding
140 + const buttonHeight = 88 * rpxToPx // 按钮高度
141 + const borderHeight = 1 * rpxToPx // 边框高度
142 +
143 + // 获取安全区域底部高度
144 + const safeAreaInsetBottom = systemInfo.safeArea
145 + ? Math.max(0, systemInfo.screenHeight - systemInfo.safeArea.bottom)
146 + : 0
147 +
148 + // 总的底部高度 = 顶部padding + 底部padding + 按钮高度 + 边框高度 + 安全区域
149 + const totalBottomHeight = topPadding + bottomPadding + buttonHeight + borderHeight + safeAreaInsetBottom
150 +
151 + const scrollHeight = Math.max(200, windowHeight - totalBottomHeight)
152 +
153 + return `height: ${scrollHeight}px;`
154 +})
155 +
156 +/**
157 + * 获取反馈类型名称
158 + */
159 +const getCategoryName = (category) => {
160 + return categoryMap[category] || '未知类型'
161 +}
162 +
163 +/**
164 + * 格式化时间
165 + */
166 +const formatTime = (timestamp) => {
167 + if (!timestamp) return ''
168 + const date = new Date(timestamp * 1000)
169 + const year = date.getFullYear()
170 + const month = String(date.getMonth() + 1).padStart(2, '0')
171 + const day = String(date.getDate()).padStart(2, '0')
172 + const hours = String(date.getHours()).padStart(2, '0')
173 + const minutes = String(date.getMinutes()).padStart(2, '0')
174 + return `${year}-${month}-${day} ${hours}:${minutes}`
175 +}
176 +
177 +/**
178 + * 获取反馈列表数据 - 使用Mock数据
179 + */
180 +const getFeedbackList = async (isRefresh = false) => {
181 + if (loading.value) return
182 +
183 + loading.value = true
184 +
185 + try {
186 + // Mock数据
187 + const mockData = [
188 + {
189 + id: 1,
190 + category: '1',
191 + note: '希望能增加夜间模式功能,这样在晚上使用时眼睛会更舒服一些。',
192 + images: ['https://picsum.photos/200/200?random=1', 'https://picsum.photos/200/200?random=2'],
193 + contact: '138****8888',
194 + created_at: Math.floor(Date.now() / 1000) - 86400,
195 + reply_content: '感谢您的建议,我们已将夜间模式功能加入开发计划,预计下个版本上线。',
196 + reply_time: Math.floor(Date.now() / 1000) - 3600
197 + },
198 + {
199 + id: 2,
200 + category: '3',
201 + note: '首页的搜索框位置有点偏上,建议调整到更容易点击的位置。',
202 + images: [],
203 + contact: 'wechat_user123',
204 + created_at: Math.floor(Date.now() / 1000) - 172800,
205 + reply_content: null,
206 + reply_time: null
207 + },
208 + {
209 + id: 3,
210 + category: '5',
211 + note: '发现有些车辆信息显示不完整,价格和配置信息缺失。',
212 + images: ['https://picsum.photos/200/200?random=3'],
213 + contact: '159****6666',
214 + created_at: Math.floor(Date.now() / 1000) - 259200,
215 + reply_content: '我们已经修复了车辆信息显示问题,感谢您的反馈!',
216 + reply_time: Math.floor(Date.now() / 1000) - 86400
217 + },
218 + {
219 + id: 4,
220 + category: '7',
221 + note: '应用启动速度有点慢,希望能优化一下性能。',
222 + images: [],
223 + contact: null,
224 + created_at: Math.floor(Date.now() / 1000) - 345600,
225 + reply_content: null,
226 + reply_time: null
227 + }
228 + ]
229 +
230 + // 模拟网络延迟
231 + await new Promise(resolve => setTimeout(resolve, 1000))
232 +
233 + const startIndex = isRefresh ? 0 : currentPage.value * pageSize.value
234 + const endIndex = startIndex + pageSize.value
235 + const newData = mockData.slice(startIndex, endIndex)
236 +
237 + if (isRefresh) {
238 + feedbackList.value = newData
239 + currentPage.value = 0
240 + } else {
241 + feedbackList.value.push(...newData)
242 + }
243 +
244 + // 判断是否还有更多数据
245 + hasMore.value = endIndex < mockData.length
246 +
247 + if (!isRefresh) {
248 + currentPage.value++
249 + }
250 +
251 + } catch (error) {
252 + console.error('获取反馈列表失败:', error)
253 + Toast.fail(error.message || '获取数据失败')
254 + } finally {
255 + loading.value = false
256 + refreshing.value = false
257 + }
258 +}
259 +
260 +/**
261 + * 下拉刷新
262 + */
263 +const onRefresh = () => {
264 + refreshing.value = true
265 + getFeedbackList(true)
266 +}
267 +
268 +/**
269 + * 加载更多
270 + */
271 +const loadMore = () => {
272 + if (!hasMore.value || loading.value) return
273 + getFeedbackList()
274 +}
275 +
276 +/**
277 + * 预览图片
278 + */
279 +const previewImage = (images, index) => {
280 + previewImages.value = images.map(src => ({ src }))
281 + previewIndex.value = index
282 + previewVisible.value = true
283 +}
284 +
285 +/**
286 + * 关闭图片预览
287 + */
288 +const closePreview = () => {
289 + previewVisible.value = false
290 +}
291 +
292 +/**
293 + * 跳转到反馈页面
294 + */
295 +const goToFeedback = () => {
296 + Taro.navigateTo({
297 + url: '/pages/feedBack/index'
298 + })
299 +}
300 +
301 +// 页面加载时获取数据
302 +onMounted(() => {
303 + getFeedbackList(true)
304 +})
305 +</script>
306 +
307 +<script>
308 +export default {
309 + name: 'FeedbackListPage'
310 +}
311 +</script>
1 <!-- 1 <!--
2 * @Date: 2022-09-19 14:11:06 2 * @Date: 2022-09-19 14:11:06
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2025-07-17 10:31:45 4 + * @LastEditTime: 2025-07-17 11:31:47
5 * @FilePath: /jgdl/src/pages/myAuthCar/index.vue 5 * @FilePath: /jgdl/src/pages/myAuthCar/index.vue
6 * @Description: 我的认证车页面 6 * @Description: 我的认证车页面
7 --> 7 -->
...@@ -42,7 +42,7 @@ ...@@ -42,7 +42,7 @@
42 <text>{{ getVerificationStatusText(item.verification_status) }}</text> 42 <text>{{ getVerificationStatusText(item.verification_status) }}</text>
43 </view> 43 </view>
44 </view> 44 </view>
45 - <text class="text-sm text-gray-500 mt-1 block">{{ item.manufacture_year }}年|续航{{ item.range_km }}km|最高时速{{ item.max_speed_kmh }}km/h</text> 45 + <text class="text-sm text-gray-500 mt-1 block">续航{{ item.range_km }}km | 最高时速{{ item.max_speed_kmh }}km/h</text>
46 <!-- 认证失败原因 --> 46 <!-- 认证失败原因 -->
47 <view v-if="item.verification_status === 7 && item.verification_reason" class="verification-reason mt-1"> 47 <view v-if="item.verification_status === 7 && item.verification_reason" class="verification-reason mt-1">
48 <text class="text-xs text-red-500">审核结果:{{ item.verification_reason }}</text> 48 <text class="text-xs text-red-500">审核结果:{{ item.verification_reason }}</text>
......