feat: 添加举报功能前端支持
- 新增 reportService API 服务 - 新增 ReportDialog 举报对话框组件 - 集成举报入口到 PostCard、CommentItem、LongPressMenu Made-with: Cursor
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -22,7 +22,8 @@ export type PostCardActionType =
|
||||
| 'unbookmark' // 取消收藏
|
||||
| 'share' // 分享
|
||||
| 'imagePress' // 点击图片
|
||||
| 'delete'; // 删除
|
||||
| 'delete' // 删除
|
||||
| 'report'; // 举报
|
||||
|
||||
/**
|
||||
* Action payload 类型
|
||||
|
||||
335
src/components/business/ReportDialog/ReportDialog.tsx
Normal file
335
src/components/business/ReportDialog/ReportDialog.tsx
Normal file
@@ -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<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;
|
||||
2
src/components/business/ReportDialog/index.ts
Normal file
2
src/components/business/ReportDialog/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { ReportDialog } from './ReportDialog';
|
||||
export type { ReportDialogProps } from './ReportDialog';
|
||||
@@ -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';
|
||||
|
||||
@@ -166,6 +166,7 @@ export interface LongPressMenuProps {
|
||||
onRecall: (messageId: string) => void;
|
||||
onDelete: (messageId: string) => void;
|
||||
onAddSticker?: (imageUrl: string) => void;
|
||||
onReport?: (message: GroupMessage) => void; // 举报消息回调
|
||||
}
|
||||
|
||||
// 聊天头部 Props
|
||||
|
||||
83
src/services/reportService.ts
Normal file
83
src/services/reportService.ts
Normal file
@@ -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<ReportResponse | null> {
|
||||
try {
|
||||
const request: CreateReportRequest = {
|
||||
target_type: targetType,
|
||||
target_id: targetId,
|
||||
reason,
|
||||
description,
|
||||
};
|
||||
const response = await api.post<ReportResponse>('/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;
|
||||
Reference in New Issue
Block a user