diff --git a/app/(tabs)/coach.tsx b/app/(tabs)/coach.tsx index 1c80645..348c04b 100644 --- a/app/(tabs)/coach.tsx +++ b/app/(tabs)/coach.tsx @@ -34,8 +34,8 @@ import { loadAiCoachSessionCache, saveAiCoachSessionCache } from '@/services/aiC import { api, getAuthToken, postTextStream } from '@/services/api'; import { selectLatestMoodRecordByDate } from '@/store/moodSlice'; import { generateWelcomeMessage, hasRecordedMoodToday } from '@/utils/welcomeMessage'; -import dayjs from 'dayjs'; import { LinearGradient } from 'expo-linear-gradient'; +import { HistoryModal } from '../../components/model/HistoryModal'; import { ActionSheet } from '../../components/ui/ActionSheet'; type Role = 'user' | 'assistant'; @@ -2099,52 +2099,16 @@ export default function CoachScreen() { )} - setHistoryVisible(false)}> - setHistoryVisible(false)}> - - - 历史会话 - refreshHistory(historyPage)} style={styles.modalRefreshBtn}> - - - - {historyLoading ? ( - - - 加载中... - - ) : ( - - {historyItems.length === 0 ? ( - 暂无会话 - ) : ( - historyItems.map((it) => ( - - handleSelectConversation(it.conversationId)} - > - {it.title || '未命名会话'} - - {dayjs(it.lastMessageAt || it.createdAt).format('YYYY/MM/DD HH:mm')} - - - confirmDeleteConversation(it.conversationId)} style={styles.historyDeleteBtn}> - - - - )) - )} - - )} - - setHistoryVisible(false)} style={styles.modalCloseBtn}> - 关闭 - - - - - + setHistoryVisible(false)} + historyLoading={historyLoading} + historyItems={historyItems} + historyPage={historyPage} + onRefreshHistory={refreshHistory} + onSelectConversation={handleSelectConversation} + onDeleteConversation={confirmDeleteConversation} + /> setPreviewImageUri(null)}> setPreviewImageUri(null)}> @@ -2495,74 +2459,6 @@ const styles = StyleSheet.create({ shadowRadius: 4, elevation: 2, }, - modalBackdrop: { - flex: 1, - backgroundColor: 'rgba(0,0,0,0.35)', - padding: 16, - justifyContent: 'flex-end', - }, - modalSheet: { - borderTopLeftRadius: 16, - borderTopRightRadius: 16, - paddingHorizontal: 12, - paddingTop: 10, - paddingBottom: 12, - }, - modalHeader: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - paddingHorizontal: 4, - paddingBottom: 8, - }, - modalTitle: { - fontSize: 16, - fontWeight: '800', - color: '#192126', - }, - modalRefreshBtn: { - width: 28, - height: 28, - borderRadius: 14, - alignItems: 'center', - justifyContent: 'center', - backgroundColor: 'rgba(0,0,0,0.06)' - }, - historyRow: { - flexDirection: 'row', - alignItems: 'center', - paddingVertical: 10, - paddingHorizontal: 8, - borderRadius: 10, - }, - historyTitle: { - fontSize: 15, - color: '#192126', - fontWeight: '600', - }, - historyMeta: { - marginTop: 2, - fontSize: 12, - color: '#687076', - }, - historyDeleteBtn: { - width: 28, - height: 28, - borderRadius: 14, - alignItems: 'center', - justifyContent: 'center', - backgroundColor: 'rgba(255,68,68,0.08)' - }, - modalFooter: { - paddingTop: 8, - alignItems: 'flex-end', - }, - modalCloseBtn: { - paddingHorizontal: 14, - paddingVertical: 8, - borderRadius: 10, - backgroundColor: 'rgba(0,0,0,0.06)' - }, previewBackdrop: { flex: 1, backgroundColor: 'rgba(0,0,0,0.85)', diff --git a/app/nutrition/records.tsx b/app/nutrition/records.tsx index 337118f..143ac15 100644 --- a/app/nutrition/records.tsx +++ b/app/nutrition/records.tsx @@ -1,6 +1,7 @@ import { NutritionRecordCard } from '@/components/NutritionRecordCard'; import { HeaderBar } from '@/components/ui/HeaderBar'; import { Colors } from '@/constants/Colors'; +import { DateSelector } from '@/components/DateSelector'; import { useColorScheme } from '@/hooks/useColorScheme'; import { DietRecord, deleteDietRecord, getDietRecords } from '@/services/dietRecords'; import { getMonthDaysZh, getMonthTitleZh, getTodayIndexInMonth } from '@/utils/date'; @@ -190,73 +191,12 @@ export default function NutritionRecordsScreen() { if (viewMode !== 'daily') return null; return ( - - setScrollWidth(e.nativeEvent.layout.width)} - > - {days.map((day, index) => { - const isSelected = index === selectedIndex; - const isToday = day.isToday; - const isDisabled = day.date?.isAfter(dayjs(), 'day') ?? false; - - return ( - { - if (!isDisabled) { - setSelectedIndex(index); - } - }} - disabled={isDisabled} - > - - {day.date?.date() ?? ''} - - - {day.dayAbbr} - - - ); - })} - - + setSelectedIndex(index)} + showMonthTitle={false} + disableFutureDates={true} + /> ); }; diff --git a/components/NutritionRecordCard.tsx b/components/NutritionRecordCard.tsx index cb640bd..fd4c523 100644 --- a/components/NutritionRecordCard.tsx +++ b/components/NutritionRecordCard.tsx @@ -1,11 +1,11 @@ import { ThemedText } from '@/components/ThemedText'; -import { ActionSheet } from '@/components/ui/ActionSheet'; import { useThemeColor } from '@/hooks/useThemeColor'; import { DietRecord } from '@/services/dietRecords'; import { Ionicons } from '@expo/vector-icons'; import dayjs from 'dayjs'; -import React, { useMemo, useState } from 'react'; +import React, { useMemo, useState, useRef } from 'react'; import { Image, StyleSheet, TouchableOpacity, View } from 'react-native'; +import Popover from 'react-native-popover-view'; export type NutritionRecordCardProps = { record: DietRecord; @@ -53,8 +53,9 @@ export function NutritionRecordCard({ const textSecondaryColor = useThemeColor({}, 'textSecondary'); const primaryColor = useThemeColor({}, 'primary'); - // ActionSheet 状态管理 - const [showActionSheet, setShowActionSheet] = useState(false); + // Popover 状态管理 + const [showPopover, setShowPopover] = useState(false); + const popoverRef = useRef(null); // 营养数据统计 const nutritionStats = useMemo(() => { @@ -112,31 +113,32 @@ export function NutritionRecordCard({ )} {/* 卡片内容 */} - {/* 主要内容区域 */} - {/* 餐次和操作按钮 */} - + {!showTimeline && ( {record.mealTime ? dayjs(record.mealTime).format('HH:mm') : '时间未设置'} )} - setShowActionSheet(true)} + onPress={() => { + setShowPopover(true) + console.log('showPopover', showPopover) + }} > @@ -144,14 +146,14 @@ export function NutritionRecordCard({ {/* 食物名称和分量 */} - {/* 左侧:食物图片 */} - - {record.imageUrl ? ( - - ) : ( - - )} - + {/* 左侧:食物图片 */} + + {record.imageUrl ? ( + + ) : ( + + )} + {record.foodName} @@ -161,11 +163,11 @@ export function NutritionRecordCard({ {record.portionDescription || `${record.weightGrams}g`} )} - - - {mealTypeLabel} - - + + + {mealTypeLabel} + + {/* 营养信息 - 紧凑标签布局 */} @@ -193,26 +195,31 @@ export function NutritionRecordCard({ )} - + - {/* ActionSheet for more options */} - setShowActionSheet(false)} - title="选择操作" - options={[ - { - id: 'delete', - title: '删除记录', - subtitle: '删除后无法恢复', - icon: 'trash-outline', - destructive: true, - onPress: () => { + {/* Popover for more options */} + setShowPopover(false)} + popoverStyle={styles.popoverContainer} + backgroundStyle={styles.popoverBackground} + > + + { + setShowPopover(false); onDelete?.(); - } - } - ]} - /> + }} + > + + + 删除记录 + + + + ); } @@ -300,6 +307,7 @@ const styles = StyleSheet.create({ position: 'absolute', top: 0, right: 0, + zIndex: 1, }, mealTypeContainer: { flex: 1, @@ -320,6 +328,7 @@ const styles = StyleSheet.create({ moreButton: { padding: 4, marginLeft: 8, + zIndex: 1, }, foodNameSection: { marginBottom: 12, @@ -379,4 +388,30 @@ const styles = StyleSheet.create({ lineHeight: 18, fontStyle: 'italic', }, + popoverContainer: { + borderRadius: 12, + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 8, + elevation: 5, + }, + popoverBackground: { + backgroundColor: 'rgba(0, 0, 0, 0.3)', + }, + popoverContent: { + minWidth: 140, + paddingVertical: 8, + }, + popoverItem: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: 16, + paddingVertical: 12, + gap: 12, + }, + popoverText: { + fontSize: 16, + fontWeight: '500', + }, }); diff --git a/components/model/HistoryModal.tsx b/components/model/HistoryModal.tsx new file mode 100644 index 0000000..59ac333 --- /dev/null +++ b/components/model/HistoryModal.tsx @@ -0,0 +1,170 @@ +import { Ionicons } from '@expo/vector-icons'; +import React from 'react'; +import { + ActivityIndicator, + Modal, + ScrollView, + StyleSheet, + Text, + TouchableOpacity, + View, +} from 'react-native'; +import dayjs from 'dayjs'; + +import { AiConversationListItem } from '@/services/aiCoach'; + +interface HistoryModalProps { + visible: boolean; + onClose: () => void; + historyLoading: boolean; + historyItems: AiConversationListItem[]; + historyPage: number; + onRefreshHistory: (page: number) => void; + onSelectConversation: (id: string) => void; + onDeleteConversation: (id: string) => void; +} + +export const HistoryModal: React.FC = ({ + visible, + onClose, + historyLoading, + historyItems, + historyPage, + onRefreshHistory, + onSelectConversation, + onDeleteConversation, +}) => { + return ( + + + + + 历史会话 + onRefreshHistory(historyPage)} + style={styles.modalRefreshBtn} + > + + + + {historyLoading ? ( + + + 加载中... + + ) : ( + + {historyItems.length === 0 ? ( + 暂无会话 + ) : ( + historyItems.map((it) => ( + + onSelectConversation(it.conversationId)} + > + {it.title || '未命名会话'} + + {dayjs(it.lastMessageAt || it.createdAt).format('YYYY/MM/DD HH:mm')} + + + onDeleteConversation(it.conversationId)} + style={styles.historyDeleteBtn} + > + + + + )) + )} + + )} + + + 关闭 + + + + + + ); +}; + +const styles = StyleSheet.create({ + modalBackdrop: { + flex: 1, + backgroundColor: 'rgba(0,0,0,0.35)', + padding: 16, + justifyContent: 'flex-end', + }, + modalSheet: { + borderTopLeftRadius: 16, + borderTopRightRadius: 16, + paddingHorizontal: 12, + paddingTop: 10, + paddingBottom: 12, + }, + modalHeader: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: 4, + paddingBottom: 8, + }, + modalTitle: { + fontSize: 16, + fontWeight: '800', + color: '#192126', + }, + modalRefreshBtn: { + width: 28, + height: 28, + borderRadius: 14, + alignItems: 'center', + justifyContent: 'center', + backgroundColor: 'rgba(0,0,0,0.06)' + }, + historyRow: { + flexDirection: 'row', + alignItems: 'center', + paddingVertical: 10, + paddingHorizontal: 8, + borderRadius: 10, + }, + historyTitle: { + fontSize: 15, + color: '#192126', + fontWeight: '600', + }, + historyMeta: { + marginTop: 2, + fontSize: 12, + color: '#687076', + }, + historyDeleteBtn: { + width: 28, + height: 28, + borderRadius: 14, + alignItems: 'center', + justifyContent: 'center', + backgroundColor: 'rgba(255,68,68,0.08)' + }, + modalFooter: { + paddingTop: 8, + alignItems: 'flex-end', + }, + modalCloseBtn: { + paddingHorizontal: 14, + paddingVertical: 8, + borderRadius: 10, + backgroundColor: 'rgba(0,0,0,0.06)' + }, +}); + +export default HistoryModal; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index a716122..ccc83f8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -49,6 +49,7 @@ "react-native-image-viewing": "^0.2.2", "react-native-markdown-display": "^7.0.2", "react-native-modal-datetime-picker": "^18.0.0", + "react-native-popover-view": "^6.1.0", "react-native-purchases": "^9.2.2", "react-native-reanimated": "~3.17.4", "react-native-render-html": "^6.3.4", @@ -6114,6 +6115,17 @@ "node": ">= 0.8" } }, + "node_modules/deprecated-react-native-prop-types": { + "version": "2.3.0", + "resolved": "https://mirrors.tencent.com/npm/deprecated-react-native-prop-types/-/deprecated-react-native-prop-types-2.3.0.tgz", + "integrity": "sha512-pWD0voFtNYxrVqvBMYf5gq3NA2GCpfodS1yNynTPc93AYA/KEMGeWDqqeUB6R2Z9ZofVhks2aeJXiuQqKNpesA==", + "license": "MIT", + "dependencies": { + "@react-native/normalize-color": "*", + "invariant": "*", + "prop-types": "*" + } + }, "node_modules/destroy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", @@ -11620,6 +11632,16 @@ "react-native": ">=0.65.0" } }, + "node_modules/react-native-popover-view": { + "version": "6.1.0", + "resolved": "https://mirrors.tencent.com/npm/react-native-popover-view/-/react-native-popover-view-6.1.0.tgz", + "integrity": "sha512-j1CB+yPwTKlBvIJBNb1AwiHyF/r+W5+AJIbHk79GRa+0z6PVtW4C7NWJWPqUVkCMOcJtewl6Pr6f2dc/87cVyQ==", + "license": "MIT", + "dependencies": { + "deprecated-react-native-prop-types": "^2.3.0", + "prop-types": "^15.8.1" + } + }, "node_modules/react-native-purchases": { "version": "9.2.2", "resolved": "https://mirrors.tencent.com/npm/react-native-purchases/-/react-native-purchases-9.2.2.tgz", diff --git a/package.json b/package.json index a04d3cb..79ff08f 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "react-native-image-viewing": "^0.2.2", "react-native-markdown-display": "^7.0.2", "react-native-modal-datetime-picker": "^18.0.0", + "react-native-popover-view": "^6.1.0", "react-native-purchases": "^9.2.2", "react-native-reanimated": "~3.17.4", "react-native-render-html": "^6.3.4",