更新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 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";
|
import { SubscriptionScreen } from "../src/screens/Subscription";
|
||||||
@@ -460,6 +460,13 @@ function NewProfileStack(props) {
|
|||||||
cardStyle: { backgroundColor: "#0F172A" },
|
cardStyle: { backgroundColor: "#0F172A" },
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name="Feedback"
|
||||||
|
component={FeedbackScreen}
|
||||||
|
options={{
|
||||||
|
cardStyle: { backgroundColor: "#0A0A0F" },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Stack.Navigator>
|
</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');
|
navigation.navigate('Subscription');
|
||||||
} else if (item.route === 'Messages') {
|
} else if (item.route === 'Messages') {
|
||||||
navigation.navigate('Messages');
|
navigation.navigate('Messages');
|
||||||
|
} else if (item.route === 'Feedback') {
|
||||||
|
navigation.navigate('Feedback');
|
||||||
} else if (item.route === 'Settings') {
|
} else if (item.route === 'Settings') {
|
||||||
navigation.navigate('SettingsDrawer');
|
navigation.navigate('SettingsDrawer');
|
||||||
} else if (item.route === 'About') {
|
} else if (item.route === 'About') {
|
||||||
|
|||||||
@@ -3,3 +3,4 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export { default as ProfileScreen } from './ProfileScreen';
|
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')
|
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):
|
class SubscriptionPlan(db.Model):
|
||||||
"""订阅套餐表"""
|
"""订阅套餐表"""
|
||||||
__tablename__ = 'subscription_plans'
|
__tablename__ = 'subscription_plans'
|
||||||
@@ -21954,6 +21989,114 @@ def delete_announcement(announcement_id):
|
|||||||
return jsonify({'code': 500, 'message': str(e)}), 500
|
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__':
|
if __name__ == '__main__':
|
||||||
# 创建数据库表
|
# 创建数据库表
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
|
|||||||
@@ -344,6 +344,21 @@ const MobileDrawer = memo(({
|
|||||||
</HStack>
|
</HStack>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{/* 意见反馈 */}
|
||||||
|
{isAuthenticated && user && (
|
||||||
|
<>
|
||||||
|
<Divider />
|
||||||
|
<Button
|
||||||
|
colorScheme="blue"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleNavigate('/feedback')}
|
||||||
|
>
|
||||||
|
意见反馈
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 移动端登录/登出按钮 */}
|
{/* 移动端登录/登出按钮 */}
|
||||||
<Divider />
|
<Divider />
|
||||||
{isAuthenticated && user ? (
|
{isAuthenticated && user ? (
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
useDisclosure
|
useDisclosure
|
||||||
} from '@chakra-ui/react';
|
} 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 { useNavigate } from 'react-router-dom';
|
||||||
import UserAvatar from './UserAvatar';
|
import UserAvatar from './UserAvatar';
|
||||||
import { useSubscription } from '../../../../hooks/useSubscription';
|
import { useSubscription } from '../../../../hooks/useSubscription';
|
||||||
@@ -221,6 +221,13 @@ const DesktopUserMenu = memo(({ user, handleLogout }) => {
|
|||||||
>
|
>
|
||||||
站内信
|
站内信
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
icon={<MessageSquare size={16} />}
|
||||||
|
onClick={() => { onClose(); navigate('/feedback'); }}
|
||||||
|
py={3}
|
||||||
|
>
|
||||||
|
意见反馈
|
||||||
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon={<Settings size={16} />}
|
icon={<Settings size={16} />}
|
||||||
onClick={handleNavigateToSettings}
|
onClick={handleNavigateToSettings}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
Badge,
|
Badge,
|
||||||
useColorModeValue
|
useColorModeValue
|
||||||
} from '@chakra-ui/react';
|
} 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 { useNavigate } from 'react-router-dom';
|
||||||
import UserAvatar from './UserAvatar';
|
import UserAvatar from './UserAvatar';
|
||||||
import { useSubscription } from '../../../../hooks/useSubscription';
|
import { useSubscription } from '../../../../hooks/useSubscription';
|
||||||
@@ -114,6 +114,9 @@ const TabletUserMenu = memo(({
|
|||||||
<MenuItem icon={<Bell size={16} />} onClick={() => navigate('/inbox')}>
|
<MenuItem icon={<Bell size={16} />} onClick={() => navigate('/inbox')}>
|
||||||
站内信
|
站内信
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
<MenuItem icon={<MessageSquare size={16} />} onClick={() => navigate('/feedback')}>
|
||||||
|
意见反馈
|
||||||
|
</MenuItem>
|
||||||
<MenuItem icon={<User size={16} />} onClick={() => navigate('/home/profile')}>
|
<MenuItem icon={<User size={16} />} onClick={() => navigate('/home/profile')}>
|
||||||
个人资料
|
个人资料
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|||||||
@@ -59,6 +59,9 @@ export const lazyComponents = {
|
|||||||
|
|
||||||
// 站内信模块
|
// 站内信模块
|
||||||
Inbox: React.lazy(() => import('@views/Inbox')),
|
Inbox: React.lazy(() => import('@views/Inbox')),
|
||||||
|
|
||||||
|
// 意见反馈模块
|
||||||
|
Feedback: React.lazy(() => import('@views/Feedback')),
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -92,4 +95,5 @@ export const {
|
|||||||
StockCommunity,
|
StockCommunity,
|
||||||
DataBrowser,
|
DataBrowser,
|
||||||
Inbox,
|
Inbox,
|
||||||
|
Feedback,
|
||||||
} = lazyComponents;
|
} = lazyComponents;
|
||||||
|
|||||||
@@ -249,6 +249,18 @@ export const routeConfig = [
|
|||||||
description: '官方公告和互动消息'
|
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