轻松聘
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

298 lines
7.3 KiB

import { Ionicons } from '@expo/vector-icons';
import { useRouter } from 'expo-router';
import { useState } from 'react';
import {
ActivityIndicator,
KeyboardAvoidingView,
Platform,
Pressable,
ScrollView,
StyleSheet,
Text,
TextInput,
View,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useLegalData } from '../../src/hooks';
import { colors } from '../../src/theme';
import type { LegalMessage } from '../../src/types';
/* ===================== 子组件:顶部导航 ===================== */
function Header({ onBack }: { onBack: () => void }) {
return (
<View style={styles.header}>
<Pressable style={styles.headerBtn} onPress={onBack} hitSlop={8}>
<Ionicons name="chevron-back" size={24} color={colors.textPrimary} />
</Pressable>
<Text style={styles.headerTitle}>阿布阿诺法律咨询</Text>
<View style={styles.headerBtn} />
</View>
);
}
/* ===================== 子组件:消息气泡 ===================== */
function MessageBubble({ message }: { message: LegalMessage }) {
const isUser = message.role === 'user';
return (
<View style={[styles.bubbleWrap, isUser ? styles.bubbleWrapRight : styles.bubbleWrapLeft]}>
{!isUser && (
<View style={styles.avatar}>
<Text style={styles.avatarText}>诺</Text>
</View>
)}
<View
style={[
styles.bubble,
isUser ? styles.bubbleUser : styles.bubbleAssistant,
]}
>
<Text style={[styles.bubbleText, isUser ? styles.bubbleTextUser : styles.bubbleTextAssistant]}>
{message.content}
</Text>
</View>
</View>
);
}
/* ===================== 页面 ===================== */
export default function LegalChatScreen() {
const router = useRouter();
const { conversation, loading, error, refresh, sendMessage } = useLegalData();
const [input, setInput] = useState('');
const [sending, setSending] = useState(false);
const handleSend = async () => {
const text = input.trim();
if (!text || sending) return;
setSending(true);
setInput('');
try {
await sendMessage(text);
} finally {
setSending(false);
}
};
return (
<SafeAreaView style={styles.safeArea} edges={['top']}>
<Header onBack={() => router.back()} />
{loading ? (
<View style={styles.stateBox}>
<ActivityIndicator size="large" color={colors.primary} />
</View>
) : error ? (
<View style={styles.stateBox}>
<Text style={styles.stateText}>加载失败,请重试</Text>
<Pressable style={styles.retryBtn} onPress={refresh}>
<Text style={styles.retryText}>重试</Text>
</Pressable>
</View>
) : (
<KeyboardAvoidingView
style={styles.flex}
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
keyboardVerticalOffset={Platform.OS === 'ios' ? 90 : 0}
>
<ScrollView
style={styles.scroll}
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
>
<View style={styles.tipCard}>
<Ionicons name="information-circle-outline" size={18} color={colors.info} />
<Text style={styles.tipText}>
阿布阿诺 AI 法律助手仅供参考,复杂问题建议升级到一对一律师咨询。
</Text>
</View>
{conversation?.messages.map((message) => (
<MessageBubble key={message.id} message={message} />
))}
</ScrollView>
<View style={styles.inputBar}>
<TextInput
style={styles.input}
placeholder="描述你的法律问题…"
placeholderTextColor={colors.textHint}
value={input}
onChangeText={setInput}
multiline
maxLength={500}
editable={!sending}
/>
<Pressable
style={[styles.sendBtn, (!input.trim() || sending) && styles.sendBtnDisabled]}
onPress={handleSend}
disabled={!input.trim() || sending}
>
<Ionicons name="send" size={18} color={colors.white} />
</Pressable>
</View>
</KeyboardAvoidingView>
)}
</SafeAreaView>
);
}
const styles = StyleSheet.create({
safeArea: {
flex: 1,
backgroundColor: colors.pageBg,
},
flex: {
flex: 1,
},
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 8,
height: 48,
backgroundColor: colors.white,
borderBottomWidth: 1,
borderBottomColor: colors.borderLight,
},
headerBtn: {
width: 40,
height: 40,
alignItems: 'center',
justifyContent: 'center',
},
headerTitle: {
fontSize: 17,
fontWeight: '700',
color: colors.textPrimary,
},
scroll: {
flex: 1,
},
scrollContent: {
padding: 8,
paddingBottom: 16,
},
tipCard: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
backgroundColor: colors.infoLight,
borderRadius: 8,
padding: 8,
marginBottom: 8,
},
tipText: {
flex: 1,
fontSize: 12,
lineHeight: 17,
color: colors.infoText,
},
bubbleWrap: {
flexDirection: 'row',
alignItems: 'flex-end',
marginBottom: 10,
},
bubbleWrapLeft: {
justifyContent: 'flex-start',
},
bubbleWrapRight: {
justifyContent: 'flex-end',
},
avatar: {
width: 32,
height: 32,
borderRadius: 16,
backgroundColor: colors.primary,
alignItems: 'center',
justifyContent: 'center',
marginRight: 8,
},
avatarText: {
fontSize: 13,
fontWeight: '700',
color: colors.white,
},
bubble: {
maxWidth: '76%',
paddingHorizontal: 12,
paddingVertical: 10,
borderRadius: 12,
},
bubbleUser: {
backgroundColor: colors.primary,
borderBottomRightRadius: 4,
},
bubbleAssistant: {
backgroundColor: colors.white,
borderBottomLeftRadius: 4,
},
bubbleText: {
fontSize: 14,
lineHeight: 20,
},
bubbleTextUser: {
color: colors.white,
},
bubbleTextAssistant: {
color: colors.textPrimary,
},
inputBar: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
paddingHorizontal: 8,
paddingVertical: 10,
backgroundColor: colors.white,
borderTopWidth: 1,
borderTopColor: colors.borderLight,
},
input: {
flex: 1,
maxHeight: 100,
minHeight: 40,
borderRadius: 20,
borderWidth: 1,
borderColor: colors.border,
paddingHorizontal: 14,
paddingVertical: 8,
fontSize: 14,
color: colors.textPrimary,
backgroundColor: colors.pageBg,
},
sendBtn: {
width: 40,
height: 40,
borderRadius: 20,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: colors.primaryBtn,
},
sendBtnDisabled: {
opacity: 0.5,
},
stateBox: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
stateText: {
marginTop: 12,
fontSize: 14,
color: colors.textSecondary,
},
retryBtn: {
marginTop: 16,
paddingHorizontal: 24,
paddingVertical: 8,
borderRadius: 18,
backgroundColor: colors.primaryBtn,
},
retryText: {
fontSize: 13,
fontWeight: '600',
color: colors.white,
},
});