style(ui): apply QQ-style design to message screens
Some checks failed
Frontend CI / ota (android) (push) Successful in 3m19s
Frontend CI / ota (ios) (push) Successful in 2m34s
Frontend CI / build-and-push-web (push) Failing after 5m8s
Frontend CI / build-android-apk (push) Successful in 43m30s

Remove decorative MaterialCommunityIcons and simplify UI to text-based
lists across GroupInfoScreen, GroupMembersScreen, PrivateChatInfoScreen,
and CommentItem components
This commit is contained in:
lafay
2026-07-08 01:47:05 +08:00
parent 995e0664d5
commit b589f1f32b
4 changed files with 282 additions and 517 deletions

View File

@@ -40,8 +40,6 @@ function createCommentItemStyles(colors: AppColors) {
paddingBottom: spacing.md, paddingBottom: spacing.md,
paddingHorizontal: spacing.md, paddingHorizontal: spacing.md,
backgroundColor: colors.background.paper, backgroundColor: colors.background.paper,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: colors.divider,
}, },
content: { content: {
flex: 1, flex: 1,

View File

@@ -2,6 +2,7 @@
* GroupInfoScreen 群组信息/设置界面 * GroupInfoScreen 群组信息/设置界面
* 显示群组详细信息,提供群组管理功能 * 显示群组详细信息,提供群组管理功能
* 支持响应式布局 * 支持响应式布局
* QQ 风格:移除装饰性图标,使用简洁的文字列表
*/ */
import React, { useState, useEffect, useCallback, useMemo } from 'react'; import React, { useState, useEffect, useCallback, useMemo } from 'react';
@@ -23,13 +24,11 @@ import {
import { SafeAreaView } from 'react-native-safe-area-context'; import { SafeAreaView } from 'react-native-safe-area-context';
import { useFocusEffect } from "expo-router/react-navigation"; import { useFocusEffect } from "expo-router/react-navigation";
import { useLocalSearchParams, useRouter } from 'expo-router'; import { useLocalSearchParams, useRouter } from 'expo-router';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import * as ImagePicker from 'expo-image-picker'; import * as ImagePicker from 'expo-image-picker';
import { import {
spacing, spacing,
fontSizes, fontSizes,
borderRadius, borderRadius,
shadows,
useAppColors, useAppColors,
type AppColors, type AppColors,
} from '../../theme'; } from '../../theme';
@@ -38,7 +37,7 @@ import { groupService } from '@/services/message';
import { uploadService } from '@/services/upload'; import { uploadService } from '@/services/upload';
import { messageService } from '@/services/message'; import { messageService } from '@/services/message';
import { Avatar, Text, Button, Loading, Divider } from '../../components/common'; import { Avatar, Text, Button, Loading, Divider } from '../../components/common';
import { useResponsive, useBreakpointGTE } from '../../hooks'; import { useResponsive } from '../../hooks';
import { import {
GroupResponse, GroupResponse,
GroupMemberResponse, GroupMemberResponse,
@@ -58,10 +57,10 @@ import { AppBackButton } from '../../components/common';
const { width: SCREEN_WIDTH } = Dimensions.get('window'); const { width: SCREEN_WIDTH } = Dimensions.get('window');
// 加群方式选项 // 加群方式选项
const JOIN_TYPE_OPTIONS: { value: JoinType; label: string; icon: string; desc: string }[] = [ const JOIN_TYPE_OPTIONS: { value: JoinType; label: string; desc: string }[] = [
{ value: 0, label: '允许任何人加入', icon: 'earth', desc: '任何人都可以直接加入群聊' }, { value: 0, label: '允许任何人加入', desc: '任何人都可以直接加入群聊' },
{ value: 1, label: '需要管理员审批', icon: 'shield-check', desc: '新成员需要管理员审核' }, { value: 1, label: '需要管理员审批', desc: '新成员需要管理员审核' },
{ value: 2, label: '不允许任何人加入', icon: 'lock', desc: '只能通过邀请加入群聊' }, { value: 2, label: '不允许任何人加入', desc: '只能通过邀请加入群聊' },
]; ];
const GroupInfoScreen: React.FC = () => { const GroupInfoScreen: React.FC = () => {
@@ -77,16 +76,7 @@ const GroupInfoScreen: React.FC = () => {
const { currentUser } = useAuthStore(); const { currentUser } = useAuthStore();
// 响应式布局 // 响应式布局
const { isDesktop, isTablet, width } = useResponsive(); const { width } = useResponsive();
const isWideScreen = useBreakpointGTE('lg');
// 计算成员网格列数
const memberColumns = useMemo(() => {
if (width >= 1200) return 6;
if (width >= 900) return 5;
if (width >= 768) return 4;
return 5; // 移动端默认
}, [width]);
// 群组信息状态 // 群组信息状态
const [group, setGroup] = useState<GroupResponse | null>(null); const [group, setGroup] = useState<GroupResponse | null>(null);
@@ -512,12 +502,6 @@ const GroupInfoScreen: React.FC = () => {
return option?.label || '未知'; return option?.label || '未知';
}; };
// 获取加群方式图标
const getJoinTypeIcon = (joinType: JoinType): string => {
const option = JOIN_TYPE_OPTIONS.find(o => o.value === joinType);
return option?.icon || 'help-circle';
};
const handleCopyGroupNo = () => { const handleCopyGroupNo = () => {
if (!group) return; if (!group) return;
Clipboard.setString(String(group.id)); Clipboard.setString(String(group.id));
@@ -530,47 +514,64 @@ const GroupInfoScreen: React.FC = () => {
return `${raw.slice(0, 6)}...${raw.slice(-4)}`; return `${raw.slice(0, 6)}...${raw.slice(-4)}`;
}; };
// 渲染设置项 // 渲染设置项QQ 风格,左侧标题,右侧值/箭头
const renderSettingItem = ( const renderSettingItem = (
icon: string,
title: string, title: string,
value?: string, value?: string,
onPress?: () => void, onPress?: () => void,
danger: boolean = false danger: boolean = false,
subtitle?: string
) => ( ) => (
<TouchableOpacity <TouchableOpacity
style={styles.settingItem} style={styles.settingItem}
onPress={onPress} onPress={onPress}
disabled={!onPress} disabled={!onPress}
activeOpacity={0.7} activeOpacity={onPress ? 0.6 : 1}
> >
<View style={[styles.settingIconContainer, danger && styles.settingIconDanger]}> <View style={styles.settingItemLeft}>
<MaterialCommunityIcons <Text variant="body" style={danger ? styles.dangerText : styles.settingItemTitle}>
name={icon as any}
size={20}
color={danger ? colors.error.main : colors.primary.main}
/>
</View>
<View style={styles.settingContent}>
<Text variant="body" style={danger ? styles.dangerText : undefined}>
{title} {title}
</Text> </Text>
{subtitle && (
<Text variant="caption" color={colors.text.secondary} numberOfLines={2} style={styles.settingItemSubtitle}>
{subtitle}
</Text>
)}
</View>
<View style={styles.settingValueRow}>
{value && ( {value && (
<Text variant="caption" color={colors.text.secondary} numberOfLines={1}> <Text variant="body" color={colors.text.secondary} numberOfLines={1} style={styles.settingValueText}>
{value} {value}
</Text> </Text>
)} )}
</View>
{onPress && ( {onPress && (
<MaterialCommunityIcons <Text variant="body" color={colors.text.hint} style={styles.settingChevron}>
name="chevron-right"
size={22} </Text>
color={colors.text.hint}
/>
)} )}
</View>
</TouchableOpacity> </TouchableOpacity>
); );
// 渲染带开关的设置项
const renderSwitchItem = (
title: string,
value: boolean,
onValueChange: () => void,
disabled?: boolean
) => (
<View style={styles.settingItem}>
<Text variant="body" style={styles.settingItemTitle}>{title}</Text>
<Switch
value={value}
onValueChange={onValueChange}
disabled={disabled}
trackColor={{ false: colors.divider, true: colors.primary.light }}
thumbColor={value ? colors.primary.main : colors.background.paper}
/>
</View>
);
if (loading) { if (loading) {
return ( return (
<SafeAreaView style={styles.container} edges={['top', 'bottom']}> <SafeAreaView style={styles.container} edges={['top', 'bottom']}>
@@ -618,7 +619,7 @@ const GroupInfoScreen: React.FC = () => {
<View style={styles.groupHeader}> <View style={styles.groupHeader}>
<Avatar <Avatar
source={group.avatar} source={group.avatar}
size={80} size={72}
name={group.name} name={group.name}
/> />
<View style={styles.groupInfo}> <View style={styles.groupInfo}>
@@ -626,12 +627,10 @@ const GroupInfoScreen: React.FC = () => {
{group.name} {group.name}
</Text> </Text>
<View style={styles.groupMeta}> <View style={styles.groupMeta}>
<MaterialCommunityIcons name="account-group" size={14} color={colors.text.secondary} />
<Text variant="caption" color={colors.text.secondary}> <Text variant="caption" color={colors.text.secondary}>
{group.member_count} {group.member_count}
</Text> </Text>
<Text variant="caption" color={colors.text.hint}> · </Text> <Text variant="caption" color={colors.text.hint}> · </Text>
<MaterialCommunityIcons name={getJoinTypeIcon(group.join_type) as any} size={14} color={colors.text.secondary} />
<Text variant="caption" color={colors.text.secondary} numberOfLines={1} style={{ flex: 1 }}> <Text variant="caption" color={colors.text.secondary} numberOfLines={1} style={{ flex: 1 }}>
{getJoinTypeText(group.join_type)} {getJoinTypeText(group.join_type)}
</Text> </Text>
@@ -641,7 +640,6 @@ const GroupInfoScreen: React.FC = () => {
{formatGroupNo(group.id)} {formatGroupNo(group.id)}
</Text> </Text>
<TouchableOpacity style={styles.copyGroupNoBtn} onPress={handleCopyGroupNo} activeOpacity={0.7}> <TouchableOpacity style={styles.copyGroupNoBtn} onPress={handleCopyGroupNo} activeOpacity={0.7}>
<MaterialCommunityIcons name="content-copy" size={14} color={colors.primary.main} />
<Text variant="caption" color={colors.primary.main} style={styles.copyGroupNoBtnText}> <Text variant="caption" color={colors.primary.main} style={styles.copyGroupNoBtnText}>
</Text> </Text>
@@ -651,14 +649,12 @@ const GroupInfoScreen: React.FC = () => {
</View> </View>
{group.description ? ( {group.description ? (
<View style={styles.descriptionContainer}> <View style={styles.descriptionContainer}>
<MaterialCommunityIcons name="information-outline" size={16} color={colors.text.secondary} />
<Text variant="body" color={colors.text.secondary} style={styles.groupDesc}> <Text variant="body" color={colors.text.secondary} style={styles.groupDesc}>
{group.description} {group.description}
</Text> </Text>
</View> </View>
) : ( ) : (
<View style={styles.descriptionContainer}> <View style={styles.descriptionContainer}>
<MaterialCommunityIcons name="information-outline" size={16} color={colors.text.hint} />
<Text variant="body" color={colors.text.hint} style={styles.groupDesc}> <Text variant="body" color={colors.text.hint} style={styles.groupDesc}>
</Text> </Text>
@@ -669,37 +665,32 @@ const GroupInfoScreen: React.FC = () => {
{/* 群公告 */} {/* 群公告 */}
{announcements.length > 0 && ( {announcements.length > 0 && (
<View style={styles.card}> <View style={styles.card}>
<View style={styles.cardHeader}> <TouchableOpacity
<View style={styles.cardIconContainer}> style={styles.announcementRow}
<MaterialCommunityIcons name="bullhorn" size={18} color={colors.warning.main} /> onPress={() => setAnnouncementModalVisible(true)}
activeOpacity={0.7}
>
<View style={styles.announcementHeader}>
<Text variant="body" style={styles.settingItemTitle}></Text>
<Text variant="body" color={colors.text.hint} style={styles.settingChevron}></Text>
</View> </View>
<Text variant="label" style={styles.cardTitle}></Text> <Text variant="body" color={colors.text.secondary} numberOfLines={2} style={styles.announcementPreviewText}>
</View>
<View style={styles.announcementContent}>
<Text variant="body" style={styles.announcementText}>
{announcements[0].content} {announcements[0].content}
</Text> </Text>
<Text variant="caption" color={colors.text.hint} style={styles.announcementTime}> </TouchableOpacity>
{(() => {
const d = new Date(announcements[0].created_at);
return Number.isNaN(d.getTime()) ? '' : d.toLocaleDateString();
})()}
</Text>
</View>
</View> </View>
)} )}
{/* 成员预览 */} {/* 成员预览 */}
<View style={styles.card}> <View style={styles.card}>
<TouchableOpacity style={styles.cardHeader} onPress={goToMembers} activeOpacity={0.7}> <TouchableOpacity style={styles.cardHeader} onPress={goToMembers} activeOpacity={0.7}>
<View style={styles.cardIconContainer}> <Text variant="body" style={styles.settingItemTitle}></Text>
<MaterialCommunityIcons name="account-multiple" size={18} color={colors.info.main} /> <View style={styles.cardValueRow}>
</View> <Text variant="body" color={colors.text.secondary} style={styles.memberCount}>
<Text variant="label" style={styles.cardTitle}></Text>
<Text variant="caption" color={colors.text.secondary} style={styles.memberCount}>
{group.member_count} {group.member_count}
</Text> </Text>
<MaterialCommunityIcons name="chevron-right" size={20} color={colors.text.hint} /> <Text variant="body" color={colors.text.hint} style={styles.settingChevron}></Text>
</View>
</TouchableOpacity> </TouchableOpacity>
<View style={styles.memberPreview}> <View style={styles.memberPreview}>
@@ -740,108 +731,42 @@ const GroupInfoScreen: React.FC = () => {
{/* 邀请成员按钮 */} {/* 邀请成员按钮 */}
<TouchableOpacity style={styles.inviteButton} onPress={openInviteModal} activeOpacity={0.8}> <TouchableOpacity style={styles.inviteButton} onPress={openInviteModal} activeOpacity={0.8}>
<View style={styles.inviteIconContainer}>
<MaterialCommunityIcons name="account-plus" size={20} color={colors.primary.main} />
</View>
<Text variant="body" color={colors.primary.main} style={styles.inviteButtonText}> <Text variant="body" color={colors.primary.main} style={styles.inviteButtonText}>
+
</Text> </Text>
<MaterialCommunityIcons name="chevron-right" size={20} color={colors.primary.main} />
</TouchableOpacity> </TouchableOpacity>
</View> </View>
{/* 群名称 / 群描述 */}
<View style={styles.card}>
<View style={styles.settingsList}>
{isAdmin && renderSettingItem('群名称', group.name, openEditModal)}
{isAdmin && <Divider style={styles.settingDivider} />}
{isOwner && renderSettingItem('加群方式', getJoinTypeText(group.join_type), () => setJoinTypeModalVisible(true))}
{isOwner && <Divider style={styles.settingDivider} />}
{isAdmin && renderSettingItem('群描述', group.description || '未设置', openEditModal)}
</View>
</View>
{/* 管理员设置 */} {/* 管理员设置 */}
{isAdmin && ( {isAdmin && (
<View style={styles.card}> <View style={styles.card}>
<View style={styles.cardHeader}>
<View style={[styles.cardIconContainer, { backgroundColor: colors.primary.light + '20' }]}>
<MaterialCommunityIcons name="shield-crown" size={18} color={colors.primary.main} />
</View>
<Text variant="label" style={styles.cardTitle}></Text>
</View>
<View style={styles.settingsList}> <View style={styles.settingsList}>
{renderSettingItem('pencil-outline', '修改群信息', undefined, openEditModal)} {renderSettingItem('发布群公告', undefined, () => setAnnouncementModalVisible(true))}
<Divider style={styles.settingDivider} />
{isOwner && renderSettingItem('earth', '加群方式', getJoinTypeText(group.join_type), () => setJoinTypeModalVisible(true))}
{isOwner && <Divider style={styles.settingDivider} />} {isOwner && <Divider style={styles.settingDivider} />}
{isOwner && ( {isOwner && renderSwitchItem('全员禁言', group.mute_all, handleToggleMuteAll)}
<View style={styles.settingItem}>
<View style={styles.settingIconContainer}>
<MaterialCommunityIcons name="microphone-off" size={20} color={colors.primary.main} />
</View>
<View style={styles.settingContent}>
<Text variant="body"></Text>
<Text variant="caption" color={colors.text.secondary}>
{group.mute_all ? '已开启' : '已关闭'}
</Text>
</View>
<Switch
value={group.mute_all}
onValueChange={handleToggleMuteAll}
trackColor={{ false: colors.divider, true: colors.primary.light }}
thumbColor={group.mute_all ? colors.primary.main : colors.background.paper}
/>
</View>
)}
<Divider style={styles.settingDivider} />
{renderSettingItem('bullhorn-outline', '发布群公告', undefined, () => setAnnouncementModalVisible(true))}
{isOwner && <Divider style={styles.settingDivider} />} {isOwner && <Divider style={styles.settingDivider} />}
{isOwner && renderSettingItem('account-switch', '转让群主', undefined, () => setTransferModalVisible(true))} {isOwner && renderSettingItem('转让群主', undefined, () => setTransferModalVisible(true))}
</View> </View>
</View> </View>
)} )}
{/* 聊天设置 */} {/* 聊天设置 */}
<View style={styles.card}> <View style={styles.card}>
<View style={styles.cardHeader}>
<View style={[styles.cardIconContainer, { backgroundColor: colors.warning.light + '20' }]}>
<MaterialCommunityIcons name="chat-outline" size={18} color={colors.warning.main} />
</View>
<Text variant="label" style={styles.cardTitle}></Text>
</View>
<View style={styles.settingsList}> <View style={styles.settingsList}>
<View style={styles.settingItem}>
<View style={styles.settingIconContainer}>
<MaterialCommunityIcons name="pin-outline" size={20} color={colors.primary.main} />
</View>
<View style={styles.settingContent}>
<Text variant="body"></Text>
<Text variant="caption" color={colors.text.secondary}>
{conversationId ? (isPinned ? '已置顶' : '未置顶') : '请从聊天页面进入后设置'}
</Text>
</View>
<Switch
value={isPinned}
onValueChange={handleTogglePinned}
disabled={!conversationId || pinLoading}
trackColor={{ false: colors.divider, true: colors.primary.light }}
thumbColor={isPinned ? colors.primary.main : colors.background.paper}
/>
</View>
<Divider style={styles.settingDivider} />
<View style={styles.settingItem}>
<View style={styles.settingIconContainer}>
<MaterialCommunityIcons name="bell-off-outline" size={20} color={colors.primary.main} />
</View>
<View style={styles.settingContent}>
<Text variant="body"></Text>
<Text variant="caption" color={colors.text.secondary}>
{conversationId ? (isNotificationMuted ? '已开启' : '未开启') : '请从聊天页面进入后设置'}
</Text>
</View>
<Switch
value={isNotificationMuted}
onValueChange={handleToggleNotificationMuted}
disabled={!conversationId}
trackColor={{ false: colors.divider, true: colors.primary.light }}
thumbColor={isNotificationMuted ? colors.primary.main : colors.background.paper}
/>
</View>
<Divider style={styles.settingDivider} />
{renderSettingItem( {renderSettingItem(
'magnify',
'查找聊天记录', '查找聊天记录',
undefined, '图片、视频、文件等',
conversationId conversationId
? () => router.push(hrefs.hrefMessageSearch({ ? () => router.push(hrefs.hrefMessageSearch({
conversationId, conversationId,
@@ -850,6 +775,10 @@ const GroupInfoScreen: React.FC = () => {
})) }))
: undefined, : undefined,
)} )}
<Divider style={styles.settingDivider} />
{renderSwitchItem('设为置顶', isPinned, handleTogglePinned, !conversationId || pinLoading)}
<Divider style={styles.settingDivider} />
{renderSwitchItem('消息免打扰', isNotificationMuted, handleToggleNotificationMuted, !conversationId)}
</View> </View>
</View> </View>
@@ -861,7 +790,6 @@ const GroupInfoScreen: React.FC = () => {
onPress={handleDissolveGroup} onPress={handleDissolveGroup}
activeOpacity={0.7} activeOpacity={0.7}
> >
<MaterialCommunityIcons name="delete-forever" size={22} color={colors.error.main} />
<Text variant="body" color={colors.error.main} style={styles.dangerButtonText}> <Text variant="body" color={colors.error.main} style={styles.dangerButtonText}>
</Text> </Text>
@@ -872,7 +800,6 @@ const GroupInfoScreen: React.FC = () => {
onPress={handleLeaveGroup} onPress={handleLeaveGroup}
activeOpacity={0.7} activeOpacity={0.7}
> >
<MaterialCommunityIcons name="logout" size={22} color={colors.error.main} />
<Text variant="body" color={colors.error.main} style={styles.dangerButtonText}> <Text variant="body" color={colors.error.main} style={styles.dangerButtonText}>
退 退
</Text> </Text>
@@ -919,7 +846,7 @@ const GroupInfoScreen: React.FC = () => {
)} )}
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity style={styles.editAvatarButton} onPress={handleSelectGroupAvatar} disabled={uploadingAvatar}> <TouchableOpacity style={styles.editAvatarButton} onPress={handleSelectGroupAvatar} disabled={uploadingAvatar}>
<MaterialCommunityIcons name="camera" size={16} color={colors.background.paper} /> <Text variant="caption" color={colors.background.paper}></Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
@@ -1032,13 +959,6 @@ const GroupInfoScreen: React.FC = () => {
onPress={() => handleSetJoinType(option.value)} onPress={() => handleSetJoinType(option.value)}
activeOpacity={0.7} activeOpacity={0.7}
> >
<View style={[styles.joinTypeIcon, group.join_type === option.value && styles.joinTypeIconSelected]}>
<MaterialCommunityIcons
name={option.icon as any}
size={24}
color={group.join_type === option.value ? colors.primary.main : colors.text.secondary}
/>
</View>
<View style={styles.joinTypeInfo}> <View style={styles.joinTypeInfo}>
<Text <Text
variant="body" variant="body"
@@ -1051,7 +971,7 @@ const GroupInfoScreen: React.FC = () => {
</Text> </Text>
</View> </View>
{group.join_type === option.value && ( {group.join_type === option.value && (
<MaterialCommunityIcons name="check-circle" size={24} color={colors.primary.main} /> <Text variant="body" color={colors.primary.main} style={styles.joinTypeCheck}></Text>
)} )}
</TouchableOpacity> </TouchableOpacity>
))} ))}
@@ -1109,7 +1029,7 @@ const GroupInfoScreen: React.FC = () => {
</View> </View>
<View style={[styles.transferRadio, transferUserId === member.user_id && styles.transferRadioSelected]}> <View style={[styles.transferRadio, transferUserId === member.user_id && styles.transferRadioSelected]}>
{transferUserId === member.user_id && ( {transferUserId === member.user_id && (
<MaterialCommunityIcons name="check" size={14} color={colors.background.paper} /> <Text variant="caption" color={colors.background.paper}></Text>
)} )}
</View> </View>
</TouchableOpacity> </TouchableOpacity>
@@ -1145,16 +1065,17 @@ function createGroupInfoStyles(colors: AppColors) {
return StyleSheet.create({ return StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
backgroundColor: colors.background.paper, backgroundColor: colors.background.default,
}, },
pageHeader: { pageHeader: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between', justifyContent: 'space-between',
paddingHorizontal: spacing.lg, paddingHorizontal: spacing.md,
paddingVertical: spacing.md, paddingVertical: spacing.sm,
backgroundColor: colors.background.paper, backgroundColor: colors.background.paper,
borderBottomWidth: 0, borderBottomWidth: 0.5,
borderBottomColor: colors.divider + '60',
}, },
pageTitle: { pageTitle: {
fontWeight: '600', fontWeight: '600',
@@ -1168,7 +1089,6 @@ function createGroupInfoStyles(colors: AppColors) {
flex: 1, flex: 1,
}, },
scrollContent: { scrollContent: {
paddingHorizontal: spacing.lg,
paddingBottom: spacing.xl * 2, paddingBottom: spacing.xl * 2,
}, },
errorContainer: { errorContainer: {
@@ -1177,16 +1097,17 @@ function createGroupInfoStyles(colors: AppColors) {
alignItems: 'center', alignItems: 'center',
}, },
// 头部区域 - 扁平化无卡片 // 头部区域 - QQ 风格简洁白底
headerCard: { headerCard: {
backgroundColor: colors.background.paper, backgroundColor: colors.background.paper,
paddingHorizontal: spacing.lg,
paddingVertical: spacing.lg, paddingVertical: spacing.lg,
marginBottom: spacing.md, marginBottom: spacing.md,
}, },
groupHeader: { groupHeader: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
marginBottom: spacing.lg, marginBottom: spacing.md,
}, },
groupInfo: { groupInfo: {
marginLeft: spacing.lg, marginLeft: spacing.lg,
@@ -1194,7 +1115,7 @@ function createGroupInfoStyles(colors: AppColors) {
}, },
groupName: { groupName: {
fontWeight: '700', fontWeight: '700',
fontSize: fontSizes['2xl'], fontSize: fontSizes.xl,
marginBottom: spacing.xs, marginBottom: spacing.xs,
}, },
groupMeta: { groupMeta: {
@@ -1209,86 +1130,68 @@ function createGroupInfoStyles(colors: AppColors) {
gap: spacing.sm, gap: spacing.sm,
}, },
copyGroupNoBtn: { copyGroupNoBtn: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: spacing.sm, paddingHorizontal: spacing.sm,
paddingVertical: 4, paddingVertical: 2,
borderRadius: borderRadius.md, borderRadius: borderRadius.sm,
backgroundColor: colors.primary.light + '15', backgroundColor: colors.background.default,
}, },
copyGroupNoBtnText: { copyGroupNoBtnText: {
marginLeft: 4,
fontWeight: '500', fontWeight: '500',
}, },
descriptionContainer: { descriptionContainer: {
flexDirection: 'row',
alignItems: 'flex-start',
backgroundColor: colors.background.default, backgroundColor: colors.background.default,
borderRadius: borderRadius.lg, borderRadius: borderRadius.md,
padding: spacing.md, padding: spacing.md,
}, },
groupDesc: { groupDesc: {
marginLeft: spacing.sm,
flex: 1, flex: 1,
lineHeight: 22, lineHeight: 22,
fontWeight: '400', fontWeight: '400',
}, },
// 通用卡片样式 - 扁平化:无阴影无圆角,使用分割线 // 通用卡片样式 - 白底分隔QQ 风格)
card: { card: {
backgroundColor: colors.background.paper, backgroundColor: colors.background.paper,
marginBottom: spacing.md, marginBottom: spacing.md,
paddingHorizontal: spacing.lg,
paddingVertical: spacing.sm,
}, },
cardHeader: { cardHeader: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
marginBottom: spacing.md, justifyContent: 'space-between',
paddingTop: spacing.sm, paddingVertical: spacing.md,
}, },
cardIconContainer: { cardValueRow: {
width: 36, flexDirection: 'row',
height: 36,
borderRadius: borderRadius.md,
backgroundColor: colors.info.light + '15',
justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
marginRight: spacing.sm,
},
cardTitle: {
fontWeight: '600',
fontSize: fontSizes.md,
flex: 1,
}, },
// 公告样式 - 扁平化 // 公告样式 - QQ 风格:标题行 + 内容预览
announcementContent: { announcementRow: {
backgroundColor: colors.warning.light + '08', paddingVertical: spacing.md,
borderRadius: borderRadius.lg,
padding: spacing.md,
borderLeftWidth: 3,
borderLeftColor: colors.warning.main,
}, },
announcementText: { announcementHeader: {
lineHeight: 22, flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: spacing.sm, marginBottom: spacing.sm,
fontWeight: '400',
}, },
announcementTime: { announcementPreviewText: {
textAlign: 'right', lineHeight: 22,
fontWeight: '500', fontWeight: '400',
}, },
// 成员预览样式 // 成员预览样式
memberCount: { memberCount: {
marginRight: spacing.xs, marginRight: spacing.xs,
fontWeight: '600', fontWeight: '500',
color: colors.text.secondary,
}, },
memberPreview: { memberPreview: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
flexWrap: 'nowrap', flexWrap: 'nowrap',
marginBottom: spacing.md, paddingVertical: spacing.md,
paddingLeft: spacing.xs, paddingLeft: spacing.xs,
}, },
memberAvatar: { memberAvatar: {
@@ -1320,8 +1223,6 @@ function createGroupInfoStyles(colors: AppColors) {
backgroundColor: colors.background.default, backgroundColor: colors.background.default,
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
borderWidth: 1,
borderColor: colors.divider + '60',
marginLeft: -8, marginLeft: -8,
paddingHorizontal: spacing.xs, paddingHorizontal: spacing.xs,
}, },
@@ -1331,88 +1232,81 @@ function createGroupInfoStyles(colors: AppColors) {
fontSize: fontSizes.sm, fontSize: fontSizes.sm,
}, },
// 邀请按钮样式 - 扁平化 // 邀请按钮样式
inviteButton: { inviteButton: {
flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
paddingVertical: spacing.md, paddingVertical: spacing.md,
borderRadius: borderRadius.lg, borderRadius: borderRadius.md,
backgroundColor: colors.primary.light + '10', backgroundColor: colors.background.default,
borderWidth: 1.5, marginTop: spacing.sm,
borderColor: colors.primary.light + '60',
borderStyle: 'dashed',
},
inviteIconContainer: {
width: 32,
height: 32,
borderRadius: 16,
backgroundColor: colors.primary.light + '20',
justifyContent: 'center',
alignItems: 'center',
marginRight: spacing.sm,
}, },
inviteButtonText: { inviteButtonText: {
fontWeight: '700', fontWeight: '600',
marginRight: spacing.xs,
}, },
// 设置项样式 - 扁平化:无圆角背景,简洁分割线 // 设置项样式 - QQ 风格:无图标,标题在左,值/箭头在右
settingsList: { settingsList: {
marginTop: 0, marginTop: 0,
}, },
settingItem: { settingItem: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between',
paddingVertical: spacing.md, paddingVertical: spacing.md,
paddingHorizontal: 0,
marginLeft: 0,
marginRight: 0,
}, },
settingIconContainer: { settingItemLeft: {
width: 36,
height: 36,
borderRadius: borderRadius.md,
backgroundColor: colors.background.default,
justifyContent: 'center',
alignItems: 'center',
marginRight: spacing.md,
},
settingIconDanger: {
backgroundColor: colors.error.light + '15',
},
settingContent: {
flex: 1, flex: 1,
justifyContent: 'center',
},
settingItemTitle: {
fontSize: fontSizes.md,
fontWeight: '500',
color: colors.text.primary,
},
settingItemSubtitle: {
marginTop: spacing.xs,
fontWeight: '400',
lineHeight: 20,
},
settingValueRow: {
flexDirection: 'row',
alignItems: 'center',
marginLeft: spacing.md,
},
settingValueText: {
maxWidth: 180,
},
settingChevron: {
fontSize: fontSizes.xl,
marginLeft: spacing.xs,
fontWeight: '300',
}, },
dangerText: { dangerText: {
color: colors.error.main, color: colors.error.main,
fontWeight: '600', fontWeight: '600',
}, },
settingDivider: { settingDivider: {
marginLeft: 52,
width: 'auto',
marginVertical: 0, marginVertical: 0,
opacity: 0.5, opacity: 0.5,
}, },
// 危险操作区域 - 扁平化 // 危险操作区域
dangerCard: { dangerCard: {
padding: 0, padding: 0,
overflow: 'hidden', overflow: 'hidden',
marginTop: spacing.sm, marginTop: 0,
}, },
dangerButton: { dangerButton: {
flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
paddingVertical: spacing.lg, paddingVertical: spacing.lg,
gap: spacing.sm,
}, },
dangerButtonText: { dangerButtonText: {
fontWeight: '600', fontWeight: '600',
}, },
// 模态框样式 - 扁平化 // 模态框样式
modalOverlay: { modalOverlay: {
flex: 1, flex: 1,
backgroundColor: 'rgba(0, 0, 0, 0.45)', backgroundColor: 'rgba(0, 0, 0, 0.45)',
@@ -1436,10 +1330,6 @@ function createGroupInfoStyles(colors: AppColors) {
borderBottomWidth: 0.5, borderBottomWidth: 0.5,
borderBottomColor: colors.divider + '60', borderBottomColor: colors.divider + '60',
}, },
modalHeaderButton: {
paddingVertical: spacing.sm,
minWidth: 60,
},
modalTitle: { modalTitle: {
fontWeight: '700', fontWeight: '700',
fontSize: fontSizes.xl, fontSize: fontSizes.xl,
@@ -1447,12 +1337,8 @@ function createGroupInfoStyles(colors: AppColors) {
saveButton: { saveButton: {
fontWeight: '600', fontWeight: '600',
}, },
confirmButton: {
fontWeight: '600',
textAlign: 'right',
},
// 编辑表单样式 - 扁平化 // 编辑表单样式
editForm: { editForm: {
alignItems: 'center', alignItems: 'center',
}, },
@@ -1481,11 +1367,9 @@ function createGroupInfoStyles(colors: AppColors) {
bottom: 0, bottom: 0,
right: 0, right: 0,
backgroundColor: colors.primary.main, backgroundColor: colors.primary.main,
width: 32, paddingHorizontal: spacing.sm,
height: 32, paddingVertical: 4,
borderRadius: 16, borderRadius: borderRadius.md,
justifyContent: 'center',
alignItems: 'center',
borderWidth: 3, borderWidth: 3,
borderColor: colors.background.paper, borderColor: colors.background.paper,
}, },
@@ -1542,36 +1426,25 @@ function createGroupInfoStyles(colors: AppColors) {
fontWeight: '500', fontWeight: '500',
}, },
// 加群方式样式 - 更现代的选中状态 // 加群方式样式
joinTypeList: { joinTypeList: {
gap: spacing.md, gap: spacing.md,
}, },
joinTypeItem: { joinTypeItem: {
flexDirection: 'row',
alignItems: 'center',
padding: spacing.md, padding: spacing.md,
borderRadius: borderRadius.lg, borderRadius: borderRadius.lg,
borderWidth: 1, borderWidth: 1,
borderColor: colors.divider + '80', borderColor: colors.divider + '80',
backgroundColor: colors.background.default, backgroundColor: colors.background.default,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
}, },
joinTypeItemSelected: { joinTypeItemSelected: {
borderColor: colors.primary.main, borderColor: colors.primary.main,
backgroundColor: colors.primary.light + '12', backgroundColor: colors.primary.light + '12',
borderWidth: 1.5, borderWidth: 1.5,
}, },
joinTypeIcon: {
width: 48,
height: 48,
borderRadius: borderRadius.lg,
backgroundColor: colors.background.paper,
justifyContent: 'center',
alignItems: 'center',
marginRight: spacing.md,
},
joinTypeIconSelected: {
backgroundColor: colors.primary.light + '25',
},
joinTypeInfo: { joinTypeInfo: {
flex: 1, flex: 1,
}, },
@@ -1579,6 +1452,10 @@ function createGroupInfoStyles(colors: AppColors) {
fontWeight: '700', fontWeight: '700',
color: colors.primary.main, color: colors.primary.main,
}, },
joinTypeCheck: {
fontSize: fontSizes.lg,
fontWeight: '700',
},
// 转让群主样式 // 转让群主样式
transferDesc: { transferDesc: {
@@ -1632,79 +1509,6 @@ function createGroupInfoStyles(colors: AppColors) {
transferButton: { transferButton: {
marginTop: spacing.sm, marginTop: spacing.sm,
}, },
// 邀请成员模态框样式
tabContainer: {
flexDirection: 'row',
backgroundColor: colors.background.default,
borderRadius: borderRadius.lg,
padding: spacing.xs,
marginBottom: spacing.md,
borderWidth: 1,
borderColor: colors.divider + '60',
},
tab: {
flex: 1,
paddingVertical: spacing.sm,
alignItems: 'center',
borderRadius: borderRadius.md,
},
tabActive: {
backgroundColor: colors.background.paper,
borderWidth: 1,
borderColor: colors.divider + '60',
},
tabTextActive: {
fontWeight: '700',
},
selectedBadge: {
backgroundColor: colors.primary.light + '20',
paddingHorizontal: spacing.md,
paddingVertical: spacing.xs,
borderRadius: borderRadius.sm,
alignSelf: 'flex-start',
marginBottom: spacing.md,
},
friendListContent: {
paddingBottom: spacing.xl,
},
friendItem: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: spacing.md,
paddingHorizontal: spacing.sm,
borderRadius: borderRadius.lg,
marginBottom: spacing.sm,
backgroundColor: colors.background.default,
},
friendItemSelected: {
backgroundColor: colors.primary.light + '12',
borderWidth: 1,
borderColor: colors.primary.light + '60',
},
friendInfo: {
flex: 1,
marginLeft: spacing.md,
marginRight: spacing.sm,
},
nickname: {
fontWeight: '600',
marginBottom: 2,
},
checkbox: {
width: 26,
height: 26,
borderRadius: 13,
borderWidth: 2,
borderColor: colors.divider,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: colors.background.paper,
},
checkboxSelected: {
backgroundColor: colors.primary.main,
borderColor: colors.primary.main,
},
}); });
} }

View File

@@ -3,9 +3,10 @@
* 显示群成员列表,支持管理员管理成员 * 显示群成员列表,支持管理员管理成员
* 支持响应式网格布局 * 支持响应式网格布局
* 使用游标分页 * 使用游标分页
* QQ 风格:移除装饰性图标,使用简洁的文字列表
*/ */
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react'; import React, { useState, useEffect, useCallback, useMemo } from 'react';
import { import {
View, View,
StyleSheet, StyleSheet,
@@ -22,7 +23,6 @@ import {
import { SafeAreaView } from 'react-native-safe-area-context'; import { SafeAreaView } from 'react-native-safe-area-context';
import { useLocalSearchParams, useRouter } from 'expo-router'; import { useLocalSearchParams, useRouter } from 'expo-router';
import { hrefUserProfile } from '../../navigation/hrefs'; import { hrefUserProfile } from '../../navigation/hrefs';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { blurActiveElement } from '../../infrastructure/platform'; import { blurActiveElement } from '../../infrastructure/platform';
import { spacing, fontSizes, borderRadius, useAppColors, type AppColors } from '../../theme'; import { spacing, fontSizes, borderRadius, useAppColors, type AppColors } from '../../theme';
import { useAuthStore } from '../../stores'; import { useAuthStore } from '../../stores';
@@ -33,8 +33,8 @@ import {
ICursorGroupMemberListPagedSource, ICursorGroupMemberListPagedSource,
} from '../../stores/group'; } from '../../stores/group';
import { enrichGroupMembersWithUserProfiles } from '../../stores/group'; import { enrichGroupMembersWithUserProfiles } from '../../stores/group';
import { Avatar, Text, Button, Loading, EmptyState, Divider, ResponsiveContainer, AppBackButton } from '../../components/common'; import { Avatar, Text, Button, Loading, EmptyState, Divider, AppBackButton } from '../../components/common';
import { useResponsive, useBreakpointGTE } from '../../hooks'; import { useResponsive } from '../../hooks';
import { useCursorPagination } from '../../hooks/useCursorPagination'; import { useCursorPagination } from '../../hooks/useCursorPagination';
import { import {
GroupMemberResponse, GroupMemberResponse,
@@ -44,13 +44,6 @@ import { firstRouteParam } from '../../navigation/paramUtils';
const { width: SCREEN_WIDTH } = Dimensions.get('window'); const { width: SCREEN_WIDTH } = Dimensions.get('window');
const GROUP_MEMBER_REMOTE_LIST_KIND: GroupMemberListSourceKind = 'cursor'; const GROUP_MEMBER_REMOTE_LIST_KIND: GroupMemberListSourceKind = 'cursor';
// 网格布局配置
const GRID_CONFIG = {
mobile: { columns: 1, itemWidth: '100%' },
tablet: { columns: 2, itemWidth: '48%' },
desktop: { columns: 3, itemWidth: '31%' },
};
// 成员分组 // 成员分组
interface MemberGroup { interface MemberGroup {
title: string; title: string;
@@ -66,15 +59,7 @@ const GroupMembersScreen: React.FC = () => {
const { currentUser } = useAuthStore(); const { currentUser } = useAuthStore();
// 响应式布局 // 响应式布局
const { isDesktop, isTablet, width } = useResponsive(); const { width } = useResponsive();
const isWideScreen = useBreakpointGTE('lg');
// 计算网格列数
const gridConfig = useMemo(() => {
if (width >= 1024) return GRID_CONFIG.desktop;
if (width >= 768) return GRID_CONFIG.tablet;
return GRID_CONFIG.mobile;
}, [width]);
const memberListSource = useMemo<ICursorGroupMemberListPagedSource>(() => { const memberListSource = useMemo<ICursorGroupMemberListPagedSource>(() => {
return createCursorGroupMemberListSource(GROUP_MEMBER_REMOTE_LIST_KIND, groupId, 50); return createCursorGroupMemberListSource(GROUP_MEMBER_REMOTE_LIST_KIND, groupId, 50);
@@ -431,17 +416,17 @@ const GroupMembersScreen: React.FC = () => {
</Text> </Text>
{item.muted && ( {item.muted && (
<View style={styles.mutedBadge}> <View style={styles.mutedBadge}>
<MaterialCommunityIcons name="microphone-off" size={12} color={colors.error.main} /> <Text variant="label" color={colors.error.main}></Text>
<Text variant="label" color={colors.error.main}> </Text>
</View> </View>
)} )}
</TouchableOpacity> </TouchableOpacity>
{canManage && ( {canManage && (
<TouchableOpacity <TouchableOpacity
style={styles.moreButton}
onPress={() => openActionModal(item)} onPress={() => openActionModal(item)}
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }} hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
> >
<MaterialCommunityIcons name="dots-vertical" size={20} color={colors.text.hint} /> <Text variant="body" color={colors.text.hint} style={styles.moreButtonText}>···</Text>
</TouchableOpacity> </TouchableOpacity>
)} )}
</View> </View>
@@ -468,7 +453,6 @@ const GroupMembersScreen: React.FC = () => {
<EmptyState <EmptyState
title="暂无成员" title="暂无成员"
description="群组还没有成员" description="群组还没有成员"
icon="account-group-outline"
/> />
); );
}; };
@@ -509,11 +493,6 @@ const GroupMembersScreen: React.FC = () => {
onPress={handleToggleAdmin} onPress={handleToggleAdmin}
disabled={actionLoading} disabled={actionLoading}
> >
<MaterialCommunityIcons
name={selectedMember.role === 'admin' ? 'account-remove' : 'account-plus'}
size={22}
color={colors.text.primary}
/>
<Text variant="body" style={styles.actionText}> <Text variant="body" style={styles.actionText}>
{selectedMember.role === 'admin' ? '取消管理员' : '设为管理员'} {selectedMember.role === 'admin' ? '取消管理员' : '设为管理员'}
</Text> </Text>
@@ -526,11 +505,6 @@ const GroupMembersScreen: React.FC = () => {
onPress={handleToggleMute} onPress={handleToggleMute}
disabled={actionLoading} disabled={actionLoading}
> >
<MaterialCommunityIcons
name={selectedMember.muted ? 'microphone' : 'microphone-off'}
size={22}
color={selectedMember.muted ? colors.success.main : colors.error.main}
/>
<Text <Text
variant="body" variant="body"
color={selectedMember.muted ? colors.success.main : colors.error.main} color={selectedMember.muted ? colors.success.main : colors.error.main}
@@ -546,7 +520,6 @@ const GroupMembersScreen: React.FC = () => {
onPress={handleRemoveMember} onPress={handleRemoveMember}
disabled={actionLoading} disabled={actionLoading}
> >
<MaterialCommunityIcons name="account-remove" size={22} color={colors.error.main} />
<Text variant="body" color={colors.error.main} style={styles.actionText}> <Text variant="body" color={colors.error.main} style={styles.actionText}>
</Text> </Text>
@@ -690,21 +663,20 @@ function createGroupMembersStyles(colors: AppColors) {
flex: 1, flex: 1,
backgroundColor: colors.background.default, backgroundColor: colors.background.default,
}, },
// Header 样式 - 扁平化 // Header 样式
pageHeader: { pageHeader: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between', justifyContent: 'space-between',
paddingHorizontal: spacing.md, paddingHorizontal: spacing.md,
paddingVertical: spacing.md, paddingVertical: spacing.sm,
backgroundColor: colors.background.paper, backgroundColor: colors.background.paper,
borderBottomWidth: 0.5, borderBottomWidth: 0.5,
borderBottomColor: colors.divider + '60', borderBottomColor: colors.divider + '60',
}, },
pageTitle: { pageTitle: {
fontWeight: '800', fontWeight: '600',
fontSize: fontSizes.xl + 1, fontSize: fontSizes.xl,
letterSpacing: 0.5,
}, },
pageHeaderPlaceholder: { pageHeaderPlaceholder: {
width: 40, width: 40,
@@ -712,6 +684,7 @@ function createGroupMembersStyles(colors: AppColors) {
}, },
section: { section: {
marginBottom: spacing.md, marginBottom: spacing.md,
backgroundColor: colors.background.paper,
}, },
sectionHeader: { sectionHeader: {
flexDirection: 'row', flexDirection: 'row',
@@ -720,8 +693,6 @@ function createGroupMembersStyles(colors: AppColors) {
paddingHorizontal: spacing.lg, paddingHorizontal: spacing.lg,
paddingVertical: spacing.md, paddingVertical: spacing.md,
backgroundColor: colors.background.default, backgroundColor: colors.background.default,
borderTopWidth: 0.5,
borderTopColor: colors.divider + '40',
}, },
memberItem: { memberItem: {
flexDirection: 'row', flexDirection: 'row',
@@ -742,25 +713,31 @@ function createGroupMembersStyles(colors: AppColors) {
marginBottom: 2, marginBottom: 2,
}, },
memberName: { memberName: {
fontWeight: '700', fontWeight: '500',
fontSize: fontSizes.md + 1, fontSize: fontSizes.md,
letterSpacing: 0.3,
}, },
roleBadge: { roleBadge: {
paddingHorizontal: spacing.sm, paddingHorizontal: spacing.sm,
paddingVertical: 2, paddingVertical: 2,
borderRadius: borderRadius.sm, borderRadius: borderRadius.sm,
marginLeft: spacing.sm, marginLeft: spacing.sm,
fontWeight: '800',
fontSize: fontSizes.xs,
color: colors.background.paper,
}, },
mutedBadge: { mutedBadge: {
flexDirection: 'row',
alignItems: 'center',
marginTop: 2, marginTop: 2,
}, },
// 模态框样式 - 更现代 moreButton: {
paddingHorizontal: spacing.sm,
paddingVertical: spacing.xs,
minWidth: 36,
alignItems: 'center',
},
moreButtonText: {
fontSize: fontSizes['2xl'],
fontWeight: '700',
lineHeight: fontSizes['2xl'],
color: colors.text.hint,
},
// 模态框样式
modalOverlay: { modalOverlay: {
flex: 1, flex: 1,
backgroundColor: 'rgba(0, 0, 0, 0.45)', backgroundColor: 'rgba(0, 0, 0, 0.45)',
@@ -781,32 +758,25 @@ function createGroupMembersStyles(colors: AppColors) {
borderBottomColor: colors.divider + '60', borderBottomColor: colors.divider + '60',
}, },
modalTitle: { modalTitle: {
fontWeight: '800', fontWeight: '700',
fontSize: fontSizes.xl + 1, fontSize: fontSizes.xl,
marginTop: spacing.sm, marginTop: spacing.sm,
marginBottom: spacing.xs, marginBottom: spacing.xs,
letterSpacing: 0.5,
}, },
actionItem: { actionItem: {
flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
paddingVertical: spacing.md, paddingVertical: spacing.md,
paddingHorizontal: spacing.sm,
marginLeft: -spacing.sm,
marginRight: -spacing.sm,
borderRadius: borderRadius.md,
borderBottomWidth: 0.5, borderBottomWidth: 0.5,
borderBottomColor: colors.divider + '40', borderBottomColor: colors.divider + '40',
}, },
actionText: { actionText: {
marginLeft: spacing.md,
fontWeight: '600', fontWeight: '600',
fontSize: fontSizes.md, fontSize: fontSizes.md,
}, },
inputLabel: { inputLabel: {
marginBottom: spacing.xs, marginBottom: spacing.xs,
fontWeight: '700', fontWeight: '600',
fontSize: fontSizes.sm + 1, fontSize: fontSizes.sm,
}, },
input: { input: {
backgroundColor: colors.background.default, backgroundColor: colors.background.default,
@@ -815,7 +785,7 @@ function createGroupMembersStyles(colors: AppColors) {
paddingVertical: spacing.md, paddingVertical: spacing.md,
fontSize: fontSizes.md, fontSize: fontSizes.md,
color: colors.text.primary, color: colors.text.primary,
borderWidth: 1.5, borderWidth: 1,
borderColor: colors.divider + '80', borderColor: colors.divider + '80',
marginBottom: spacing.md, marginBottom: spacing.md,
}, },

View File

@@ -15,8 +15,7 @@ import {
} from 'react-native'; } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context'; import { SafeAreaView } from 'react-native-safe-area-context';
import { useRouter, useLocalSearchParams } from 'expo-router'; import { useRouter, useLocalSearchParams } from 'expo-router';
import { MaterialCommunityIcons } from '@expo/vector-icons'; import { spacing, fontSizes, useAppColors, type AppColors } from '../../theme';
import { spacing, borderRadius, shadows, fontSizes, useAppColors, type AppColors } from '../../theme';
import { useAuthStore } from '../../stores'; import { useAuthStore } from '../../stores';
import { authService } from '@/services/auth'; import { authService } from '@/services/auth';
import { messageService } from '@/services/message'; import { messageService } from '@/services/message';
@@ -262,34 +261,28 @@ const PrivateChatInfoScreen: React.FC = () => {
); );
}; };
// 渲染设置项(带开关 // 渲染开关设置项 - QQ 风格:左侧标题,右侧开关
const renderSwitchItem = ( const renderSwitchItem = (
icon: string,
title: string, title: string,
value: boolean, value: boolean,
onValueChange: () => void onValueChange: () => void
) => ( ) => (
<View style={styles.settingItem}> <View style={styles.settingItem}>
<View style={styles.settingIconContainer}> <Text variant="body" style={styles.settingTitle}>{title}</Text>
<MaterialCommunityIcons name={icon as any} size={20} color={colors.primary.main} />
</View>
<Text variant="body" style={styles.settingTitle}>
{title}
</Text>
<Switch <Switch
value={value} value={value}
onValueChange={onValueChange} onValueChange={onValueChange}
trackColor={{ false: '#E0E0E0', true: colors.primary.light }} trackColor={{ false: colors.divider, true: colors.primary.light }}
thumbColor={value ? colors.primary.main : colors.background.default} thumbColor={value ? colors.primary.main : colors.background.paper}
/> />
</View> </View>
); );
// 渲染设置项(带箭头 // 渲染操作项 - QQ 风格:左侧标题,右侧值/箭头
const renderActionItem = ( const renderActionItem = (
icon: string,
title: string, title: string,
onPress: () => void, onPress: () => void,
value?: string,
danger: boolean = false danger: boolean = false
) => ( ) => (
<TouchableOpacity <TouchableOpacity
@@ -297,17 +290,17 @@ const PrivateChatInfoScreen: React.FC = () => {
onPress={onPress} onPress={onPress}
activeOpacity={0.7} activeOpacity={0.7}
> >
<View style={[styles.settingIconContainer, danger && styles.settingIconDanger]}> <Text variant="body" style={danger ? styles.dangerText : styles.settingTitle}>
<MaterialCommunityIcons
name={icon as any}
size={20}
color={danger ? colors.error.main : colors.primary.main}
/>
</View>
<Text variant="body" style={danger ? [styles.settingTitle, styles.dangerText] : styles.settingTitle}>
{title} {title}
</Text> </Text>
<MaterialCommunityIcons name="chevron-right" size={22} color={colors.text.hint} /> <View style={styles.settingValueRow}>
{value && (
<Text variant="body" color={colors.text.secondary} numberOfLines={1} style={styles.settingValueText}>
{value}
</Text>
)}
<Text variant="body" color={colors.text.hint} style={styles.settingChevron}></Text>
</View>
</TouchableOpacity> </TouchableOpacity>
); );
@@ -351,7 +344,7 @@ const PrivateChatInfoScreen: React.FC = () => {
> >
<Avatar <Avatar
source={displayUser.avatar} source={displayUser.avatar}
size={80} size={72}
name={displayUser.nickname || ''} name={displayUser.nickname || ''}
/> />
<View style={styles.userInfo}> <View style={styles.userInfo}>
@@ -364,59 +357,53 @@ const PrivateChatInfoScreen: React.FC = () => {
</Text> </Text>
)} )}
</View> </View>
<MaterialCommunityIcons name="chevron-right" size={24} color={colors.text.hint} /> <Text variant="body" color={colors.text.hint} style={styles.settingChevron}></Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
{/* 聊天设置 */} {/* 聊天设置 */}
<View style={styles.section}>
<Text variant="caption" color={colors.text.secondary} style={styles.sectionTitle}>
</Text>
<View style={styles.card}> <View style={styles.card}>
{renderSwitchItem('bell-off-outline', '消息免打扰', isMuted, toggleMute)} <View style={styles.settingsList}>
{renderSwitchItem('消息免打扰', isMuted, toggleMute)}
<Divider style={styles.divider} /> <Divider style={styles.divider} />
{renderSwitchItem('pin-outline', '置顶聊天', isPinned, togglePin)} {renderSwitchItem('置顶聊天', isPinned, togglePin)}
</View> </View>
</View> </View>
{/* 聊天记录管理 */} {/* 聊天记录管理 */}
<View style={styles.section}>
<Text variant="caption" color={colors.text.secondary} style={styles.sectionTitle}>
</Text>
<View style={styles.card}> <View style={styles.card}>
{renderActionItem('magnify', '查找聊天记录', handleSearchMessages)} <View style={styles.settingsList}>
{renderActionItem('查找聊天记录', handleSearchMessages)}
<Divider style={styles.divider} /> <Divider style={styles.divider} />
{renderActionItem('delete-sweep-outline', '清空聊天记录', handleClearHistory, true)} {renderActionItem('清空聊天记录', handleClearHistory, undefined, true)}
</View> </View>
</View> </View>
{/* 其他操作 */} {/* 其他操作 */}
<View style={styles.section}>
<Text variant="caption" color={colors.text.secondary} style={styles.sectionTitle}>
</Text>
<View style={styles.card}> <View style={styles.card}>
{renderActionItem('shield-alert-outline', '投诉', handleReport, true)} <View style={styles.settingsList}>
{renderActionItem('投诉', handleReport, undefined, true)}
<Divider style={styles.divider} /> <Divider style={styles.divider} />
{renderActionItem( {renderActionItem(
isBlocked ? 'account-check-outline' : 'account-cancel-outline',
isBlocked ? '取消拉黑' : '拉黑用户', isBlocked ? '取消拉黑' : '拉黑用户',
handleBlockUser, handleBlockUser,
undefined,
true true
)} )}
</View> </View>
</View> </View>
{/* 删除聊天按钮 */} {/* 删除聊天按钮 */}
<View style={styles.section}> <View style={[styles.card, styles.dangerCard]}>
<Button <TouchableOpacity
title="删除并退出聊天" style={styles.dangerButton}
onPress={handleDeleteAndExit} onPress={handleDeleteAndExit}
variant="danger" activeOpacity={0.7}
style={styles.deleteButton} >
/> <Text variant="body" color={colors.error.main} style={styles.dangerButtonText}>
退
</Text>
</TouchableOpacity>
</View> </View>
</ScrollView> </ScrollView>
</SafeAreaView> </SafeAreaView>
@@ -427,16 +414,17 @@ function createPrivateChatInfoStyles(colors: AppColors) {
return StyleSheet.create({ return StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
backgroundColor: colors.background.paper, backgroundColor: colors.background.default,
}, },
pageHeader: { pageHeader: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between', justifyContent: 'space-between',
paddingHorizontal: spacing.lg, paddingHorizontal: spacing.md,
paddingVertical: spacing.md, paddingVertical: spacing.sm,
backgroundColor: colors.background.paper, backgroundColor: colors.background.paper,
borderBottomWidth: 0, borderBottomWidth: 0.5,
borderBottomColor: colors.divider + '60',
}, },
pageTitle: { pageTitle: {
fontWeight: '600', fontWeight: '600',
@@ -450,11 +438,11 @@ function createPrivateChatInfoStyles(colors: AppColors) {
flex: 1, flex: 1,
}, },
scrollContent: { scrollContent: {
paddingHorizontal: spacing.lg, paddingBottom: spacing.xl * 2,
paddingBottom: spacing.xl,
}, },
headerCard: { headerCard: {
backgroundColor: colors.background.paper, backgroundColor: colors.background.paper,
paddingHorizontal: spacing.lg,
paddingVertical: spacing.lg, paddingVertical: spacing.lg,
marginBottom: spacing.md, marginBottom: spacing.md,
}, },
@@ -468,57 +456,62 @@ function createPrivateChatInfoStyles(colors: AppColors) {
}, },
userName: { userName: {
fontWeight: '700', fontWeight: '700',
fontSize: fontSizes['2xl'], fontSize: fontSizes.xl,
marginBottom: spacing.xs, marginBottom: spacing.xs,
}, },
section: {
marginTop: spacing.lg,
},
sectionTitle: {
marginBottom: spacing.sm,
marginLeft: spacing.sm,
fontWeight: '500',
fontSize: fontSizes.sm,
color: colors.text.secondary,
},
card: { card: {
backgroundColor: colors.background.paper, backgroundColor: colors.background.paper,
overflow: 'hidden', marginBottom: spacing.md,
paddingHorizontal: spacing.lg,
paddingVertical: spacing.sm,
},
settingsList: {
marginTop: 0,
}, },
settingItem: { settingItem: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between',
paddingVertical: spacing.md, paddingVertical: spacing.md,
paddingHorizontal: 0,
},
settingIconContainer: {
width: 36,
height: 36,
borderRadius: borderRadius.md,
backgroundColor: colors.background.default,
justifyContent: 'center',
alignItems: 'center',
marginRight: spacing.md,
},
settingIconDanger: {
backgroundColor: colors.error.light + '15',
}, },
settingTitle: { settingTitle: {
flex: 1, fontSize: fontSizes.md,
fontWeight: '500',
color: colors.text.primary,
},
settingValueRow: {
flexDirection: 'row',
alignItems: 'center',
marginLeft: spacing.md,
},
settingValueText: {
maxWidth: 180,
},
settingChevron: {
fontSize: fontSizes.xl,
marginLeft: spacing.xs,
fontWeight: '300',
}, },
dangerText: { dangerText: {
color: colors.error.main, color: colors.error.main,
fontWeight: '600', fontWeight: '600',
}, },
divider: { divider: {
marginLeft: 52,
width: 'auto',
marginVertical: 0, marginVertical: 0,
opacity: 0.5,
}, },
deleteButton: { dangerCard: {
marginTop: spacing.sm, padding: 0,
borderRadius: borderRadius.lg, overflow: 'hidden',
height: 52, marginTop: 0,
},
dangerButton: {
alignItems: 'center',
justifyContent: 'center',
paddingVertical: spacing.lg,
},
dangerButtonText: {
fontWeight: '600',
}, },
}); });
} }