- 新增「设置」Tab,支持物品分类的增删改(数据库持久化) - 物品分类由硬编码枚举迁移为动态分类,通过 CategoriesContext 全局共享 - items 表新增 unit(单位)、spec(规格)字段,附带旧库迁移 - markers 表 CHECK 约束扩展支持第 4 楼(楼顶),附带重建表迁移 - SVG 地图渲染:overdraw 预渲染提升清晰度,嵌入图片改用原生 Image 渲染 - FoldableLayout 改为 showDetail prop 控制动画宽度 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
212 lines
5.7 KiB
TypeScript
212 lines
5.7 KiB
TypeScript
import { useCallback, useState } from 'react'
|
|
import {
|
|
Alert,
|
|
FlatList,
|
|
StyleSheet,
|
|
Text,
|
|
TextInput,
|
|
TouchableOpacity,
|
|
View,
|
|
} from 'react-native'
|
|
import { Ionicons } from '@expo/vector-icons'
|
|
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
|
import { deleteCategory, insertCategory, updateCategory, type Category } from '../src/db/categories'
|
|
import { useCategories } from '../src/features/settings/CategoriesContext'
|
|
|
|
export default function SettingsScreen() {
|
|
const insets = useSafeAreaInsets()
|
|
const { categories, reload } = useCategories()
|
|
const [newName, setNewName] = useState('')
|
|
const [editingId, setEditingId] = useState<number | null>(null)
|
|
const [editingName, setEditingName] = useState('')
|
|
|
|
const handleAdd = useCallback(async () => {
|
|
const name = newName.trim()
|
|
if (!name) return
|
|
try {
|
|
await insertCategory(name)
|
|
setNewName('')
|
|
await reload()
|
|
} catch (err) {
|
|
console.error('新增分类失败:', err)
|
|
}
|
|
}, [newName, reload])
|
|
|
|
function startEdit(cat: Category) {
|
|
setEditingId(cat.id)
|
|
setEditingName(cat.name)
|
|
}
|
|
|
|
const handleUpdate = useCallback(async () => {
|
|
if (editingId === null) return
|
|
const name = editingName.trim()
|
|
if (!name) return
|
|
try {
|
|
await updateCategory(editingId, name)
|
|
setEditingId(null)
|
|
await reload()
|
|
} catch (err) {
|
|
console.error('更新分类失败:', err)
|
|
}
|
|
}, [editingId, editingName, reload])
|
|
|
|
function handleDelete(cat: Category) {
|
|
Alert.alert(
|
|
'删除分类',
|
|
`确认删除「${cat.name}」?已登记物品的分类标签不受影响。`,
|
|
[
|
|
{ text: '取消', style: 'cancel' },
|
|
{
|
|
text: '删除',
|
|
style: 'destructive',
|
|
onPress: async () => {
|
|
try {
|
|
await deleteCategory(cat.id)
|
|
await reload()
|
|
} catch (err) {
|
|
console.error('删除分类失败:', err)
|
|
}
|
|
},
|
|
},
|
|
],
|
|
)
|
|
}
|
|
|
|
return (
|
|
<View style={[styles.container, { paddingBottom: insets.bottom }]}>
|
|
<Text style={styles.sectionTitle}>物品分类</Text>
|
|
|
|
<FlatList
|
|
data={categories}
|
|
keyExtractor={(item) => String(item.id)}
|
|
style={styles.list}
|
|
ItemSeparatorComponent={() => <View style={styles.separator} />}
|
|
renderItem={({ item }) =>
|
|
editingId === item.id ? (
|
|
<View style={styles.row}>
|
|
<TextInput
|
|
style={styles.editInput}
|
|
value={editingName}
|
|
onChangeText={setEditingName}
|
|
autoFocus
|
|
returnKeyType="done"
|
|
onSubmitEditing={handleUpdate}
|
|
/>
|
|
<TouchableOpacity onPress={handleUpdate} hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}>
|
|
<Ionicons name="checkmark" size={22} color="#10B981" />
|
|
</TouchableOpacity>
|
|
<TouchableOpacity onPress={() => setEditingId(null)} hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}>
|
|
<Ionicons name="close" size={22} color="#9CA3AF" />
|
|
</TouchableOpacity>
|
|
</View>
|
|
) : (
|
|
<View style={styles.row}>
|
|
<Text style={styles.rowName}>{item.name}</Text>
|
|
<TouchableOpacity onPress={() => startEdit(item)} hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}>
|
|
<Ionicons name="pencil-outline" size={18} color="#6B7280" />
|
|
</TouchableOpacity>
|
|
<TouchableOpacity onPress={() => handleDelete(item)} hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}>
|
|
<Ionicons name="trash-outline" size={18} color="#EF4444" />
|
|
</TouchableOpacity>
|
|
</View>
|
|
)
|
|
}
|
|
/>
|
|
|
|
{/* 新增行 */}
|
|
<View style={styles.addRow}>
|
|
<TextInput
|
|
style={styles.addInput}
|
|
value={newName}
|
|
onChangeText={setNewName}
|
|
placeholder="输入新分类名称"
|
|
placeholderTextColor="#9CA3AF"
|
|
returnKeyType="done"
|
|
onSubmitEditing={handleAdd}
|
|
/>
|
|
<TouchableOpacity style={styles.addBtn} onPress={handleAdd} activeOpacity={0.8}>
|
|
<Ionicons name="add" size={22} color="#fff" />
|
|
</TouchableOpacity>
|
|
</View>
|
|
</View>
|
|
)
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: { flex: 1, backgroundColor: '#F9FAFB' },
|
|
|
|
sectionTitle: {
|
|
fontSize: 12,
|
|
fontWeight: '600',
|
|
color: '#6B7280',
|
|
textTransform: 'uppercase',
|
|
letterSpacing: 0.5,
|
|
paddingHorizontal: 16,
|
|
paddingTop: 20,
|
|
paddingBottom: 8,
|
|
},
|
|
|
|
list: {
|
|
backgroundColor: '#fff',
|
|
borderTopWidth: StyleSheet.hairlineWidth,
|
|
borderTopColor: '#E5E7EB',
|
|
borderBottomWidth: StyleSheet.hairlineWidth,
|
|
borderBottomColor: '#E5E7EB',
|
|
},
|
|
|
|
separator: {
|
|
height: StyleSheet.hairlineWidth,
|
|
backgroundColor: '#E5E7EB',
|
|
marginLeft: 16,
|
|
},
|
|
|
|
row: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
paddingHorizontal: 16,
|
|
paddingVertical: 13,
|
|
gap: 12,
|
|
backgroundColor: '#fff',
|
|
},
|
|
|
|
rowName: { flex: 1, fontSize: 15, color: '#111827' },
|
|
|
|
editInput: {
|
|
flex: 1,
|
|
fontSize: 15,
|
|
color: '#111827',
|
|
borderBottomWidth: 1.5,
|
|
borderBottomColor: '#10B981',
|
|
paddingVertical: 2,
|
|
},
|
|
|
|
addRow: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: 10,
|
|
paddingHorizontal: 16,
|
|
paddingVertical: 16,
|
|
},
|
|
|
|
addInput: {
|
|
flex: 1,
|
|
height: 44,
|
|
borderWidth: 1,
|
|
borderColor: '#D1D5DB',
|
|
borderRadius: 10,
|
|
paddingHorizontal: 14,
|
|
fontSize: 15,
|
|
backgroundColor: '#fff',
|
|
color: '#111827',
|
|
},
|
|
|
|
addBtn: {
|
|
width: 44,
|
|
height: 44,
|
|
borderRadius: 10,
|
|
backgroundColor: '#10B981',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
})
|