feat: 更新文章功能和相关依赖
- 新增文章详情页面,支持根据文章 ID 加载和展示文章内容 - 添加文章卡片组件,展示推荐文章的标题、封面和阅读量 - 更新文章服务,支持获取文章列表和根据 ID 获取文章详情 - 集成腾讯云 COS SDK,支持文件上传功能 - 优化打卡功能,支持按日期加载和展示打卡记录 - 更新相关依赖,确保项目兼容性和功能完整性 - 调整样式以适应新功能的展示和交互
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { BlurView } from 'expo-blur';
|
||||
import * as ImagePicker from 'expo-image-picker';
|
||||
import { useLocalSearchParams, useRouter } from 'expo-router';
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
@@ -7,7 +8,7 @@ import {
|
||||
Alert,
|
||||
FlatList,
|
||||
Image,
|
||||
KeyboardAvoidingView,
|
||||
Keyboard,
|
||||
Modal,
|
||||
Platform,
|
||||
ScrollView,
|
||||
@@ -17,17 +18,22 @@ import {
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import Markdown from 'react-native-markdown-display';
|
||||
import Animated, { FadeInDown, FadeInUp, Layout } from 'react-native-reanimated';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
|
||||
import { HeaderBar } from '@/components/ui/HeaderBar';
|
||||
import { Colors } from '@/constants/Colors';
|
||||
import { useAppSelector } from '@/hooks/redux';
|
||||
import { buildCosKey, buildPublicUrl } from '@/constants/Cos';
|
||||
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||
import { deleteConversation, getConversationDetail, listConversations, type AiConversationListItem } from '@/services/aiCoach';
|
||||
import { loadAiCoachSessionCache, saveAiCoachSessionCache } from '@/services/aiCoachSession';
|
||||
import { api, getAuthToken, postTextStream } from '@/services/api';
|
||||
import { uploadWithRetry } from '@/services/cos';
|
||||
import { updateUser as updateUserApi } from '@/services/users';
|
||||
import type { CheckinRecord } from '@/store/checkinSlice';
|
||||
import { fetchMyProfile, updateProfile } from '@/store/userSlice';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
type Role = 'user' | 'assistant';
|
||||
@@ -67,14 +73,30 @@ export default function AICoachChatScreen() {
|
||||
const didInitialScrollRef = useRef(false);
|
||||
const [composerHeight, setComposerHeight] = useState<number>(80);
|
||||
const shouldAutoScrollRef = useRef(false);
|
||||
const [keyboardOffset, setKeyboardOffset] = useState(0);
|
||||
const pendingAssistantIdRef = useRef<string | null>(null);
|
||||
const [selectedImages, setSelectedImages] = useState<Array<{
|
||||
id: string;
|
||||
localUri: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
progress: number;
|
||||
uploadedKey?: string;
|
||||
uploadedUrl?: string;
|
||||
error?: string;
|
||||
}>>([]);
|
||||
const [previewImageUri, setPreviewImageUri] = useState<string | null>(null);
|
||||
|
||||
const planDraft = useAppSelector((s) => s.trainingPlan?.draft);
|
||||
const checkin = useAppSelector((s) => (s as any).checkin);
|
||||
const dispatch = useAppDispatch();
|
||||
const userProfile = useAppSelector((s) => (s as any)?.user?.profile);
|
||||
|
||||
const chips = useMemo(() => [
|
||||
{ key: 'posture', label: '体态评估', action: () => router.push('/ai-posture-assessment') },
|
||||
{ key: 'plan', label: 'AI制定训练计划', action: () => handleQuickPlan() },
|
||||
{ key: 'analyze', label: '分析运动记录', action: () => handleAnalyzeRecords() },
|
||||
{ key: 'weight', label: '记体重', action: () => insertWeightInputCard() },
|
||||
], [router, planDraft, checkin]);
|
||||
|
||||
const scrollToEnd = useCallback(() => {
|
||||
@@ -132,6 +154,29 @@ export default function AICoachChatScreen() {
|
||||
}
|
||||
}, [composerHeight, isAtBottom, scrollToEnd]);
|
||||
|
||||
// 键盘事件:在键盘弹出时,将输入区与悬浮按钮一起上移,避免遮挡
|
||||
useEffect(() => {
|
||||
let showSub: any = null;
|
||||
let hideSub: any = null;
|
||||
if (Platform.OS === 'ios') {
|
||||
showSub = Keyboard.addListener('keyboardWillChangeFrame', (e: any) => {
|
||||
try {
|
||||
const height = Math.max(0, (e.endCoordinates?.height ?? 0) - insets.bottom);
|
||||
setKeyboardOffset(height);
|
||||
} catch { setKeyboardOffset(0); }
|
||||
});
|
||||
} else {
|
||||
showSub = Keyboard.addListener('keyboardDidShow', (e: any) => {
|
||||
try { setKeyboardOffset(Math.max(0, e.endCoordinates?.height ?? 0)); } catch { setKeyboardOffset(0); }
|
||||
});
|
||||
hideSub = Keyboard.addListener('keyboardDidHide', () => setKeyboardOffset(0));
|
||||
}
|
||||
return () => {
|
||||
try { showSub?.remove?.(); } catch { }
|
||||
try { hideSub?.remove?.(); } catch { }
|
||||
};
|
||||
}, [insets.bottom]);
|
||||
|
||||
const streamAbortRef = useRef<{ abort: () => void } | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -246,6 +291,7 @@ export default function AICoachChatScreen() {
|
||||
const userMsg: ChatMessage = { id: userMsgId, role: 'user', content: text };
|
||||
shouldAutoScrollRef.current = isAtBottom;
|
||||
setMessages((m) => [...m, userMsg, { id: assistantId, role: 'assistant', content: '' }]);
|
||||
pendingAssistantIdRef.current = assistantId;
|
||||
|
||||
setIsSending(true);
|
||||
setIsStreaming(true);
|
||||
@@ -281,6 +327,7 @@ export default function AICoachChatScreen() {
|
||||
setIsStreaming(false);
|
||||
streamAbortRef.current = null;
|
||||
if (cidFromHeader && !conversationId) setConversationId(cidFromHeader);
|
||||
pendingAssistantIdRef.current = null;
|
||||
try { console.log('[AI_CHAT][api] end', { cidFromHeader, hadChunks: receivedAnyChunk }); } catch { }
|
||||
};
|
||||
|
||||
@@ -289,6 +336,7 @@ export default function AICoachChatScreen() {
|
||||
setIsSending(false);
|
||||
setIsStreaming(false);
|
||||
streamAbortRef.current = null;
|
||||
pendingAssistantIdRef.current = null;
|
||||
// 流式失败时的降级:尝试一次性非流式
|
||||
try {
|
||||
const bodyNoStream = { ...body, stream: false };
|
||||
@@ -314,10 +362,59 @@ export default function AICoachChatScreen() {
|
||||
}
|
||||
|
||||
async function send(text: string) {
|
||||
if (!text.trim() || isSending) return;
|
||||
if (isSending) return;
|
||||
const trimmed = text.trim();
|
||||
setInput('');
|
||||
await sendStream(trimmed);
|
||||
if (!trimmed && selectedImages.length === 0) return;
|
||||
|
||||
async function ensureImagesUploaded(): Promise<string[]> {
|
||||
const urls: string[] = [];
|
||||
for (const img of selectedImages) {
|
||||
if (img.uploadedUrl) {
|
||||
urls.push(img.uploadedUrl);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const resp = await fetch(img.localUri);
|
||||
const blob = await resp.blob();
|
||||
const ext = (() => {
|
||||
const t = (blob.type || '').toLowerCase();
|
||||
if (t.includes('png')) return 'png';
|
||||
if (t.includes('webp')) return 'webp';
|
||||
if (t.includes('heic')) return 'heic';
|
||||
if (t.includes('heif')) return 'heif';
|
||||
return 'jpg';
|
||||
})();
|
||||
const key = buildCosKey({ prefix: 'images/chat', ext });
|
||||
const res = await uploadWithRetry({
|
||||
key,
|
||||
body: blob,
|
||||
contentType: blob.type || 'image/jpeg',
|
||||
onProgress: ({ percent }: { percent?: number }) => {
|
||||
const p = typeof percent === 'number' ? percent : 0;
|
||||
setSelectedImages((prev) => prev.map((it) => it.id === img.id ? { ...it, progress: p } : it));
|
||||
},
|
||||
} as any);
|
||||
const url = buildPublicUrl(res.key);
|
||||
urls.push(url);
|
||||
setSelectedImages((prev) => prev.map((it) => it.id === img.id ? { ...it, uploadedKey: res.key, uploadedUrl: url, progress: 1 } : it));
|
||||
} catch (e: any) {
|
||||
setSelectedImages((prev) => prev.map((it) => it.id === img.id ? { ...it, error: e?.message || '上传失败' } : it));
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
return urls;
|
||||
}
|
||||
|
||||
try {
|
||||
const urls = await ensureImagesUploaded();
|
||||
const mdImages = urls.map((u) => ``).join('\n\n');
|
||||
const composed = [trimmed, mdImages].filter(Boolean).join('\n\n');
|
||||
setInput('');
|
||||
setSelectedImages([]);
|
||||
await sendStream(composed);
|
||||
} catch (e: any) {
|
||||
Alert.alert('上传失败', e?.message || '图片上传失败,请稍后重试');
|
||||
}
|
||||
}
|
||||
|
||||
function handleQuickPlan() {
|
||||
@@ -378,6 +475,37 @@ export default function AICoachChatScreen() {
|
||||
send(prompt);
|
||||
}
|
||||
|
||||
const pickImages = useCallback(async () => {
|
||||
try {
|
||||
const result = await ImagePicker.launchImageLibraryAsync({
|
||||
mediaTypes: ImagePicker.MediaTypeOptions.Images,
|
||||
allowsMultipleSelection: true,
|
||||
selectionLimit: 4,
|
||||
quality: 0.9,
|
||||
} as any);
|
||||
if ((result as any).canceled) return;
|
||||
const assets = (result as any).assets || [];
|
||||
const next = assets.map((a: any) => ({
|
||||
id: `${a.assetId || a.fileName || a.uri}_${Math.random().toString(36).slice(2, 8)}`,
|
||||
localUri: a.uri,
|
||||
width: a.width,
|
||||
height: a.height,
|
||||
progress: 0,
|
||||
}));
|
||||
setSelectedImages((prev) => {
|
||||
const merged = [...prev, ...next];
|
||||
return merged.slice(0, 4);
|
||||
});
|
||||
setTimeout(scrollToEnd, 0);
|
||||
} catch (e: any) {
|
||||
Alert.alert('错误', e?.message || '选择图片失败');
|
||||
}
|
||||
}, [scrollToEnd]);
|
||||
|
||||
const removeSelectedImage = useCallback((id: string) => {
|
||||
setSelectedImages((prev) => prev.filter((it) => it.id !== id));
|
||||
}, []);
|
||||
|
||||
function renderItem({ item }: { item: ChatMessage }) {
|
||||
const isUser = item.role === 'user';
|
||||
return (
|
||||
@@ -399,12 +527,88 @@ export default function AICoachChatScreen() {
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Text style={[styles.bubbleText, { color: isUser ? theme.onPrimary : '#192126' }]}>{item.content}</Text>
|
||||
{renderBubbleContent(item)}
|
||||
</View>
|
||||
{false}
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
|
||||
function renderBubbleContent(item: ChatMessage) {
|
||||
if (!item.content?.trim() && isStreaming && pendingAssistantIdRef.current === item.id) {
|
||||
return <Text style={[styles.bubbleText, { color: '#687076' }]}>正在思考…</Text>;
|
||||
}
|
||||
if (item.content?.startsWith('__WEIGHT_INPUT_CARD__')) {
|
||||
const preset = (() => {
|
||||
const m = item.content.split('\n')?.[1];
|
||||
const v = parseFloat(m || '');
|
||||
return isNaN(v) ? '' : String(v);
|
||||
})();
|
||||
return (
|
||||
<View style={{ gap: 8 }}>
|
||||
<Text style={[styles.bubbleText, { color: '#192126', fontWeight: '700' }]}>记录今日体重</Text>
|
||||
<View style={styles.weightRow}>
|
||||
<TextInput
|
||||
placeholder="例如 60.5"
|
||||
keyboardType="decimal-pad"
|
||||
defaultValue={preset}
|
||||
placeholderTextColor={'#687076'}
|
||||
style={styles.weightInput}
|
||||
onSubmitEditing={(e) => handleSubmitWeight(e.nativeEvent.text)}
|
||||
returnKeyType="done"
|
||||
blurOnSubmit
|
||||
/>
|
||||
<Text style={styles.weightUnit}>kg</Text>
|
||||
<TouchableOpacity accessibilityRole="button" style={styles.weightSaveBtn} onPress={() => handleSubmitWeight((preset || '').toString())}>
|
||||
<Text style={{ color: '#192126', fontWeight: '700' }}>保存</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<Text style={{ color: '#687076', fontSize: 12 }}>按回车或点击保存,即可将该体重同步到账户并发送到对话。</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Markdown style={markdownStyles} mergeStyle>
|
||||
{item.content}
|
||||
</Markdown>
|
||||
);
|
||||
}
|
||||
|
||||
function insertWeightInputCard() {
|
||||
const id = `wcard_${Date.now()}`;
|
||||
const preset = userProfile?.weight ? Number(userProfile.weight) : undefined;
|
||||
const payload = `__WEIGHT_INPUT_CARD__\n${preset ?? ''}`;
|
||||
setMessages((prev) => [...prev, { id, role: 'assistant', content: payload }]);
|
||||
setTimeout(scrollToEnd, 0);
|
||||
}
|
||||
|
||||
async function handleSubmitWeight(text?: string) {
|
||||
const val = parseFloat(String(text ?? '').trim());
|
||||
if (isNaN(val) || val <= 0 || val > 500) {
|
||||
Alert.alert('请输入有效体重', '请填写合理的公斤数,例如 60.5');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// 本地更新
|
||||
dispatch(updateProfile({ weight: val }));
|
||||
// 后端同步(若有 userId 则更稳妥;后端实现容错)
|
||||
try {
|
||||
const userId = (userProfile as any)?.userId || (userProfile as any)?.id || (userProfile as any)?._id;
|
||||
if (userId) {
|
||||
await updateUserApi({ userId, weight: val });
|
||||
await dispatch(fetchMyProfile() as any);
|
||||
}
|
||||
} catch (e) {
|
||||
// 不阻断对话体验
|
||||
}
|
||||
// 在对话中插入“确认消息”并发送给教练
|
||||
const textMsg = `我记录了今日体重:${val} kg。请基于这一变化给出训练/营养建议。`;
|
||||
await send(textMsg);
|
||||
} catch (e: any) {
|
||||
Alert.alert('保存失败', e?.message || '请稍后重试');
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[styles.screen, { backgroundColor: theme.background }]}>
|
||||
<HeaderBar
|
||||
@@ -434,7 +638,7 @@ export default function AICoachChatScreen() {
|
||||
}}
|
||||
contentContainerStyle={{ paddingHorizontal: 14, paddingTop: 8 }}
|
||||
ListFooterComponent={() => (
|
||||
<View style={{ height: insets.bottom + composerHeight + (isAtBottom ? 0 : 56) + 16 }} />
|
||||
<View style={{ height: insets.bottom + keyboardOffset + composerHeight + (isAtBottom ? 0 : 56) + 16 }} />
|
||||
)}
|
||||
onContentSizeChange={() => {
|
||||
// 首次内容变化强制滚底,其余仅在接近底部时滚动
|
||||
@@ -454,53 +658,90 @@ export default function AICoachChatScreen() {
|
||||
showsVerticalScrollIndicator={false}
|
||||
/>
|
||||
|
||||
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : 'height'} keyboardVerticalOffset={insets.top}>
|
||||
<BlurView
|
||||
intensity={18}
|
||||
tint={'light'}
|
||||
style={[styles.composerWrap, { paddingBottom: insets.bottom + 10 }]}
|
||||
onLayout={(e) => {
|
||||
const h = e.nativeEvent.layout.height;
|
||||
if (h && Math.abs(h - composerHeight) > 0.5) setComposerHeight(h);
|
||||
}}
|
||||
<BlurView
|
||||
intensity={18}
|
||||
tint={'light'}
|
||||
style={[styles.composerWrap, { paddingBottom: insets.bottom + 10, bottom: keyboardOffset }]}
|
||||
onLayout={(e) => {
|
||||
const h = e.nativeEvent.layout.height;
|
||||
if (h && Math.abs(h - composerHeight) > 0.5) setComposerHeight(h);
|
||||
}}
|
||||
>
|
||||
<ScrollView
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
decelerationRate="fast"
|
||||
snapToAlignment="start"
|
||||
style={styles.chipsRowScroll}
|
||||
contentContainerStyle={{ paddingHorizontal: 6, gap: 8 }}
|
||||
>
|
||||
<View style={styles.chipsRow}>
|
||||
{chips.map((c) => (
|
||||
<TouchableOpacity key={c.key} style={[styles.chip, { borderColor: 'rgba(187,242,70,0.35)', backgroundColor: 'rgba(187,242,70,0.12)' }]} onPress={c.action}>
|
||||
<Text style={[styles.chipText, { color: '#192126' }]}>{c.label}</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
|
||||
<View style={[styles.inputRow, { borderColor: 'rgba(187,242,70,0.35)', backgroundColor: 'rgba(187,242,70,0.08)' }]}>
|
||||
<TextInput
|
||||
placeholder="问我任何与普拉提相关的问题..."
|
||||
placeholderTextColor={theme.textMuted}
|
||||
style={[styles.input, { color: '#192126' }]}
|
||||
value={input}
|
||||
onChangeText={setInput}
|
||||
multiline
|
||||
onSubmitEditing={() => send(input)}
|
||||
blurOnSubmit={false}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
accessibilityRole="button"
|
||||
disabled={!input.trim() || isSending}
|
||||
onPress={() => send(input)}
|
||||
style={[
|
||||
styles.sendBtn,
|
||||
{ backgroundColor: theme.primary, opacity: input.trim() && !isSending ? 1 : 0.5 }
|
||||
]}
|
||||
>
|
||||
{isSending ? (
|
||||
<ActivityIndicator color={theme.onPrimary} />
|
||||
) : (
|
||||
<Ionicons name="arrow-up" size={18} color={theme.onPrimary} />
|
||||
)}
|
||||
{chips.map((c) => (
|
||||
<TouchableOpacity key={c.key} style={[styles.chip, { borderColor: 'rgba(187,242,70,0.35)', backgroundColor: 'rgba(187,242,70,0.12)' }]} onPress={c.action}>
|
||||
<Text style={[styles.chipText, { color: '#192126' }]}>{c.label}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</BlurView>
|
||||
</KeyboardAvoidingView>
|
||||
))}
|
||||
</ScrollView>
|
||||
|
||||
{!!selectedImages.length && (
|
||||
<ScrollView
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
style={styles.imagesRow}
|
||||
contentContainerStyle={{ paddingHorizontal: 6, gap: 8 }}
|
||||
>
|
||||
{selectedImages.map((img) => (
|
||||
<View key={img.id} style={styles.imageThumbWrap}>
|
||||
<TouchableOpacity accessibilityRole="imagebutton" onPress={() => setPreviewImageUri(img.uploadedUrl || img.localUri)}>
|
||||
<Image source={{ uri: img.uploadedUrl || img.localUri }} style={styles.imageThumb} />
|
||||
</TouchableOpacity>
|
||||
{!!(img.progress > 0 && img.progress < 1) && (
|
||||
<View style={styles.imageProgressOverlay}>
|
||||
<Text style={styles.imageProgressText}>{Math.round((img.progress || 0) * 100)}%</Text>
|
||||
</View>
|
||||
)}
|
||||
<TouchableOpacity accessibilityRole="button" onPress={() => removeSelectedImage(img.id)} style={styles.imageRemoveBtn}>
|
||||
<Ionicons name="close" size={12} color="#fff" />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
))}
|
||||
</ScrollView>
|
||||
)}
|
||||
|
||||
<View style={[styles.inputRow, { borderColor: 'rgba(187,242,70,0.35)', backgroundColor: 'rgba(187,242,70,0.08)' }]}>
|
||||
<TouchableOpacity
|
||||
accessibilityRole="button"
|
||||
onPress={pickImages}
|
||||
style={[styles.mediaBtn, { backgroundColor: 'rgba(187,242,70,0.16)' }]}
|
||||
>
|
||||
<Ionicons name="image-outline" size={18} color={'#192126'} />
|
||||
</TouchableOpacity>
|
||||
<TextInput
|
||||
placeholder="问我任何与普拉提相关的问题..."
|
||||
placeholderTextColor={theme.textMuted}
|
||||
style={[styles.input, { color: '#192126' }]}
|
||||
value={input}
|
||||
onChangeText={setInput}
|
||||
multiline
|
||||
onSubmitEditing={() => send(input)}
|
||||
blurOnSubmit={false}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
accessibilityRole="button"
|
||||
disabled={(!input.trim() && selectedImages.length === 0) || isSending}
|
||||
onPress={() => send(input)}
|
||||
style={[
|
||||
styles.sendBtn,
|
||||
{ backgroundColor: theme.primary, opacity: (input.trim() || selectedImages.length > 0) && !isSending ? 1 : 0.5 }
|
||||
]}
|
||||
>
|
||||
{isSending ? (
|
||||
<ActivityIndicator color={theme.onPrimary} />
|
||||
) : (
|
||||
<Ionicons name="arrow-up" size={18} color={theme.onPrimary} />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</BlurView>
|
||||
|
||||
{!isAtBottom && (
|
||||
<TouchableOpacity
|
||||
@@ -558,6 +799,15 @@ export default function AICoachChatScreen() {
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</Modal>
|
||||
<Modal transparent visible={!!previewImageUri} animationType="fade" onRequestClose={() => setPreviewImageUri(null)}>
|
||||
<TouchableOpacity activeOpacity={1} style={styles.previewBackdrop} onPress={() => setPreviewImageUri(null)}>
|
||||
<View style={styles.previewBox}>
|
||||
{previewImageUri ? (
|
||||
<Image source={{ uri: previewImageUri }} style={styles.previewImage} resizeMode="contain" />
|
||||
) : null}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</Modal>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -613,6 +863,34 @@ const styles = StyleSheet.create({
|
||||
fontSize: 15,
|
||||
lineHeight: 22,
|
||||
},
|
||||
weightRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
},
|
||||
weightInput: {
|
||||
flex: 1,
|
||||
height: 36,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(0,0,0,0.08)',
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 10,
|
||||
backgroundColor: 'rgba(255,255,255,0.9)',
|
||||
color: '#192126',
|
||||
},
|
||||
weightUnit: {
|
||||
color: '#192126',
|
||||
fontWeight: '700',
|
||||
},
|
||||
weightSaveBtn: {
|
||||
height: 36,
|
||||
paddingHorizontal: 12,
|
||||
borderRadius: 8,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: 'rgba(187,242,70,0.6)'
|
||||
},
|
||||
// markdown 基础样式承载容器的字体尺寸保持与气泡一致
|
||||
composerWrap: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
@@ -624,11 +902,13 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
chipsRow: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
gap: 8,
|
||||
paddingHorizontal: 6,
|
||||
marginBottom: 8,
|
||||
},
|
||||
chipsRowScroll: {
|
||||
marginBottom: 8,
|
||||
},
|
||||
chip: {
|
||||
paddingHorizontal: 10,
|
||||
height: 34,
|
||||
@@ -642,6 +922,47 @@ const styles = StyleSheet.create({
|
||||
fontSize: 13,
|
||||
fontWeight: '600',
|
||||
},
|
||||
imagesRow: {
|
||||
maxHeight: 92,
|
||||
marginBottom: 8,
|
||||
},
|
||||
imageThumbWrap: {
|
||||
width: 72,
|
||||
height: 72,
|
||||
borderRadius: 12,
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
backgroundColor: 'rgba(0,0,0,0.06)'
|
||||
},
|
||||
imageThumb: {
|
||||
width: '100%',
|
||||
height: '100%'
|
||||
},
|
||||
imageRemoveBtn: {
|
||||
position: 'absolute',
|
||||
right: 4,
|
||||
top: 4,
|
||||
width: 20,
|
||||
height: 20,
|
||||
borderRadius: 10,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: 'rgba(0,0,0,0.45)'
|
||||
},
|
||||
imageProgressOverlay: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: 'rgba(0,0,0,0.35)',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
imageProgressText: {
|
||||
color: '#fff',
|
||||
fontWeight: '700'
|
||||
},
|
||||
inputRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
@@ -650,6 +971,14 @@ const styles = StyleSheet.create({
|
||||
borderRadius: 16,
|
||||
backgroundColor: 'rgba(0,0,0,0.04)'
|
||||
},
|
||||
mediaBtn: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 12,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginRight: 6,
|
||||
},
|
||||
input: {
|
||||
flex: 1,
|
||||
fontSize: 15,
|
||||
@@ -748,6 +1077,66 @@ const styles = StyleSheet.create({
|
||||
borderRadius: 10,
|
||||
backgroundColor: 'rgba(0,0,0,0.06)'
|
||||
},
|
||||
previewBackdrop: {
|
||||
flex: 1,
|
||||
backgroundColor: 'rgba(0,0,0,0.85)',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 16,
|
||||
},
|
||||
previewBox: {
|
||||
width: '100%',
|
||||
height: '80%',
|
||||
borderRadius: 12,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
previewImage: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
});
|
||||
|
||||
const markdownStyles = {
|
||||
body: {
|
||||
color: '#192126',
|
||||
fontSize: 15,
|
||||
lineHeight: 22,
|
||||
},
|
||||
paragraph: {
|
||||
marginTop: 2,
|
||||
marginBottom: 2,
|
||||
},
|
||||
bullet_list: {
|
||||
marginVertical: 4,
|
||||
},
|
||||
ordered_list: {
|
||||
marginVertical: 4,
|
||||
},
|
||||
list_item: {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
code_inline: {
|
||||
backgroundColor: 'rgba(0,0,0,0.06)',
|
||||
borderRadius: 4,
|
||||
paddingHorizontal: 4,
|
||||
paddingVertical: 2,
|
||||
},
|
||||
code_block: {
|
||||
backgroundColor: 'rgba(0,0,0,0.06)',
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 6,
|
||||
},
|
||||
fence: {
|
||||
backgroundColor: 'rgba(0,0,0,0.06)',
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 6,
|
||||
},
|
||||
heading1: { fontSize: 20, fontWeight: '800', marginVertical: 6 },
|
||||
heading2: { fontSize: 18, fontWeight: '800', marginVertical: 6 },
|
||||
heading3: { fontSize: 16, fontWeight: '800', marginVertical: 6 },
|
||||
link: { color: '#246BFD' },
|
||||
} as const;
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user