diff --git a/MeAgent/navigation/Screens.js b/MeAgent/navigation/Screens.js
index 343b9979..c1f86863 100644
--- a/MeAgent/navigation/Screens.js
+++ b/MeAgent/navigation/Screens.js
@@ -58,7 +58,7 @@ import MemberList from "../src/screens/Social/MemberList";
import SocialNotifications from "../src/screens/Social/Notifications";
// 新个人中心页面
-import { ProfileScreen as NewProfileScreen } from "../src/screens/Profile";
+import { ProfileScreen as NewProfileScreen, FeedbackScreen } from "../src/screens/Profile";
// 订阅管理页面
import { SubscriptionScreen } from "../src/screens/Subscription";
@@ -460,6 +460,13 @@ function NewProfileStack(props) {
cardStyle: { backgroundColor: "#0F172A" },
}}
/>
+
);
}
diff --git a/MeAgent/src/screens/Profile/FeedbackScreen.js b/MeAgent/src/screens/Profile/FeedbackScreen.js
new file mode 100644
index 00000000..c9f90184
--- /dev/null
+++ b/MeAgent/src/screens/Profile/FeedbackScreen.js
@@ -0,0 +1,589 @@
+/**
+ * 意见反馈页面
+ * 支持提交反馈和查看历史
+ */
+
+import React, { useState, useEffect, useCallback } from 'react';
+import {
+ StyleSheet,
+ ScrollView,
+ KeyboardAvoidingView,
+ Platform,
+ RefreshControl,
+ Alert,
+} from 'react-native';
+import {
+ Box,
+ VStack,
+ HStack,
+ Text,
+ Icon,
+ Pressable,
+ Input,
+ TextArea,
+ Spinner,
+ useToast,
+} from 'native-base';
+import { Ionicons } from '@expo/vector-icons';
+import { LinearGradient } from 'expo-linear-gradient';
+import { useNavigation } from '@react-navigation/native';
+import { SafeAreaView } from 'react-native-safe-area-context';
+
+import { useAuth } from '../../contexts/AuthContext';
+import { feedbackService } from '../../services/feedbackService';
+
+// 反馈类型配置
+const FEEDBACK_TYPES = [
+ { value: 'bug', label: 'Bug', icon: 'bug', color: '#EF4444' },
+ { value: 'suggestion', label: '建议', icon: 'bulb', color: '#F59E0B' },
+ { value: 'complaint', label: '投诉', icon: 'warning', color: '#F97316' },
+ { value: 'general', label: '其他', icon: 'help-circle', color: '#6B7280' },
+];
+
+// 状态配置
+const STATUS_CONFIG = {
+ pending: { label: '待处理', color: '#6B7280' },
+ processing: { label: '处理中', color: '#3B82F6' },
+ resolved: { label: '已解决', color: '#10B981' },
+ closed: { label: '已关闭', color: '#6B7280' },
+};
+
+/**
+ * 标签页选择器
+ */
+const TabSelector = ({ activeTab, onTabChange }) => {
+ const tabs = [
+ { key: 'submit', label: '提交反馈', icon: 'send' },
+ { key: 'history', label: '历史记录', icon: 'time' },
+ ];
+
+ return (
+
+ {tabs.map((tab) => (
+ onTabChange(tab.key)}
+ >
+
+
+
+
+ {tab.label}
+
+
+
+
+ ))}
+
+ );
+};
+
+/**
+ * 类型选择器
+ */
+const TypeSelector = ({ selectedType, onTypeChange }) => {
+ return (
+
+ {FEEDBACK_TYPES.map((type) => (
+ onTypeChange(type.value)}
+ mb={2}
+ >
+
+
+
+
+ {type.label}
+
+
+
+
+ ))}
+
+ );
+};
+
+/**
+ * 提交反馈表单
+ */
+const SubmitFeedbackForm = ({ onSuccess }) => {
+ const [formData, setFormData] = useState({
+ type: 'suggestion',
+ title: '',
+ content: '',
+ contact: '',
+ });
+ const [submitting, setSubmitting] = useState(false);
+ const toast = useToast();
+
+ const handleSubmit = async () => {
+ if (!formData.content.trim()) {
+ toast.show({
+ description: '请填写反馈内容',
+ placement: 'top',
+ bgColor: 'warning.500',
+ });
+ return;
+ }
+
+ try {
+ setSubmitting(true);
+ await feedbackService.submit({
+ type: formData.type,
+ title: formData.title.trim() || undefined,
+ content: formData.content.trim(),
+ contact: formData.contact.trim() || undefined,
+ });
+
+ toast.show({
+ description: '反馈提交成功,感谢您的宝贵意见!',
+ placement: 'top',
+ bgColor: 'success.500',
+ });
+
+ // 重置表单
+ setFormData({
+ type: 'suggestion',
+ title: '',
+ content: '',
+ contact: '',
+ });
+
+ // 通知刷新历史
+ if (onSuccess) onSuccess();
+ } catch (error) {
+ toast.show({
+ description: error.message || '提交失败,请稍后重试',
+ placement: 'top',
+ bgColor: 'error.500',
+ });
+ } finally {
+ setSubmitting(false);
+ }
+ };
+
+ return (
+
+ {/* 反馈类型 */}
+
+
+ 反馈类型
+
+ setFormData({ ...formData, type })}
+ />
+
+
+ {/* 标题 */}
+
+
+ 标题(可选)
+
+ setFormData({ ...formData, title: text })}
+ placeholder="简短描述您的反馈"
+ placeholderTextColor="gray.600"
+ bg="rgba(255,255,255,0.05)"
+ borderColor="rgba(255,255,255,0.1)"
+ color="white"
+ fontSize={14}
+ py={3}
+ px={4}
+ borderRadius={12}
+ maxLength={100}
+ _focus={{
+ borderColor: '#7C3AED',
+ bg: 'rgba(124, 58, 237, 0.1)',
+ }}
+ />
+
+
+ {/* 内容 */}
+
+
+
+ 反馈内容
+
+
+ {formData.content.length}/2000
+
+
+
+
+ {/* 联系方式 */}
+
+
+ 联系方式(可选,方便我们沟通)
+
+ setFormData({ ...formData, contact: text })}
+ placeholder="手机号/邮箱/微信"
+ placeholderTextColor="gray.600"
+ bg="rgba(255,255,255,0.05)"
+ borderColor="rgba(255,255,255,0.1)"
+ color="white"
+ fontSize={14}
+ py={3}
+ px={4}
+ borderRadius={12}
+ _focus={{
+ borderColor: '#7C3AED',
+ bg: 'rgba(124, 58, 237, 0.1)',
+ }}
+ />
+
+
+ {/* 提交按钮 */}
+
+
+ {submitting ? (
+
+ ) : (
+
+
+
+ 提交反馈
+
+
+ )}
+
+
+
+ );
+};
+
+/**
+ * 反馈历史列表
+ */
+const FeedbackHistoryList = ({ refreshTrigger }) => {
+ const [feedbacks, setFeedbacks] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [refreshing, setRefreshing] = useState(false);
+ const [page, setPage] = useState(1);
+ const [hasMore, setHasMore] = useState(true);
+
+ const loadFeedbacks = useCallback(async (pageNum = 1, append = false) => {
+ try {
+ if (pageNum === 1) setLoading(true);
+ const data = await feedbackService.getList({ page: pageNum, perPage: 10 });
+
+ if (append) {
+ setFeedbacks((prev) => [...prev, ...(data.feedbacks || [])]);
+ } else {
+ setFeedbacks(data.feedbacks || []);
+ }
+ setHasMore(data.pagination?.has_next || false);
+ } catch (error) {
+ console.error('加载反馈历史失败:', error);
+ } finally {
+ setLoading(false);
+ setRefreshing(false);
+ }
+ }, []);
+
+ useEffect(() => {
+ loadFeedbacks(1);
+ }, [loadFeedbacks, refreshTrigger]);
+
+ const handleRefresh = () => {
+ setRefreshing(true);
+ setPage(1);
+ loadFeedbacks(1);
+ };
+
+ const handleLoadMore = () => {
+ if (!hasMore || loading) return;
+ const nextPage = page + 1;
+ setPage(nextPage);
+ loadFeedbacks(nextPage, true);
+ };
+
+ const formatDateTime = (dateStr) => {
+ if (!dateStr) return '';
+ const date = new Date(dateStr);
+ return `${date.getMonth() + 1}/${date.getDate()} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
+ };
+
+ const getTypeConfig = (type) => {
+ return FEEDBACK_TYPES.find((t) => t.value === type) || FEEDBACK_TYPES[3];
+ };
+
+ if (loading && feedbacks.length === 0) {
+ return (
+
+
+
+ );
+ }
+
+ if (feedbacks.length === 0) {
+ return (
+
+
+
+ 暂无反馈记录
+
+
+ 提交反馈后会在这里显示
+
+
+ );
+ }
+
+ return (
+
+ }
+ onScrollEndDrag={handleLoadMore}
+ >
+
+ {feedbacks.map((feedback) => {
+ const typeConfig = getTypeConfig(feedback.feedback_type);
+ const statusConfig = STATUS_CONFIG[feedback.status] || STATUS_CONFIG.pending;
+
+ return (
+
+
+ {/* 头部 */}
+
+
+
+
+
+
+ {typeConfig.label}
+
+
+
+
+
+ {statusConfig.label}
+
+
+
+
+ {formatDateTime(feedback.created_at)}
+
+
+
+ {/* 标题 */}
+ {feedback.title && (
+
+ {feedback.title}
+
+ )}
+
+ {/* 内容 */}
+
+ {feedback.content}
+
+
+ {/* 管理员回复 */}
+ {feedback.admin_reply && (
+
+
+
+
+ 官方回复
+
+
+
+ {feedback.admin_reply}
+
+
+ )}
+
+
+ );
+ })}
+
+ {/* 加载更多 */}
+ {hasMore && (
+
+
+
+ 加载更多
+
+
+
+ )}
+
+
+ );
+};
+
+/**
+ * 意见反馈主页面
+ */
+const FeedbackScreen = () => {
+ const navigation = useNavigation();
+ const [activeTab, setActiveTab] = useState('submit');
+ const [refreshTrigger, setRefreshTrigger] = useState(0);
+
+ const handleSubmitSuccess = () => {
+ setRefreshTrigger((prev) => prev + 1);
+ };
+
+ return (
+
+
+ {/* 背景渐变 */}
+
+
+ {/* 头部导航 */}
+
+ navigation.goBack()} hitSlop={10}>
+
+
+
+
+
+ 意见反馈
+
+
+
+ {/* 标签页选择器 */}
+
+
+ {/* 内容区域 */}
+
+ {activeTab === 'submit' ? (
+
+
+
+
+ ) : (
+
+ )}
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+ headerGradient: {
+ position: 'absolute',
+ top: 0,
+ left: 0,
+ right: 0,
+ height: 200,
+ },
+ submitButton: {
+ paddingVertical: 14,
+ borderRadius: 12,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+});
+
+export default FeedbackScreen;
diff --git a/MeAgent/src/screens/Profile/ProfileScreen.js b/MeAgent/src/screens/Profile/ProfileScreen.js
index 9b199ea4..bbe4cde4 100644
--- a/MeAgent/src/screens/Profile/ProfileScreen.js
+++ b/MeAgent/src/screens/Profile/ProfileScreen.js
@@ -358,6 +358,8 @@ const ProfileScreen = () => {
navigation.navigate('Subscription');
} else if (item.route === 'Messages') {
navigation.navigate('Messages');
+ } else if (item.route === 'Feedback') {
+ navigation.navigate('Feedback');
} else if (item.route === 'Settings') {
navigation.navigate('SettingsDrawer');
} else if (item.route === 'About') {
diff --git a/MeAgent/src/screens/Profile/index.js b/MeAgent/src/screens/Profile/index.js
index cb916e9e..303ebd9c 100644
--- a/MeAgent/src/screens/Profile/index.js
+++ b/MeAgent/src/screens/Profile/index.js
@@ -3,3 +3,4 @@
*/
export { default as ProfileScreen } from './ProfileScreen';
+export { default as FeedbackScreen } from './FeedbackScreen';
diff --git a/MeAgent/src/services/feedbackService.js b/MeAgent/src/services/feedbackService.js
new file mode 100644
index 00000000..f5f8b297
--- /dev/null
+++ b/MeAgent/src/services/feedbackService.js
@@ -0,0 +1,65 @@
+/**
+ * 意见反馈服务
+ * 提交和查询用户反馈
+ */
+
+import { apiRequest } from './api';
+
+/**
+ * 提交反馈
+ * @param {object} data - 反馈数据
+ * @param {string} data.type - 反馈类型 (general/bug/suggestion/complaint)
+ * @param {string} data.title - 标题(可选)
+ * @param {string} data.content - 内容(必填)
+ * @param {string} data.contact - 联系方式(可选)
+ * @param {string[]} data.images - 图片URL列表(可选)
+ */
+const submit = async (data) => {
+ const response = await apiRequest('/api/user/feedback', {
+ method: 'POST',
+ body: JSON.stringify(data),
+ });
+
+ if (response.code !== 200) {
+ throw new Error(response.message || '提交反馈失败');
+ }
+
+ return response.data;
+};
+
+/**
+ * 获取反馈历史列表
+ * @param {object} options - 分页选项
+ * @param {number} options.page - 页码
+ * @param {number} options.perPage - 每页数量
+ * @param {string} options.status - 筛选状态(可选)
+ */
+const getList = async (options = {}) => {
+ const { page = 1, perPage = 10, status = '' } = options;
+
+ const params = new URLSearchParams({
+ page: String(page),
+ per_page: String(perPage),
+ });
+
+ if (status) {
+ params.append('status', status);
+ }
+
+ const response = await apiRequest(`/api/user/feedback?${params}`, {
+ method: 'GET',
+ });
+
+ if (response.code !== 200) {
+ throw new Error(response.message || '获取反馈历史失败');
+ }
+
+ return response.data;
+};
+
+export const feedbackService = {
+ submit,
+ getList,
+};
+
+export default feedbackService;
diff --git a/app.py b/app.py
index f71ad397..fd919b4d 100755
--- a/app.py
+++ b/app.py
@@ -1677,6 +1677,41 @@ class AnnouncementReadStatus(db.Model):
announcement = db.relationship('Announcement', backref='read_statuses')
+class Feedback(db.Model):
+ """用户反馈表"""
+ __tablename__ = 'user_feedback'
+
+ id = db.Column(db.Integer, primary_key=True)
+ user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True, index=True)
+ feedback_type = db.Column(db.String(50), default='general') # general/bug/suggestion/complaint
+ title = db.Column(db.String(200))
+ content = db.Column(db.Text, nullable=False)
+ contact = db.Column(db.String(100)) # 联系方式(可选,默认用户手机/邮箱)
+ images = db.Column(db.JSON) # 图片URL列表
+ status = db.Column(db.String(20), default='pending') # pending/processing/resolved/closed
+ admin_reply = db.Column(db.Text) # 管理员回复
+ created_at = db.Column(db.DateTime, default=beijing_now)
+ updated_at = db.Column(db.DateTime, default=beijing_now, onupdate=beijing_now)
+
+ # 关系
+ user = db.relationship('User', backref='feedbacks')
+
+ def to_dict(self):
+ return {
+ 'id': self.id,
+ 'user_id': self.user_id,
+ 'feedback_type': self.feedback_type,
+ 'title': self.title,
+ 'content': self.content,
+ 'contact': self.contact,
+ 'images': self.images or [],
+ 'status': self.status,
+ 'admin_reply': self.admin_reply,
+ 'created_at': self.created_at.isoformat() if self.created_at else None,
+ 'updated_at': self.updated_at.isoformat() if self.updated_at else None,
+ }
+
+
class SubscriptionPlan(db.Model):
"""订阅套餐表"""
__tablename__ = 'subscription_plans'
@@ -21954,6 +21989,114 @@ def delete_announcement(announcement_id):
return jsonify({'code': 500, 'message': str(e)}), 500
+# ==================== 意见反馈 API ====================
+
+@app.route('/api/user/feedback', methods=['POST'])
+@login_required
+def submit_feedback():
+ """
+ 提交用户反馈
+ 请求体:
+ {
+ "type": "bug", // general/bug/suggestion/complaint
+ "title": "反馈标题", // 可选
+ "content": "反馈内容", // 必填
+ "contact": "联系方式", // 可选,默认取用户手机/邮箱
+ "images": ["url1", "url2"] // 可选
+ }
+ """
+ try:
+ data = request.get_json()
+ if not data:
+ return jsonify({'code': 400, 'message': '请求数据无效'}), 400
+
+ content = data.get('content', '').strip()
+ if not content:
+ return jsonify({'code': 400, 'message': '反馈内容不能为空'}), 400
+
+ # 获取联系方式(优先使用用户提供的,否则使用用户的手机/邮箱)
+ contact = data.get('contact', '').strip()
+ if not contact:
+ contact = current_user.phone or current_user.email or ''
+
+ feedback = Feedback(
+ user_id=current_user.id,
+ feedback_type=data.get('type', 'general'),
+ title=data.get('title', '').strip() or None,
+ content=content,
+ contact=contact,
+ images=data.get('images') or None,
+ status='pending'
+ )
+
+ db.session.add(feedback)
+ db.session.commit()
+
+ return jsonify({
+ 'code': 200,
+ 'message': '反馈提交成功,感谢您的宝贵意见!',
+ 'data': {
+ 'feedback_id': feedback.id,
+ 'status': feedback.status,
+ 'created_at': feedback.created_at.isoformat() if feedback.created_at else None
+ }
+ })
+
+ except Exception as e:
+ db.session.rollback()
+ app.logger.error(f"提交反馈失败: {e}")
+ return jsonify({'code': 500, 'message': str(e)}), 500
+
+
+@app.route('/api/user/feedback', methods=['GET'])
+@login_required
+def get_my_feedback():
+ """
+ 获取当前用户的反馈历史
+ 查询参数:
+ - page: 页码,默认1
+ - per_page: 每页数量,默认10
+ - status: 筛选状态(可选)
+ """
+ try:
+ page = request.args.get('page', 1, type=int)
+ per_page = min(request.args.get('per_page', 10, type=int), 50)
+ status_filter = request.args.get('status', '').strip()
+
+ query = db.session.query(Feedback).filter(
+ Feedback.user_id == current_user.id
+ )
+
+ if status_filter:
+ query = query.filter(Feedback.status == status_filter)
+
+ # 按创建时间倒序
+ query = query.order_by(desc(Feedback.created_at))
+
+ # 分页
+ pagination = query.paginate(page=page, per_page=per_page, error_out=False)
+ feedbacks = pagination.items
+
+ return jsonify({
+ 'code': 200,
+ 'data': {
+ 'feedbacks': [f.to_dict() for f in feedbacks],
+ 'pagination': {
+ 'page': pagination.page,
+ 'per_page': pagination.per_page,
+ 'total': pagination.total,
+ 'pages': pagination.pages,
+ 'has_next': pagination.has_next,
+ 'has_prev': pagination.has_prev
+ }
+ }
+ })
+
+ except Exception as e:
+ app.logger.error(f"获取反馈历史失败: {e}")
+ return jsonify({'code': 500, 'message': str(e)}), 500
+
+
if __name__ == '__main__':
# 创建数据库表
with app.app_context():
diff --git a/src/components/Navbars/components/MobileDrawer/MobileDrawer.js b/src/components/Navbars/components/MobileDrawer/MobileDrawer.js
index 69d448f4..4458a1fe 100644
--- a/src/components/Navbars/components/MobileDrawer/MobileDrawer.js
+++ b/src/components/Navbars/components/MobileDrawer/MobileDrawer.js
@@ -344,6 +344,21 @@ const MobileDrawer = memo(({
+ {/* 意见反馈 */}
+ {isAuthenticated && user && (
+ <>
+
+
+ >
+ )}
+
{/* 移动端登录/登出按钮 */}
{isAuthenticated && user ? (
diff --git a/src/components/Navbars/components/UserMenu/DesktopUserMenu.js b/src/components/Navbars/components/UserMenu/DesktopUserMenu.js
index 340e31c8..f1196abc 100644
--- a/src/components/Navbars/components/UserMenu/DesktopUserMenu.js
+++ b/src/components/Navbars/components/UserMenu/DesktopUserMenu.js
@@ -15,7 +15,7 @@ import {
Button,
useDisclosure
} from '@chakra-ui/react';
-import { Settings, LogOut, Bell } from 'lucide-react';
+import { Settings, LogOut, Bell, MessageSquare } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import UserAvatar from './UserAvatar';
import { useSubscription } from '../../../../hooks/useSubscription';
@@ -221,6 +221,13 @@ const DesktopUserMenu = memo(({ user, handleLogout }) => {
>
站内信
+ }
+ onClick={() => { onClose(); navigate('/feedback'); }}
+ py={3}
+ >
+ 意见反馈
+
}
onClick={handleNavigateToSettings}
diff --git a/src/components/Navbars/components/UserMenu/TabletUserMenu.js b/src/components/Navbars/components/UserMenu/TabletUserMenu.js
index b76a4c2c..325d1f27 100644
--- a/src/components/Navbars/components/UserMenu/TabletUserMenu.js
+++ b/src/components/Navbars/components/UserMenu/TabletUserMenu.js
@@ -14,7 +14,7 @@ import {
Badge,
useColorModeValue
} from '@chakra-ui/react';
-import { Star, Calendar, User, Settings, Home, LogOut, Crown, Bell } from 'lucide-react';
+import { Star, Calendar, User, Settings, Home, LogOut, Crown, Bell, MessageSquare } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import UserAvatar from './UserAvatar';
import { useSubscription } from '../../../../hooks/useSubscription';
@@ -114,6 +114,9 @@ const TabletUserMenu = memo(({
} onClick={() => navigate('/inbox')}>
站内信
+ } onClick={() => navigate('/feedback')}>
+ 意见反馈
+
} onClick={() => navigate('/home/profile')}>
个人资料
diff --git a/src/routes/lazy-components.js b/src/routes/lazy-components.js
index e5ec9d1c..d1622b2d 100644
--- a/src/routes/lazy-components.js
+++ b/src/routes/lazy-components.js
@@ -59,6 +59,9 @@ export const lazyComponents = {
// 站内信模块
Inbox: React.lazy(() => import('@views/Inbox')),
+
+ // 意见反馈模块
+ Feedback: React.lazy(() => import('@views/Feedback')),
};
/**
@@ -92,4 +95,5 @@ export const {
StockCommunity,
DataBrowser,
Inbox,
+ Feedback,
} = lazyComponents;
diff --git a/src/routes/routeConfig.js b/src/routes/routeConfig.js
index 0a502182..aef7b1dc 100644
--- a/src/routes/routeConfig.js
+++ b/src/routes/routeConfig.js
@@ -249,6 +249,18 @@ export const routeConfig = [
description: '官方公告和互动消息'
}
},
+
+ // ==================== 意见反馈模块 ====================
+ {
+ path: 'feedback',
+ component: lazyComponents.Feedback,
+ protection: PROTECTION_MODES.REDIRECT, // 需要登录才能提交反馈
+ layout: 'main',
+ meta: {
+ title: '意见反馈',
+ description: '提交建议和问题反馈'
+ }
+ },
];
/**
diff --git a/src/services/feedbackService.js b/src/services/feedbackService.js
new file mode 100644
index 00000000..5e878646
--- /dev/null
+++ b/src/services/feedbackService.js
@@ -0,0 +1,74 @@
+/**
+ * 意见反馈服务
+ * 提交和查询用户反馈
+ */
+
+import { getApiBase } from './api';
+
+const API_BASE = getApiBase();
+
+/**
+ * 提交反馈
+ * @param {object} data - 反馈数据
+ * @param {string} data.type - 反馈类型 (general/bug/suggestion/complaint)
+ * @param {string} data.title - 标题(可选)
+ * @param {string} data.content - 内容(必填)
+ * @param {string} data.contact - 联系方式(可选)
+ * @param {string[]} data.images - 图片URL列表(可选)
+ */
+export const submitFeedback = async (data) => {
+ const response = await fetch(`${API_BASE}/api/user/feedback`, {
+ method: 'POST',
+ credentials: 'include',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(data),
+ });
+
+ const result = await response.json();
+
+ if (result.code !== 200) {
+ throw new Error(result.message || '提交反馈失败');
+ }
+
+ return result.data;
+};
+
+/**
+ * 获取反馈历史列表
+ * @param {object} options - 分页选项
+ * @param {number} options.page - 页码
+ * @param {number} options.perPage - 每页数量
+ * @param {string} options.status - 筛选状态(可选)
+ */
+export const getFeedbackList = async (options = {}) => {
+ const { page = 1, perPage = 10, status = '' } = options;
+
+ const params = new URLSearchParams({
+ page: String(page),
+ per_page: String(perPage),
+ });
+
+ if (status) {
+ params.append('status', status);
+ }
+
+ const response = await fetch(`${API_BASE}/api/user/feedback?${params}`, {
+ method: 'GET',
+ credentials: 'include',
+ });
+
+ const result = await response.json();
+
+ if (result.code !== 200) {
+ throw new Error(result.message || '获取反馈历史失败');
+ }
+
+ return result.data;
+};
+
+export default {
+ submitFeedback,
+ getFeedbackList,
+};
diff --git a/src/views/Feedback/index.js b/src/views/Feedback/index.js
new file mode 100644
index 00000000..a50876cd
--- /dev/null
+++ b/src/views/Feedback/index.js
@@ -0,0 +1,444 @@
+/**
+ * 意见反馈页面
+ * 支持提交反馈和查看历史
+ */
+
+import React, { useState, useEffect, useCallback } from 'react';
+import {
+ Box,
+ VStack,
+ HStack,
+ Text,
+ Heading,
+ Tabs,
+ TabList,
+ TabPanels,
+ Tab,
+ TabPanel,
+ Badge,
+ Card,
+ CardBody,
+ Icon,
+ Spinner,
+ Center,
+ Button,
+ useColorModeValue,
+ FormControl,
+ FormLabel,
+ Input,
+ Textarea,
+ Select,
+ useToast,
+ Alert,
+ AlertIcon,
+ Divider,
+} from '@chakra-ui/react';
+import {
+ MessageSquare,
+ Send,
+ History,
+ Bug,
+ Lightbulb,
+ AlertTriangle,
+ HelpCircle,
+ Clock,
+ CheckCircle,
+ XCircle,
+ MessageCircle,
+} from 'lucide-react';
+import { submitFeedback, getFeedbackList } from '../../services/feedbackService';
+
+// 反馈类型配置
+const FEEDBACK_TYPES = [
+ { value: 'bug', label: 'Bug 报告', icon: Bug, color: 'red' },
+ { value: 'suggestion', label: '功能建议', icon: Lightbulb, color: 'yellow' },
+ { value: 'complaint', label: '问题投诉', icon: AlertTriangle, color: 'orange' },
+ { value: 'general', label: '其他反馈', icon: HelpCircle, color: 'gray' },
+];
+
+// 状态配置
+const STATUS_CONFIG = {
+ pending: { label: '待处理', color: 'gray', icon: Clock },
+ processing: { label: '处理中', color: 'blue', icon: MessageCircle },
+ resolved: { label: '已解决', color: 'green', icon: CheckCircle },
+ closed: { label: '已关闭', color: 'gray', icon: XCircle },
+};
+
+// 格式化时间
+const formatDateTime = (dateStr) => {
+ if (!dateStr) return '';
+ const date = new Date(dateStr);
+ 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}`;
+};
+
+// 提交反馈表单组件
+const SubmitFeedbackForm = ({ onSuccess }) => {
+ const [formData, setFormData] = useState({
+ type: 'suggestion',
+ title: '',
+ content: '',
+ contact: '',
+ });
+ const [submitting, setSubmitting] = useState(false);
+ const toast = useToast();
+
+ const inputBg = useColorModeValue('white', 'gray.700');
+ const borderColor = useColorModeValue('gray.200', 'gray.600');
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+
+ if (!formData.content.trim()) {
+ toast({
+ title: '请填写反馈内容',
+ status: 'warning',
+ duration: 2000,
+ });
+ return;
+ }
+
+ try {
+ setSubmitting(true);
+ await submitFeedback({
+ type: formData.type,
+ title: formData.title.trim() || undefined,
+ content: formData.content.trim(),
+ contact: formData.contact.trim() || undefined,
+ });
+
+ toast({
+ title: '反馈提交成功',
+ description: '感谢您的宝贵意见,我们会尽快处理!',
+ status: 'success',
+ duration: 3000,
+ });
+
+ // 重置表单
+ setFormData({
+ type: 'suggestion',
+ title: '',
+ content: '',
+ contact: '',
+ });
+
+ // 通知父组件刷新历史
+ if (onSuccess) onSuccess();
+ } catch (error) {
+ toast({
+ title: '提交失败',
+ description: error.message,
+ status: 'error',
+ duration: 3000,
+ });
+ } finally {
+ setSubmitting(false);
+ }
+ };
+
+ return (
+
+
+ {/* 反馈类型 */}
+
+ 反馈类型
+
+
+
+ {/* 标题(可选) */}
+
+
+ 标题
+
+ (可选)
+
+
+ setFormData({ ...formData, title: e.target.value })}
+ placeholder="简短描述您的反馈"
+ bg={inputBg}
+ borderColor={borderColor}
+ maxLength={100}
+ />
+
+
+ {/* 反馈内容 */}
+
+ 反馈内容
+
+
+ {/* 联系方式(可选) */}
+
+
+ 联系方式
+
+ (可选,方便我们与您沟通)
+
+
+ setFormData({ ...formData, contact: e.target.value })}
+ placeholder="手机号/邮箱/微信"
+ bg={inputBg}
+ borderColor={borderColor}
+ />
+
+
+ {/* 提交按钮 */}
+ }
+ isLoading={submitting}
+ loadingText="提交中..."
+ >
+ 提交反馈
+
+
+
+ );
+};
+
+// 反馈历史列表组件
+const FeedbackHistoryList = ({ refreshTrigger }) => {
+ const [feedbacks, setFeedbacks] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [hasMore, setHasMore] = useState(true);
+ const [page, setPage] = useState(1);
+
+ const cardBg = useColorModeValue('white', 'gray.800');
+ const borderColor = useColorModeValue('gray.100', 'gray.700');
+ const replyBg = useColorModeValue('blue.50', 'rgba(66, 153, 225, 0.1)');
+
+ const loadFeedbacks = useCallback(async (pageNum = 1, append = false) => {
+ try {
+ setLoading(true);
+ const data = await getFeedbackList({ page: pageNum, perPage: 10 });
+
+ if (append) {
+ setFeedbacks((prev) => [...prev, ...(data.feedbacks || [])]);
+ } else {
+ setFeedbacks(data.feedbacks || []);
+ }
+ setHasMore(data.pagination?.has_next || false);
+ } catch (error) {
+ console.error('加载反馈历史失败:', error);
+ } finally {
+ setLoading(false);
+ }
+ }, []);
+
+ useEffect(() => {
+ loadFeedbacks(1);
+ }, [loadFeedbacks, refreshTrigger]);
+
+ const handleLoadMore = () => {
+ const nextPage = page + 1;
+ setPage(nextPage);
+ loadFeedbacks(nextPage, true);
+ };
+
+ const getTypeConfig = (type) => {
+ return FEEDBACK_TYPES.find((t) => t.value === type) || FEEDBACK_TYPES[3];
+ };
+
+ if (loading && feedbacks.length === 0) {
+ return (
+
+
+
+ );
+ }
+
+ if (feedbacks.length === 0) {
+ return (
+
+
+ 暂无反馈记录
+
+ 提交反馈后会在这里显示
+
+
+ );
+ }
+
+ return (
+
+ {feedbacks.map((feedback) => {
+ const typeConfig = getTypeConfig(feedback.feedback_type);
+ const statusConfig = STATUS_CONFIG[feedback.status] || STATUS_CONFIG.pending;
+ const TypeIcon = typeConfig.icon;
+ const StatusIcon = statusConfig.icon;
+
+ return (
+
+
+
+ {/* 头部:类型 + 状态 + 时间 */}
+
+
+
+ {typeConfig.label}
+ {feedback.title && (
+
+ {feedback.title}
+
+ )}
+
+
+
+
+
+ {statusConfig.label}
+
+
+
+ {formatDateTime(feedback.created_at)}
+
+
+
+
+ {/* 反馈内容 */}
+
+ {feedback.content}
+
+
+ {/* 管理员回复 */}
+ {feedback.admin_reply && (
+ <>
+
+
+
+
+
+ 官方回复
+
+ {feedback.updated_at && (
+
+ {formatDateTime(feedback.updated_at)}
+
+ )}
+
+
+ {feedback.admin_reply}
+
+
+ >
+ )}
+
+
+
+ );
+ })}
+
+ {/* 加载更多 */}
+ {hasMore && (
+
+
+
+ )}
+
+ );
+};
+
+// 主页面组件
+const FeedbackPage = () => {
+ const [refreshTrigger, setRefreshTrigger] = useState(0);
+
+ const bgColor = useColorModeValue('gray.50', 'gray.900');
+ const cardBg = useColorModeValue('white', 'gray.800');
+
+ const handleSubmitSuccess = () => {
+ setRefreshTrigger((prev) => prev + 1);
+ };
+
+ return (
+
+
+ {/* 页面标题 */}
+
+
+ 意见反馈
+
+
+ {/* 提示信息 */}
+
+
+
+ 您的反馈对我们非常重要,我们会认真阅读每一条建议并尽快改进。
+
+
+
+ {/* 标签页 */}
+
+
+
+
+
+
+
+ 提交反馈
+
+
+
+
+
+ 反馈历史
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default FeedbackPage;