feat(medications): 实现完整的用药管理功能
添加了药物管理的核心功能,包括: - 药物列表展示和状态管理 - 添加新药物的完整流程 - 服药记录的创建和状态更新 - 药物管理界面,支持激活/停用操作 - Redux状态管理和API服务层 - 相关类型定义和辅助函数 主要文件: - app/(tabs)/medications.tsx - 主界面,集成Redux数据 - app/medications/add-medication.tsx - 添加药物流程 - app/medications/manage-medications.tsx - 药物管理界面 - store/medicationsSlice.ts - Redux状态管理 - services/medications.ts - API服务层 - types/medication.ts - 类型定义
This commit is contained in:
@@ -1,17 +1,26 @@
|
||||
import { DateSelector } from '@/components/DateSelector';
|
||||
import { MedicationCard, type Medication, type MedicationStatus } from '@/components/medication/MedicationCard';
|
||||
import { MedicationCard } from '@/components/medication/MedicationCard';
|
||||
import { ThemedText } from '@/components/ThemedText';
|
||||
import { IconSymbol } from '@/components/ui/IconSymbol';
|
||||
import { Colors } from '@/constants/Colors';
|
||||
import { useAppSelector } from '@/hooks/redux';
|
||||
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||
import { fetchMedicationRecords, fetchMedications, selectMedicationDisplayItemsByDate, selectMedicationsLoading } from '@/store/medicationsSlice';
|
||||
import { DEFAULT_MEMBER_NAME } from '@/store/userSlice';
|
||||
import { useFocusEffect } from '@react-navigation/native';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import 'dayjs/locale/zh-cn';
|
||||
import { GlassView, isLiquidGlassAvailable } from 'expo-glass-effect';
|
||||
import { Image } from 'expo-image';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { ScrollView, StyleSheet, TouchableOpacity, View } from 'react-native';
|
||||
import { router } from 'expo-router';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
|
||||
dayjs.locale('zh-cn');
|
||||
@@ -20,133 +29,71 @@ type MedicationFilter = 'all' | 'taken' | 'missed';
|
||||
|
||||
type ThemeColors = (typeof Colors)[keyof typeof Colors];
|
||||
|
||||
const MEDICATION_IMAGES = {
|
||||
bottle: require('@/assets/images/icons/icon-healthy-diet.png'),
|
||||
drops: require('@/assets/images/icons/icon-remind.png'),
|
||||
vitamins: require('@/assets/images/icons/icon-blood-oxygen.png'),
|
||||
};
|
||||
|
||||
export default function MedicationsScreen() {
|
||||
const dispatch = useAppDispatch();
|
||||
const insets = useSafeAreaInsets();
|
||||
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
||||
const colors: ThemeColors = Colors[theme];
|
||||
const userProfile = useAppSelector((state) => state.user.profile);
|
||||
|
||||
const [selectedDate, setSelectedDate] = useState<Dayjs>(dayjs());
|
||||
const [selectedDateIndex, setSelectedDateIndex] = useState<number>(selectedDate.date() - 1);
|
||||
const [activeFilter, setActiveFilter] = useState<MedicationFilter>('all');
|
||||
|
||||
const scheduledMedications = useMemo(() => {
|
||||
const today = dayjs();
|
||||
const todayKey = today.format('YYYY-MM-DD');
|
||||
const yesterdayKey = today.subtract(1, 'day').format('YYYY-MM-DD');
|
||||
const twoDaysAgoKey = today.subtract(2, 'day').format('YYYY-MM-DD');
|
||||
// 从 Redux 获取数据
|
||||
const selectedKey = selectedDate.format('YYYY-MM-DD');
|
||||
const medicationsForDay = useAppSelector((state) => selectMedicationDisplayItemsByDate(selectedKey)(state));
|
||||
const loading = useAppSelector(selectMedicationsLoading);
|
||||
|
||||
return {
|
||||
[todayKey]: [
|
||||
{
|
||||
id: 'med-1',
|
||||
name: 'Metformin',
|
||||
dosage: '1 粒胶囊',
|
||||
scheduledTime: '09:00',
|
||||
frequency: '每日',
|
||||
status: 'upcoming' as MedicationStatus,
|
||||
image: MEDICATION_IMAGES.bottle,
|
||||
},
|
||||
{
|
||||
id: 'med-2',
|
||||
name: 'Captopril',
|
||||
dosage: '2 粒胶囊',
|
||||
scheduledTime: '20:00',
|
||||
frequency: '每日',
|
||||
status: 'upcoming' as MedicationStatus,
|
||||
image: MEDICATION_IMAGES.vitamins,
|
||||
},
|
||||
{
|
||||
id: 'med-3',
|
||||
name: 'B 12',
|
||||
dosage: '1 次注射',
|
||||
scheduledTime: '22:00',
|
||||
frequency: '每日',
|
||||
status: 'taken' as MedicationStatus,
|
||||
image: MEDICATION_IMAGES.vitamins,
|
||||
},
|
||||
{
|
||||
id: 'med-4',
|
||||
name: 'I-DROP MGD',
|
||||
dosage: '2 滴',
|
||||
scheduledTime: '22:00',
|
||||
frequency: '每日',
|
||||
status: 'missed' as MedicationStatus,
|
||||
image: MEDICATION_IMAGES.drops,
|
||||
},
|
||||
{
|
||||
id: 'med-5',
|
||||
name: 'Niacin',
|
||||
dosage: '0.5 片',
|
||||
scheduledTime: '22:00',
|
||||
frequency: '每日',
|
||||
status: 'missed' as MedicationStatus,
|
||||
image: MEDICATION_IMAGES.bottle,
|
||||
},
|
||||
],
|
||||
[yesterdayKey]: [
|
||||
{
|
||||
id: 'med-6',
|
||||
name: 'B 12',
|
||||
dosage: '1 次注射',
|
||||
scheduledTime: '22:00',
|
||||
frequency: '每日',
|
||||
status: 'taken' as MedicationStatus,
|
||||
image: MEDICATION_IMAGES.vitamins,
|
||||
},
|
||||
],
|
||||
[twoDaysAgoKey]: [
|
||||
{
|
||||
id: 'med-7',
|
||||
name: 'I-DROP MGD',
|
||||
dosage: '2 滴',
|
||||
scheduledTime: '22:00',
|
||||
frequency: '每日',
|
||||
status: 'missed' as MedicationStatus,
|
||||
image: MEDICATION_IMAGES.drops,
|
||||
},
|
||||
{
|
||||
id: 'med-8',
|
||||
name: 'Niacin',
|
||||
dosage: '0.5 片',
|
||||
scheduledTime: '22:00',
|
||||
frequency: '每日',
|
||||
status: 'missed' as MedicationStatus,
|
||||
image: MEDICATION_IMAGES.bottle,
|
||||
},
|
||||
],
|
||||
} as Record<string, Medication[]>;
|
||||
const handleOpenAddMedication = useCallback(() => {
|
||||
router.push('/medications/add-medication');
|
||||
}, []);
|
||||
|
||||
const handleOpenMedicationManagement = useCallback(() => {
|
||||
router.push('/medications/manage-medications');
|
||||
}, []);
|
||||
|
||||
// 加载药物和记录数据
|
||||
useEffect(() => {
|
||||
dispatch(fetchMedications());
|
||||
dispatch(fetchMedicationRecords({ date: selectedKey }));
|
||||
}, [dispatch, selectedKey]);
|
||||
|
||||
// 页面聚焦时刷新数据,确保从添加页面返回时能看到最新数据
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
dispatch(fetchMedications({ isActive: true }));
|
||||
dispatch(fetchMedicationRecords({ date: selectedKey }));
|
||||
}, [dispatch, selectedKey])
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setActiveFilter('all');
|
||||
}, [selectedDate]);
|
||||
|
||||
const selectedKey = selectedDate.format('YYYY-MM-DD');
|
||||
const medicationsForDay = scheduledMedications[selectedKey] ?? [];
|
||||
// 为每个药物添加默认图片(如果没有图片)
|
||||
const medicationsWithImages = useMemo(() => {
|
||||
return medicationsForDay.map((med: any) => ({
|
||||
...med,
|
||||
image: med.image || require('@/assets/images/icons/icon-healthy-diet.png'), // 默认使用瓶子图标
|
||||
}));
|
||||
}, [medicationsForDay]);
|
||||
|
||||
const filteredMedications = useMemo(() => {
|
||||
if (activeFilter === 'all') {
|
||||
return medicationsForDay;
|
||||
return medicationsWithImages;
|
||||
}
|
||||
return medicationsForDay.filter((item) => item.status === activeFilter);
|
||||
}, [activeFilter, medicationsForDay]);
|
||||
return medicationsWithImages.filter((item: any) => item.status === activeFilter);
|
||||
}, [activeFilter, medicationsWithImages]);
|
||||
|
||||
const counts = useMemo(() => {
|
||||
const taken = medicationsForDay.filter((item) => item.status === 'taken').length;
|
||||
const missed = medicationsForDay.filter((item) => item.status === 'missed').length;
|
||||
const taken = medicationsWithImages.filter((item: any) => item.status === 'taken').length;
|
||||
const missed = medicationsWithImages.filter((item: any) => item.status === 'missed').length;
|
||||
return {
|
||||
all: medicationsForDay.length,
|
||||
all: medicationsWithImages.length,
|
||||
taken,
|
||||
missed,
|
||||
};
|
||||
}, [medicationsForDay]);
|
||||
}, [medicationsWithImages]);
|
||||
|
||||
const displayName = userProfile.name?.trim() || DEFAULT_MEMBER_NAME;
|
||||
const headerDateLabel = selectedDate.isSame(dayjs(), 'day')
|
||||
@@ -183,6 +130,47 @@ export default function MedicationsScreen() {
|
||||
欢迎来到用药助手!
|
||||
</ThemedText>
|
||||
</View>
|
||||
<View style={styles.headerActions}>
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.7}
|
||||
onPress={handleOpenMedicationManagement}
|
||||
>
|
||||
{isLiquidGlassAvailable() ? (
|
||||
<GlassView
|
||||
style={styles.headerAddButton}
|
||||
glassEffectStyle="clear"
|
||||
tintColor="rgba(255, 255, 255, 0.3)"
|
||||
isInteractive={true}
|
||||
>
|
||||
<IconSymbol name="pills.fill" size={18} color="#333" />
|
||||
</GlassView>
|
||||
) : (
|
||||
<View style={[styles.headerAddButton, styles.fallbackAddButton]}>
|
||||
<IconSymbol name="pills.fill" size={18} color="#333" />
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.7}
|
||||
onPress={handleOpenAddMedication}
|
||||
>
|
||||
{isLiquidGlassAvailable() ? (
|
||||
<GlassView
|
||||
style={styles.headerAddButton}
|
||||
glassEffectStyle="clear"
|
||||
tintColor="rgba(255, 255, 255, 0.3)"
|
||||
isInteractive={true}
|
||||
>
|
||||
<IconSymbol name="plus" size={18} color="#333" />
|
||||
</GlassView>
|
||||
) : (
|
||||
<View style={[styles.headerAddButton, styles.fallbackAddButton]}>
|
||||
<IconSymbol name="plus" size={18} color="#333" />
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.sectionSpacing}>
|
||||
@@ -258,18 +246,10 @@ export default function MedicationsScreen() {
|
||||
<ThemedText style={[styles.emptySubtitle, { color: colors.textMuted }]}>
|
||||
还未添加任何用药计划,快来补充吧。
|
||||
</ThemedText>
|
||||
<TouchableOpacity
|
||||
style={[styles.primaryButton, { backgroundColor: colors.primary }]}
|
||||
>
|
||||
<IconSymbol name="plus" size={18} color={colors.onPrimary} />
|
||||
<ThemedText style={[styles.primaryButtonText, { color: colors.onPrimary }]}>
|
||||
添加用药
|
||||
</ThemedText>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
) : (
|
||||
<View style={styles.cardsWrapper}>
|
||||
{filteredMedications.map((item) => (
|
||||
{filteredMedications.map((item: any) => (
|
||||
<MedicationCard
|
||||
key={item.id}
|
||||
medication={item}
|
||||
@@ -322,7 +302,25 @@ const styles = StyleSheet.create({
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 16,
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
headerActions: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 10,
|
||||
},
|
||||
headerAddButton: {
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 16,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
fallbackAddButton: {
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(255, 255, 255, 0.3)',
|
||||
},
|
||||
avatar: {
|
||||
width: 60,
|
||||
@@ -420,4 +418,14 @@ const styles = StyleSheet.create({
|
||||
cardsWrapper: {
|
||||
gap: 16,
|
||||
},
|
||||
loadingContainer: {
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 24,
|
||||
paddingVertical: 48,
|
||||
borderRadius: 24,
|
||||
gap: 16,
|
||||
},
|
||||
loadingText: {
|
||||
fontSize: 14,
|
||||
},
|
||||
});
|
||||
|
||||
1437
app/medications/add-medication.tsx
Normal file
1437
app/medications/add-medication.tsx
Normal file
File diff suppressed because it is too large
Load Diff
401
app/medications/manage-medications.tsx
Normal file
401
app/medications/manage-medications.tsx
Normal file
@@ -0,0 +1,401 @@
|
||||
import { ThemedText } from '@/components/ThemedText';
|
||||
import { HeaderBar } from '@/components/ui/HeaderBar';
|
||||
import { IconSymbol } from '@/components/ui/IconSymbol';
|
||||
import { Colors } from '@/constants/Colors';
|
||||
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||
import { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding';
|
||||
import {
|
||||
fetchMedications,
|
||||
selectMedications,
|
||||
selectMedicationsLoading,
|
||||
updateMedicationAction,
|
||||
} from '@/store/medicationsSlice';
|
||||
import type { Medication, MedicationForm } from '@/types/medication';
|
||||
import { useFocusEffect } from '@react-navigation/native';
|
||||
import dayjs from 'dayjs';
|
||||
import { Image } from 'expo-image';
|
||||
import { router } from 'expo-router';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Alert,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Switch,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
|
||||
type FilterType = 'all' | 'active' | 'inactive';
|
||||
|
||||
const FORM_LABELS: Record<MedicationForm, string> = {
|
||||
capsule: '胶囊',
|
||||
pill: '药片',
|
||||
injection: '注射',
|
||||
spray: '喷雾',
|
||||
drop: '滴剂',
|
||||
syrup: '糖浆',
|
||||
other: '其他',
|
||||
};
|
||||
|
||||
const FILTER_CONFIG: Array<{ key: FilterType; label: string }> = [
|
||||
{ key: 'all', label: '全部' },
|
||||
{ key: 'active', label: '进行中' },
|
||||
{ key: 'inactive', label: '已停用' },
|
||||
];
|
||||
|
||||
const DEFAULT_IMAGE = require('@/assets/images/icons/icon-healthy-diet.png');
|
||||
|
||||
export default function ManageMedicationsScreen() {
|
||||
const dispatch = useAppDispatch();
|
||||
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
||||
const colors = Colors[theme];
|
||||
const safeAreaTop = useSafeAreaTop();
|
||||
const insets = useSafeAreaInsets();
|
||||
const medications = useAppSelector(selectMedications);
|
||||
const loading = useAppSelector(selectMedicationsLoading);
|
||||
const [activeFilter, setActiveFilter] = useState<FilterType>('all');
|
||||
const [pendingMedicationId, setPendingMedicationId] = useState<string | null>(null);
|
||||
|
||||
const updateLoading = loading.update;
|
||||
const listLoading = loading.medications && medications.length === 0;
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
dispatch(fetchMedications());
|
||||
}, [dispatch])
|
||||
);
|
||||
|
||||
// 优化:使用更精确的依赖项,只有当药品数量或激活状态改变时才重新计算
|
||||
const medicationsHash = useMemo(() => {
|
||||
return medications.map(m => `${m.id}-${m.isActive}`).join('|');
|
||||
}, [medications]);
|
||||
|
||||
const counts = useMemo<Record<FilterType, number>>(() => {
|
||||
const active = medications.filter((med) => med.isActive).length;
|
||||
const inactive = medications.length - active;
|
||||
return {
|
||||
all: medications.length,
|
||||
active,
|
||||
inactive,
|
||||
};
|
||||
}, [medicationsHash]);
|
||||
|
||||
const filteredMedications = useMemo(() => {
|
||||
switch (activeFilter) {
|
||||
case 'active':
|
||||
return medications.filter((med) => med.isActive);
|
||||
case 'inactive':
|
||||
return medications.filter((med) => !med.isActive);
|
||||
default:
|
||||
return medications;
|
||||
}
|
||||
}, [activeFilter, medicationsHash]);
|
||||
|
||||
const handleToggleMedication = useCallback(
|
||||
async (medication: Medication, nextValue: boolean) => {
|
||||
if (pendingMedicationId) return;
|
||||
try {
|
||||
setPendingMedicationId(medication.id);
|
||||
await dispatch(
|
||||
updateMedicationAction({
|
||||
id: medication.id,
|
||||
isActive: nextValue,
|
||||
})
|
||||
).unwrap();
|
||||
} catch (error) {
|
||||
console.error('更新药物状态失败', error);
|
||||
Alert.alert('操作失败', '切换药物状态时发生问题,请稍后重试。');
|
||||
} finally {
|
||||
setPendingMedicationId(null);
|
||||
}
|
||||
},
|
||||
[dispatch, pendingMedicationId]
|
||||
);
|
||||
|
||||
// 创建独立的药品卡片组件,使用 React.memo 优化渲染
|
||||
const MedicationCard = React.memo(({ medication }: { medication: Medication }) => {
|
||||
const dosageLabel = `${medication.dosageValue} ${medication.dosageUnit || ''} ${FORM_LABELS[medication.form] ?? ''}`.trim();
|
||||
const frequencyLabel = `${medication.repeatPattern === 'daily' ? '每日' : medication.repeatPattern === 'weekly' ? '每周' : '自定义'} | ${dosageLabel}`;
|
||||
const startDateLabel = dayjs(medication.startDate).isValid()
|
||||
? dayjs(medication.startDate).format('M月D日')
|
||||
: '未知日期';
|
||||
const reminderLabel = medication.medicationTimes?.length
|
||||
? medication.medicationTimes.join('、')
|
||||
: `${medication.timesPerDay} 次/日`;
|
||||
|
||||
return (
|
||||
<View style={[styles.card, { backgroundColor: colors.surface }]}>
|
||||
<View style={styles.cardInfo}>
|
||||
<Image
|
||||
source={medication.photoUrl ? { uri: medication.photoUrl } : DEFAULT_IMAGE}
|
||||
style={styles.cardImage}
|
||||
contentFit="cover"
|
||||
/>
|
||||
<View style={styles.cardTexts}>
|
||||
<ThemedText style={styles.cardTitle}>{medication.name}</ThemedText>
|
||||
<ThemedText style={[styles.cardMeta, { color: colors.textSecondary }]}>
|
||||
{frequencyLabel}
|
||||
</ThemedText>
|
||||
<ThemedText style={[styles.cardMeta, { color: colors.textMuted }]}>
|
||||
{`开始于 ${startDateLabel} | 提醒:${reminderLabel}`}
|
||||
</ThemedText>
|
||||
</View>
|
||||
</View>
|
||||
<Switch
|
||||
value={medication.isActive}
|
||||
onValueChange={(value) => handleToggleMedication(medication, value)}
|
||||
disabled={updateLoading || pendingMedicationId === medication.id}
|
||||
trackColor={{ false: '#D9D9D9', true: colors.primary }}
|
||||
thumbColor={medication.isActive ? '#fff' : '#fff'}
|
||||
ios_backgroundColor="#D9D9D9"
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}, (prevProps, nextProps) => {
|
||||
// 自定义比较函数,只有当药品的 isActive 状态或 ID 改变时才重新渲染
|
||||
return (
|
||||
prevProps.medication.id === nextProps.medication.id &&
|
||||
prevProps.medication.isActive === nextProps.medication.isActive &&
|
||||
prevProps.medication.name === nextProps.medication.name &&
|
||||
prevProps.medication.photoUrl === nextProps.medication.photoUrl
|
||||
);
|
||||
});
|
||||
|
||||
MedicationCard.displayName = 'MedicationCard';
|
||||
|
||||
const renderMedicationCard = useCallback(
|
||||
(medication: Medication) => {
|
||||
return <MedicationCard key={medication.id} medication={medication} />;
|
||||
},
|
||||
[handleToggleMedication, pendingMedicationId, updateLoading, colors]
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={[styles.container, { backgroundColor: colors.background }]}>
|
||||
<HeaderBar
|
||||
title="药品管理"
|
||||
onBack={() => router.back()}
|
||||
variant="minimal"
|
||||
transparent
|
||||
/>
|
||||
|
||||
<View style={{ paddingTop: safeAreaTop }} />
|
||||
|
||||
<ScrollView
|
||||
contentContainerStyle={[
|
||||
styles.content,
|
||||
{ paddingBottom: insets.bottom + 32 },
|
||||
]}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<View style={styles.pageHeader}>
|
||||
<View>
|
||||
<ThemedText style={styles.title}>我的用药</ThemedText>
|
||||
<ThemedText style={[styles.subtitle, { color: colors.textMuted }]}>
|
||||
管理所有药品的状态与提醒
|
||||
</ThemedText>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
style={[styles.addButton, { backgroundColor: colors.primary }]}
|
||||
activeOpacity={0.85}
|
||||
onPress={() => router.push('/medications/add-medication')}
|
||||
>
|
||||
<IconSymbol name="plus" size={20} color={colors.onPrimary} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<View style={[styles.segmented, { backgroundColor: colors.surface }]}>
|
||||
{FILTER_CONFIG.map((filter) => {
|
||||
const isActive = filter.key === activeFilter;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={filter.key}
|
||||
style={[
|
||||
styles.segmentButton,
|
||||
isActive && { backgroundColor: colors.primary },
|
||||
]}
|
||||
activeOpacity={0.85}
|
||||
onPress={() => setActiveFilter(filter.key)}
|
||||
>
|
||||
<ThemedText
|
||||
style={[
|
||||
styles.segmentLabel,
|
||||
{ color: isActive ? colors.onPrimary : colors.textSecondary },
|
||||
]}
|
||||
>
|
||||
{filter.label}
|
||||
</ThemedText>
|
||||
<View
|
||||
style={[
|
||||
styles.segmentBadge,
|
||||
{
|
||||
backgroundColor: isActive
|
||||
? colors.onPrimary
|
||||
: `${colors.primary}15`,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<ThemedText
|
||||
style={[
|
||||
styles.segmentBadgeLabel,
|
||||
{ color: isActive ? colors.primary : colors.textSecondary },
|
||||
]}
|
||||
>
|
||||
{counts[filter.key] ?? 0}
|
||||
</ThemedText>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
|
||||
{listLoading ? (
|
||||
<View style={[styles.loading, { backgroundColor: colors.surface }]}>
|
||||
<ActivityIndicator color={colors.primary} />
|
||||
<ThemedText style={styles.loadingText}>正在载入药品信息...</ThemedText>
|
||||
</View>
|
||||
) : filteredMedications.length === 0 ? (
|
||||
<View style={[styles.empty, { backgroundColor: colors.surface }]}>
|
||||
<Image source={DEFAULT_IMAGE} style={styles.emptyImage} contentFit="contain" />
|
||||
<ThemedText style={styles.emptyTitle}>暂无药品</ThemedText>
|
||||
<ThemedText style={[styles.emptySubtitle, { color: colors.textSecondary }]}>
|
||||
还没有相关药品记录,点击右上角添加
|
||||
</ThemedText>
|
||||
</View>
|
||||
) : (
|
||||
<View style={styles.list}>{filteredMedications.map(renderMedicationCard)}</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
content: {
|
||||
paddingHorizontal: 20,
|
||||
gap: 20,
|
||||
},
|
||||
pageHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
},
|
||||
title: {
|
||||
fontSize: 26,
|
||||
fontWeight: '600',
|
||||
},
|
||||
subtitle: {
|
||||
marginTop: 6,
|
||||
fontSize: 14,
|
||||
},
|
||||
addButton: {
|
||||
width: 44,
|
||||
height: 44,
|
||||
borderRadius: 22,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
segmented: {
|
||||
flexDirection: 'row',
|
||||
padding: 6,
|
||||
borderRadius: 20,
|
||||
gap: 6,
|
||||
},
|
||||
segmentButton: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: 16,
|
||||
paddingVertical: 10,
|
||||
gap: 8,
|
||||
},
|
||||
segmentLabel: {
|
||||
fontSize: 15,
|
||||
fontWeight: '600',
|
||||
},
|
||||
segmentBadge: {
|
||||
minWidth: 28,
|
||||
paddingHorizontal: 8,
|
||||
height: 24,
|
||||
borderRadius: 12,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
segmentBadgeLabel: {
|
||||
fontSize: 12,
|
||||
fontWeight: '700',
|
||||
},
|
||||
list: {
|
||||
gap: 14,
|
||||
},
|
||||
card: {
|
||||
borderRadius: 22,
|
||||
padding: 14,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
shadowColor: '#000',
|
||||
shadowOpacity: 0.05,
|
||||
shadowRadius: 6,
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
elevation: 1,
|
||||
},
|
||||
cardInfo: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
flex: 1,
|
||||
},
|
||||
cardImage: {
|
||||
width: 52,
|
||||
height: 52,
|
||||
borderRadius: 16,
|
||||
backgroundColor: '#F2F2F2',
|
||||
},
|
||||
cardTexts: {
|
||||
flex: 1,
|
||||
gap: 4,
|
||||
},
|
||||
cardTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
},
|
||||
cardMeta: {
|
||||
fontSize: 13,
|
||||
},
|
||||
loading: {
|
||||
borderRadius: 22,
|
||||
paddingVertical: 32,
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
},
|
||||
loadingText: {
|
||||
fontSize: 14,
|
||||
},
|
||||
empty: {
|
||||
borderRadius: 22,
|
||||
paddingVertical: 32,
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
emptyImage: {
|
||||
width: 120,
|
||||
height: 120,
|
||||
},
|
||||
emptyTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
},
|
||||
emptySubtitle: {
|
||||
fontSize: 14,
|
||||
textAlign: 'center',
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user