- summary 新增明细/聚合/分组三 Tab,对应 SummaryAggregateList(楼层→分类) 和 SummaryGroupList(按物品名分组,可展开查看分布位置) - 新增 exportDataOnly() 仅导出 CSV 不含照片,header 新增两个导出按钮 - MarkerList 聚合 Tab 从 placeholder 升级为按颜色+分类分组的实际视图 - 蓝图新增 ai-recognize 模块(planned),引入 expo-image-manipulator / expo-secure-store 依赖为后续 AI 识别功能做准备 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
122 lines
4.2 KiB
TypeScript
122 lines
4.2 KiB
TypeScript
import { Alert, StyleSheet, Text, TouchableOpacity, View } from 'react-native'
|
|
import { Ionicons } from '@expo/vector-icons'
|
|
import { useSummaryData } from '../src/features/summary/useSummaryData'
|
|
import { FilterBar } from '../src/features/summary/FilterBar'
|
|
import { SummaryStats } from '../src/features/summary/SummaryStats'
|
|
import { SummaryList } from '../src/features/summary/SummaryList'
|
|
import { SummaryAggregateList } from '../src/features/summary/SummaryAggregateList'
|
|
import { SummaryGroupList } from '../src/features/summary/SummaryGroupList'
|
|
import { exportWithPhotos, exportDataOnly } from '../src/features/summary/exportData'
|
|
import type { FloorNumber } from '@repo/types'
|
|
import { Stack, useFocusEffect } from 'expo-router'
|
|
import { useCallback, useState } from 'react'
|
|
|
|
type ViewMode = 'detail' | 'aggregate' | 'group'
|
|
|
|
export default function SummaryScreen() {
|
|
const { rows, filter, setFilter, refresh } = useSummaryData()
|
|
const [viewMode, setViewMode] = useState<ViewMode>('detail')
|
|
|
|
useFocusEffect(useCallback(() => {
|
|
refresh()
|
|
}, []))
|
|
|
|
async function handleExportData() {
|
|
try {
|
|
await exportDataOnly(rows)
|
|
} catch (err) {
|
|
Alert.alert('导出失败', String(err))
|
|
}
|
|
}
|
|
|
|
async function handleExportWithPhotos() {
|
|
try {
|
|
await exportWithPhotos(rows)
|
|
} catch (err) {
|
|
Alert.alert('导出失败', String(err))
|
|
}
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<Stack.Screen
|
|
options={{
|
|
headerRight: () => (
|
|
<View style={styles.headerButtons}>
|
|
<TouchableOpacity onPress={handleExportData} style={styles.exportBtn}>
|
|
<Ionicons name="document-outline" size={22} color="#3B82F6" />
|
|
</TouchableOpacity>
|
|
<TouchableOpacity onPress={handleExportWithPhotos} style={styles.exportBtn}>
|
|
<Ionicons name="share-outline" size={22} color="#10B981" />
|
|
</TouchableOpacity>
|
|
</View>
|
|
),
|
|
}}
|
|
/>
|
|
<View style={styles.container}>
|
|
<SummaryStats rows={rows} />
|
|
<View style={styles.tabRow}>
|
|
<TouchableOpacity
|
|
style={[styles.tab, viewMode === 'detail' && styles.tabActive]}
|
|
onPress={() => setViewMode('detail')}
|
|
>
|
|
<Text style={[styles.tabText, viewMode === 'detail' && styles.tabTextActive]}>明细</Text>
|
|
</TouchableOpacity>
|
|
<TouchableOpacity
|
|
style={[styles.tab, viewMode === 'aggregate' && styles.tabActive]}
|
|
onPress={() => setViewMode('aggregate')}
|
|
>
|
|
<Text style={[styles.tabText, viewMode === 'aggregate' && styles.tabTextActive]}>聚合</Text>
|
|
</TouchableOpacity>
|
|
<TouchableOpacity
|
|
style={[styles.tab, viewMode === 'group' && styles.tabActive]}
|
|
onPress={() => setViewMode('group')}
|
|
>
|
|
<Text style={[styles.tabText, viewMode === 'group' && styles.tabTextActive]}>分组</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
<FilterBar
|
|
floor={filter.floor}
|
|
keyword={filter.keyword}
|
|
onFloorChange={(floor) =>
|
|
setFilter((f) => ({ ...f, floor: floor as FloorNumber | 0 }))
|
|
}
|
|
onKeywordChange={(keyword) => setFilter((f) => ({ ...f, keyword }))}
|
|
/>
|
|
{viewMode === 'detail' && <SummaryList rows={rows} />}
|
|
{viewMode === 'aggregate' && <SummaryAggregateList rows={rows} />}
|
|
{viewMode === 'group' && <SummaryGroupList rows={rows} />}
|
|
</View>
|
|
</>
|
|
)
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: { flex: 1, backgroundColor: '#fff' },
|
|
headerButtons: { flexDirection: 'row', alignItems: 'center' },
|
|
exportBtn: { paddingHorizontal: 8 },
|
|
tabRow: {
|
|
flexDirection: 'row',
|
|
paddingHorizontal: 12,
|
|
paddingVertical: 8,
|
|
gap: 8,
|
|
borderBottomWidth: StyleSheet.hairlineWidth,
|
|
borderBottomColor: '#E5E7EB',
|
|
backgroundColor: '#fff',
|
|
},
|
|
tab: {
|
|
paddingHorizontal: 18,
|
|
paddingVertical: 6,
|
|
borderRadius: 16,
|
|
borderWidth: 1,
|
|
borderColor: '#D1D5DB',
|
|
backgroundColor: '#F9FAFB',
|
|
},
|
|
tabActive: {
|
|
borderColor: '#10B981',
|
|
backgroundColor: '#ECFDF5',
|
|
},
|
|
tabText: { fontSize: 13, color: '#6B7280' },
|
|
tabTextActive: { color: '#065F46', fontWeight: '700' },
|
|
})
|