更新ios
This commit is contained in:
@@ -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" },
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="Feedback"
|
||||
component={FeedbackScreen}
|
||||
options={{
|
||||
cardStyle: { backgroundColor: "#0A0A0F" },
|
||||
}}
|
||||
/>
|
||||
</Stack.Navigator>
|
||||
);
|
||||
}
|
||||
|
||||
589
MeAgent/src/screens/Profile/FeedbackScreen.js
Normal file
589
MeAgent/src/screens/Profile/FeedbackScreen.js
Normal file
@@ -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 (
|
||||
<HStack mx={4} bg="rgba(255,255,255,0.05)" borderRadius={12} p={1}>
|
||||
{tabs.map((tab) => (
|
||||
<Pressable
|
||||
key={tab.key}
|
||||
flex={1}
|
||||
onPress={() => onTabChange(tab.key)}
|
||||
>
|
||||
<Box
|
||||
py={2.5}
|
||||
borderRadius={10}
|
||||
bg={activeTab === tab.key ? 'rgba(124, 58, 237, 0.3)' : 'transparent'}
|
||||
alignItems="center"
|
||||
>
|
||||
<HStack space={2} alignItems="center">
|
||||
<Icon
|
||||
as={Ionicons}
|
||||
name={tab.icon}
|
||||
size="sm"
|
||||
color={activeTab === tab.key ? '#A78BFA' : 'gray.500'}
|
||||
/>
|
||||
<Text
|
||||
color={activeTab === tab.key ? 'white' : 'gray.500'}
|
||||
fontSize={14}
|
||||
fontWeight={activeTab === tab.key ? 'bold' : 'medium'}
|
||||
>
|
||||
{tab.label}
|
||||
</Text>
|
||||
</HStack>
|
||||
</Box>
|
||||
</Pressable>
|
||||
))}
|
||||
</HStack>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 类型选择器
|
||||
*/
|
||||
const TypeSelector = ({ selectedType, onTypeChange }) => {
|
||||
return (
|
||||
<HStack space={2} flexWrap="wrap">
|
||||
{FEEDBACK_TYPES.map((type) => (
|
||||
<Pressable
|
||||
key={type.value}
|
||||
onPress={() => onTypeChange(type.value)}
|
||||
mb={2}
|
||||
>
|
||||
<Box
|
||||
px={4}
|
||||
py={2}
|
||||
borderRadius={20}
|
||||
borderWidth={1}
|
||||
borderColor={selectedType === type.value ? type.color : 'rgba(255,255,255,0.1)'}
|
||||
bg={selectedType === type.value ? `${type.color}20` : 'transparent'}
|
||||
>
|
||||
<HStack space={1.5} alignItems="center">
|
||||
<Icon
|
||||
as={Ionicons}
|
||||
name={type.icon}
|
||||
size="xs"
|
||||
color={selectedType === type.value ? type.color : 'gray.500'}
|
||||
/>
|
||||
<Text
|
||||
color={selectedType === type.value ? type.color : 'gray.500'}
|
||||
fontSize={13}
|
||||
fontWeight={selectedType === type.value ? 'bold' : 'medium'}
|
||||
>
|
||||
{type.label}
|
||||
</Text>
|
||||
</HStack>
|
||||
</Box>
|
||||
</Pressable>
|
||||
))}
|
||||
</HStack>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 提交反馈表单
|
||||
*/
|
||||
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 (
|
||||
<VStack space={5} px={4} mt={4}>
|
||||
{/* 反馈类型 */}
|
||||
<Box>
|
||||
<Text color="gray.400" fontSize={13} mb={2}>
|
||||
反馈类型
|
||||
</Text>
|
||||
<TypeSelector
|
||||
selectedType={formData.type}
|
||||
onTypeChange={(type) => setFormData({ ...formData, type })}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* 标题 */}
|
||||
<Box>
|
||||
<Text color="gray.400" fontSize={13} mb={2}>
|
||||
标题(可选)
|
||||
</Text>
|
||||
<Input
|
||||
value={formData.title}
|
||||
onChangeText={(text) => 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)',
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* 内容 */}
|
||||
<Box>
|
||||
<HStack justifyContent="space-between" alignItems="center" mb={2}>
|
||||
<Text color="gray.400" fontSize={13}>
|
||||
反馈内容
|
||||
</Text>
|
||||
<Text color="gray.600" fontSize={11}>
|
||||
{formData.content.length}/2000
|
||||
</Text>
|
||||
</HStack>
|
||||
<TextArea
|
||||
value={formData.content}
|
||||
onChangeText={(text) => setFormData({ ...formData, content: 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}
|
||||
h={150}
|
||||
maxLength={2000}
|
||||
autoCompleteType={undefined}
|
||||
_focus={{
|
||||
borderColor: '#7C3AED',
|
||||
bg: 'rgba(124, 58, 237, 0.1)',
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* 联系方式 */}
|
||||
<Box>
|
||||
<Text color="gray.400" fontSize={13} mb={2}>
|
||||
联系方式(可选,方便我们沟通)
|
||||
</Text>
|
||||
<Input
|
||||
value={formData.contact}
|
||||
onChangeText={(text) => 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)',
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* 提交按钮 */}
|
||||
<Pressable onPress={handleSubmit} disabled={submitting}>
|
||||
<LinearGradient
|
||||
colors={['#7C3AED', '#EC4899']}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 0 }}
|
||||
style={styles.submitButton}
|
||||
>
|
||||
{submitting ? (
|
||||
<Spinner color="white" size="sm" />
|
||||
) : (
|
||||
<HStack space={2} alignItems="center">
|
||||
<Icon as={Ionicons} name="send" size="sm" color="white" />
|
||||
<Text color="white" fontSize={16} fontWeight="bold">
|
||||
提交反馈
|
||||
</Text>
|
||||
</HStack>
|
||||
)}
|
||||
</LinearGradient>
|
||||
</Pressable>
|
||||
</VStack>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 反馈历史列表
|
||||
*/
|
||||
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 (
|
||||
<Box flex={1} alignItems="center" justifyContent="center" py={12}>
|
||||
<Spinner size="lg" color="#7C3AED" />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (feedbacks.length === 0) {
|
||||
return (
|
||||
<Box flex={1} alignItems="center" justifyContent="center" py={12}>
|
||||
<Icon as={Ionicons} name="time-outline" size="3xl" color="gray.600" mb={3} />
|
||||
<Text color="gray.500" fontSize={14}>
|
||||
暂无反馈记录
|
||||
</Text>
|
||||
<Text color="gray.600" fontSize={12} mt={1}>
|
||||
提交反馈后会在这里显示
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
showsVerticalScrollIndicator={false}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
onRefresh={handleRefresh}
|
||||
tintColor="#7C3AED"
|
||||
colors={['#7C3AED']}
|
||||
/>
|
||||
}
|
||||
onScrollEndDrag={handleLoadMore}
|
||||
>
|
||||
<VStack space={3} px={4} mt={4} pb={20}>
|
||||
{feedbacks.map((feedback) => {
|
||||
const typeConfig = getTypeConfig(feedback.feedback_type);
|
||||
const statusConfig = STATUS_CONFIG[feedback.status] || STATUS_CONFIG.pending;
|
||||
|
||||
return (
|
||||
<Box
|
||||
key={feedback.id}
|
||||
bg="rgba(255,255,255,0.03)"
|
||||
borderWidth={1}
|
||||
borderColor="rgba(255,255,255,0.06)"
|
||||
borderRadius={16}
|
||||
p={4}
|
||||
>
|
||||
<VStack space={3}>
|
||||
{/* 头部 */}
|
||||
<HStack justifyContent="space-between" alignItems="center">
|
||||
<HStack space={2} alignItems="center">
|
||||
<Box
|
||||
px={2}
|
||||
py={0.5}
|
||||
borderRadius={6}
|
||||
bg={`${typeConfig.color}20`}
|
||||
>
|
||||
<HStack space={1} alignItems="center">
|
||||
<Icon
|
||||
as={Ionicons}
|
||||
name={typeConfig.icon}
|
||||
size="2xs"
|
||||
color={typeConfig.color}
|
||||
/>
|
||||
<Text color={typeConfig.color} fontSize={11} fontWeight="bold">
|
||||
{typeConfig.label}
|
||||
</Text>
|
||||
</HStack>
|
||||
</Box>
|
||||
<Box
|
||||
px={2}
|
||||
py={0.5}
|
||||
borderRadius={6}
|
||||
bg={`${statusConfig.color}20`}
|
||||
>
|
||||
<Text color={statusConfig.color} fontSize={11} fontWeight="medium">
|
||||
{statusConfig.label}
|
||||
</Text>
|
||||
</Box>
|
||||
</HStack>
|
||||
<Text color="gray.600" fontSize={11}>
|
||||
{formatDateTime(feedback.created_at)}
|
||||
</Text>
|
||||
</HStack>
|
||||
|
||||
{/* 标题 */}
|
||||
{feedback.title && (
|
||||
<Text color="white" fontSize={14} fontWeight="bold">
|
||||
{feedback.title}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{/* 内容 */}
|
||||
<Text color="gray.400" fontSize={13} numberOfLines={3}>
|
||||
{feedback.content}
|
||||
</Text>
|
||||
|
||||
{/* 管理员回复 */}
|
||||
{feedback.admin_reply && (
|
||||
<Box
|
||||
bg="rgba(59, 130, 246, 0.1)"
|
||||
borderLeftWidth={3}
|
||||
borderLeftColor="#3B82F6"
|
||||
p={3}
|
||||
borderRadius={8}
|
||||
mt={1}
|
||||
>
|
||||
<HStack space={1.5} alignItems="center" mb={1.5}>
|
||||
<Icon as={Ionicons} name="chatbubble" size="xs" color="#3B82F6" />
|
||||
<Text color="#60A5FA" fontSize={12} fontWeight="bold">
|
||||
官方回复
|
||||
</Text>
|
||||
</HStack>
|
||||
<Text color="gray.300" fontSize={12}>
|
||||
{feedback.admin_reply}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</VStack>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* 加载更多 */}
|
||||
{hasMore && (
|
||||
<Pressable onPress={handleLoadMore}>
|
||||
<Box py={3} alignItems="center">
|
||||
<Text color="gray.500" fontSize={13}>
|
||||
加载更多
|
||||
</Text>
|
||||
</Box>
|
||||
</Pressable>
|
||||
)}
|
||||
</VStack>
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 意见反馈主页面
|
||||
*/
|
||||
const FeedbackScreen = () => {
|
||||
const navigation = useNavigation();
|
||||
const [activeTab, setActiveTab] = useState('submit');
|
||||
const [refreshTrigger, setRefreshTrigger] = useState(0);
|
||||
|
||||
const handleSubmitSuccess = () => {
|
||||
setRefreshTrigger((prev) => prev + 1);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box flex={1} bg="#0A0A0F">
|
||||
<SafeAreaView style={styles.container} edges={['top']}>
|
||||
{/* 背景渐变 */}
|
||||
<LinearGradient
|
||||
colors={['rgba(124, 58, 237, 0.1)', 'transparent']}
|
||||
style={styles.headerGradient}
|
||||
/>
|
||||
|
||||
{/* 头部导航 */}
|
||||
<HStack px={4} py={3} alignItems="center">
|
||||
<Pressable onPress={() => navigation.goBack()} hitSlop={10}>
|
||||
<Box
|
||||
bg="rgba(255,255,255,0.05)"
|
||||
p={2}
|
||||
borderRadius={10}
|
||||
>
|
||||
<Icon as={Ionicons} name="arrow-back" size="sm" color="white" />
|
||||
</Box>
|
||||
</Pressable>
|
||||
<Text color="white" fontSize={18} fontWeight="bold" ml={3}>
|
||||
意见反馈
|
||||
</Text>
|
||||
</HStack>
|
||||
|
||||
{/* 标签页选择器 */}
|
||||
<TabSelector activeTab={activeTab} onTabChange={setActiveTab} />
|
||||
|
||||
{/* 内容区域 */}
|
||||
<KeyboardAvoidingView
|
||||
style={{ flex: 1 }}
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||
>
|
||||
{activeTab === 'submit' ? (
|
||||
<ScrollView showsVerticalScrollIndicator={false}>
|
||||
<SubmitFeedbackForm onSuccess={handleSubmitSuccess} />
|
||||
<Box height={100} />
|
||||
</ScrollView>
|
||||
) : (
|
||||
<FeedbackHistoryList refreshTrigger={refreshTrigger} />
|
||||
)}
|
||||
</KeyboardAvoidingView>
|
||||
</SafeAreaView>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
@@ -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') {
|
||||
|
||||
@@ -3,3 +3,4 @@
|
||||
*/
|
||||
|
||||
export { default as ProfileScreen } from './ProfileScreen';
|
||||
export { default as FeedbackScreen } from './FeedbackScreen';
|
||||
|
||||
65
MeAgent/src/services/feedbackService.js
Normal file
65
MeAgent/src/services/feedbackService.js
Normal file
@@ -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;
|
||||
143
app.py
143
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():
|
||||
|
||||
@@ -344,6 +344,21 @@ const MobileDrawer = memo(({
|
||||
</HStack>
|
||||
</Box>
|
||||
|
||||
{/* 意见反馈 */}
|
||||
{isAuthenticated && user && (
|
||||
<>
|
||||
<Divider />
|
||||
<Button
|
||||
colorScheme="blue"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleNavigate('/feedback')}
|
||||
>
|
||||
意见反馈
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 移动端登录/登出按钮 */}
|
||||
<Divider />
|
||||
{isAuthenticated && user ? (
|
||||
|
||||
@@ -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 }) => {
|
||||
>
|
||||
站内信
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
icon={<MessageSquare size={16} />}
|
||||
onClick={() => { onClose(); navigate('/feedback'); }}
|
||||
py={3}
|
||||
>
|
||||
意见反馈
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
icon={<Settings size={16} />}
|
||||
onClick={handleNavigateToSettings}
|
||||
|
||||
@@ -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(({
|
||||
<MenuItem icon={<Bell size={16} />} onClick={() => navigate('/inbox')}>
|
||||
站内信
|
||||
</MenuItem>
|
||||
<MenuItem icon={<MessageSquare size={16} />} onClick={() => navigate('/feedback')}>
|
||||
意见反馈
|
||||
</MenuItem>
|
||||
<MenuItem icon={<User size={16} />} onClick={() => navigate('/home/profile')}>
|
||||
个人资料
|
||||
</MenuItem>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -249,6 +249,18 @@ export const routeConfig = [
|
||||
description: '官方公告和互动消息'
|
||||
}
|
||||
},
|
||||
|
||||
// ==================== 意见反馈模块 ====================
|
||||
{
|
||||
path: 'feedback',
|
||||
component: lazyComponents.Feedback,
|
||||
protection: PROTECTION_MODES.REDIRECT, // 需要登录才能提交反馈
|
||||
layout: 'main',
|
||||
meta: {
|
||||
title: '意见反馈',
|
||||
description: '提交建议和问题反馈'
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
74
src/services/feedbackService.js
Normal file
74
src/services/feedbackService.js
Normal file
@@ -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,
|
||||
};
|
||||
444
src/views/Feedback/index.js
Normal file
444
src/views/Feedback/index.js
Normal file
@@ -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 (
|
||||
<Box as="form" onSubmit={handleSubmit}>
|
||||
<VStack spacing={5} align="stretch">
|
||||
{/* 反馈类型 */}
|
||||
<FormControl>
|
||||
<FormLabel fontWeight="medium">反馈类型</FormLabel>
|
||||
<Select
|
||||
value={formData.type}
|
||||
onChange={(e) => setFormData({ ...formData, type: e.target.value })}
|
||||
bg={inputBg}
|
||||
borderColor={borderColor}
|
||||
>
|
||||
{FEEDBACK_TYPES.map((type) => (
|
||||
<option key={type.value} value={type.value}>
|
||||
{type.label}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
{/* 标题(可选) */}
|
||||
<FormControl>
|
||||
<FormLabel fontWeight="medium">
|
||||
标题
|
||||
<Text as="span" color="gray.500" fontWeight="normal" ml={2} fontSize="sm">
|
||||
(可选)
|
||||
</Text>
|
||||
</FormLabel>
|
||||
<Input
|
||||
value={formData.title}
|
||||
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
|
||||
placeholder="简短描述您的反馈"
|
||||
bg={inputBg}
|
||||
borderColor={borderColor}
|
||||
maxLength={100}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
{/* 反馈内容 */}
|
||||
<FormControl isRequired>
|
||||
<FormLabel fontWeight="medium">反馈内容</FormLabel>
|
||||
<Textarea
|
||||
value={formData.content}
|
||||
onChange={(e) => setFormData({ ...formData, content: e.target.value })}
|
||||
placeholder="请详细描述您的问题或建议..."
|
||||
bg={inputBg}
|
||||
borderColor={borderColor}
|
||||
rows={6}
|
||||
maxLength={2000}
|
||||
/>
|
||||
<Text fontSize="xs" color="gray.500" textAlign="right" mt={1}>
|
||||
{formData.content.length}/2000
|
||||
</Text>
|
||||
</FormControl>
|
||||
|
||||
{/* 联系方式(可选) */}
|
||||
<FormControl>
|
||||
<FormLabel fontWeight="medium">
|
||||
联系方式
|
||||
<Text as="span" color="gray.500" fontWeight="normal" ml={2} fontSize="sm">
|
||||
(可选,方便我们与您沟通)
|
||||
</Text>
|
||||
</FormLabel>
|
||||
<Input
|
||||
value={formData.contact}
|
||||
onChange={(e) => setFormData({ ...formData, contact: e.target.value })}
|
||||
placeholder="手机号/邮箱/微信"
|
||||
bg={inputBg}
|
||||
borderColor={borderColor}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
{/* 提交按钮 */}
|
||||
<Button
|
||||
type="submit"
|
||||
colorScheme="blue"
|
||||
size="lg"
|
||||
leftIcon={<Send size={18} />}
|
||||
isLoading={submitting}
|
||||
loadingText="提交中..."
|
||||
>
|
||||
提交反馈
|
||||
</Button>
|
||||
</VStack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
// 反馈历史列表组件
|
||||
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 (
|
||||
<Center py={12}>
|
||||
<Spinner size="lg" color="blue.500" />
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
|
||||
if (feedbacks.length === 0) {
|
||||
return (
|
||||
<Center py={12} flexDirection="column">
|
||||
<Icon as={History} boxSize={12} color="gray.400" mb={4} />
|
||||
<Text color="gray.500">暂无反馈记录</Text>
|
||||
<Text color="gray.400" fontSize="sm" mt={1}>
|
||||
提交反馈后会在这里显示
|
||||
</Text>
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<VStack spacing={4} align="stretch">
|
||||
{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 (
|
||||
<Card
|
||||
key={feedback.id}
|
||||
bg={cardBg}
|
||||
borderWidth="1px"
|
||||
borderColor={borderColor}
|
||||
shadow="sm"
|
||||
>
|
||||
<CardBody>
|
||||
<VStack align="stretch" spacing={3}>
|
||||
{/* 头部:类型 + 状态 + 时间 */}
|
||||
<HStack justify="space-between" wrap="wrap">
|
||||
<HStack spacing={2}>
|
||||
<Icon as={TypeIcon} color={`${typeConfig.color}.500`} boxSize={4} />
|
||||
<Badge colorScheme={typeConfig.color}>{typeConfig.label}</Badge>
|
||||
{feedback.title && (
|
||||
<Text fontWeight="semibold" noOfLines={1}>
|
||||
{feedback.title}
|
||||
</Text>
|
||||
)}
|
||||
</HStack>
|
||||
<HStack spacing={2}>
|
||||
<Badge colorScheme={statusConfig.color} variant="subtle">
|
||||
<HStack spacing={1}>
|
||||
<Icon as={StatusIcon} boxSize={3} />
|
||||
<Text>{statusConfig.label}</Text>
|
||||
</HStack>
|
||||
</Badge>
|
||||
<Text fontSize="xs" color="gray.500">
|
||||
{formatDateTime(feedback.created_at)}
|
||||
</Text>
|
||||
</HStack>
|
||||
</HStack>
|
||||
|
||||
{/* 反馈内容 */}
|
||||
<Text color="gray.700" fontSize="sm" whiteSpace="pre-wrap">
|
||||
{feedback.content}
|
||||
</Text>
|
||||
|
||||
{/* 管理员回复 */}
|
||||
{feedback.admin_reply && (
|
||||
<>
|
||||
<Divider />
|
||||
<Box bg={replyBg} p={3} borderRadius="md" borderLeft="3px solid" borderColor="blue.400">
|
||||
<HStack spacing={2} mb={2}>
|
||||
<Icon as={MessageCircle} color="blue.500" boxSize={4} />
|
||||
<Text fontWeight="medium" fontSize="sm" color="blue.600">
|
||||
官方回复
|
||||
</Text>
|
||||
{feedback.updated_at && (
|
||||
<Text fontSize="xs" color="gray.500">
|
||||
{formatDateTime(feedback.updated_at)}
|
||||
</Text>
|
||||
)}
|
||||
</HStack>
|
||||
<Text fontSize="sm" color="gray.700" whiteSpace="pre-wrap">
|
||||
{feedback.admin_reply}
|
||||
</Text>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</VStack>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* 加载更多 */}
|
||||
{hasMore && (
|
||||
<Center pt={2}>
|
||||
<Button variant="ghost" size="sm" onClick={handleLoadMore} isLoading={loading}>
|
||||
加载更多
|
||||
</Button>
|
||||
</Center>
|
||||
)}
|
||||
</VStack>
|
||||
);
|
||||
};
|
||||
|
||||
// 主页面组件
|
||||
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 (
|
||||
<Box minH="100vh" bg={bgColor} py={8}>
|
||||
<Box maxW="800px" mx="auto" px={4}>
|
||||
{/* 页面标题 */}
|
||||
<HStack mb={6}>
|
||||
<Icon as={MessageSquare} boxSize={6} color="blue.500" />
|
||||
<Heading size="lg">意见反馈</Heading>
|
||||
</HStack>
|
||||
|
||||
{/* 提示信息 */}
|
||||
<Alert status="info" borderRadius="md" mb={6}>
|
||||
<AlertIcon />
|
||||
<Text fontSize="sm">
|
||||
您的反馈对我们非常重要,我们会认真阅读每一条建议并尽快改进。
|
||||
</Text>
|
||||
</Alert>
|
||||
|
||||
{/* 标签页 */}
|
||||
<Card bg={cardBg} shadow="sm">
|
||||
<CardBody p={0}>
|
||||
<Tabs variant="enclosed" colorScheme="blue">
|
||||
<TabList borderBottomWidth="1px" px={4} pt={4}>
|
||||
<Tab fontWeight="medium">
|
||||
<HStack spacing={2}>
|
||||
<Icon as={Send} boxSize={4} />
|
||||
<Text>提交反馈</Text>
|
||||
</HStack>
|
||||
</Tab>
|
||||
<Tab fontWeight="medium">
|
||||
<HStack spacing={2}>
|
||||
<Icon as={History} boxSize={4} />
|
||||
<Text>反馈历史</Text>
|
||||
</HStack>
|
||||
</Tab>
|
||||
</TabList>
|
||||
|
||||
<TabPanels>
|
||||
<TabPanel p={6}>
|
||||
<SubmitFeedbackForm onSuccess={handleSubmitSuccess} />
|
||||
</TabPanel>
|
||||
<TabPanel p={6}>
|
||||
<FeedbackHistoryList refreshTrigger={refreshTrigger} />
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeedbackPage;
|
||||
Reference in New Issue
Block a user