From bb780a42ab1cbad30b9dbe867109879e0e2863c1 Mon Sep 17 00:00:00 2001 From: oneao Date: Thu, 20 Nov 2025 22:32:10 +0800 Subject: [PATCH] u --- .../base-app/src/app/(tabs)/love.tsx | 175 +++++++++++---- .../base-project/base-app/src/app/_layout.tsx | 41 ++-- .../src/app/money/category/setting/index.tsx | 207 ++++++++++++++++++ .../src/app/money/record/editor/index.tsx | 7 +- .../src/components/common/ao/index.ts | 2 + .../base-app/src/components/custom/Header.tsx | 27 ++- .../src/components/custom/TabsProvider.tsx | 168 -------------- .../base-app/src/request/index.ts | 4 +- .../base-app/src/types/money/record.ts | 2 +- code/base-project/base-go-v2/configs/app.yaml | 2 +- 10 files changed, 389 insertions(+), 246 deletions(-) create mode 100644 code/base-project/base-app/src/app/money/category/setting/index.tsx delete mode 100644 code/base-project/base-app/src/components/custom/TabsProvider.tsx diff --git a/code/base-project/base-app/src/app/(tabs)/love.tsx b/code/base-project/base-app/src/app/(tabs)/love.tsx index d8747f21..73ea479e 100644 --- a/code/base-project/base-app/src/app/(tabs)/love.tsx +++ b/code/base-project/base-app/src/app/(tabs)/love.tsx @@ -1,49 +1,140 @@ -import { StyleSheet, Text, View } from 'react-native' +import type Animated from 'react-native-reanimated' +import type { SortableGridRenderItem } from 'react-native-sortables' +import type { MoneyCategoryItem } from '@/types/money/category' +import { useCallback, useRef, useState } from 'react' + +import { Platform, Animated as RNAnimated, Text, Vibration, View } from 'react-native' +import { useAnimatedRef } from 'react-native-reanimated' import Sortable from 'react-native-sortables' +import { AButton, ASegmentedTabs, LucideIcon } from '@/components/common/ao' +import MoneyCategoryIcon from '@/components/common/MoneyCategoryIcon' +import CustomHeader from '@/components/custom/Header' -const DATA = [ - 'Poland', - 'Germany', - 'France', - 'Italy', - 'Spain', - 'Portugal', - 'Greece', - 'Great Britain', - 'United States', - 'Canada', - 'Australia', - 'New Zealand', -] +// 生成 100 条假数据 +function generateFakeCategories(): MoneyCategoryItem[] { + return Array.from({ length: 100 }, (_, index) => ({ + id: index + 1, + name: `类别 ${index + 1}`, + icon: 'Coin', // 默认图标 + })) +} + +export default function Example() { + const [selectedTab, setSelectedTab] = useState(0) + const scrollableRef = useAnimatedRef() + const deleteTimeoutRef = useRef(null) + + const [categories, setCategories] = useState(generateFakeCategories()) + const [draggingId, setDraggingId] = useState(null) + const [isInDeleteZone, setIsInDeleteZone] = useState(false) + + const renderItem = useCallback>( + ({ item }) => ( + + + + + {item.name} + + ), + [], + ) + + // 拖入删除区时启动延迟删除 + const handleItemEnter = () => { + if (!draggingId) + return + setIsInDeleteZone(true) + Vibration.vibrate(50) + + // 延迟 2 秒删除 + deleteTimeoutRef.current = setTimeout(() => { + setCategories(prev => prev.filter(c => c.id !== draggingId)) + setDraggingId(null) + setIsInDeleteZone(false) + deleteTimeoutRef.current = null + }, 2000) + } + + // 拖出删除区取消删除 + const handleItemLeave = () => { + setIsInDeleteZone(false) + if (deleteTimeoutRef.current) { + clearTimeout(deleteTimeoutRef.current) + deleteTimeoutRef.current = null + } + } -export default function Flex() { return ( - - - {DATA.map(item => ( - - {item} - - ))} - + + + + + + + + + {/* Grid */} + setDraggingId(Number(data.key))} + onDragEnd={() => setDraggingId(null)} + /> + + {/* 删除区 */} + + + 拖到这里删除 + + + + {/* 新增按钮 */} + + + 添加分类 + + + + + + + ) } - -const styles = StyleSheet.create({ - cell: { - backgroundColor: '#36877F', - borderRadius: 9999, - padding: 10, - alignItems: 'center', - justifyContent: 'center', - alignSelf: 'stretch', // ✅ 关键:让子元素撑满父容器宽度 - width: '100%', - }, -}) diff --git a/code/base-project/base-app/src/app/_layout.tsx b/code/base-project/base-app/src/app/_layout.tsx index 91369cc4..d40a51b5 100644 --- a/code/base-project/base-app/src/app/_layout.tsx +++ b/code/base-project/base-app/src/app/_layout.tsx @@ -5,6 +5,7 @@ import { Stack, useRouter } from 'expo-router' import * as SplashScreen from 'expo-splash-screen' import { StatusBar } from 'expo-status-bar' import { useEffect } from 'react' +import { GestureHandlerRootView } from 'react-native-gesture-handler' import { KeyboardProvider } from 'react-native-keyboard-controller' import { MenuProvider } from 'react-native-popup-menu' import { AButton } from '@/components/common/ao' @@ -15,7 +16,6 @@ import { useUserStore } from '@/store/user' import { ToastProviderWrapper } from '@/utils/toast' import '@/styles/global.css' import 'react-native-reanimated' -import 'react-native-gesture-handler' SplashScreen.preventAutoHideAsync() @@ -74,25 +74,28 @@ export default function RootLayout() { - - + - - - + + + + + + + diff --git a/code/base-project/base-app/src/app/money/category/setting/index.tsx b/code/base-project/base-app/src/app/money/category/setting/index.tsx new file mode 100644 index 00000000..3aa03605 --- /dev/null +++ b/code/base-project/base-app/src/app/money/category/setting/index.tsx @@ -0,0 +1,207 @@ +import type { SortableGridRenderItem } from 'react-native-sortables' +import type { MoneyCategoryItem } from '@/types/money/category' +import { useCallback, useEffect, useState } from 'react' +import { Platform, Text, Vibration, View } from 'react-native' + +import Animated, { useAnimatedRef } from 'react-native-reanimated' +import Sortable, { DragStartParams } from 'react-native-sortables' +import { AButton, ASegmentedTabs, LucideIcon } from '@/components/common/ao' +import MoneyCategoryIcon from '@/components/common/MoneyCategoryIcon' +import CustomHeader from '@/components/custom/Header' +import { useMoneyCategoryStore } from '@/store/moneyCategory' + +export default function Example() { + const [selectedTab, setSelectedTab] = useState(0) + const moneyCategoryStore = useMoneyCategoryStore() + const scrollableRef = useAnimatedRef() + + const [categories, setCategories] = useState([]) + + const [draggingId, setDraggingId] = useState('') + const [draggingDeleteZone, setDraggingDeleteZone] = useState(false) + const [deleteTimer, setDeleteTimer] = useState(null) + + useEffect(() => { + setCategories(selectedTab === 0 ? moneyCategoryStore.getExpenseCategories() : moneyCategoryStore.getIncomeCategories()) + }, [selectedTab]) + + const renderItem = useCallback>( + ({ item }) => ( + + + + + + {item.name} + + + ), + [], + ) + + return ( + + + + + + { + setSelectedTab(index) + }} + tabs={[ + { + label: '收入', + }, + { + label: '支出', + }, + ]} + style={{ + width: 140, + }} + /> + + + + 添加分类 + + + + + + + + + + { + Vibration.vibrate(50) + setDraggingId(data.key) + }} + onDragEnd={() => { + setDraggingId('') + }} + /> + + + { + if (!draggingId) + return + + setDraggingDeleteZone(true) + Vibration.vibrate(50) + + // 设置 1 秒后删除 + const timer = setTimeout(() => { + setCategories(prev => prev.filter(c => c.id !== Number(draggingId))) + setDraggingId('') + setDraggingDeleteZone(false) + setDeleteTimer(null) + }, 1000) + + setDeleteTimer(timer as unknown as number) + }} + onItemLeave={() => { + setDraggingDeleteZone(false) + if (deleteTimer) { + clearTimeout(deleteTimer) + setDeleteTimer(null) + } + }} + onItemDrop={() => { + setDraggingDeleteZone(false) + if (deleteTimer) { + clearTimeout(deleteTimer) + setDeleteTimer(null) + } + }} + minActivationDistance={40} + > + + + + 拖到这里删除 + + + + + + + + 恢复 + + + + 保存 + + + + + + + + + ) +} diff --git a/code/base-project/base-app/src/app/money/record/editor/index.tsx b/code/base-project/base-app/src/app/money/record/editor/index.tsx index e86cbc13..e0cac360 100644 --- a/code/base-project/base-app/src/app/money/record/editor/index.tsx +++ b/code/base-project/base-app/src/app/money/record/editor/index.tsx @@ -148,7 +148,12 @@ export default function MoenyRecordEditor() { + { + router.navigate('/money/category/setting') + }} + > 管理 diff --git a/code/base-project/base-app/src/components/common/ao/index.ts b/code/base-project/base-app/src/components/common/ao/index.ts index 9332a187..07856ec3 100644 --- a/code/base-project/base-app/src/components/common/ao/index.ts +++ b/code/base-project/base-app/src/components/common/ao/index.ts @@ -7,6 +7,7 @@ import FormItem from './FormItem' import Input from './Input' import LucideIcon from './LucideIcon' import Modal from './Modal' +import SegmentedTabs from './SegmentedTabs' export { Button as AButton, @@ -19,5 +20,6 @@ export { FormRef as AFormRef, Input as AInput, Modal as AModal, + SegmentedTabs as ASegmentedTabs, LucideIcon, } diff --git a/code/base-project/base-app/src/components/custom/Header.tsx b/code/base-project/base-app/src/components/custom/Header.tsx index 0706e3ce..714f4d3c 100644 --- a/code/base-project/base-app/src/components/custom/Header.tsx +++ b/code/base-project/base-app/src/components/custom/Header.tsx @@ -70,14 +70,17 @@ export default function CustomHeader(props: CustomHeaderProps) { style, ]} > + + { + left || ( + + router.back()}> + + + + ) + } {/* 左侧 */} - - {left || ( - router.back()}> - - - )} - {/* 标题 */} @@ -86,15 +89,15 @@ export default function CustomHeader(props: CustomHeaderProps) { : title} - {/* 右侧 */} - - {right || ( + {right || ( + - )} - + + )} + ), }} diff --git a/code/base-project/base-app/src/components/custom/TabsProvider.tsx b/code/base-project/base-app/src/components/custom/TabsProvider.tsx deleted file mode 100644 index 37c16d32..00000000 --- a/code/base-project/base-app/src/components/custom/TabsProvider.tsx +++ /dev/null @@ -1,168 +0,0 @@ -import type { ReactNode } from 'react' -import type { LayoutChangeEvent } from 'react-native' -import React, { createContext, useContext, useEffect, useRef, useState } from 'react' -import { - Animated, - LayoutChangeEvent as RNLayoutChangeEvent, - ScrollView, - StyleSheet, - Text, - TouchableOpacity, - View, -} from 'react-native' -import PagerView from 'react-native-pager-view' - -interface TabsContextType { - pagerRef: React.RefObject - scrollX: Animated.Value - index: number - setIndex: (i: number) => void -} - -const TabsContext = createContext(null) - -interface TabsProviderProps { - children: ReactNode -} - -export function TabsProvider({ children }: TabsProviderProps) { - const pagerRef = useRef(null!) - const scrollX = useRef(new Animated.Value(0)).current - const [index, setIndex] = useState(0) - - return ( - - {children} - - ) -} - -// ---------------- Tabs (Header) ---------------- -interface TabsProps { - tabs: string[] - leftContent?: ReactNode - rightContent?: ReactNode -} - -function useSafeTabsContext(): TabsContextType { - const ctx = useContext(TabsContext) - if (!ctx) { - const pagerRef = useRef(null!) - const scrollX = useRef(new Animated.Value(0)).current - const [index, setIndex] = useState(0) - return { pagerRef, scrollX, index, setIndex } - } - return ctx -} - -TabsProvider.Tabs = function Tabs({ tabs, leftContent, rightContent }: TabsProps) { - const ctx = useSafeTabsContext() - - const tabLayouts = useRef<{ x: number, width: number }[]>([]) - const scrollRef = useRef(null) - - const onTabLayout = (e: LayoutChangeEvent, i: number) => { - const { x, width } = e.nativeEvent.layout - tabLayouts.current[i] = { x, width } - } - - const measured = tabLayouts.current.length === tabs.length - - const underlineX = measured - ? ctx.scrollX.interpolate({ - inputRange: tabs.map((_, i) => i), - outputRange: tabLayouts.current.map(t => t.x), - }) - : 0 - - const underlineWidth = measured - ? ctx.scrollX.interpolate({ - inputRange: tabs.map((_, i) => i), - outputRange: tabLayouts.current.map(t => t.width), - }) - : 0 - - // 自动滚动 ScrollView 保持选中 Tab 可见 - useEffect(() => { - if (!measured || !scrollRef.current) - return - const layout = tabLayouts.current[ctx.index] - if (!layout) - return - scrollRef.current.scrollTo({ x: Math.max(layout.x - 50, 0), animated: true }) - }, [ctx.index, measured]) - - return ( - - {leftContent && {leftContent}} - - - {tabs.map((tab, i) => ( - { - ctx.pagerRef.current?.setPage(i) - ctx.setIndex(i) - }} - onLayout={e => onTabLayout(e, i)} - style={{ paddingHorizontal: 20, paddingVertical: 12 }} - > - {tab} - - ))} - - {measured && ( - - )} - - - {rightContent && {rightContent}} - - ) -} - -// ---------------- Content ---------------- -TabsProvider.Content = function Content({ tabs }: { tabs: string[] }) { - const ctx = useSafeTabsContext() - - return ( - - ctx.scrollX.setValue(e.nativeEvent.position + e.nativeEvent.offset)} - onPageSelected={e => ctx.setIndex(e.nativeEvent.position)} - > - {tabs.map((tab, i) => ( - - - {tab} - {' '} - 页面内容 - - - ))} - - ) -} - -const styles = StyleSheet.create({ - underline: { - position: 'absolute', - bottom: 0, - height: 2, - backgroundColor: '#007AFF', - }, -}) diff --git a/code/base-project/base-app/src/request/index.ts b/code/base-project/base-app/src/request/index.ts index 89b78e6c..b475c5bd 100644 --- a/code/base-project/base-app/src/request/index.ts +++ b/code/base-project/base-app/src/request/index.ts @@ -30,8 +30,8 @@ function isWhiteListed(url: string) { } const alovaInstance = createAlova({ - // baseURL: 'http://192.168.5.6:8085/app', - baseURL: 'http://192.168.1.136:8085/app', + baseURL: 'http://192.168.5.6:8086/app', + // baseURL: 'http://192.168.1.136:8085/app', requestAdapter: axiosRequestAdapter(), timeout: 5000, cacheFor: null, diff --git a/code/base-project/base-app/src/types/money/record.ts b/code/base-project/base-app/src/types/money/record.ts index 2461993a..8ce94cee 100644 --- a/code/base-project/base-app/src/types/money/record.ts +++ b/code/base-project/base-app/src/types/money/record.ts @@ -9,7 +9,7 @@ export interface MoneyRecordBase { remark: string record_time: string create_time: string - update_time: string + update_time?: string } // 查询条件(请求参数) diff --git a/code/base-project/base-go-v2/configs/app.yaml b/code/base-project/base-go-v2/configs/app.yaml index 0246608d..c97b4429 100644 --- a/code/base-project/base-go-v2/configs/app.yaml +++ b/code/base-project/base-go-v2/configs/app.yaml @@ -1,5 +1,5 @@ server: - port: 8085 + port: 8086 baseUrl: '/app' snowflake: