This commit is contained in:
oneao committed 2026-01-14 22:25:14 +08:00
1 parent cb4562361a
commit b38583a9fe
11 files changed
+174 -85

No files matched your search

@@ -1,37 +1,67 @@
import { useEffect, useState } from 'react'
import { Pressable, ScrollView, Text, View } from 'react-native'
import { Pressable, Text, View } from 'react-native'
import { AButton } from '@/components/common/ao'
import MoneyCategoryIcon from '@/components/common/MoneyCategoryIcon'
import {
Drawer,
DrawerBackdrop,
DrawerBody,
DrawerContent,
DrawerFooter,
DrawerHeader,
} from '@/components/ui/drawer'
import { Input, InputField } from '@/components/ui/input'
import { useMoneyCategoryStore } from '@/store/moneyCategory'
import { MoneyCategoryEnum } from '@/types/money/category'
interface CreateCategoryProps {
open: boolean
onClose?: () => void
type: MoneyCategoryEnum
}
interface CreateCategoryForm {
icon: string
name: string
}
export default function CreateCategory({
open,
onClose,
type,
}: CreateCategoryProps) {
const moneyCategoryStore = useMoneyCategoryStore()
const [isOpen, setIsOpen] = useState(open)
const [form, setForm] = useState<CreateCategoryForm>({
icon: '',
name: '',
})
const [confirmDisabled, setConfirmDisabled] = useState(true)
const [confirmLoading, setConfirmLoading] = useState(false)
useEffect(() => {
setIsOpen(open)
}, [open])
useEffect(() => {
if (form.name && form.name.length < 6) {
setConfirmDisabled(false)
}
else {
setConfirmDisabled(true)
}
}, [form.name])
function handleClose() {
setIsOpen(false)
onClose && onClose()
setForm({
icon: '',
name: '',
})
}
return (
@@ -45,60 +75,65 @@ export default function CreateCategory({
>
<DrawerHeader className="px-4 flex flex-col gap-4">
<View className="flex flex-row items-center justify-between w-full">
<Pressable>
<Text className="text-gray-600">取消</Text>
</Pressable>
<AButton size="xs" variant="outline" onPress={handleClose}>取消</AButton>
<Text className="text-xl font-medium">类别</Text>
<Text className="text-lg font-medium">{type === MoneyCategoryEnum.Expense ? '添加支出分类' : '添加收入分类'}</Text>
<Pressable>
<Text className="text-gray-600">确认</Text>
</Pressable>
<AButton size="xs" disabled={confirmDisabled} loading={confirmLoading}>确认</AButton>
</View>
<Input
variant="outline"
>
<InputField placeholder="请输入名称" />
<InputField placeholder="请输入名称(不超过5个字符)" />
</Input>
</DrawerHeader>
<DrawerBody>
<ScrollView className="flex-1">
{Object.entries(moneyCategoryStore.ICON_GROUPS).map(([groupName, icons]) => (
<View key={groupName} className="mb-4">
<Text className="text-center mb-4">
{groupName}
</Text>
<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={() => {
console.log('select icon:', icon)
<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',
}}
>
<View
style={{
width: 46,
height: 46,
borderRadius: 28,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F3F4F6',
}}
>
<MoneyCategoryIcon name={icon} size={28} />
</View>
</Pressable>
))}
</View>
<MoneyCategoryIcon
name={icon}
size={32}
color={
icon === form.icon ? '#fff' : '#000'
}
/>
</View>
</Pressable>
))}
</View>
))}
</ScrollView>
</View>
))}
</DrawerBody>
<DrawerFooter />
</DrawerContent>
</Drawer>
)
@@ -9,10 +9,11 @@ import { AButton, ATab, LucideIcon } from '@/components/common/ao'
import MoneyCategoryIcon from '@/components/common/MoneyCategoryIcon'
import CustomHeader from '@/components/custom/Header'
import { useMoneyCategoryStore } from '@/store/moneyCategory'
import { MoneyCategoryEnum } from '@/types/money/category'
import CreateCategory from './components/CreateCategory'
export default function MoneyCategorySetting() {
const [selectedTab, setSelectedTab] = useState(0)
const [selectedTab, setSelectedTab] = useState(MoneyCategoryEnum.Expense)
const moneyCategoryStore = useMoneyCategoryStore()
const scrollableRef = useAnimatedRef<Animated.ScrollView>()
@@ -25,7 +26,7 @@ export default function MoneyCategorySetting() {
const [openCreateCategory, setOpenCreateCategory] = useState<boolean>(false)
useEffect(() => {
setCategories(selectedTab === 0 ? moneyCategoryStore.getExpenseCategories() : moneyCategoryStore.getIncomeCategories())
setCategories(selectedTab === MoneyCategoryEnum.Expense ? moneyCategoryStore.getExpenseCategories() : moneyCategoryStore.getIncomeCategories())
}, [selectedTab])
const renderItem = useCallback<SortableGridRenderItem<MoneyCategoryItem>>(
@@ -69,18 +70,18 @@ export default function MoneyCategorySetting() {
<View className="flex-1">
<View className="pt-2 mb-4 px-3 flex flex-row items-center justify-between">
<ATab
defaultIndex={selectedTab}
onChange={(index) => {
setSelectedTab(index)
defaultKey={selectedTab}
onChange={(key) => {
setSelectedTab(key as MoneyCategoryEnum)
}}
data={[
{
key: 'income',
label: '收入',
key: MoneyCategoryEnum.Expense,
label: '支出',
},
{
key: 'expense',
label: '支出',
key: MoneyCategoryEnum.Income,
label: '收入',
},
]}
/>
@@ -189,6 +190,7 @@ export default function MoneyCategorySetting() {
</View>
<CreateCategory
type={selectedTab}
open={openCreateCategory}
onClose={() => setOpenCreateCategory(false)}
/>
@@ -4,14 +4,14 @@ import PagerView from 'react-native-pager-view'
interface TabItem {
label: string
key: string
key: string | number
content?: React.ReactNode
}
interface TabProps {
data?: TabItem[]
defaultIndex?: number
onChange?: (index: number) => void
defaultKey?: TabItem['key']
onChange?: (key: TabItem['key']) => void
tabBarClass?: string
tabItemClass?: string
activeTabItemClass?: string
@@ -19,47 +19,66 @@ interface TabProps {
export default function Tab({
data,
defaultIndex = 0,
defaultKey,
onChange,
tabBarClass = 'flex flex-row gap-2 overflow-hidden',
tabItemClass = 'items-center justify-center rounded-md px-4 py-2 bg-gray-200',
activeTabItemClass = 'items-center justify-center rounded-md px-4 py-2 bg-gray-800',
}: TabProps) {
const [selectedIndex, setSelectedIndex] = useState(defaultIndex)
const pagerRef = useRef<PagerView>(null)
if (!data || data.length === 0)
return null
// 判断是否至少有一个 tab 含 content
// 默认选中 key(没传就用第一个)
const initialKey = defaultKey ?? data[0].key
const [selectedKey, setSelectedKey] = useState(initialKey)
const pagerRef = useRef<PagerView>(null)
// 是否有 content
const hasContent = useMemo(
() => data.some(item => item.content),
[data],
)
const handlePress = (index: number) => {
setSelectedIndex(index)
onChange?.(index)
if (hasContent)
pagerRef.current?.setPage(index)
// key -> index
const getIndexByKey = (key: TabItem['key']) =>
data.findIndex(item => item.key === key)
// index -> key
const getKeyByIndex = (index: number) =>
data[index]?.key
const handlePress = (key: TabItem['key']) => {
setSelectedKey(key)
onChange?.(key)
if (hasContent) {
const index = getIndexByKey(key)
if (index !== -1)
pagerRef.current?.setPage(index)
}
}
const onPageSelected = (e: any) => {
const index = e.nativeEvent.position
setSelectedIndex(index)
onChange?.(index)
const key = getKeyByIndex(index)
if (key !== undefined) {
setSelectedKey(key)
onChange?.(key)
}
}
return (
<View className="flex-1">
{/* tab bar */}
<View className={tabBarClass}>
{data.map((item, index) => {
const isActive = index === selectedIndex
{data.map((item) => {
const isActive = item.key === selectedKey
return (
<Pressable
key={item.key}
onPress={() => handlePress(index)}
onPress={() => handlePress(item.key)}
className={isActive ? activeTabItemClass : tabItemClass}
>
<Text className={isActive ? 'text-white' : 'text-gray-700'}>
@@ -74,7 +93,7 @@ export default function Tab({
<PagerView
ref={pagerRef}
style={{ flex: 1, marginTop: 16 }}
initialPage={defaultIndex}
initialPage={getIndexByKey(initialKey)}
onPageSelected={onPageSelected}
>
{data.map(item => (
+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,
@@ -28,3 +28,8 @@ export interface UpdateMoneyCategorySortItem {
export interface UpdateMoneyCategorySortParams {
data: UpdateMoneyCategorySortItem[]
}
export enum MoneyCategoryEnum {
Expense = 0, // 支出
Income = 1, // 收入
}