- 新增 reportService API 服务 - 新增 ReportDialog 举报对话框组件 - 集成举报入口到 PostCard、CommentItem、LongPressMenu Made-with: Cursor
335 lines
9.0 KiB
TypeScript
335 lines
9.0 KiB
TypeScript
/**
|
||
* 举报对话框组件
|
||
* 支持举报帖子、评论、消息
|
||
*/
|
||
|
||
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; |