hookehuyr

feat(反馈): 新增反馈列表页面并实现反馈提交功能

- 添加反馈列表页面,包含列表展示、分页加载和图片预览功能
- 修改原反馈页面为新增反馈页面,实现表单提交和API对接
- 创建反馈相关API接口文件
- 更新路由配置和导航链接
1 +/*
2 + * @Date: 2024-01-01 00:00:00
3 + * @LastEditors: hookehuyr hookehuyr@gmail.com
4 + * @LastEditTime: 2025-09-09 22:36:42
5 + * @FilePath: /lls_program/src/api/feedback.js
6 + * @Description: 意见反馈相关接口
7 + */
8 +import { fn, fetch } from './fn';
9 +
10 +const Api = {
11 + GET_FEEDBACK_LIST: '/srv/?a=feedback&t=list',
12 + SUBMIT_FEEDBACK: '/srv/?a=feedback&t=add',
13 +}
14 +
15 +/**
16 + * @description: 获取意见反馈列表
17 + * @param {Object} params - 请求参数
18 + * @param {number} [params.page=0] - 页码,从0开始
19 + * @param {number} [params.limit=10] - 每页数量
20 + * @returns {Promise} 返回意见反馈列表
21 + * @returns {Object} response - 响应对象
22 + * @returns {number} response.code - 响应状态码
23 + * @returns {string} response.msg - 响应消息
24 + * @returns {Object} response.data - 响应数据
25 + * @returns {Array} response.data.list - 反馈列表
26 + * @returns {number} response.data.list[].id - 反馈ID
27 + * @returns {string} response.data.list[].status - 状态 (PENDING=待处理, PROCESSED=已处理)
28 + * @returns {string} response.data.list[].images - 图片
29 + * @returns {string} response.data.list[].name - 姓名
30 + * @returns {string} response.data.list[].contact - 联系方式
31 + * @returns {string} response.data.list[].note - 反馈内容
32 + * @returns {string} response.data.list[].reply - 回复
33 + * @returns {string} response.data.list[].reply_time - 回复时间
34 + */
35 +export const getFeedbackListAPI = (params) => fn(fetch.get(Api.GET_FEEDBACK_LIST, params));
36 +
37 +/**
38 + * @description: 提交意见反馈
39 + * @param {Object} params - 请求参数
40 + * @param {string} params.note - 反馈内容(必填)
41 + * @param {string} [params.name] - 姓名
42 + * @param {string} [params.contact] - 联系方式
43 + * @param {Array} [params.images] - 图片数组
44 + * @returns {Promise} 返回提交结果
45 + * @returns {Object} response - 响应对象
46 + * @returns {number} response.code - 响应状态码
47 + * @returns {string} response.msg - 响应消息
48 + */
49 +export const submitFeedbackAPI = (params) => fn(fetch.post(Api.SUBMIT_FEEDBACK, params));
...@@ -20,6 +20,7 @@ export default { ...@@ -20,6 +20,7 @@ export default {
20 'pages/Profile/index', 20 'pages/Profile/index',
21 'pages/AddProfile/index', 21 'pages/AddProfile/index',
22 'pages/Feedback/index', 22 'pages/Feedback/index',
23 + 'pages/FeedbackList/index',
23 'pages/PointsDetail/index', 24 'pages/PointsDetail/index',
24 'pages/RewardDetail/index', 25 'pages/RewardDetail/index',
25 'pages/PrivacyPolicy/index', 26 'pages/PrivacyPolicy/index',
......
1 +/*
2 + * @Date: 2025-08-27 18:25:19
3 + * @LastEditors: hookehuyr hookehuyr@gmail.com
4 + * @LastEditTime: 2025-09-09 22:42:18
5 + * @FilePath: /lls_program/src/pages/Feedback/index.config.js
6 + * @Description: 文件描述
7 + */
1 export default { 8 export default {
2 - navigationBarTitleText: '意见反馈' 9 + navigationBarTitleText: '新增反馈'
3 } 10 }
......
...@@ -111,6 +111,7 @@ import { ref } from 'vue'; ...@@ -111,6 +111,7 @@ import { ref } from 'vue';
111 import Taro from '@tarojs/taro'; 111 import Taro from '@tarojs/taro';
112 import { Photograph } from '@nutui/icons-vue-taro'; 112 import { Photograph } from '@nutui/icons-vue-taro';
113 import BASE_URL from '@/utils/config'; 113 import BASE_URL from '@/utils/config';
114 +import { submitFeedbackAPI } from '@/api/feedback';
114 115
115 const feedbackText = ref(''); 116 const feedbackText = ref('');
116 const screenshots = ref([]); 117 const screenshots = ref([]);
...@@ -252,7 +253,7 @@ const deleteImage = (index) => { ...@@ -252,7 +253,7 @@ const deleteImage = (index) => {
252 /** 253 /**
253 * 提交反馈 254 * 提交反馈
254 */ 255 */
255 -const submitFeedback = () => { 256 +const submitFeedback = async () => {
256 if (!feedbackText.value) { 257 if (!feedbackText.value) {
257 showToast('请描述您遇到的问题或建议', 'none'); 258 showToast('请描述您遇到的问题或建议', 'none');
258 return; 259 return;
...@@ -266,14 +267,47 @@ const submitFeedback = () => { ...@@ -266,14 +267,47 @@ const submitFeedback = () => {
266 return; 267 return;
267 } 268 }
268 269
269 - // 在实际应用中,这里会处理提交逻辑,例如上传图片和发送数据到服务器 270 + try {
270 - showToast('提交成功'); 271 + // 显示提交中提示
272 + Taro.showLoading({
273 + title: '提交中...',
274 + mask: true
275 + });
271 276
272 - // 提交成功后清空表单 277 + // 准备提交数据
273 - feedbackText.value = ''; 278 + const submitData = {
274 - screenshots.value = []; 279 + note: feedbackText.value,
275 - name.value = ''; 280 + name: name.value,
276 - contact.value = ''; 281 + contact: contact.value,
282 + images: screenshots.value.map(item => item.url)
283 + };
284 +
285 + // 调用提交API
286 + const response = await submitFeedbackAPI(submitData);
287 +
288 + Taro.hideLoading();
289 +
290 + if (response.code === 1) {
291 + showToast('提交成功');
292 +
293 + // 提交成功后清空表单
294 + feedbackText.value = '';
295 + screenshots.value = [];
296 + name.value = '';
297 + contact.value = '';
298 +
299 + // 延迟返回上一页
300 + setTimeout(() => {
301 + Taro.navigateBack();
302 + }, 1500);
303 + } else {
304 + showToast(response.msg || '提交失败,请重试', 'none');
305 + }
306 + } catch (error) {
307 + Taro.hideLoading();
308 + console.error('提交反馈失败:', error);
309 + showToast('提交失败,请重试', 'none');
310 + }
277 }; 311 };
278 </script> 312 </script>
279 313
......
1 +export default {
2 + navigationBarTitleText: '意见反馈',
3 + navigationBarBackgroundColor: '#ffffff',
4 + navigationBarTextStyle: 'black',
5 + backgroundColor: '#f5f5f5'
6 +}
...\ No newline at end of file ...\ No newline at end of file
1 +// 意见反馈列表页面样式
2 +.feedback-list {
3 + min-height: 100vh;
4 + background-color: #f9fafb;
5 + padding-bottom: 120rpx; // 为固定按钮留出空间
6 +
7 + .feedback-item {
8 + background: white;
9 + border-radius: 24rpx;
10 + margin: 16rpx 32rpx;
11 + padding: 32rpx;
12 + box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
13 + transition: all 0.3s ease;
14 +
15 + &:active {
16 + transform: scale(0.98);
17 + }
18 +
19 + .feedback-header {
20 + display: flex;
21 + justify-content: space-between;
22 + align-items: center;
23 + margin-bottom: 16rpx;
24 +
25 + .feedback-id {
26 + font-size: 28rpx;
27 + color: #666;
28 + font-weight: 500;
29 + }
30 +
31 + .feedback-status {
32 + padding: 8rpx 16rpx;
33 + border-radius: 12rpx;
34 + font-size: 24rpx;
35 + font-weight: 500;
36 +
37 + &.pending {
38 + background-color: #fef3c7;
39 + color: #d97706;
40 + }
41 +
42 + &.processed {
43 + background-color: #d1fae5;
44 + color: #059669;
45 + }
46 + }
47 + }
48 +
49 + .feedback-content {
50 + margin-bottom: 16rpx;
51 +
52 + .feedback-note {
53 + font-size: 32rpx;
54 + color: #333;
55 + line-height: 1.5;
56 + margin-bottom: 16rpx;
57 + }
58 +
59 + .feedback-meta {
60 + display: flex;
61 + flex-wrap: wrap;
62 + gap: 16rpx;
63 + font-size: 26rpx;
64 + color: #666;
65 +
66 + .meta-item {
67 + display: flex;
68 + align-items: center;
69 + }
70 + }
71 + }
72 +
73 + .feedback-images {
74 + display: flex;
75 + flex-wrap: wrap;
76 + gap: 16rpx;
77 + margin-bottom: 16rpx;
78 +
79 + .feedback-image {
80 + width: 120rpx;
81 + height: 120rpx;
82 + border-radius: 12rpx;
83 + overflow: hidden;
84 + }
85 + }
86 +
87 + .feedback-reply {
88 + background-color: #f8fafc;
89 + border-radius: 16rpx;
90 + padding: 24rpx;
91 + margin-top: 16rpx;
92 +
93 + .reply-label {
94 + font-size: 26rpx;
95 + color: #666;
96 + margin-bottom: 8rpx;
97 + }
98 +
99 + .reply-content {
100 + font-size: 30rpx;
101 + color: #333;
102 + line-height: 1.5;
103 + margin-bottom: 8rpx;
104 + }
105 +
106 + .reply-time {
107 + font-size: 24rpx;
108 + color: #999;
109 + }
110 + }
111 + }
112 +
113 + .empty-state {
114 + text-align: center;
115 + padding: 120rpx 32rpx;
116 +
117 + .empty-icon {
118 + font-size: 120rpx;
119 + color: #d1d5db;
120 + margin-bottom: 32rpx;
121 + }
122 +
123 + .empty-title {
124 + font-size: 36rpx;
125 + color: #6b7280;
126 + margin-bottom: 16rpx;
127 + }
128 +
129 + .empty-desc {
130 + font-size: 28rpx;
131 + color: #9ca3af;
132 + }
133 + }
134 +
135 + .load-more {
136 + text-align: center;
137 + padding: 32rpx;
138 + color: #3b82f6;
139 + font-size: 28rpx;
140 + }
141 +
142 + .no-more {
143 + text-align: center;
144 + padding: 32rpx;
145 + color: #9ca3af;
146 + font-size: 28rpx;
147 + }
148 +}
149 +
150 +// 固定按钮样式
151 +.fixed-button {
152 + position: fixed;
153 + bottom: 32rpx;
154 + left: 32rpx;
155 + right: 32rpx;
156 + background: linear-gradient(135deg, var(--primary-color), var(--primary-color));
157 + color: white;
158 + border-radius: 24rpx;
159 + padding: 32rpx;
160 + text-align: center;
161 + font-size: 32rpx;
162 + font-weight: 600;
163 + box-shadow: 0 8rpx 24rpx rgba(var(--primary-color), 0.3);
164 + z-index: 1000;
165 + transition: all 0.3s ease;
166 +
167 + &:active {
168 + transform: scale(0.95);
169 + box-shadow: 0 4rpx 12rpx rgba(var(--primary-color), 0.3);
170 + }
171 +}
1 +<template>
2 + <view class="feedback-list">
3 + <!-- Loading State -->
4 + <view v-if="loading" class="flex justify-center items-center py-20">
5 + <view class="text-gray-500">加载中...</view>
6 + </view>
7 +
8 + <!-- Content -->
9 + <view v-else>
10 + <!-- Feedback List -->
11 + <view v-if="feedbackList.length > 0">
12 + <view
13 + v-for="feedback in feedbackList"
14 + :key="feedback.id"
15 + class="feedback-item"
16 + >
17 + <!-- Header -->
18 + <view class="feedback-header">
19 + <view class="feedback-id">反馈 #{{ feedback.id }}</view>
20 + <view
21 + class="feedback-status"
22 + :class="{
23 + 'pending': feedback.status === 'PENDING',
24 + 'processed': feedback.status === 'PROCESSED'
25 + }"
26 + >
27 + {{ feedback.status === 'PENDING' ? '待处理' : '已处理' }}
28 + </view>
29 + </view>
30 +
31 + <!-- Content -->
32 + <view class="feedback-content">
33 + <view class="feedback-note">{{ feedback.note }}</view>
34 +
35 + <view class="feedback-meta" v-if="feedback.name || feedback.contact">
36 + <view class="meta-item" v-if="feedback.name">
37 + <text>👤 {{ feedback.name }}</text>
38 + </view>
39 + <view class="meta-item" v-if="feedback.contact">
40 + <text>📞 {{ feedback.contact }}</text>
41 + </view>
42 + </view>
43 + </view>
44 +
45 + <!-- Images -->
46 + <view v-if="feedback.images && feedback.images.length > 0" class="feedback-images">
47 + <image
48 + v-for="(image, index) in feedback.images"
49 + :key="index"
50 + :src="image"
51 + class="feedback-image"
52 + mode="aspectFill"
53 + @tap="previewImage(image, feedback.images)"
54 + />
55 + </view>
56 +
57 + <!-- Reply -->
58 + <view v-if="feedback.reply" class="feedback-reply">
59 + <view class="reply-label">官方回复:</view>
60 + <view class="reply-content">{{ feedback.reply }}</view>
61 + <view v-if="feedback.reply_time" class="reply-time">
62 + 回复时间:{{ formatDate(feedback.reply_time) }}
63 + </view>
64 + </view>
65 + </view>
66 + </view>
67 +
68 + <!-- Empty State -->
69 + <view v-else class="empty-state">
70 + <view class="empty-icon">💬</view>
71 + <view class="empty-title">暂无反馈记录</view>
72 + <view class="empty-desc">您还没有提交过任何意见反馈</view>
73 + </view>
74 +
75 + <!-- Load More -->
76 + <view v-if="hasMore && feedbackList.length > 0" class="load-more" @click="loadMore">
77 + {{ loadingMore ? '加载中...' : '加载更多' }}
78 + </view>
79 +
80 + <!-- No More Data -->
81 + <view v-if="!hasMore && feedbackList.length > 0" class="no-more">
82 + 没有更多数据了
83 + </view>
84 + </view>
85 +
86 + <!-- Fixed Button -->
87 + <view class="fixed-button" @click="goToFeedback">
88 + 新增反馈
89 + </view>
90 + </view>
91 +</template>
92 +
93 +<script setup>
94 +import { ref, onMounted } from 'vue';
95 +import Taro from '@tarojs/taro';
96 +import { useDidShow } from '@tarojs/taro';
97 +import { getFeedbackListAPI } from '@/api/feedback';
98 +
99 +// 响应式数据
100 +const loading = ref(false);
101 +const loadingMore = ref(false);
102 +const feedbackList = ref([]);
103 +const hasMore = ref(true);
104 +const currentPage = ref(0);
105 +const pageSize = ref(10);
106 +
107 +/**
108 + * @description: 加载反馈列表
109 + * @param {boolean} isLoadMore - 是否为加载更多
110 + */
111 +const loadFeedbackList = async (isLoadMore = false) => {
112 + try {
113 + if (isLoadMore) {
114 + loadingMore.value = true;
115 + } else {
116 + loading.value = true;
117 + currentPage.value = 0;
118 + feedbackList.value = [];
119 + }
120 +
121 + const params = {
122 + page: currentPage.value,
123 + limit: pageSize.value
124 + };
125 +
126 + const response = await getFeedbackListAPI(params);
127 +
128 + if (response.code === 1 && response.data && response.data.list) {
129 + const newList = response.data.list.map(item => ({
130 + ...item,
131 + }));
132 +
133 + if (isLoadMore) {
134 + feedbackList.value = [...feedbackList.value, ...newList];
135 + } else {
136 + feedbackList.value = newList;
137 + }
138 +
139 + // 判断是否还有更多数据
140 + hasMore.value = newList.length === pageSize.value;
141 +
142 + if (hasMore.value) {
143 + currentPage.value++;
144 + }
145 + } else {
146 + hasMore.value = false;
147 + if (!isLoadMore) {
148 + feedbackList.value = [];
149 + }
150 + }
151 + } catch (error) {
152 + console.error('加载反馈列表失败:', error);
153 + Taro.showToast({
154 + title: '加载失败,请重试',
155 + icon: 'none'
156 + });
157 + hasMore.value = false;
158 + } finally {
159 + loading.value = false;
160 + loadingMore.value = false;
161 + }
162 +};
163 +
164 +/**
165 + * @description: 加载更多
166 + */
167 +const loadMore = () => {
168 + if (!loadingMore.value && hasMore.value) {
169 + loadFeedbackList(true);
170 + }
171 +};
172 +
173 +/**
174 + * @description: 格式化日期
175 + * @param {string} dateString - 日期字符串
176 + * @returns {string} 格式化后的日期
177 + */
178 +const formatDate = (dateString) => {
179 + if (!dateString) return '';
180 + const date = new Date(dateString);
181 + return date.toLocaleString('zh-CN', {
182 + year: 'numeric',
183 + month: '2-digit',
184 + day: '2-digit',
185 + hour: '2-digit',
186 + minute: '2-digit'
187 + });
188 +};
189 +
190 +/**
191 + * @description: 预览图片
192 + * @param {string} current - 当前图片
193 + * @param {Array} images - 图片列表
194 + */
195 +const previewImage = (current, images) => {
196 + Taro.previewImage({
197 + current,
198 + urls: images
199 + });
200 +};
201 +
202 +/**
203 + * @description: 跳转到新增反馈页面
204 + */
205 +const goToFeedback = () => {
206 + Taro.navigateTo({
207 + url: '/pages/Feedback/index'
208 + });
209 +};
210 +
211 +// 页面显示时刷新数据
212 +useDidShow(() => {
213 + loadFeedbackList();
214 +});
215 +
216 +// 页面加载时获取数据
217 +onMounted(() => {
218 + loadFeedbackList();
219 +});
220 +</script>
221 +
222 +<style lang="less">
223 +@import './index.less';
224 +</style>
...@@ -80,7 +80,7 @@ const allMenuItems = [ ...@@ -80,7 +80,7 @@ const allMenuItems = [
80 icon: Message, 80 icon: Message,
81 label: '意见反馈', 81 label: '意见反馈',
82 color: 'bg-blue-500', 82 color: 'bg-blue-500',
83 - onClick: () => Taro.navigateTo({ url: '/pages/Feedback/index' }) 83 + onClick: () => Taro.navigateTo({ url: '/pages/FeedbackList/index' })
84 }, 84 },
85 { 85 {
86 id: 'agreement', 86 id: 'agreement',
......