diff --git a/src/components/business/CommentItem.tsx b/src/components/business/CommentItem.tsx index 305eabe..41c8f4e 100644 --- a/src/components/business/CommentItem.tsx +++ b/src/components/business/CommentItem.tsx @@ -29,6 +29,7 @@ interface CommentItemProps { onDelete?: (comment: Comment) => void; // 删除评论的回调 onImagePress?: (images: ImageGridItem[], index: number) => void; // 点击图片查看大图 currentUserId?: string; // 当前用户ID,用于判断子评论作者 + onReport?: (comment: Comment) => void; // 举报评论的回调 } function createCommentItemStyles(colors: AppColors) { diff --git a/src/components/business/PostCard/types.ts b/src/components/business/PostCard/types.ts index 5585ced..f367321 100644 --- a/src/components/business/PostCard/types.ts +++ b/src/components/business/PostCard/types.ts @@ -22,7 +22,8 @@ export type PostCardActionType = | 'unbookmark' // 取消收藏 | 'share' // 分享 | 'imagePress' // 点击图片 - | 'delete'; // 删除 + | 'delete' // 删除 + | 'report'; // 举报 /** * Action payload 类型 diff --git a/src/components/business/ReportDialog/ReportDialog.tsx b/src/components/business/ReportDialog/ReportDialog.tsx new file mode 100644 index 0000000..cead925 --- /dev/null +++ b/src/components/business/ReportDialog/ReportDialog.tsx @@ -0,0 +1,335 @@ +/** + * 举报对话框组件 + * 支持举报帖子、评论、消息 + */ + +import React, { useState, useCallback } from 'react'; +import { + Modal, + View, + Text, + TouchableOpacity, + StyleSheet, + ScrollView, + TextInput, + ActivityIndicator, + Alert, +} from 'react-native'; +import { MaterialCommunityIcons } from '@expo/vector-icons'; +import { useAppColors } from '../../../theme'; +import { spacing, borderRadius, fontSizes } from '../../../theme'; +import reportService, { ReportReason, ReportTargetType, REPORT_REASONS } from '../../../services/reportService'; + +export interface ReportDialogProps { + visible: boolean; + targetType: ReportTargetType; + targetId: string; + onClose: () => void; + onSuccess?: () => void; +} + +// 目标类型中文名称映射 +const TARGET_TYPE_LABELS: Record = { + post: '帖子', + comment: '评论', + message: '消息', +}; + +export const ReportDialog: React.FC = ({ + visible, + targetType, + targetId, + onClose, + onSuccess, +}) => { + const colors = useAppColors(); + const styles = useStyles(colors); + const [selectedReason, setSelectedReason] = useState(null); + const [description, setDescription] = useState(''); + const [submitting, setSubmitting] = useState(false); + + // 提交举报 + const handleSubmit = useCallback(async () => { + if (!selectedReason) { + Alert.alert('提示', '请选择举报原因'); + return; + } + + setSubmitting(true); + try { + const result = await reportService.createReport( + targetType, + targetId, + selectedReason, + description.trim() || undefined + ); + + if (result) { + Alert.alert('举报成功', '感谢您的反馈,我们会尽快处理', [ + { + text: '确定', + onPress: () => { + onClose(); + onSuccess?.(); + }, + }, + ]); + } else { + Alert.alert('举报失败', '请稍后重试'); + } + } catch (error) { + console.error('Submit report error:', error); + Alert.alert('举报失败', '网络错误,请稍后重试'); + } finally { + setSubmitting(false); + } + }, [selectedReason, description, targetType, targetId, onClose, onSuccess]); + + // 重置状态 + const handleClose = useCallback(() => { + setSelectedReason(null); + setDescription(''); + onClose(); + }, [onClose]); + + return ( + + + + {/* 头部 */} + + + 举报{TARGET_TYPE_LABELS[targetType]} + + + + + + + {/* 内容 */} + + {/* 举报原因选择 */} + 请选择举报原因 + + {REPORT_REASONS.map((item) => ( + setSelectedReason(item.value)} + disabled={submitting} + > + + + {selectedReason === item.value && ( + + )} + + {item.label} + + + ))} + + + {/* 补充说明 */} + 补充说明(选填) + + {description.length}/500 + + + {/* 底部按钮 */} + + + 取消 + + + {submitting ? ( + + ) : ( + 提交举报 + )} + + + + + + ); +}; + +// 创建样式 +const useStyles = (colors: ReturnType) => + StyleSheet.create({ + overlay: { + flex: 1, + backgroundColor: 'rgba(0, 0, 0, 0.5)', + justifyContent: 'center', + alignItems: 'center', + padding: spacing.lg, + }, + container: { + backgroundColor: colors.backgroundPrimary, + borderRadius: borderRadius.lg, + width: '100%', + maxWidth: 400, + maxHeight: '80%', + }, + header: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + padding: spacing.lg, + borderBottomWidth: 1, + borderBottomColor: colors.borderLight, + }, + title: { + fontSize: fontSizes.lg, + fontWeight: '600', + color: colors.textPrimary, + }, + closeButton: { + padding: spacing.xs, + }, + content: { + padding: spacing.lg, + maxHeight: 400, + }, + sectionTitle: { + fontSize: fontSizes.md, + fontWeight: '500', + color: colors.textPrimary, + marginBottom: spacing.sm, + }, + reasonList: { + marginBottom: spacing.lg, + }, + reasonItem: { + paddingVertical: spacing.sm, + borderBottomWidth: 1, + borderBottomColor: colors.borderLight, + }, + reasonItemSelected: { + backgroundColor: colors.backgroundSecondary, + marginHorizontal: -spacing.sm, + paddingHorizontal: spacing.sm, + borderRadius: borderRadius.sm, + }, + radioContainer: { + flexDirection: 'row', + alignItems: 'center', + }, + radio: { + width: 20, + height: 20, + borderRadius: 10, + borderWidth: 2, + borderColor: colors.border, + justifyContent: 'center', + alignItems: 'center', + marginRight: spacing.sm, + }, + radioSelected: { + borderColor: colors.primary, + }, + radioDot: { + width: 10, + height: 10, + borderRadius: 5, + backgroundColor: colors.primary, + }, + reasonLabel: { + fontSize: fontSizes.md, + color: colors.textPrimary, + }, + textInput: { + borderWidth: 1, + borderColor: colors.border, + borderRadius: borderRadius.md, + padding: spacing.sm, + fontSize: fontSizes.md, + color: colors.textPrimary, + minHeight: 100, + textAlignVertical: 'top', + }, + charCount: { + fontSize: fontSizes.sm, + color: colors.textTertiary, + textAlign: 'right', + marginTop: spacing.xs, + }, + footer: { + flexDirection: 'row', + padding: spacing.lg, + borderTopWidth: 1, + borderTopColor: colors.borderLight, + gap: spacing.sm, + }, + cancelButton: { + flex: 1, + paddingVertical: spacing.sm, + borderRadius: borderRadius.md, + borderWidth: 1, + borderColor: colors.border, + alignItems: 'center', + }, + cancelButtonText: { + fontSize: fontSizes.md, + color: colors.textSecondary, + }, + submitButton: { + flex: 1, + paddingVertical: spacing.sm, + borderRadius: borderRadius.md, + backgroundColor: colors.primary, + alignItems: 'center', + }, + submitButtonDisabled: { + backgroundColor: colors.primary + '60', + }, + submitButtonText: { + fontSize: fontSizes.md, + color: '#FFFFFF', + fontWeight: '500', + }, + }); + +export default ReportDialog; \ No newline at end of file diff --git a/src/components/business/ReportDialog/index.ts b/src/components/business/ReportDialog/index.ts new file mode 100644 index 0000000..1dcc04a --- /dev/null +++ b/src/components/business/ReportDialog/index.ts @@ -0,0 +1,2 @@ +export { ReportDialog } from './ReportDialog'; +export type { ReportDialogProps } from './ReportDialog'; \ No newline at end of file diff --git a/src/components/business/index.ts b/src/components/business/index.ts index 91395e8..1269082 100644 --- a/src/components/business/index.ts +++ b/src/components/business/index.ts @@ -22,3 +22,4 @@ export { default as TabBar } from './TabBar'; export { default as VoteCard } from './VoteCard'; export { default as VoteEditor } from './VoteEditor'; export { default as VotePreview } from './VotePreview'; +export { default as ReportDialog } from './ReportDialog'; diff --git a/src/screens/message/components/ChatScreen/types.ts b/src/screens/message/components/ChatScreen/types.ts index e803f02..20a1389 100644 --- a/src/screens/message/components/ChatScreen/types.ts +++ b/src/screens/message/components/ChatScreen/types.ts @@ -166,6 +166,7 @@ export interface LongPressMenuProps { onRecall: (messageId: string) => void; onDelete: (messageId: string) => void; onAddSticker?: (imageUrl: string) => void; + onReport?: (message: GroupMessage) => void; // 举报消息回调 } // 聊天头部 Props diff --git a/src/services/reportService.ts b/src/services/reportService.ts new file mode 100644 index 0000000..2cf1b4e --- /dev/null +++ b/src/services/reportService.ts @@ -0,0 +1,83 @@ +/** + * 举报服务 + * 处理帖子、评论、消息的举报功能 + */ + +import { api } from './api'; + +// 举报原因类型 +export type ReportReason = 'spam' | 'inappropriate' | 'harassment' | 'misinformation' | 'other'; + +// 举报目标类型 +export type ReportTargetType = 'post' | 'comment' | 'message'; + +// 创建举报请求 +interface CreateReportRequest { + target_type: ReportTargetType; + target_id: string; + reason: ReportReason; + description?: string; +} + +// 举报响应 +export interface ReportResponse { + id: string; + reporter_id: string; + target_type: ReportTargetType; + target_id: string; + reason: ReportReason; + description?: string; + status: 'pending' | 'processing' | 'resolved' | 'rejected'; + created_at: string; +} + +// 举报原因选项 +export const REPORT_REASONS: { value: ReportReason; label: string }[] = [ + { value: 'spam', label: '垃圾广告' }, + { value: 'inappropriate', label: '违规内容' }, + { value: 'harassment', label: '辱骂/攻击' }, + { value: 'misinformation', label: '虚假信息' }, + { value: 'other', label: '其他' }, +]; + +// 举报服务类 +class ReportService { + /** + * 创建举报 + * @param targetType 举报目标类型 + * @param targetId 目标ID + * @param reason 举报原因 + * @param description 详细描述(可选) + */ + async createReport( + targetType: ReportTargetType, + targetId: string, + reason: ReportReason, + description?: string + ): Promise { + try { + const request: CreateReportRequest = { + target_type: targetType, + target_id: targetId, + reason, + description, + }; + const response = await api.post('/reports', request); + return response.data; + } catch (error) { + console.error('创建举报失败:', error); + return null; + } + } + + /** + * 获取举报原因标签 + */ + getReasonLabel(reason: ReportReason): string { + const item = REPORT_REASONS.find(r => r.value === reason); + return item?.label ?? reason; + } +} + +export const reportService = new ReportService(); +export default reportService;