This commit is contained in:
oneao committed 2026-01-21 17:19:36 +08:00
1 parent 1ccae92d0f
commit 52a20dfa22
9 files changed
+131 -286

No files matched your search

+1
View File
@@ -55,6 +55,7 @@
"react-native-sortables": "^1.9.3",
"react-native-svg": "15.12.1",
"react-native-web": "^0.21.0",
"react-native-worklets": "0.5.1",
"tailwind-merge": "^3.4.0",
"zustand": "^5.0.8"
},
+11
View File
@@ -143,6 +143,9 @@ importers:
react-native-web:
specifier: ^0.21.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:
specifier: ^3.4.0
version: 3.4.0
@@ -3964,48 +3967,56 @@ packages:
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
lightningcss-linux-arm64-gnu@1.30.2:
resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
lightningcss-linux-arm64-musl@1.27.0:
resolution: {integrity: sha512-rCGBm2ax7kQ9pBSeITfCW9XSVF69VX+fm5DIpvDZQl4NnQoMQyRwhZQm9pd59m8leZ1IesRqWk2v/DntMo26lg==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
lightningcss-linux-arm64-musl@1.30.2:
resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
lightningcss-linux-x64-gnu@1.27.0:
resolution: {integrity: sha512-Dk/jovSI7qqhJDiUibvaikNKI2x6kWPN79AQiD/E/KeQWMjdGe9kw51RAgoWFDi0coP4jinaH14Nrt/J8z3U4A==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
lightningcss-linux-x64-gnu@1.30.2:
resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
lightningcss-linux-x64-musl@1.27.0:
resolution: {integrity: sha512-QKjTxXm8A9s6v9Tg3Fk0gscCQA1t/HMoF7Woy1u68wCk5kS4fR+q3vXa1p3++REW784cRAtkYKrPy6JKibrEZA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [musl]
lightningcss-linux-x64-musl@1.30.2:
resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [musl]
lightningcss-win32-arm64-msvc@1.27.0:
resolution: {integrity: sha512-/wXegPS1hnhkeG4OXQKEMQeJd48RDC3qdh+OA8pCuOPCyvnm/yEayrJdJVqzBsqpy1aJklRCVxscpFur80o6iQ==}
@@ -56,7 +56,7 @@ export default function Money() {
useFocusEffect(
useCallback(() => {
// 页面显示 / 返回时
console.log('===shohw===')
console.log('===show===')
return () => {
// 页面离开(可选)
@@ -1,7 +1,6 @@
import { useRouter } from 'expo-router'
import { useEffect } from 'react'
import { Image, Text, View } from 'react-native'
import * as QQAPI from 'react-native-qq-lib'
import CustomHeader from '@/components/custom/Header'
import { Button, ButtonText } from '@/components/ui/button'
@@ -12,10 +11,6 @@ export default function AuthIndex() {
router.push(url)
}
useEffect(() => {
QQAPI.login()
}, [])
return (
<View className="flex-1">
<CustomHeader
@@ -1,18 +1,10 @@
import type { MoneyCategoryItem } from '@/types/money/category'
import { useEffect, useState } from 'react'
import type { MoneyCategoryEnum, MoneyCategoryItem } from '@/types/money/category'
import { useEffect, useRef, useState } from 'react'
import { Pressable, Text, View } from 'react-native'
import { Drawer } from 'react-native-drawer-layout'
import { AButton, AInput } from '@/components/common/ao'
import MoneyCategoryIcon from '@/components/common/MoneyCategoryIcon'
import {
Drawer,
DrawerBackdrop,
DrawerBody,
DrawerContent,
DrawerFooter,
DrawerHeader,
} from '@/components/ui/drawer'
import { useMoneyCategoryStore } from '@/store/moneyCategory'
import { MoneyCategoryEnum } from '@/types/money/category'
export interface EditCategoryForm {
id?: number
@@ -38,155 +30,122 @@ export default function CreateCategory({
const moneyCategoryStore = useMoneyCategoryStore()
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 [confirmLoading, setConfirmLoading] = useState(false)
const scrollRef = useRef<any>(null)
const iconPositions = useRef<Record<string, number>>({}) // 记录每个 icon 的 y 坐标
// Drawer 打开/关闭控制
useEffect(() => {
setIsOpen(open)
if (!open) {
setForm({
icon: '',
name: '',
})
}
if (!open)
setForm({ icon: '', name: '' })
}, [open])
// 确认按钮状态
useEffect(() => {
if (form.name && form.name.length <= 5 && form.icon) {
setConfirmDisabled(false)
}
else {
setConfirmDisabled(true)
}
setConfirmDisabled(!(form.name && form.name.length <= 5 && form.icon))
}, [form.name, form.icon])
// 编辑已有分类时填充数据
useEffect(() => {
if (category !== null) {
if (category)
setForm(category)
}
}, [category])
function handleClose() {
setForm({
icon: '',
name: '',
})
setForm({ icon: '', name: '' })
setIsOpen(false)
onClose && onClose()
onClose?.()
}
return (
<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)
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
open={isOpen}
onClose={handleClose}
drawerPosition="bottom"
>
</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} />
</View>
<Text style={{
marginBottom: 4,
fontSize: 14,
}}
<Text
style={{
marginTop: 4,
fontSize: 14,
}}
>
{item.name}
</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 { 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 MoneyCategoryIcon from '@/components/common/MoneyCategoryIcon'
@@ -20,27 +20,15 @@ export default function CategoryGrid({
className,
style,
}: CategoryGridProps) {
const flatListRef = useRef<FlatList<any>>(null)
const NUM_COLUMNS = 4
// 每行 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])
const flatListRef = useRef<FlatList<any>>(null)
// 自动滚动到选中行
useEffect(() => {
if (!selectedCategory || !flatListRef.current)
return
// 找到选中项下标
const itemIndex = data.findIndex(item => item.id === selectedCategory.id)
if (itemIndex === -1 || !flatListRef.current)
return
@@ -59,8 +47,8 @@ export default function CategoryGrid({
return (
<FlatList
ref={flatListRef}
data={memoItems}
numColumns={4}
data={data}
numColumns={NUM_COLUMNS}
keyExtractor={item => item.id.toString()}
showsVerticalScrollIndicator={false}
className={`${className}`}
@@ -69,14 +57,15 @@ export default function CategoryGrid({
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' }}
style={{
width: `${100 / NUM_COLUMNS}%`,
height: 80,
alignItems: 'center',
}}
onPress={() => onSelectCategory?.(item)}
>
<View
@@ -96,7 +85,7 @@ export default function CategoryGrid({
color={isSelected ? '#fff' : '#000'}
/>
</View>
<Text style={{ marginBottom: 4, fontSize: 14, textAlign: 'center' }}>
<Text style={{ marginTop: 4, fontSize: 14, textAlign: 'center' }}>
{item.name}
</Text>
</Pressable>
+2 -2
View File
@@ -32,8 +32,8 @@ function isWhiteListed(url: string) {
}
const alovaInstance = createAlova({
baseURL: 'http://192.168.5.6:8086/app',
// baseURL: 'http://192.168.1.137:8086/app',
// baseURL: 'http://192.168.5.6:8086/app',
baseURL: 'http://192.168.1.137:8086/app',
requestAdapter: axiosRequestAdapter(),
timeout: 5000,
cacheFor: null,