u
This commit is contained in:
1 parent
1ccae92d0f
commit
52a20dfa22
9 files changed
+131
-286
No files matched your search
@@ -55,6 +55,7 @@
|
|||||||
"react-native-sortables": "^1.9.3",
|
"react-native-sortables": "^1.9.3",
|
||||||
"react-native-svg": "15.12.1",
|
"react-native-svg": "15.12.1",
|
||||||
"react-native-web": "^0.21.0",
|
"react-native-web": "^0.21.0",
|
||||||
|
"react-native-worklets": "0.5.1",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
"zustand": "^5.0.8"
|
"zustand": "^5.0.8"
|
||||||
},
|
},
|
||||||
|
|||||||
Generated
+11
@@ -143,6 +143,9 @@ importers:
|
|||||||
react-native-web:
|
react-native-web:
|
||||||
specifier: ^0.21.0
|
specifier: ^0.21.0
|
||||||
version: 0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
version: 0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
|
react-native-worklets:
|
||||||
|
specifier: 0.5.1
|
||||||
|
version: 0.5.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
|
||||||
tailwind-merge:
|
tailwind-merge:
|
||||||
specifier: ^3.4.0
|
specifier: ^3.4.0
|
||||||
version: 3.4.0
|
version: 3.4.0
|
||||||
@@ -3964,48 +3967,56 @@ packages:
|
|||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
lightningcss-linux-arm64-gnu@1.30.2:
|
lightningcss-linux-arm64-gnu@1.30.2:
|
||||||
resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==}
|
resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
lightningcss-linux-arm64-musl@1.27.0:
|
lightningcss-linux-arm64-musl@1.27.0:
|
||||||
resolution: {integrity: sha512-rCGBm2ax7kQ9pBSeITfCW9XSVF69VX+fm5DIpvDZQl4NnQoMQyRwhZQm9pd59m8leZ1IesRqWk2v/DntMo26lg==}
|
resolution: {integrity: sha512-rCGBm2ax7kQ9pBSeITfCW9XSVF69VX+fm5DIpvDZQl4NnQoMQyRwhZQm9pd59m8leZ1IesRqWk2v/DntMo26lg==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
lightningcss-linux-arm64-musl@1.30.2:
|
lightningcss-linux-arm64-musl@1.30.2:
|
||||||
resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==}
|
resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
lightningcss-linux-x64-gnu@1.27.0:
|
lightningcss-linux-x64-gnu@1.27.0:
|
||||||
resolution: {integrity: sha512-Dk/jovSI7qqhJDiUibvaikNKI2x6kWPN79AQiD/E/KeQWMjdGe9kw51RAgoWFDi0coP4jinaH14Nrt/J8z3U4A==}
|
resolution: {integrity: sha512-Dk/jovSI7qqhJDiUibvaikNKI2x6kWPN79AQiD/E/KeQWMjdGe9kw51RAgoWFDi0coP4jinaH14Nrt/J8z3U4A==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
lightningcss-linux-x64-gnu@1.30.2:
|
lightningcss-linux-x64-gnu@1.30.2:
|
||||||
resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==}
|
resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
lightningcss-linux-x64-musl@1.27.0:
|
lightningcss-linux-x64-musl@1.27.0:
|
||||||
resolution: {integrity: sha512-QKjTxXm8A9s6v9Tg3Fk0gscCQA1t/HMoF7Woy1u68wCk5kS4fR+q3vXa1p3++REW784cRAtkYKrPy6JKibrEZA==}
|
resolution: {integrity: sha512-QKjTxXm8A9s6v9Tg3Fk0gscCQA1t/HMoF7Woy1u68wCk5kS4fR+q3vXa1p3++REW784cRAtkYKrPy6JKibrEZA==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
lightningcss-linux-x64-musl@1.30.2:
|
lightningcss-linux-x64-musl@1.30.2:
|
||||||
resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==}
|
resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
lightningcss-win32-arm64-msvc@1.27.0:
|
lightningcss-win32-arm64-msvc@1.27.0:
|
||||||
resolution: {integrity: sha512-/wXegPS1hnhkeG4OXQKEMQeJd48RDC3qdh+OA8pCuOPCyvnm/yEayrJdJVqzBsqpy1aJklRCVxscpFur80o6iQ==}
|
resolution: {integrity: sha512-/wXegPS1hnhkeG4OXQKEMQeJd48RDC3qdh+OA8pCuOPCyvnm/yEayrJdJVqzBsqpy1aJklRCVxscpFur80o6iQ==}
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export default function Money() {
|
|||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
// 页面显示 / 返回时
|
// 页面显示 / 返回时
|
||||||
console.log('===shohw===')
|
console.log('===show===')
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
// 页面离开(可选)
|
// 页面离开(可选)
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useRouter } from 'expo-router'
|
import { useRouter } from 'expo-router'
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { Image, Text, View } from 'react-native'
|
import { Image, Text, View } from 'react-native'
|
||||||
import * as QQAPI from 'react-native-qq-lib'
|
|
||||||
import CustomHeader from '@/components/custom/Header'
|
import CustomHeader from '@/components/custom/Header'
|
||||||
import { Button, ButtonText } from '@/components/ui/button'
|
import { Button, ButtonText } from '@/components/ui/button'
|
||||||
|
|
||||||
@@ -12,10 +11,6 @@ export default function AuthIndex() {
|
|||||||
router.push(url)
|
router.push(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
QQAPI.login()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View className="flex-1">
|
<View className="flex-1">
|
||||||
<CustomHeader
|
<CustomHeader
|
||||||
|
|||||||
+100
-141
@@ -1,18 +1,10 @@
|
|||||||
import type { MoneyCategoryItem } from '@/types/money/category'
|
import type { MoneyCategoryEnum, MoneyCategoryItem } from '@/types/money/category'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { Pressable, Text, View } from 'react-native'
|
import { Pressable, Text, View } from 'react-native'
|
||||||
|
import { Drawer } from 'react-native-drawer-layout'
|
||||||
import { AButton, AInput } from '@/components/common/ao'
|
import { AButton, AInput } from '@/components/common/ao'
|
||||||
import MoneyCategoryIcon from '@/components/common/MoneyCategoryIcon'
|
import MoneyCategoryIcon from '@/components/common/MoneyCategoryIcon'
|
||||||
import {
|
|
||||||
Drawer,
|
|
||||||
DrawerBackdrop,
|
|
||||||
DrawerBody,
|
|
||||||
DrawerContent,
|
|
||||||
DrawerFooter,
|
|
||||||
DrawerHeader,
|
|
||||||
} from '@/components/ui/drawer'
|
|
||||||
import { useMoneyCategoryStore } from '@/store/moneyCategory'
|
import { useMoneyCategoryStore } from '@/store/moneyCategory'
|
||||||
import { MoneyCategoryEnum } from '@/types/money/category'
|
|
||||||
|
|
||||||
export interface EditCategoryForm {
|
export interface EditCategoryForm {
|
||||||
id?: number
|
id?: number
|
||||||
@@ -38,155 +30,122 @@ export default function CreateCategory({
|
|||||||
const moneyCategoryStore = useMoneyCategoryStore()
|
const moneyCategoryStore = useMoneyCategoryStore()
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = useState(open)
|
const [isOpen, setIsOpen] = useState(open)
|
||||||
|
const [form, setForm] = useState<EditCategoryForm>({ icon: '', name: '' })
|
||||||
const [form, setForm] = useState<EditCategoryForm>({
|
|
||||||
icon: '',
|
|
||||||
name: '',
|
|
||||||
})
|
|
||||||
|
|
||||||
const [confirmDisabled, setConfirmDisabled] = useState(true)
|
const [confirmDisabled, setConfirmDisabled] = useState(true)
|
||||||
const [confirmLoading, setConfirmLoading] = useState(false)
|
const [confirmLoading, setConfirmLoading] = useState(false)
|
||||||
|
|
||||||
|
const scrollRef = useRef<any>(null)
|
||||||
|
const iconPositions = useRef<Record<string, number>>({}) // 记录每个 icon 的 y 坐标
|
||||||
|
|
||||||
|
// Drawer 打开/关闭控制
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsOpen(open)
|
setIsOpen(open)
|
||||||
if (!open) {
|
if (!open)
|
||||||
setForm({
|
setForm({ icon: '', name: '' })
|
||||||
icon: '',
|
|
||||||
name: '',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, [open])
|
}, [open])
|
||||||
|
|
||||||
|
// 确认按钮状态
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (form.name && form.name.length <= 5 && form.icon) {
|
setConfirmDisabled(!(form.name && form.name.length <= 5 && form.icon))
|
||||||
setConfirmDisabled(false)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
setConfirmDisabled(true)
|
|
||||||
}
|
|
||||||
}, [form.name, form.icon])
|
}, [form.name, form.icon])
|
||||||
|
|
||||||
|
// 编辑已有分类时填充数据
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (category !== null) {
|
if (category)
|
||||||
setForm(category)
|
setForm(category)
|
||||||
}
|
|
||||||
}, [category])
|
}, [category])
|
||||||
|
|
||||||
function handleClose() {
|
function handleClose() {
|
||||||
setForm({
|
setForm({ icon: '', name: '' })
|
||||||
icon: '',
|
|
||||||
name: '',
|
|
||||||
})
|
|
||||||
setIsOpen(false)
|
setIsOpen(false)
|
||||||
onClose && onClose()
|
onClose?.()
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer isOpen={isOpen} onClose={handleClose} size="lg" anchor="bottom">
|
<Drawer
|
||||||
<DrawerBackdrop />
|
open={isOpen}
|
||||||
<DrawerContent
|
onClose={handleClose}
|
||||||
style={{
|
drawerPosition="bottom"
|
||||||
paddingLeft: 0,
|
>
|
||||||
paddingRight: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DrawerHeader className="px-4 flex flex-col gap-4">
|
|
||||||
<View className="flex flex-row items-center justify-between w-full">
|
|
||||||
<AButton size="xs" variant="outline" onPress={handleClose}>取消</AButton>
|
|
||||||
|
|
||||||
<Text className="text-lg font-medium">{type === MoneyCategoryEnum.Expense ? '添加支出分类' : '添加收入分类'}</Text>
|
|
||||||
|
|
||||||
<AButton
|
|
||||||
size="xs"
|
|
||||||
disabled={confirmDisabled}
|
|
||||||
loading={confirmLoading}
|
|
||||||
onPress={async () => {
|
|
||||||
if (!onConfirm) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
setConfirmLoading(true)
|
|
||||||
|
|
||||||
const trimmedForm = {
|
|
||||||
...form,
|
|
||||||
name: form.name.trim(),
|
|
||||||
}
|
|
||||||
|
|
||||||
await onConfirm(trimmedForm)
|
|
||||||
}
|
|
||||||
catch (err) {
|
|
||||||
console.error(err)
|
|
||||||
// 外部报错时,保持弹窗打开,loading 结束
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
setConfirmLoading(false)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
确认
|
|
||||||
</AButton>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<AInput
|
|
||||||
value={form.name}
|
|
||||||
placeholder="请输入名称(不超过5个字符)"
|
|
||||||
onChangeText={(text) => {
|
|
||||||
setForm((val) => {
|
|
||||||
return {
|
|
||||||
...val,
|
|
||||||
name: text,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</DrawerHeader>
|
|
||||||
<DrawerBody className="mb-16">
|
|
||||||
{Object.entries(moneyCategoryStore.ICON_GROUPS).map(([groupName, icons]) => (
|
|
||||||
<View key={groupName} className="mb-2">
|
|
||||||
<Text className="text-center text-sm mb-4">
|
|
||||||
{groupName}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<View className="flex flex-row flex-wrap">
|
|
||||||
{icons.map(icon => (
|
|
||||||
<Pressable
|
|
||||||
key={icon}
|
|
||||||
className="w-1/5 mb-3 flex items-center"
|
|
||||||
onPress={() => {
|
|
||||||
setForm(val => ({
|
|
||||||
...val,
|
|
||||||
icon,
|
|
||||||
}))
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
width: 46,
|
|
||||||
height: 46,
|
|
||||||
borderRadius: 28,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
backgroundColor: icon === form.icon ? '#333' : '#F3F4F6',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<MoneyCategoryIcon
|
|
||||||
name={icon}
|
|
||||||
size={32}
|
|
||||||
color={
|
|
||||||
icon === form.icon ? '#fff' : '#000'
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</Pressable>
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
))}
|
|
||||||
</DrawerBody>
|
|
||||||
|
|
||||||
<DrawerFooter />
|
|
||||||
</DrawerContent>
|
|
||||||
</Drawer>
|
</Drawer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// <Drawer isOpen={isOpen} onClose={handleClose} size="lg" anchor="bottom">
|
||||||
|
// <DrawerBackdrop />
|
||||||
|
// <DrawerContent style={{ paddingLeft: 0, paddingRight: 0 }}>
|
||||||
|
// <DrawerHeader className="px-4 flex flex-col gap-4">
|
||||||
|
// <View className="flex flex-row items-center justify-between w-full">
|
||||||
|
// <AButton size="xs" variant="outline" onPress={handleClose}>
|
||||||
|
// 取消
|
||||||
|
// </AButton>
|
||||||
|
// <Text className="text-lg font-medium">
|
||||||
|
// {type === MoneyCategoryEnum.Expense ? '添加支出分类' : '添加收入分类'}
|
||||||
|
// </Text>
|
||||||
|
// <AButton
|
||||||
|
// size="xs"
|
||||||
|
// disabled={confirmDisabled}
|
||||||
|
// loading={confirmLoading}
|
||||||
|
// onPress={async () => {
|
||||||
|
// if (!onConfirm)
|
||||||
|
// return
|
||||||
|
// try {
|
||||||
|
// setConfirmLoading(true)
|
||||||
|
// await onConfirm({ ...form, name: form.name.trim() })
|
||||||
|
// }
|
||||||
|
// finally {
|
||||||
|
// setConfirmLoading(false)
|
||||||
|
// }
|
||||||
|
// }}
|
||||||
|
// >
|
||||||
|
// 确认
|
||||||
|
// </AButton>
|
||||||
|
// </View>
|
||||||
|
// <AInput
|
||||||
|
// value={form.name}
|
||||||
|
// placeholder="请输入名称(不超过5个字符)"
|
||||||
|
// onChangeText={text => setForm(val => ({ ...val, name: text }))}
|
||||||
|
// />
|
||||||
|
// </DrawerHeader>
|
||||||
|
|
||||||
|
// <DrawerBody ref={scrollRef} className="mb-16">
|
||||||
|
// {Object.entries(moneyCategoryStore.ICON_GROUPS).map(([groupName, icons]) => (
|
||||||
|
// <View key={groupName} className="mb-4">
|
||||||
|
// <Text className="text-center text-sm mb-3">{groupName}</Text>
|
||||||
|
// <View className="flex flex-row flex-wrap">
|
||||||
|
// {icons.map(icon => (
|
||||||
|
// <Pressable
|
||||||
|
// key={icon}
|
||||||
|
// className="w-1/5 mb-3 flex items-center"
|
||||||
|
// onPress={() => setForm(val => ({ ...val, icon }))}
|
||||||
|
// >
|
||||||
|
// <View
|
||||||
|
// onLayout={(e) => {
|
||||||
|
// console.log(e.nativeEvent.layout)
|
||||||
|
// iconPositions.current[icon] = e.nativeEvent.layout.y
|
||||||
|
// }}
|
||||||
|
// style={{
|
||||||
|
// width: 46,
|
||||||
|
// height: 46,
|
||||||
|
// borderRadius: 28,
|
||||||
|
// justifyContent: 'center',
|
||||||
|
// alignItems: 'center',
|
||||||
|
// backgroundColor: icon === form.icon ? '#333' : '#F3F4F6',
|
||||||
|
// }}
|
||||||
|
// >
|
||||||
|
// <MoneyCategoryIcon
|
||||||
|
// name={icon}
|
||||||
|
// size={32}
|
||||||
|
// color={icon === form.icon ? '#fff' : '#000'}
|
||||||
|
// />
|
||||||
|
// </View>
|
||||||
|
// </Pressable>
|
||||||
|
// ))}
|
||||||
|
// </View>
|
||||||
|
// </View>
|
||||||
|
// ))}
|
||||||
|
// </DrawerBody>
|
||||||
|
|
||||||
|
// <DrawerFooter />
|
||||||
|
// </DrawerContent>
|
||||||
|
// </Drawer>
|
||||||
@@ -54,10 +54,11 @@ export default function MoneyCategorySetting() {
|
|||||||
>
|
>
|
||||||
<MoneyCategoryIcon size={28} name={item.icon} />
|
<MoneyCategoryIcon size={28} name={item.icon} />
|
||||||
</View>
|
</View>
|
||||||
<Text style={{
|
<Text
|
||||||
marginBottom: 4,
|
style={{
|
||||||
fontSize: 14,
|
marginTop: 4,
|
||||||
}}
|
fontSize: 14,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{item.name}
|
{item.name}
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -1,111 +0,0 @@
|
|||||||
import type { ViewStyle } from 'react-native'
|
|
||||||
import type { MoneyCategoryItem } from '@/types/money/category'
|
|
||||||
import React, { useEffect, useMemo, useRef } from 'react'
|
|
||||||
import { FlatList, Pressable, Text, View } from 'react-native'
|
|
||||||
import MoneyCategoryIcon from '@/components/common/MoneyCategoryIcon'
|
|
||||||
|
|
||||||
interface CategoryGridProps {
|
|
||||||
data: MoneyCategoryItem[]
|
|
||||||
selectedCategory?: MoneyCategoryItem | null
|
|
||||||
onSelectCategory?: (item: MoneyCategoryItem | null) => void
|
|
||||||
|
|
||||||
className?: string
|
|
||||||
style?: ViewStyle
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function CategoryGrid({
|
|
||||||
data,
|
|
||||||
selectedCategory,
|
|
||||||
onSelectCategory,
|
|
||||||
className,
|
|
||||||
style,
|
|
||||||
}: CategoryGridProps) {
|
|
||||||
const flatListRef = useRef<FlatList<any>>(null)
|
|
||||||
|
|
||||||
// 每行 4 个,补齐空项
|
|
||||||
const memoItems = useMemo(() => {
|
|
||||||
const items = [...data]
|
|
||||||
const remainder = items.length % 4
|
|
||||||
if (remainder > 0) {
|
|
||||||
const emptyCount = 4 - remainder
|
|
||||||
for (let i = 0; i < emptyCount; i++) {
|
|
||||||
items.push({ id: `empty-${i}` } as any)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return items
|
|
||||||
}, [data])
|
|
||||||
|
|
||||||
// 自动滚动到选中行
|
|
||||||
useEffect(() => {
|
|
||||||
if (!selectedCategory || !flatListRef.current)
|
|
||||||
return
|
|
||||||
|
|
||||||
// 找到选中项下标
|
|
||||||
const itemIndex = data.findIndex(item => item.id === selectedCategory.id)
|
|
||||||
if (itemIndex === -1 || !flatListRef.current)
|
|
||||||
return
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
if (flatListRef.current) {
|
|
||||||
flatListRef.current.scrollToIndex({
|
|
||||||
index: Math.floor(itemIndex / 4),
|
|
||||||
animated: true,
|
|
||||||
viewPosition: 0.5,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, 0)
|
|
||||||
}, [selectedCategory])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FlatList
|
|
||||||
ref={flatListRef}
|
|
||||||
data={memoItems}
|
|
||||||
numColumns={4}
|
|
||||||
keyExtractor={item => item.id.toString()}
|
|
||||||
showsVerticalScrollIndicator={false}
|
|
||||||
className={`${className}`}
|
|
||||||
style={[style]}
|
|
||||||
getItemLayout={(data, index) => {
|
|
||||||
return { length: 80, offset: 80 * index, index }
|
|
||||||
}}
|
|
||||||
renderItem={({ item }) => {
|
|
||||||
if (!('name' in item))
|
|
||||||
return <View style={{ flex: 1 / 4, margin: 4 }} />
|
|
||||||
|
|
||||||
const isSelected = selectedCategory?.id === item.id
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Pressable
|
|
||||||
style={{ flex: 1 / 4, height: 80, alignItems: 'center' }}
|
|
||||||
onPress={() => onSelectCategory?.(item)}
|
|
||||||
onLayout={(event) => {
|
|
||||||
const { height } = event.nativeEvent.layout
|
|
||||||
console.log('Pressable 高度:', height)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
width: 46,
|
|
||||||
height: 46,
|
|
||||||
borderRadius: 28,
|
|
||||||
marginTop: 4,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
backgroundColor: isSelected ? '#333' : '#F3F4F6',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<MoneyCategoryIcon
|
|
||||||
name={item.icon}
|
|
||||||
size={28}
|
|
||||||
color={isSelected ? '#fff' : '#000'}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
<Text style={{ marginBottom: 4, fontSize: 14, textAlign: 'center' }}>
|
|
||||||
{item.name}
|
|
||||||
</Text>
|
|
||||||
</Pressable>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { ViewStyle } from 'react-native'
|
import type { ViewStyle } from 'react-native'
|
||||||
import type { MoneyCategoryItem } from '@/types/money/category'
|
import type { MoneyCategoryItem } from '@/types/money/category'
|
||||||
import React, { useEffect, useMemo, useRef } from 'react'
|
import React, { useEffect, useRef } from 'react'
|
||||||
import { FlatList, Pressable, Text, View } from 'react-native'
|
import { FlatList, Pressable, Text, View } from 'react-native'
|
||||||
import MoneyCategoryIcon from '@/components/common/MoneyCategoryIcon'
|
import MoneyCategoryIcon from '@/components/common/MoneyCategoryIcon'
|
||||||
|
|
||||||
@@ -20,27 +20,15 @@ export default function CategoryGrid({
|
|||||||
className,
|
className,
|
||||||
style,
|
style,
|
||||||
}: CategoryGridProps) {
|
}: CategoryGridProps) {
|
||||||
const flatListRef = useRef<FlatList<any>>(null)
|
const NUM_COLUMNS = 4
|
||||||
|
|
||||||
// 每行 4 个,补齐空项
|
const flatListRef = useRef<FlatList<any>>(null)
|
||||||
const memoItems = useMemo(() => {
|
|
||||||
const items = [...data]
|
|
||||||
const remainder = items.length % 4
|
|
||||||
if (remainder > 0) {
|
|
||||||
const emptyCount = 4 - remainder
|
|
||||||
for (let i = 0; i < emptyCount; i++) {
|
|
||||||
items.push({ id: `empty-${i}` } as any)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return items
|
|
||||||
}, [data])
|
|
||||||
|
|
||||||
// 自动滚动到选中行
|
// 自动滚动到选中行
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedCategory || !flatListRef.current)
|
if (!selectedCategory || !flatListRef.current)
|
||||||
return
|
return
|
||||||
|
|
||||||
// 找到选中项下标
|
|
||||||
const itemIndex = data.findIndex(item => item.id === selectedCategory.id)
|
const itemIndex = data.findIndex(item => item.id === selectedCategory.id)
|
||||||
if (itemIndex === -1 || !flatListRef.current)
|
if (itemIndex === -1 || !flatListRef.current)
|
||||||
return
|
return
|
||||||
@@ -59,8 +47,8 @@ export default function CategoryGrid({
|
|||||||
return (
|
return (
|
||||||
<FlatList
|
<FlatList
|
||||||
ref={flatListRef}
|
ref={flatListRef}
|
||||||
data={memoItems}
|
data={data}
|
||||||
numColumns={4}
|
numColumns={NUM_COLUMNS}
|
||||||
keyExtractor={item => item.id.toString()}
|
keyExtractor={item => item.id.toString()}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
className={`${className}`}
|
className={`${className}`}
|
||||||
@@ -69,14 +57,15 @@ export default function CategoryGrid({
|
|||||||
return { length: 80, offset: 80 * index, index }
|
return { length: 80, offset: 80 * index, index }
|
||||||
}}
|
}}
|
||||||
renderItem={({ item }) => {
|
renderItem={({ item }) => {
|
||||||
if (!('name' in item))
|
|
||||||
return <View style={{ flex: 1 / 4, margin: 4 }} />
|
|
||||||
|
|
||||||
const isSelected = selectedCategory?.id === item.id
|
const isSelected = selectedCategory?.id === item.id
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
style={{ flex: 1 / 4, height: 80, alignItems: 'center' }}
|
style={{
|
||||||
|
width: `${100 / NUM_COLUMNS}%`,
|
||||||
|
height: 80,
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
onPress={() => onSelectCategory?.(item)}
|
onPress={() => onSelectCategory?.(item)}
|
||||||
>
|
>
|
||||||
<View
|
<View
|
||||||
@@ -96,7 +85,7 @@ export default function CategoryGrid({
|
|||||||
color={isSelected ? '#fff' : '#000'}
|
color={isSelected ? '#fff' : '#000'}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<Text style={{ marginBottom: 4, fontSize: 14, textAlign: 'center' }}>
|
<Text style={{ marginTop: 4, fontSize: 14, textAlign: 'center' }}>
|
||||||
{item.name}
|
{item.name}
|
||||||
</Text>
|
</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
|
|||||||
@@ -32,8 +32,8 @@ function isWhiteListed(url: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const alovaInstance = createAlova({
|
const alovaInstance = createAlova({
|
||||||
baseURL: 'http://192.168.5.6:8086/app',
|
// baseURL: 'http://192.168.5.6:8086/app',
|
||||||
// baseURL: 'http://192.168.1.137:8086/app',
|
baseURL: 'http://192.168.1.137:8086/app',
|
||||||
requestAdapter: axiosRequestAdapter(),
|
requestAdapter: axiosRequestAdapter(),
|
||||||
timeout: 5000,
|
timeout: 5000,
|
||||||
cacheFor: null,
|
cacheFor: null,
|
||||||
|
|||||||
Reference in new issue
Block a user