hookehuyr

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

- 添加反馈列表页面,包含列表展示、分页加载和图片预览功能
- 修改原反馈页面为新增反馈页面,实现表单提交和API对接
- 创建反馈相关API接口文件
- 更新路由配置和导航链接
/*
* @Date: 2024-01-01 00:00:00
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-09-09 22:36:42
* @FilePath: /lls_program/src/api/feedback.js
* @Description: 意见反馈相关接口
*/
import { fn, fetch } from './fn';
const Api = {
GET_FEEDBACK_LIST: '/srv/?a=feedback&t=list',
SUBMIT_FEEDBACK: '/srv/?a=feedback&t=add',
}
/**
* @description: 获取意见反馈列表
* @param {Object} params - 请求参数
* @param {number} [params.page=0] - 页码,从0开始
* @param {number} [params.limit=10] - 每页数量
* @returns {Promise} 返回意见反馈列表
* @returns {Object} response - 响应对象
* @returns {number} response.code - 响应状态码
* @returns {string} response.msg - 响应消息
* @returns {Object} response.data - 响应数据
* @returns {Array} response.data.list - 反馈列表
* @returns {number} response.data.list[].id - 反馈ID
* @returns {string} response.data.list[].status - 状态 (PENDING=待处理, PROCESSED=已处理)
* @returns {string} response.data.list[].images - 图片
* @returns {string} response.data.list[].name - 姓名
* @returns {string} response.data.list[].contact - 联系方式
* @returns {string} response.data.list[].note - 反馈内容
* @returns {string} response.data.list[].reply - 回复
* @returns {string} response.data.list[].reply_time - 回复时间
*/
export const getFeedbackListAPI = (params) => fn(fetch.get(Api.GET_FEEDBACK_LIST, params));
/**
* @description: 提交意见反馈
* @param {Object} params - 请求参数
* @param {string} params.note - 反馈内容(必填)
* @param {string} [params.name] - 姓名
* @param {string} [params.contact] - 联系方式
* @param {Array} [params.images] - 图片数组
* @returns {Promise} 返回提交结果
* @returns {Object} response - 响应对象
* @returns {number} response.code - 响应状态码
* @returns {string} response.msg - 响应消息
*/
export const submitFeedbackAPI = (params) => fn(fetch.post(Api.SUBMIT_FEEDBACK, params));
......@@ -20,6 +20,7 @@ export default {
'pages/Profile/index',
'pages/AddProfile/index',
'pages/Feedback/index',
'pages/FeedbackList/index',
'pages/PointsDetail/index',
'pages/RewardDetail/index',
'pages/PrivacyPolicy/index',
......
/*
* @Date: 2025-08-27 18:25:19
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-09-09 22:42:18
* @FilePath: /lls_program/src/pages/Feedback/index.config.js
* @Description: 文件描述
*/
export default {
navigationBarTitleText: '意见反馈'
navigationBarTitleText: '新增反馈'
}
......
......@@ -111,6 +111,7 @@ import { ref } from 'vue';
import Taro from '@tarojs/taro';
import { Photograph } from '@nutui/icons-vue-taro';
import BASE_URL from '@/utils/config';
import { submitFeedbackAPI } from '@/api/feedback';
const feedbackText = ref('');
const screenshots = ref([]);
......@@ -252,7 +253,7 @@ const deleteImage = (index) => {
/**
* 提交反馈
*/
const submitFeedback = () => {
const submitFeedback = async () => {
if (!feedbackText.value) {
showToast('请描述您遇到的问题或建议', 'none');
return;
......@@ -266,14 +267,47 @@ const submitFeedback = () => {
return;
}
// 在实际应用中,这里会处理提交逻辑,例如上传图片和发送数据到服务器
showToast('提交成功');
try {
// 显示提交中提示
Taro.showLoading({
title: '提交中...',
mask: true
});
// 提交成功后清空表单
feedbackText.value = '';
screenshots.value = [];
name.value = '';
contact.value = '';
// 准备提交数据
const submitData = {
note: feedbackText.value,
name: name.value,
contact: contact.value,
images: screenshots.value.map(item => item.url)
};
// 调用提交API
const response = await submitFeedbackAPI(submitData);
Taro.hideLoading();
if (response.code === 1) {
showToast('提交成功');
// 提交成功后清空表单
feedbackText.value = '';
screenshots.value = [];
name.value = '';
contact.value = '';
// 延迟返回上一页
setTimeout(() => {
Taro.navigateBack();
}, 1500);
} else {
showToast(response.msg || '提交失败,请重试', 'none');
}
} catch (error) {
Taro.hideLoading();
console.error('提交反馈失败:', error);
showToast('提交失败,请重试', 'none');
}
};
</script>
......
export default {
navigationBarTitleText: '意见反馈',
navigationBarBackgroundColor: '#ffffff',
navigationBarTextStyle: 'black',
backgroundColor: '#f5f5f5'
}
\ No newline at end of file
// 意见反馈列表页面样式
.feedback-list {
min-height: 100vh;
background-color: #f9fafb;
padding-bottom: 120rpx; // 为固定按钮留出空间
.feedback-item {
background: white;
border-radius: 24rpx;
margin: 16rpx 32rpx;
padding: 32rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
transition: all 0.3s ease;
&:active {
transform: scale(0.98);
}
.feedback-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
.feedback-id {
font-size: 28rpx;
color: #666;
font-weight: 500;
}
.feedback-status {
padding: 8rpx 16rpx;
border-radius: 12rpx;
font-size: 24rpx;
font-weight: 500;
&.pending {
background-color: #fef3c7;
color: #d97706;
}
&.processed {
background-color: #d1fae5;
color: #059669;
}
}
}
.feedback-content {
margin-bottom: 16rpx;
.feedback-note {
font-size: 32rpx;
color: #333;
line-height: 1.5;
margin-bottom: 16rpx;
}
.feedback-meta {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
font-size: 26rpx;
color: #666;
.meta-item {
display: flex;
align-items: center;
}
}
}
.feedback-images {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
margin-bottom: 16rpx;
.feedback-image {
width: 120rpx;
height: 120rpx;
border-radius: 12rpx;
overflow: hidden;
}
}
.feedback-reply {
background-color: #f8fafc;
border-radius: 16rpx;
padding: 24rpx;
margin-top: 16rpx;
.reply-label {
font-size: 26rpx;
color: #666;
margin-bottom: 8rpx;
}
.reply-content {
font-size: 30rpx;
color: #333;
line-height: 1.5;
margin-bottom: 8rpx;
}
.reply-time {
font-size: 24rpx;
color: #999;
}
}
}
.empty-state {
text-align: center;
padding: 120rpx 32rpx;
.empty-icon {
font-size: 120rpx;
color: #d1d5db;
margin-bottom: 32rpx;
}
.empty-title {
font-size: 36rpx;
color: #6b7280;
margin-bottom: 16rpx;
}
.empty-desc {
font-size: 28rpx;
color: #9ca3af;
}
}
.load-more {
text-align: center;
padding: 32rpx;
color: #3b82f6;
font-size: 28rpx;
}
.no-more {
text-align: center;
padding: 32rpx;
color: #9ca3af;
font-size: 28rpx;
}
}
// 固定按钮样式
.fixed-button {
position: fixed;
bottom: 32rpx;
left: 32rpx;
right: 32rpx;
background: linear-gradient(135deg, var(--primary-color), var(--primary-color));
color: white;
border-radius: 24rpx;
padding: 32rpx;
text-align: center;
font-size: 32rpx;
font-weight: 600;
box-shadow: 0 8rpx 24rpx rgba(var(--primary-color), 0.3);
z-index: 1000;
transition: all 0.3s ease;
&:active {
transform: scale(0.95);
box-shadow: 0 4rpx 12rpx rgba(var(--primary-color), 0.3);
}
}
<template>
<view class="feedback-list">
<!-- Loading State -->
<view v-if="loading" class="flex justify-center items-center py-20">
<view class="text-gray-500">加载中...</view>
</view>
<!-- Content -->
<view v-else>
<!-- Feedback List -->
<view v-if="feedbackList.length > 0">
<view
v-for="feedback in feedbackList"
:key="feedback.id"
class="feedback-item"
>
<!-- Header -->
<view class="feedback-header">
<view class="feedback-id">反馈 #{{ feedback.id }}</view>
<view
class="feedback-status"
:class="{
'pending': feedback.status === 'PENDING',
'processed': feedback.status === 'PROCESSED'
}"
>
{{ feedback.status === 'PENDING' ? '待处理' : '已处理' }}
</view>
</view>
<!-- Content -->
<view class="feedback-content">
<view class="feedback-note">{{ feedback.note }}</view>
<view class="feedback-meta" v-if="feedback.name || feedback.contact">
<view class="meta-item" v-if="feedback.name">
<text>👤 {{ feedback.name }}</text>
</view>
<view class="meta-item" v-if="feedback.contact">
<text>📞 {{ feedback.contact }}</text>
</view>
</view>
</view>
<!-- Images -->
<view v-if="feedback.images && feedback.images.length > 0" class="feedback-images">
<image
v-for="(image, index) in feedback.images"
:key="index"
:src="image"
class="feedback-image"
mode="aspectFill"
@tap="previewImage(image, feedback.images)"
/>
</view>
<!-- Reply -->
<view v-if="feedback.reply" class="feedback-reply">
<view class="reply-label">官方回复:</view>
<view class="reply-content">{{ feedback.reply }}</view>
<view v-if="feedback.reply_time" class="reply-time">
回复时间:{{ formatDate(feedback.reply_time) }}
</view>
</view>
</view>
</view>
<!-- Empty State -->
<view v-else class="empty-state">
<view class="empty-icon">💬</view>
<view class="empty-title">暂无反馈记录</view>
<view class="empty-desc">您还没有提交过任何意见反馈</view>
</view>
<!-- Load More -->
<view v-if="hasMore && feedbackList.length > 0" class="load-more" @click="loadMore">
{{ loadingMore ? '加载中...' : '加载更多' }}
</view>
<!-- No More Data -->
<view v-if="!hasMore && feedbackList.length > 0" class="no-more">
没有更多数据了
</view>
</view>
<!-- Fixed Button -->
<view class="fixed-button" @click="goToFeedback">
新增反馈
</view>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import Taro from '@tarojs/taro';
import { useDidShow } from '@tarojs/taro';
import { getFeedbackListAPI } from '@/api/feedback';
// 响应式数据
const loading = ref(false);
const loadingMore = ref(false);
const feedbackList = ref([]);
const hasMore = ref(true);
const currentPage = ref(0);
const pageSize = ref(10);
/**
* @description: 加载反馈列表
* @param {boolean} isLoadMore - 是否为加载更多
*/
const loadFeedbackList = async (isLoadMore = false) => {
try {
if (isLoadMore) {
loadingMore.value = true;
} else {
loading.value = true;
currentPage.value = 0;
feedbackList.value = [];
}
const params = {
page: currentPage.value,
limit: pageSize.value
};
const response = await getFeedbackListAPI(params);
if (response.code === 1 && response.data && response.data.list) {
const newList = response.data.list.map(item => ({
...item,
}));
if (isLoadMore) {
feedbackList.value = [...feedbackList.value, ...newList];
} else {
feedbackList.value = newList;
}
// 判断是否还有更多数据
hasMore.value = newList.length === pageSize.value;
if (hasMore.value) {
currentPage.value++;
}
} else {
hasMore.value = false;
if (!isLoadMore) {
feedbackList.value = [];
}
}
} catch (error) {
console.error('加载反馈列表失败:', error);
Taro.showToast({
title: '加载失败,请重试',
icon: 'none'
});
hasMore.value = false;
} finally {
loading.value = false;
loadingMore.value = false;
}
};
/**
* @description: 加载更多
*/
const loadMore = () => {
if (!loadingMore.value && hasMore.value) {
loadFeedbackList(true);
}
};
/**
* @description: 格式化日期
* @param {string} dateString - 日期字符串
* @returns {string} 格式化后的日期
*/
const formatDate = (dateString) => {
if (!dateString) return '';
const date = new Date(dateString);
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
};
/**
* @description: 预览图片
* @param {string} current - 当前图片
* @param {Array} images - 图片列表
*/
const previewImage = (current, images) => {
Taro.previewImage({
current,
urls: images
});
};
/**
* @description: 跳转到新增反馈页面
*/
const goToFeedback = () => {
Taro.navigateTo({
url: '/pages/Feedback/index'
});
};
// 页面显示时刷新数据
useDidShow(() => {
loadFeedbackList();
});
// 页面加载时获取数据
onMounted(() => {
loadFeedbackList();
});
</script>
<style lang="less">
@import './index.less';
</style>
......@@ -80,7 +80,7 @@ const allMenuItems = [
icon: Message,
label: '意见反馈',
color: 'bg-blue-500',
onClick: () => Taro.navigateTo({ url: '/pages/Feedback/index' })
onClick: () => Taro.navigateTo({ url: '/pages/FeedbackList/index' })
},
{
id: 'agreement',
......