u
This commit is contained in:
1 parent
cb4562361a
commit
b38583a9fe
11 files changed
+174
-85
No files matched your search
@@ -63,6 +63,7 @@
|
||||
"@types/big.js": "^6.2.2",
|
||||
"@types/react": "~19.1.0",
|
||||
"babel-plugin-module-resolver": "^5.0.2",
|
||||
"baseline-browser-mapping": "^2.9.14",
|
||||
"eslint": "^9.38.0",
|
||||
"react-native-iconfont-cli": "^2.2.4",
|
||||
"react-native-svg-transformer": "^1.5.2",
|
||||
|
||||
Generated
+7
-4
@@ -162,6 +162,9 @@ importers:
|
||||
babel-plugin-module-resolver:
|
||||
specifier: ^5.0.2
|
||||
version: 5.0.2
|
||||
baseline-browser-mapping:
|
||||
specifier: ^2.9.14
|
||||
version: 2.9.14
|
||||
eslint:
|
||||
specifier: ^9.38.0
|
||||
version: 9.39.1(jiti@1.21.7)
|
||||
@@ -2509,8 +2512,8 @@ packages:
|
||||
base64-js@1.5.1:
|
||||
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
||||
|
||||
baseline-browser-mapping@2.8.28:
|
||||
resolution: {integrity: sha512-gYjt7OIqdM0PcttNYP2aVrr2G0bMALkBaoehD4BuRGjAOtipg0b6wHg1yNL+s5zSnLZZrGHOw4IrND8CD+3oIQ==}
|
||||
baseline-browser-mapping@2.9.14:
|
||||
resolution: {integrity: sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg==}
|
||||
hasBin: true
|
||||
|
||||
better-opn@3.0.2:
|
||||
@@ -9105,7 +9108,7 @@ snapshots:
|
||||
|
||||
base64-js@1.5.1: {}
|
||||
|
||||
baseline-browser-mapping@2.8.28: {}
|
||||
baseline-browser-mapping@2.9.14: {}
|
||||
|
||||
better-opn@3.0.2:
|
||||
dependencies:
|
||||
@@ -9146,7 +9149,7 @@ snapshots:
|
||||
|
||||
browserslist@4.28.0:
|
||||
dependencies:
|
||||
baseline-browser-mapping: 2.8.28
|
||||
baseline-browser-mapping: 2.9.14
|
||||
caniuse-lite: 1.0.30001754
|
||||
electron-to-chromium: 1.5.252
|
||||
node-releases: 2.0.27
|
||||
|
||||
+76
-41
@@ -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 => (
|
||||
|
||||
@@ -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, // 收入
|
||||
}
|
||||
Reference in new issue
Block a user