react-phone-apps/apps/property-survey/app/settings.tsx
lanrtop eab08d8c96 feat: 新增物品分类管理与第四楼支持
- 新增「设置」Tab,支持物品分类的增删改(数据库持久化)
- 物品分类由硬编码枚举迁移为动态分类,通过 CategoriesContext 全局共享
- items 表新增 unit(单位)、spec(规格)字段,附带旧库迁移
- markers 表 CHECK 约束扩展支持第 4 楼(楼顶),附带重建表迁移
- SVG 地图渲染:overdraw 预渲染提升清晰度,嵌入图片改用原生 Image 渲染
- FoldableLayout 改为 showDetail prop 控制动画宽度

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 23:12:52 +09:00

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',
},
})