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

@@ -87,7 +87,7 @@ func RegisterDefault(ctx context.Context, c *app.RequestContext) {
} }
// 初始化用户分类 // 初始化用户分类
err = initMoneyCategoryTx(ctx, tx, params.ID) err = InitMoneyCategoryTx(ctx, tx, params.ID)
if err != nil { if err != nil {
errorx.AddError(c, err) errorx.AddError(c, err)
return return
@@ -97,8 +97,15 @@ func RegisterDefault(ctx context.Context, c *app.RequestContext) {
response.Success(c).Message("注册成功").Send() response.Success(c).Message("注册成功").Send()
} }
func initMoneyCategoryTx(ctx context.Context, tx pgx.Tx, userId int64) error { func InitMoneyCategoryTx(
sysCategories, err := db.Queries.ListMoneySysCategory(ctx) ctx context.Context,
tx pgx.Tx,
userId int64,
) error {
q := db.Queries.WithTx(tx)
sysCategories, err := q.ListMoneySysCategory(ctx)
if err != nil { if err != nil {
return err return err
} }
@@ -118,9 +125,12 @@ func initMoneyCategoryTx(ctx context.Context, tx pgx.Tx, userId int64) error {
}) })
} }
_, err = db.Queries.WithTx(tx).BatchInsertMoneyUserCategories(ctx, userCategories) // ⚠️ 注意:这里是 DeleteMoneyCategoryByUserId,不是 ById(你代码里疑似写错)
if err := q.DeleteMoneyCategoryByUserId(ctx, userId); err != nil {
return err
}
if err != nil { if _, err := q.BatchInsertMoneyUserCategories(ctx, userCategories); err != nil {
return err return err
} }
@@ -67,7 +67,7 @@ func DeleteUserMoneyCategory(ctx context.Context, c *app.RequestContext) {
return return
} }
err = db.Queries.DeleteUserMoneyCategoryById(ctx, req.Id) err = db.Queries.DeleteMoneyCategoryById(ctx, req.Id)
if err != nil { if err != nil {
errorx.AddError(c, err) errorx.AddError(c, err)
@@ -7,7 +7,7 @@ SELECT *
FROM money_user_category FROM money_user_category
WHERE user_id = $1; WHERE user_id = $1;
-- name: DeleteUserMoneyCategoryById :exec -- name: DeleteMoneyCategoryById :exec
DELETE FROM money_user_category DELETE FROM money_user_category
WHERE id = $1; WHERE id = $1;
@@ -15,3 +15,7 @@ WHERE id = $1;
UPDATE money_user_category UPDATE money_user_category
SET sort_number = $2 SET sort_number = $2
WHERE id = $1; WHERE id = $1;
-- name: DeleteMoneyCategoryByUserId :exec
DELETE FROM money_user_category
WHERE user_id = $1;
@@ -18,13 +18,23 @@ type BatchInsertMoneyUserCategoriesParams struct {
SortNumber int32 SortNumber int32
} }
const deleteUserMoneyCategoryById = `-- name: DeleteUserMoneyCategoryById :exec const deleteMoneyCategoryById = `-- name: DeleteMoneyCategoryById :exec
DELETE FROM money_user_category DELETE FROM money_user_category
WHERE id = $1 WHERE id = $1
` `
func (q *Queries) DeleteUserMoneyCategoryById(ctx context.Context, id int64) error { func (q *Queries) DeleteMoneyCategoryById(ctx context.Context, id int64) error {
_, err := q.db.Exec(ctx, deleteUserMoneyCategoryById, id) _, err := q.db.Exec(ctx, deleteMoneyCategoryById, id)
return err
}
const deleteMoneyCategoryByUserId = `-- name: DeleteMoneyCategoryByUserId :exec
DELETE FROM money_user_category
WHERE user_id = $1
`
func (q *Queries) DeleteMoneyCategoryByUserId(ctx context.Context, userID int64) error {
_, err := q.db.Exec(ctx, deleteMoneyCategoryByUserId, userID)
return err return err
} }
+1
View File
@@ -63,6 +63,7 @@
"@types/big.js": "^6.2.2", "@types/big.js": "^6.2.2",
"@types/react": "~19.1.0", "@types/react": "~19.1.0",
"babel-plugin-module-resolver": "^5.0.2", "babel-plugin-module-resolver": "^5.0.2",
"baseline-browser-mapping": "^2.9.14",
"eslint": "^9.38.0", "eslint": "^9.38.0",
"react-native-iconfont-cli": "^2.2.4", "react-native-iconfont-cli": "^2.2.4",
"react-native-svg-transformer": "^1.5.2", "react-native-svg-transformer": "^1.5.2",
+7 -4
View File
@@ -162,6 +162,9 @@ importers:
babel-plugin-module-resolver: babel-plugin-module-resolver:
specifier: ^5.0.2 specifier: ^5.0.2
version: 5.0.2 version: 5.0.2
baseline-browser-mapping:
specifier: ^2.9.14
version: 2.9.14
eslint: eslint:
specifier: ^9.38.0 specifier: ^9.38.0
version: 9.39.1(jiti@1.21.7) version: 9.39.1(jiti@1.21.7)
@@ -2509,8 +2512,8 @@ packages:
base64-js@1.5.1: base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
baseline-browser-mapping@2.8.28: baseline-browser-mapping@2.9.14:
resolution: {integrity: sha512-gYjt7OIqdM0PcttNYP2aVrr2G0bMALkBaoehD4BuRGjAOtipg0b6wHg1yNL+s5zSnLZZrGHOw4IrND8CD+3oIQ==} resolution: {integrity: sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg==}
hasBin: true hasBin: true
better-opn@3.0.2: better-opn@3.0.2:
@@ -9105,7 +9108,7 @@ snapshots:
base64-js@1.5.1: {} base64-js@1.5.1: {}
baseline-browser-mapping@2.8.28: {} baseline-browser-mapping@2.9.14: {}
better-opn@3.0.2: better-opn@3.0.2:
dependencies: dependencies:
@@ -9146,7 +9149,7 @@ snapshots:
browserslist@4.28.0: browserslist@4.28.0:
dependencies: dependencies:
baseline-browser-mapping: 2.8.28 baseline-browser-mapping: 2.9.14
caniuse-lite: 1.0.30001754 caniuse-lite: 1.0.30001754
electron-to-chromium: 1.5.252 electron-to-chromium: 1.5.252
node-releases: 2.0.27 node-releases: 2.0.27
@@ -1,37 +1,67 @@
import { useEffect, useState } from 'react' 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 MoneyCategoryIcon from '@/components/common/MoneyCategoryIcon'
import { import {
Drawer, Drawer,
DrawerBackdrop, DrawerBackdrop,
DrawerBody, DrawerBody,
DrawerContent, DrawerContent,
DrawerFooter,
DrawerHeader, DrawerHeader,
} from '@/components/ui/drawer' } from '@/components/ui/drawer'
import { Input, InputField } from '@/components/ui/input' import { Input, InputField } from '@/components/ui/input'
import { useMoneyCategoryStore } from '@/store/moneyCategory' import { useMoneyCategoryStore } from '@/store/moneyCategory'
import { MoneyCategoryEnum } from '@/types/money/category'
interface CreateCategoryProps { interface CreateCategoryProps {
open: boolean open: boolean
onClose?: () => void onClose?: () => void
type: MoneyCategoryEnum
}
interface CreateCategoryForm {
icon: string
name: string
} }
export default function CreateCategory({ export default function CreateCategory({
open, open,
onClose, onClose,
type,
}: CreateCategoryProps) { }: CreateCategoryProps) {
const moneyCategoryStore = useMoneyCategoryStore() const moneyCategoryStore = useMoneyCategoryStore()
const [isOpen, setIsOpen] = useState(open) const [isOpen, setIsOpen] = useState(open)
const [form, setForm] = useState<CreateCategoryForm>({
icon: '',
name: '',
})
const [confirmDisabled, setConfirmDisabled] = useState(true)
const [confirmLoading, setConfirmLoading] = useState(false)
useEffect(() => { useEffect(() => {
setIsOpen(open) setIsOpen(open)
}, [open]) }, [open])
useEffect(() => {
if (form.name && form.name.length < 6) {
setConfirmDisabled(false)
}
else {
setConfirmDisabled(true)
}
}, [form.name])
function handleClose() { function handleClose() {
setIsOpen(false) setIsOpen(false)
onClose && onClose() onClose && onClose()
setForm({
icon: '',
name: '',
})
} }
return ( return (
@@ -45,60 +75,65 @@ export default function CreateCategory({
> >
<DrawerHeader className="px-4 flex flex-col gap-4"> <DrawerHeader className="px-4 flex flex-col gap-4">
<View className="flex flex-row items-center justify-between w-full"> <View className="flex flex-row items-center justify-between w-full">
<Pressable> <AButton size="xs" variant="outline" onPress={handleClose}>取消</AButton>
<Text className="text-gray-600">取消</Text>
</Pressable>
<Text className="text-xl font-medium">类别</Text> <Text className="text-lg font-medium">{type === MoneyCategoryEnum.Expense ? '添加支出分类' : '添加收入分类'}</Text>
<Pressable> <AButton size="xs" disabled={confirmDisabled} loading={confirmLoading}>确认</AButton>
<Text className="text-gray-600">确认</Text>
</Pressable>
</View> </View>
<Input <Input
variant="outline" variant="outline"
> >
<InputField placeholder="请输入名称" /> <InputField placeholder="请输入名称(不超过5个字符)" />
</Input> </Input>
</DrawerHeader> </DrawerHeader>
<DrawerBody> <DrawerBody className="mb-16">
<ScrollView className="flex-1"> {Object.entries(moneyCategoryStore.ICON_GROUPS).map(([groupName, icons]) => (
{Object.entries(moneyCategoryStore.ICON_GROUPS).map(([groupName, icons]) => ( <View key={groupName} className="mb-2">
<View key={groupName} className="mb-4"> <Text className="text-center text-sm mb-4">
<Text className="text-center mb-4"> {groupName}
{groupName} </Text>
</Text>
<View className="flex flex-row flex-wrap"> <View className="flex flex-row flex-wrap">
{icons.map(icon => ( {icons.map(icon => (
<Pressable <Pressable
key={icon} key={icon}
className="w-1/5 mb-3 flex items-center" className="w-1/5 mb-3 flex items-center"
onPress={() => { onPress={() => {
console.log('select icon:', icon) setForm(val => ({
...val,
icon,
}))
}}
>
<View
style={{
width: 46,
height: 46,
borderRadius: 28,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: icon === form.icon ? '#333' : '#F3F4F6',
}} }}
> >
<View <MoneyCategoryIcon
style={{ name={icon}
width: 46, size={32}
height: 46, color={
borderRadius: 28, icon === form.icon ? '#fff' : '#000'
justifyContent: 'center', }
alignItems: 'center', />
backgroundColor: '#F3F4F6', </View>
}} </Pressable>
> ))}
<MoneyCategoryIcon name={icon} size={28} />
</View>
</Pressable>
))}
</View>
</View> </View>
))} </View>
</ScrollView> ))}
</DrawerBody> </DrawerBody>
<DrawerFooter />
</DrawerContent> </DrawerContent>
</Drawer> </Drawer>
) )
@@ -9,10 +9,11 @@ import { AButton, ATab, LucideIcon } from '@/components/common/ao'
import MoneyCategoryIcon from '@/components/common/MoneyCategoryIcon' import MoneyCategoryIcon from '@/components/common/MoneyCategoryIcon'
import CustomHeader from '@/components/custom/Header' import CustomHeader from '@/components/custom/Header'
import { useMoneyCategoryStore } from '@/store/moneyCategory' import { useMoneyCategoryStore } from '@/store/moneyCategory'
import { MoneyCategoryEnum } from '@/types/money/category'
import CreateCategory from './components/CreateCategory' import CreateCategory from './components/CreateCategory'
export default function MoneyCategorySetting() { export default function MoneyCategorySetting() {
const [selectedTab, setSelectedTab] = useState(0) const [selectedTab, setSelectedTab] = useState(MoneyCategoryEnum.Expense)
const moneyCategoryStore = useMoneyCategoryStore() const moneyCategoryStore = useMoneyCategoryStore()
const scrollableRef = useAnimatedRef<Animated.ScrollView>() const scrollableRef = useAnimatedRef<Animated.ScrollView>()
@@ -25,7 +26,7 @@ export default function MoneyCategorySetting() {
const [openCreateCategory, setOpenCreateCategory] = useState<boolean>(false) const [openCreateCategory, setOpenCreateCategory] = useState<boolean>(false)
useEffect(() => { useEffect(() => {
setCategories(selectedTab === 0 ? moneyCategoryStore.getExpenseCategories() : moneyCategoryStore.getIncomeCategories()) setCategories(selectedTab === MoneyCategoryEnum.Expense ? moneyCategoryStore.getExpenseCategories() : moneyCategoryStore.getIncomeCategories())
}, [selectedTab]) }, [selectedTab])
const renderItem = useCallback<SortableGridRenderItem<MoneyCategoryItem>>( const renderItem = useCallback<SortableGridRenderItem<MoneyCategoryItem>>(
@@ -69,18 +70,18 @@ export default function MoneyCategorySetting() {
<View className="flex-1"> <View className="flex-1">
<View className="pt-2 mb-4 px-3 flex flex-row items-center justify-between"> <View className="pt-2 mb-4 px-3 flex flex-row items-center justify-between">
<ATab <ATab
defaultIndex={selectedTab} defaultKey={selectedTab}
onChange={(index) => { onChange={(key) => {
setSelectedTab(index) setSelectedTab(key as MoneyCategoryEnum)
}} }}
data={[ data={[
{ {
key: 'income', key: MoneyCategoryEnum.Expense,
label: '收入', label: '支出',
}, },
{ {
key: 'expense', key: MoneyCategoryEnum.Income,
label: '支出', label: '收入',
}, },
]} ]}
/> />
@@ -189,6 +190,7 @@ export default function MoneyCategorySetting() {
</View> </View>
<CreateCategory <CreateCategory
type={selectedTab}
open={openCreateCategory} open={openCreateCategory}
onClose={() => setOpenCreateCategory(false)} onClose={() => setOpenCreateCategory(false)}
/> />
@@ -4,14 +4,14 @@ import PagerView from 'react-native-pager-view'
interface TabItem { interface TabItem {
label: string label: string
key: string key: string | number
content?: React.ReactNode content?: React.ReactNode
} }
interface TabProps { interface TabProps {
data?: TabItem[] data?: TabItem[]
defaultIndex?: number defaultKey?: TabItem['key']
onChange?: (index: number) => void onChange?: (key: TabItem['key']) => void
tabBarClass?: string tabBarClass?: string
tabItemClass?: string tabItemClass?: string
activeTabItemClass?: string activeTabItemClass?: string
@@ -19,47 +19,66 @@ interface TabProps {
export default function Tab({ export default function Tab({
data, data,
defaultIndex = 0, defaultKey,
onChange, onChange,
tabBarClass = 'flex flex-row gap-2 overflow-hidden', tabBarClass = 'flex flex-row gap-2 overflow-hidden',
tabItemClass = 'items-center justify-center rounded-md px-4 py-2 bg-gray-200', 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', activeTabItemClass = 'items-center justify-center rounded-md px-4 py-2 bg-gray-800',
}: TabProps) { }: TabProps) {
const [selectedIndex, setSelectedIndex] = useState(defaultIndex)
const pagerRef = useRef<PagerView>(null)
if (!data || data.length === 0) if (!data || data.length === 0)
return null 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( const hasContent = useMemo(
() => data.some(item => item.content), () => data.some(item => item.content),
[data], [data],
) )
const handlePress = (index: number) => { // key -> index
setSelectedIndex(index) const getIndexByKey = (key: TabItem['key']) =>
onChange?.(index) data.findIndex(item => item.key === key)
if (hasContent)
pagerRef.current?.setPage(index) // 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 onPageSelected = (e: any) => {
const index = e.nativeEvent.position const index = e.nativeEvent.position
setSelectedIndex(index) const key = getKeyByIndex(index)
onChange?.(index)
if (key !== undefined) {
setSelectedKey(key)
onChange?.(key)
}
} }
return ( return (
<View className="flex-1"> <View className="flex-1">
{/* tab bar */} {/* tab bar */}
<View className={tabBarClass}> <View className={tabBarClass}>
{data.map((item, index) => { {data.map((item) => {
const isActive = index === selectedIndex const isActive = item.key === selectedKey
return ( return (
<Pressable <Pressable
key={item.key} key={item.key}
onPress={() => handlePress(index)} onPress={() => handlePress(item.key)}
className={isActive ? activeTabItemClass : tabItemClass} className={isActive ? activeTabItemClass : tabItemClass}
> >
<Text className={isActive ? 'text-white' : 'text-gray-700'}> <Text className={isActive ? 'text-white' : 'text-gray-700'}>
@@ -74,7 +93,7 @@ export default function Tab({
<PagerView <PagerView
ref={pagerRef} ref={pagerRef}
style={{ flex: 1, marginTop: 16 }} style={{ flex: 1, marginTop: 16 }}
initialPage={defaultIndex} initialPage={getIndexByKey(initialKey)}
onPageSelected={onPageSelected} onPageSelected={onPageSelected}
> >
{data.map(item => ( {data.map(item => (
+2 -2
View File
@@ -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,
@@ -28,3 +28,8 @@ export interface UpdateMoneyCategorySortItem {
export interface UpdateMoneyCategorySortParams { export interface UpdateMoneyCategorySortParams {
data: UpdateMoneyCategorySortItem[] data: UpdateMoneyCategorySortItem[]
} }
export enum MoneyCategoryEnum {
Expense = 0, // 支出
Income = 1, // 收入
}