Files
frontend/src/components/business/ReportDialog/ReportDialog.tsx
lafay e0ee29caf8
All checks were successful
Frontend CI / build-and-push-web (push) Successful in 2m45s
Frontend CI / ota-android (push) Successful in 12m44s
Frontend CI / build-android-apk (push) Successful in 52m27s
feat: 添加举报功能前端支持
- 新增 reportService API 服务
- 新增 ReportDialog 举报对话框组件
- 集成举报入口到 PostCard、CommentItem、LongPressMenu

Made-with: Cursor
2026-03-29 20:18:49 +08:00

335 lines
9.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 举报对话框组件
* 支持举报帖子、评论、消息
*/
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<ReportTargetType, string> = {
post: '帖子',
comment: '评论',
message: '消息',
};
export const ReportDialog: React.FC<ReportDialogProps> = ({
visible,
targetType,
targetId,
onClose,
onSuccess,
}) => {
const colors = useAppColors();
const styles = useStyles(colors);
const [selectedReason, setSelectedReason] = useState<ReportReason | null>(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 (
<Modal
visible={visible}
transparent
animationType="fade"
onRequestClose={handleClose}
>
<View style={styles.overlay}>
<View style={styles.container}>
{/* 头部 */}
<View style={styles.header}>
<Text style={styles.title}>
{TARGET_TYPE_LABELS[targetType]}
</Text>
<TouchableOpacity
style={styles.closeButton}
onPress={handleClose}
disabled={submitting}
>
<MaterialCommunityIcons
name="close"
size={24}
color={colors.textSecondary}
/>
</TouchableOpacity>
</View>
{/* 内容 */}
<ScrollView style={styles.content} showsVerticalScrollIndicator={false}>
{/* 举报原因选择 */}
<Text style={styles.sectionTitle}></Text>
<View style={styles.reasonList}>
{REPORT_REASONS.map((item) => (
<TouchableOpacity
key={item.value}
style={[
styles.reasonItem,
selectedReason === item.value && styles.reasonItemSelected,
]}
onPress={() => setSelectedReason(item.value)}
disabled={submitting}
>
<View style={styles.radioContainer}>
<View
style={[
styles.radio,
selectedReason === item.value && styles.radioSelected,
]}
>
{selectedReason === item.value && (
<View style={styles.radioDot} />
)}
</View>
<Text style={styles.reasonLabel}>{item.label}</Text>
</View>
</TouchableOpacity>
))}
</View>
{/* 补充说明 */}
<Text style={styles.sectionTitle}></Text>
<TextInput
style={styles.textInput}
placeholder="请提供更多详细信息..."
placeholderTextColor={colors.textTertiary}
multiline
maxLength={500}
value={description}
onChangeText={setDescription}
editable={!submitting}
/>
<Text style={styles.charCount}>{description.length}/500</Text>
</ScrollView>
{/* 底部按钮 */}
<View style={styles.footer}>
<TouchableOpacity
style={styles.cancelButton}
onPress={handleClose}
disabled={submitting}
>
<Text style={styles.cancelButtonText}></Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.submitButton,
(!selectedReason || submitting) && styles.submitButtonDisabled,
]}
onPress={handleSubmit}
disabled={!selectedReason || submitting}
>
{submitting ? (
<ActivityIndicator size="small" color="#FFFFFF" />
) : (
<Text style={styles.submitButtonText}></Text>
)}
</TouchableOpacity>
</View>
</View>
</View>
</Modal>
);
};
// 创建样式
const useStyles = (colors: ReturnType<typeof useAppColors>) =>
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;