Files
frontend/src/components/business/SystemMessageItem.tsx
2026-03-16 17:47:10 +08:00

384 lines
13 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.
/**
* SystemMessageItem 系统消息项组件
* 根据系统消息类型显示不同图标和内容
* 参考 QQ 10000 系统消息和微信服务通知样式
*/
import React from 'react';
import { View, TouchableOpacity, StyleSheet } from 'react-native';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { formatDistanceToNow } from 'date-fns';
import { zhCN } from 'date-fns/locale';
import { colors, spacing, borderRadius } from '../../theme';
import { SystemMessageResponse, SystemMessageType } from '../../types/dto';
import Text from '../common/Text';
import Avatar from '../common/Avatar';
import { useResponsive, useResponsiveValue } from '../../hooks/useResponsive';
interface SystemMessageItemProps {
message: SystemMessageResponse;
onPress?: () => void;
onAvatarPress?: () => void; // 头像点击回调
onRequestAction?: (approve: boolean) => void;
requestActionLoading?: boolean;
}
// 系统消息类型到图标和颜色的映射
const getSystemMessageIcon = (
systemType: SystemMessageType
): { icon: keyof typeof MaterialCommunityIcons.glyphMap; color: string; bgColor: string } => {
switch (systemType) {
case 'like_post':
case 'like_comment':
case 'like_reply':
case 'favorite_post':
return {
icon: 'heart',
color: colors.error.main,
bgColor: colors.error.light + '20',
};
case 'comment':
return {
icon: 'comment',
color: colors.info.main,
bgColor: colors.info.light + '20',
};
case 'reply':
return {
icon: 'reply',
color: colors.success.main,
bgColor: colors.success.light + '20',
};
case 'follow':
return {
icon: 'account-plus',
color: '#9C27B0', // 紫色
bgColor: '#9C27B020',
};
case 'mention':
return {
icon: 'at',
color: colors.warning.main,
bgColor: colors.warning.light + '20',
};
case 'system':
return {
icon: 'cog',
color: colors.text.secondary,
bgColor: colors.background.disabled,
};
case 'announcement':
case 'announce':
return {
icon: 'bullhorn',
color: colors.primary.main,
bgColor: colors.primary.light + '20',
};
case 'group_invite':
return {
icon: 'account-multiple-plus',
color: colors.primary.main,
bgColor: colors.primary.light + '20',
};
case 'group_join_apply':
return {
icon: 'account-clock',
color: colors.warning.main,
bgColor: colors.warning.light + '20',
};
case 'group_join_approved':
return {
icon: 'check-circle',
color: colors.success.main,
bgColor: colors.success.light + '20',
};
case 'group_join_rejected':
return {
icon: 'close-circle',
color: colors.error.main,
bgColor: colors.error.light + '20',
};
default:
return {
icon: 'bell',
color: colors.text.secondary,
bgColor: colors.background.disabled,
};
}
};
// 获取系统消息标题
const getSystemMessageTitle = (message: SystemMessageResponse): string => {
const { system_type, extra_data } = message;
// 兼容后端返回的 actor_name 和 operator_name
const operatorName = extra_data?.actor_name || extra_data?.operator_name;
const groupName = extra_data?.group_name || extra_data?.target_title;
switch (system_type) {
case 'like_post':
return operatorName ? `${operatorName} 赞了你的帖子` : '有人赞了你的帖子';
case 'like_comment':
return operatorName ? `${operatorName} 赞了你的评论` : '有人赞了你的评论';
case 'like_reply':
return operatorName ? `${operatorName} 赞了你的回复` : '有人赞了你的回复';
case 'favorite_post':
return operatorName ? `${operatorName} 收藏了你的帖子` : '有人收藏了你的帖子';
case 'comment':
return operatorName ? `${operatorName} 评论了你的帖子` : '有人评论了你的帖子';
case 'reply':
return operatorName ? `${operatorName} 回复了你` : '有人回复了你';
case 'follow':
return operatorName ? `${operatorName} 关注了你` : '有人关注了你';
case 'mention':
return operatorName ? `${operatorName} @提到了你` : '有人@提到了你';
case 'system':
return '系统通知';
case 'announcement':
return '公告';
case 'group_invite':
return operatorName
? `${operatorName} 邀请加入群聊 ${groupName || ''}`.trim()
: `收到群邀请${groupName ? `${groupName}` : ''}`;
case 'group_join_apply':
if (extra_data?.request_status === 'accepted') {
return operatorName ? `${operatorName} 已同意` : '该请求已同意';
}
if (extra_data?.request_status === 'rejected') {
return operatorName ? `${operatorName} 已拒绝` : '该请求已拒绝';
}
return operatorName
? `${operatorName} 申请加入群聊 ${groupName || ''}`.trim()
: `收到加群申请${groupName ? `${groupName}` : ''}`;
case 'group_join_approved':
return '加群申请已通过';
case 'group_join_rejected':
return '加群申请被拒绝';
default:
return '通知';
}
};
const SystemMessageItem: React.FC<SystemMessageItemProps> = ({
message,
onPress,
onAvatarPress,
onRequestAction,
requestActionLoading = false,
}) => {
// 响应式适配
const { isWideScreen, isDesktop } = useResponsive();
const containerPadding = useResponsiveValue({ xs: spacing.md, lg: spacing.lg, xl: spacing.xl });
const avatarSize = useResponsiveValue({ xs: 40, lg: 48, xl: 52 });
const iconSize = useResponsiveValue({ xs: 20, lg: 22, xl: 24 });
const contentFontSize = useResponsiveValue({ xs: 14, lg: 15, xl: 16 });
const titleFontSize = useResponsiveValue({ xs: 14, lg: 15, xl: 16 });
const timeFontSize = useResponsiveValue({ xs: 12, lg: 13, xl: 14 });
// 格式化时间
const formatTime = (dateString: string): string => {
try {
return formatDistanceToNow(new Date(dateString), {
addSuffix: true,
locale: zhCN,
});
} catch {
return '';
}
};
const { icon, color, bgColor } = getSystemMessageIcon(message.system_type);
const title = getSystemMessageTitle(message);
const { extra_data } = message;
// 兼容后端返回的 actor_name 和 operator_name
const operatorName = extra_data?.actor_name || extra_data?.operator_name;
const operatorAvatar = extra_data?.avatar_url || extra_data?.operator_avatar;
const groupAvatar = extra_data?.group_avatar;
const requestStatus = extra_data?.request_status;
const isActionable =
(message.system_type === 'group_invite' || message.system_type === 'group_join_apply') &&
requestStatus === 'pending' &&
!!onRequestAction;
const styles = React.useMemo(() => StyleSheet.create({
container: {
flexDirection: 'row',
alignItems: 'flex-start',
padding: containerPadding,
backgroundColor: colors.background.paper,
borderBottomWidth: 1,
borderBottomColor: colors.divider,
},
iconContainer: {
marginRight: isWideScreen ? spacing.lg : spacing.md,
},
iconWrapper: {
width: avatarSize,
height: avatarSize,
borderRadius: avatarSize / 2,
justifyContent: 'center',
alignItems: 'center',
},
content: {
flex: 1,
justifyContent: 'center',
},
titleRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: spacing.xs,
},
title: {
fontWeight: '600',
flex: 1,
marginRight: spacing.sm,
fontSize: titleFontSize,
},
time: {
fontSize: timeFontSize,
},
messageContent: {
lineHeight: isWideScreen ? 22 : 18,
fontSize: contentFontSize,
},
extraInfo: {
flexDirection: 'row',
alignItems: 'center',
marginTop: spacing.xs,
backgroundColor: colors.background.default,
paddingHorizontal: isWideScreen ? spacing.md : spacing.sm,
paddingVertical: spacing.xs,
borderRadius: borderRadius.sm,
alignSelf: 'flex-start',
},
extraText: {
marginLeft: spacing.xs,
flex: 1,
},
actionsRow: {
flexDirection: 'row',
marginTop: spacing.sm,
},
actionBtn: {
paddingVertical: isWideScreen ? spacing.sm : spacing.xs,
paddingHorizontal: isWideScreen ? spacing.lg : spacing.md,
borderRadius: borderRadius.sm,
borderWidth: 1,
marginRight: spacing.sm,
},
rejectBtn: {
borderColor: colors.error.light,
backgroundColor: colors.error.light + '18',
},
approveBtn: {
borderColor: colors.success.light,
backgroundColor: colors.success.light + '18',
},
arrowContainer: {
marginLeft: spacing.sm,
justifyContent: 'center',
alignItems: 'center',
height: 20,
},
}), [containerPadding, isWideScreen, avatarSize, titleFontSize, timeFontSize, contentFontSize]);
// 判断是否显示操作者头像
const showOperatorAvatar = ['follow', 'like_post', 'like_comment', 'like_reply', 'favorite_post', 'comment', 'reply', 'mention', 'group_join_apply'].includes(
message.system_type
);
const showGroupAvatar = message.system_type === 'group_invite';
return (
<TouchableOpacity
style={styles.container}
onPress={onPress}
activeOpacity={onPress ? 0.7 : 1}
disabled={!onPress}
>
{/* 图标/头像区域 */}
<View style={styles.iconContainer}>
{showGroupAvatar ? (
<Avatar
source={groupAvatar || ''}
size={avatarSize}
name={extra_data?.group_name || '群聊'}
/>
) : showOperatorAvatar ? (
<Avatar
source={operatorAvatar || ''}
size={avatarSize}
name={operatorName}
onPress={onAvatarPress}
/>
) : (
<View style={[styles.iconWrapper, { backgroundColor: bgColor }]}>
<MaterialCommunityIcons name={icon} size={iconSize} color={color} />
</View>
)}
</View>
{/* 内容区域 */}
<View style={styles.content}>
{/* 标题行 */}
<View style={styles.titleRow}>
<Text variant="body" style={styles.title} numberOfLines={1}>
{title}
</Text>
<Text variant="caption" color={colors.text.secondary} style={styles.time}>
{formatTime(message.created_at)}
</Text>
</View>
{/* 消息内容 */}
<Text variant="caption" color={colors.text.secondary} numberOfLines={2} style={styles.messageContent}>
{message.content}
</Text>
{/* 附加信息 - 优先显示 target_title图标根据 target_type 区分 */}
{(extra_data?.target_title || extra_data?.post_title || extra_data?.comment_preview) &&
!['group_invite', 'group_join_apply', 'group_join_approved', 'group_join_rejected'].includes(message.system_type) &&
(() => {
const isCommentType = ['comment', 'reply'].includes(extra_data?.target_type ?? '');
const previewText = extra_data?.target_title || extra_data?.post_title || extra_data?.comment_preview;
const iconName = isCommentType ? 'comment-outline' : 'file-document-outline';
return (
<View style={styles.extraInfo}>
<MaterialCommunityIcons name={iconName} size={12} color={colors.text.hint} />
<Text variant="caption" color={colors.text.hint} numberOfLines={1} style={styles.extraText}>
{previewText}
</Text>
</View>
);
})()}
{isActionable && (
<View style={styles.actionsRow}>
<TouchableOpacity
style={[styles.actionBtn, styles.rejectBtn]}
onPress={() => onRequestAction?.(false)}
disabled={requestActionLoading}
>
<Text variant="caption" color={colors.error.main}></Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.actionBtn, styles.approveBtn]}
onPress={() => onRequestAction?.(true)}
disabled={requestActionLoading}
>
<Text variant="caption" color={colors.success.main}></Text>
</TouchableOpacity>
</View>
)}
</View>
{/* 右侧箭头(如果有跳转) */}
{onPress && (
<View style={styles.arrowContainer}>
<MaterialCommunityIcons name="chevron-right" size={20} color={colors.text.hint} />
</View>
)}
</TouchableOpacity>
);
};
export default SystemMessageItem;