diff --git a/code/base-project/base-app/package.json b/code/base-project/base-app/package.json index 9f4fb3c2..bf332bab 100644 --- a/code/base-project/base-app/package.json +++ b/code/base-project/base-app/package.json @@ -14,7 +14,7 @@ "@gluestack-ui/core": "^3.0.12", "@gluestack-ui/utils": "^3.0.11", "@gluestack/ui-next-adapter": "^3.0.3", - "@legendapp/motion": "^2.4.0", + "@legendapp/motion": "2.4.0", "@react-native-async-storage/async-storage": "2.2.0", "@react-native-community/datetimepicker": "8.4.4", "@react-native-segmented-control/segmented-control": "2.5.7", @@ -47,8 +47,8 @@ "react-native-reanimated": "~4.1.3", "react-native-safe-area-context": "~5.6.1", "react-native-screens": "~4.16.0", + "react-native-sortables": "^1.9.3", "react-native-svg": "15.12.1", - "react-native-tab-view": "^4.2.0", "react-native-web": "^0.21.0", "react-native-worklets": "0.5.1", "tailwind-merge": "^3.4.0", diff --git a/code/base-project/base-app/pnpm-lock.yaml b/code/base-project/base-app/pnpm-lock.yaml index eebf1b95..5024eb16 100644 --- a/code/base-project/base-app/pnpm-lock.yaml +++ b/code/base-project/base-app/pnpm-lock.yaml @@ -21,7 +21,7 @@ importers: specifier: ^3.0.3 version: 3.0.3 '@legendapp/motion': - specifier: ^2.4.0 + specifier: 2.4.0 version: 2.4.0(nativewind@4.2.1(react-native-reanimated@4.1.5(@babel/core@7.28.5)(react-native-worklets@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))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-svg@15.12.1(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)(tailwindcss@3.4.18(yaml@2.8.1)))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) '@react-native-async-storage/async-storage': specifier: 2.2.0 @@ -119,12 +119,12 @@ importers: react-native-screens: specifier: ~4.16.0 version: 4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + react-native-sortables: + specifier: ^1.9.3 + version: 1.9.3(react-native-gesture-handler@2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-reanimated@4.1.5(@babel/core@7.28.5)(react-native-worklets@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))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) react-native-svg: specifier: 15.12.1 version: 15.12.1(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) - react-native-tab-view: - specifier: ^4.2.0 - version: 4.2.0(react-native-pager-view@6.9.1(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) react-native-web: specifier: ^0.21.0 version: 0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -3911,48 +3911,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==} @@ -4836,6 +4844,11 @@ packages: react: '*' react-native: '*' + react-native-haptic-feedback@2.3.3: + resolution: {integrity: sha512-svS4D5PxfNv8o68m9ahWfwje5NqukM3qLS48+WTdhbDkNUkOhP9rDfDSRHzlhk4zq+ISjyw95EhLeh8NkKX5vQ==} + peerDependencies: + react-native: '>=0.60.0' + react-native-iconfont-cli@2.2.4: resolution: {integrity: sha512-qTjfHG2f/sc3/YKWZJvIJk/SwiTu8C9/SvtoLxRX+GM/6pvOJq0ge6rcV3vTh4s3QJDuNDOD6+a4hGD6vv2vSg==} hasBin: true @@ -4892,6 +4905,14 @@ packages: react: '*' react-native: '*' + react-native-sortables@1.9.3: + resolution: {integrity: sha512-VLhW9+3AVEaJNwwQSgN+n/Qe+YRB0C0mNWTjHhyzcZ+YjY4BmJao4bZxl5lD6EsfqZ1Ij6B2ZdxjNlSkUXrvow==} + peerDependencies: + react: '*' + react-native: '*' + react-native-gesture-handler: '>=2.0.0' + react-native-reanimated: '>=3.0.0' + react-native-svg-transformer@1.5.2: resolution: {integrity: sha512-eW4hOtrd30s4SRdN4X1XYxTCu1czsxDGQKmfQ3RFbZMN5yw4ZmiKGGr+lXbQW4uDGZvSoGd9FHL1f+rgGoKg8Q==} peerDependencies: @@ -4904,13 +4925,6 @@ packages: react: '*' react-native: '*' - react-native-tab-view@4.2.0: - resolution: {integrity: sha512-TUbh7Yr0tE/99t1pJQLbQ+4/Px67xkT7/r3AhfV+93Q3WoUira0Lx7yuKUP2C118doqxub8NCLERwcqsHr29nQ==} - peerDependencies: - react: '>= 18.2.0' - react-native: '*' - react-native-pager-view: '>= 6.0.0' - react-native-web@0.21.2: resolution: {integrity: sha512-SO2t9/17zM4iEnFvlu2DA9jqNbzNhoUP+AItkoCOyFmDMOhUnBBznBDCYN92fGdfAkfQlWzPoez6+zLxFNsZEg==} peerDependencies: @@ -11782,6 +11796,11 @@ snapshots: react: 19.1.0 react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0) + react-native-haptic-feedback@2.3.3(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0)): + dependencies: + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0) + optional: true + react-native-iconfont-cli@2.2.4(react-native-svg@15.12.1(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): dependencies: colors: 1.4.0 @@ -11844,6 +11863,15 @@ snapshots: react-native-is-edge-to-edge: 1.2.1(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) warn-once: 0.1.1 + react-native-sortables@1.9.3(react-native-gesture-handler@2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-reanimated@4.1.5(@babel/core@7.28.5)(react-native-worklets@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))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): + dependencies: + react: 19.1.0 + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0) + react-native-gesture-handler: 2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + react-native-reanimated: 4.1.5(@babel/core@7.28.5)(react-native-worklets@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))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + optionalDependencies: + react-native-haptic-feedback: 2.3.3(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0)) + react-native-svg-transformer@1.5.2(react-native-svg@15.12.1(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(typescript@5.9.3): dependencies: '@svgr/core': 8.1.0(typescript@5.9.3) @@ -11864,13 +11892,6 @@ snapshots: react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0) warn-once: 0.1.1 - react-native-tab-view@4.2.0(react-native-pager-view@6.9.1(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): - dependencies: - react: 19.1.0 - react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0) - react-native-pager-view: 6.9.1(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) - use-latest-callback: 0.2.6(react@19.1.0) - react-native-web@0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: '@babel/runtime': 7.28.4 diff --git a/code/base-project/base-app/src/app/(tabs)/chat.tsx b/code/base-project/base-app/src/app/(tabs)/chat.tsx index f7144df8..c666d8e2 100644 --- a/code/base-project/base-app/src/app/(tabs)/chat.tsx +++ b/code/base-project/base-app/src/app/(tabs)/chat.tsx @@ -1,33 +1,21 @@ import { Text, View } from 'react-native' -import PagerTabs from '@/components/custom/PagerTabs' +import { ACell, ACellGroup } from '@/components/common/ao' export default function Demo() { return ( - - - Left - - )} - right={( - - right - - )} - routes={[ - { - key: 'tab1', - title: 'Tab 1', - element: 第一个页面内容, - }, - { - key: 'tab2', - title: 'Tab 2', - element: 第二个页面内容, - }, - ]} - /> + + + + + + + + ) } 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 4d8aa2b2..d8747f21 100644 --- a/code/base-project/base-app/src/app/(tabs)/love.tsx +++ b/code/base-project/base-app/src/app/(tabs)/love.tsx @@ -1,18 +1,49 @@ -import { useRouter } from 'expo-router' -import { Text, View } from 'react-native' -import { AButton } from '@/components/common/ao' +import { StyleSheet, Text, View } from 'react-native' +import Sortable from 'react-native-sortables' -export default function Love() { - const router = useRouter() +const DATA = [ + 'Poland', + 'Germany', + 'France', + 'Italy', + 'Spain', + 'Portugal', + 'Greece', + 'Great Britain', + 'United States', + 'Canada', + 'Australia', + 'New Zealand', +] +export default function Flex() { return ( - - { - router.navigate('/auth') - }} + + - 跳转到登录页面 - + {DATA.map(item => ( + + {item} + + ))} + ) } + +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/(tabs)/money/components/AddDrawer.tsx b/code/base-project/base-app/src/app/(tabs)/money/components/AddDrawer.tsx deleted file mode 100644 index 83768e3a..00000000 --- a/code/base-project/base-app/src/app/(tabs)/money/components/AddDrawer.tsx +++ /dev/null @@ -1,630 +0,0 @@ -import type { MoneyCategoryItem } from '@/types/money/category' -import type { MoneyRecordBase, MoneyRecordItem } from '@/types/money/record' -import Big from 'big.js' -import { useEffect, useMemo, useState } from 'react' -import { FlatList, Keyboard, Pressable, StyleSheet, Text, TouchableWithoutFeedback, View } from 'react-native' -import { useSafeAreaInsets } from 'react-native-safe-area-context' -import { AInput, LucideIcon } from '@/components/common/ao' -import { ADatePicker } from '@/components/common/ao/index' -import SegmentedTabs from '@/components/common/ao/SegmentedTabs' -import { - Drawer, - DrawerBackdrop, - DrawerContent, - DrawerHeader, -} from '@/components/ui/drawer' -import { useKeyboardHeight } from '@/hooks/useKeyboardHeight' -import { queryMoneyCategoryApi } from '@/request/api/money/category' -import { insertMoneyRecordApi, updateMoneyRecordApi } from '@/request/api/money/record' -import { useMoneyCategoryStore } from '@/store/moneyCategory' -import { getCurrentDate, toDateString } from '@/utils/time' -import { toast } from '@/utils/toast' -import MoneyCategoryIcon from './MoneyCategoryIcon' - -interface AddMoneyDrawerProps { - isOpen: boolean - updateData?: MoneyRecordItem | null - onClose?: () => void - onConfirm?: (result: MoneyRecordItem) => void -} - -interface KeydownItem { - key: string | number - content: React.ReactNode -} - -const keys: KeydownItem[] = [ - { key: '1', content: 1 }, - { key: '2', content: 2 }, - { key: '3', content: 3 }, - { key: 'del', content: }, - { key: '4', content: 4 }, - { key: '5', content: 5 }, - { key: '6', content: 6 }, - { key: '+', content: }, - { key: '7', content: 7 }, - { key: '8', content: 8 }, - { key: '9', content: 9 }, - { key: '-', content: }, - { key: 'clear', content: ( - - 重置 - - ) }, - { key: '0', content: 0 }, - { key: '.', content: }, - { key: '/', content: ( - - 完成 - - ) }, -] - -export default function AddMoneyDrawer({ - isOpen, - onClose, - onConfirm, - updateData, -}: AddMoneyDrawerProps) { - const moneyCategoryStore = useMoneyCategoryStore() - - const insets = useSafeAreaInsets() - // 选择顺序:0 支出 1收入 - const [selectedIndex, setSelectedIndex] = useState(0) - // 自定义键盘 - const [showKeydown, setShowKeydown] = useState(false) - // 自定义键盘计算过程 - const [keydownInput, setKeydownInput] = useState('') - // 日期选择 - const [showDatePicker, setShowDatePicker] = useState(false) - // 备注聚焦 - const [remarkIsFocused, setRemarkIsFocused] = useState(false) - // 选中分类 - const [selectedCategory, setSelectedCategory] = useState(null) - - const keyboardHeight = useKeyboardHeight() - - const defaultMoneyRecord: MoneyRecordBase = { - id: -1, - category_id: -1, - user_id: -1, - amount: '0.00', - remark: '', - record_time: getCurrentDate(), - create_time: '', - } - - const [moneyRecord, setMoneyRecord] = useState(defaultMoneyRecord) - - function handleSelectCategory(item: MoneyCategoryItem) { - setSelectedCategory(item) - - setShowKeydown(true) - } - - const fetchCategories = async () => { - try { - if (moneyCategoryStore.categories.length === 0) { - const res = await queryMoneyCategoryApi() - if (res.isSuccess) { - moneyCategoryStore.setCategories(res.data) - } - } - } - catch (err) { - console.error('获取分类失败', err) - } - } - - useEffect(() => { - if (updateData) { - const type = updateData.category_type - setSelectedIndex(type) - setMoneyRecord({ - ...updateData, - record_time: toDateString(updateData.record_time), - }) - setShowKeydown(true) - - if (type === 0) { - // 支出 - moneyCategoryStore.expense.forEach((item) => { - if (item.id === updateData.category_id) { - setSelectedCategory(item) - } - }) - } - else if (type === 1) { - // 收入 - moneyCategoryStore.income.forEach((item) => { - if (item.id === updateData.category_id) { - setSelectedCategory(item) - } - }) - } - } - }, [isOpen]) - - function resetData() { - // 清空键盘记录 - setKeydownInput('') - // 关闭默认键盘 - setShowKeydown(false) - // 设置默认值 - setMoneyRecord(defaultMoneyRecord) - // 选中支出 - setSelectedIndex(0) - // 关闭日期 - setShowDatePicker(false) - // 清空选中类别 - setSelectedCategory(null) - } - - // 关闭整个drawer处理 - function onMainDrawerClose() { - resetData() - - onClose?.() - } - - // 计算过程 - const handlePress = (key: string | number) => { - setKeydownInput((prev) => { - let newInput = prev - - if (key === 'del') { - newInput = prev.slice(0, -1) - } - else if (key === 'clear') { - setMoneyRecord({ - ...moneyRecord, - amount: '0.00', - remark: '', - record_time: getCurrentDate(), - }) - return '' - } - else if (key === '/') { - saveRecord() - return prev - } - else { - const keyStr = key.toString() - - // 检查是否要输入运算符(+ 或 -) - if (keyStr === '+' || keyStr === '-') { - // 如果当前输入为空,不允许以运算符开头 - if (prev === '') { - return prev - } - - // 获取最后一个字符 - const lastChar = prev.slice(-1) - - // 如果最后一个字符已经是运算符,则替换而不是追加 - if (lastChar === '+' || lastChar === '-') { - newInput = prev.slice(0, -1) + keyStr - } - // 如果最后一个字符是小数点,不允许在小数点后直接加运算符 - else if (lastChar === '.') { - return prev - } - else { - newInput = prev + keyStr - } - } - // 检查是否要输入小数点(.) - else if (keyStr === '.') { - // 如果当前输入为空,不允许以小数点开头,改为输入"0." - if (prev === '') { - newInput = '0.' - } - else { - // 获取最后一个字符 - const lastChar = prev.slice(-1) - - // 如果最后一个字符是运算符,则需要在运算符后添加"0."而不是单独的"." - if (lastChar === '+' || lastChar === '-') { - newInput = `${prev}0.` - } - else { - // 检查当前数字是否已经包含小数点 - const parts = prev.split(/[+-]/) - const currentNumber = parts[parts.length - 1] - - // 如果当前数字中还没有小数点,可以添加 - if (!currentNumber.includes('.')) { - newInput = `${prev}.` - } - else { - // 已经有小数点了,不重复添加 - newInput = prev - } - } - } - } - // 处理数字输入 - else if (/^\d$/.test(keyStr)) { - // 检查当前数字的小数位数 - const parts = prev.split(/[+-]/) - const currentNumber = parts[parts.length - 1] - - // 如果当前数字包含小数点,检查小数位数 - if (currentNumber.includes('.')) { - const decimalParts = currentNumber.split('.') - // 如果小数部分已经达到2位,不允许继续输入 - if (decimalParts.length > 1 && decimalParts[1].length >= 2) { - newInput = prev // 不改变输入 - } - else { - newInput = prev + keyStr - } - } - else { - // 没有小数点,正常输入数字 - newInput = prev + keyStr - } - } - else { - // 对于其他字符,正常追加 - newInput = prev + keyStr - } - } - - // 计算结果 - try { - const computed = calculateWithBig(newInput) - setMoneyRecord({ - ...moneyRecord, - amount: computed, - }) - } - catch (e) { - setMoneyRecord({ - ...moneyRecord, - amount: '0', - }) - } - - return newInput - }) - } - - function calculateWithBig(expression: string): string { - if (!expression.trim()) - return '0' - - // 移除表达式末尾的运算符(+或-),避免不完整的表达式 - let cleanExpression = expression.trim() - const lastChar = cleanExpression.slice(-1) - if (lastChar === '+' || lastChar === '-') { - cleanExpression = cleanExpression.slice(0, -1) - } - - if (!cleanExpression) - return '0' - - try { - // 使用更智能的解析方式,支持基本的加减运算 - const numbers = cleanExpression.split(/[+-]/).filter(Boolean) - const operators = cleanExpression.split(/[^+-]/).filter(Boolean) - - if (numbers.length === 0) - return '0' - - let result = new Big(numbers[0]) - - for (let i = 0; i < operators.length; i++) { - const operator = operators[i] - const nextNumber = new Big(numbers[i + 1]) - - if (operator === '+') { - result = result.plus(nextNumber) - } - else if (operator === '-') { - result = result.minus(nextNumber) - } - } - - // 限制结果为最多两位小数 - return result.toFixed(2) - } - catch (error) { - // 如果解析失败,尝试只计算数字部分 - const tokens = expression.match(/[+-]?\d+(\.\d+)?/g) - if (!tokens || tokens.length === 0) - return '0' - - let total = new Big(0) - tokens.forEach((token) => { - total = total.plus(new Big(token)) - }) - - // 限制结果为最多两位小数 - return total.toFixed(2) - } - } - - // 保存 - function saveRecord() { - if (selectedCategory === null) { - toast.show({ - title: '请选择分类', - }) - return - } - - if (moneyRecord.amount === '0.00') { - toast.show({ - title: '请输入金额', - }) - return - } - - // 去掉金额负号 - const amount = moneyRecord.amount.replace(/^-+/, '') - - const isUpdate = updateData && updateData !== null - - if (isUpdate) { - updateMoneyRecordApi({ - id: moneyRecord.id, - category_id: selectedCategory.id, - amount, - remark: moneyRecord.remark, - record_time: moneyRecord.record_time, - }).then((res) => { - if (res.isSuccess) { - const result: MoneyRecordItem = { - ...moneyRecord, - category_id: selectedCategory.id, - category_name: selectedCategory.name, - category_icon: selectedCategory.icon, - category_type: selectedCategory.type, - } - - onConfirm?.(result) - - resetData() - } - }).catch((err) => { - console.log(err) - }) - } - else { - insertMoneyRecordApi({ - ...moneyRecord, - amount, - category_id: selectedCategory.id, - }) - .then((res) => { - if (res.isSuccess) { - const result: MoneyRecordItem = { - ...res.data, - category_name: selectedCategory.name, - category_icon: selectedCategory.icon, - category_type: selectedCategory.type, - } - - onConfirm?.(result) - resetData() - } - else { - toast.show({ - title: res.message || '保存失败', - }) - } - }) - .catch((err) => { - console.log(err) - }) - } - } - - return ( - <> - - - - { - fetchCategories() - }} - > - - - - - 取消 - - - - - {(() => { - const list = selectedIndex === 1 ? moneyCategoryStore.income : moneyCategoryStore.expense - - const memoItems = useMemo(() => { - const items = [...list] - 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 - }, [list]) - - return ( - item.id.toString()} - showsVerticalScrollIndicator={false} - renderItem={({ item }) => { - if (!('name' in item)) { - return - } - - const isSelected = selectedCategory?.id === item.id - return ( - handleSelectCategory(item)} - > - - - - - {item.name} - - - ) - }} - /> - ) - })()} - - - {showKeydown && ( - - - - - setShowDatePicker(true)} - > - - {moneyRecord.record_time} - - - {moneyRecord.amount} - - - {keydownInput} - { - setMoneyRecord({ - ...moneyRecord, - remark: val, - }) - }} - onFocus={() => setRemarkIsFocused(true)} - onBlur={() => setRemarkIsFocused(false)} - /> - - - - {keys.map((item, idx) => { - const isFirstCol = idx % 4 === 0 - const isFirstRow = idx < 4 - const isLastCol = (idx + 1) % 4 === 0 - const isLastRow = idx >= 12 - - return ( - handlePress(item.key)} - style={{ - width: '25%', - height: '25%', - justifyContent: 'center', - alignItems: 'center', - borderTopWidth: isFirstRow ? 0.3 : 0, - borderLeftWidth: isFirstCol ? 0.3 : 0, - borderRightWidth: !isLastCol ? 0.3 : 0, - borderBottomWidth: !isLastRow ? 0.3 : 0, - borderColor: '#ddd', - }} - > - {({ pressed }) => { - const defaultBg = item.key === '/' ? '#e6d7b8' : '#fff' - const pressedBg = item.key === '/' ? '#f5e9d4' : '#f2f2f2' - - return ( - <> - - {item.content} - - ) - }} - - ) - })} - - {remarkIsFocused && keyboardHeight > 220 && ( - - )} - - )} - - - - { - setShowDatePicker(false) - }} - onConfirm={(date) => { - setMoneyRecord({ - ...moneyRecord, - record_time: date, - }) - setShowDatePicker(false) - }} - /> - - ) -} diff --git a/code/base-project/base-app/src/app/(tabs)/money/components/TabSelector.tsx b/code/base-project/base-app/src/app/(tabs)/money/components/TabSelector.tsx deleted file mode 100644 index 2e202539..00000000 --- a/code/base-project/base-app/src/app/(tabs)/money/components/TabSelector.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { Pressable, Text, View } from 'react-native' -import Animated, { useAnimatedStyle, withSpring, withTiming } from 'react-native-reanimated' -// import { IconQuxian1 } from '@/components/iconfont' - -interface TabSelectorProps { - options: string[] - selected: number - onSelect: (tab: number) => void -} - -const TabSelector: React.FC = ({ options, selected, onSelect }) => { - return ( - - {options.map((tab, index) => { - const isActive = selected === index - - const animatedStyle = useAnimatedStyle(() => ({ - transform: [ - { rotate: '50deg' }, - { scale: withSpring(isActive ? 1 : 0.8, { damping: 12, stiffness: 120 }) }, - ], - opacity: withTiming(isActive ? 1 : 0, { duration: 200 }), - })) - - return ( - - onSelect(index)} - className="h-full items-center justify-center" - > - - {tab} - - - - {/* */} - - - - ) - })} - - ) -} - -export default TabSelector diff --git a/code/base-project/base-app/src/app/(tabs)/money/components/TransactionsList.tsx b/code/base-project/base-app/src/app/(tabs)/money/components/TransactionsList.tsx index a6d6822a..32ed99bc 100644 --- a/code/base-project/base-app/src/app/(tabs)/money/components/TransactionsList.tsx +++ b/code/base-project/base-app/src/app/(tabs)/money/components/TransactionsList.tsx @@ -10,18 +10,20 @@ import { } from 'react-native-popup-menu' import { LucideIcon } from '@/components/common/ao' import EmptyState from '@/components/common/EmptyStatus' -import MoneyCategoryIcon from './MoneyCategoryIcon' +import MoneyCategoryIcon from '@/components/common/MoneyCategoryIcon' interface StaticTransactionsProps { records: MoneyRecordGroup[] onUpdate?: (data: MoneyRecordItem) => void onDelete?: (id: number) => void + onDetail?: (id: number) => void } export default function StaticTransactions({ records, onUpdate, onDelete, + onDetail, }: StaticTransactionsProps) { const [selectedId, setSelectedId] = useState(null) @@ -87,6 +89,7 @@ export default function StaticTransactions({ placement: 'bottom', preferredPlacement: 'bottom', anchorStyle: { backgroundColor: 'transparent' }, + closeAnimationDuration: 0, }} onSelect={() => setSelectedId(null)} onBackdropPress={() => setSelectedId(null)} @@ -137,6 +140,7 @@ export default function StaticTransactions({ }, }} > + 删除 + { + onDetail?.(item.id) + }} + > + + + 详细 + + ) diff --git a/code/base-project/base-app/src/app/(tabs)/money/index.tsx b/code/base-project/base-app/src/app/(tabs)/money/index.tsx index 9f47b8ed..34643e9f 100644 --- a/code/base-project/base-app/src/app/(tabs)/money/index.tsx +++ b/code/base-project/base-app/src/app/(tabs)/money/index.tsx @@ -1,4 +1,3 @@ -import type { MoneyRecordGroup, MoneyRecordItem, QueryMoneyRecordsResult } from '@/types/money/record' import { useRouter } from 'expo-router' import React, { useEffect, useState } from 'react' import { Text, TouchableOpacity, View } from 'react-native' @@ -7,7 +6,7 @@ import { ADatePicker, LucideIcon } from '@/components/common/ao' import { Fab, FabIcon } from '@/components/ui/fab' import { AddIcon } from '@/components/ui/icon' import { Progress, ProgressFilledTrack } from '@/components/ui/progress' -import { deleteMoneyRecordApi, queryMoneyRecordsApi } from '@/request/api/money/record' +import { queryMoneyRecordsApi } from '@/request/api/money/record' import { useMoneyRecordStore } from '@/store/moneyRecord' import { getCurrentYearMonth } from '@/utils/time' import { toast } from '@/utils/toast' @@ -133,6 +132,12 @@ export default function Money() { params: { id: data.id }, }) }} + onDetail={(id) => { + router.navigate({ + pathname: '/money/record/detail', + params: { id }, + }) + }} onDelete={(id) => { moneyRecordStore.deleteMoneyRecord(id) }} diff --git a/code/base-project/base-app/src/app/_layout.tsx b/code/base-project/base-app/src/app/_layout.tsx index 7e95dee0..91369cc4 100644 --- a/code/base-project/base-app/src/app/_layout.tsx +++ b/code/base-project/base-app/src/app/_layout.tsx @@ -10,6 +10,7 @@ import { MenuProvider } from 'react-native-popup-menu' import { AButton } from '@/components/common/ao' import EmptyState from '@/components/common/EmptyStatus' import { GluestackUIProvider } from '@/components/ui/gluestack-ui-provider' +import { useMoneyCategoryStore } from '@/store/moneyCategory' import { useUserStore } from '@/store/user' import { ToastProviderWrapper } from '@/utils/toast' import '@/styles/global.css' @@ -42,6 +43,7 @@ export function ErrorBoundary({ error, retry }: ErrorBoundaryProps) { export default function RootLayout() { const router = useRouter() const userStore = useUserStore() + const moneyCategoryStore = useMoneyCategoryStore() const [fontsLoaded] = useFonts({ SpaceMono: require('@/static/fonts/SpaceMono-Regular.ttf'), @@ -60,10 +62,12 @@ export default function RootLayout() { } if (!userStore.isLoggedIn()) { - // 未登录直接跳到 auth router.replace('/auth/index') return null } + + moneyCategoryStore.init() + return ( diff --git a/code/base-project/base-app/src/app/money/record/detail/index.tsx b/code/base-project/base-app/src/app/money/record/detail/index.tsx new file mode 100644 index 00000000..8654d633 --- /dev/null +++ b/code/base-project/base-app/src/app/money/record/detail/index.tsx @@ -0,0 +1,89 @@ +import { useLocalSearchParams, useRouter } from 'expo-router' +import { useState } from 'react' +import { Text, View } from 'react-native' +import { AButton, ACell, ACellGroup, AModal } from '@/components/common/ao' +import MoneyCategoryIcon from '@/components/common/MoneyCategoryIcon' +import CustomHeader from '@/components/custom/Header' +import { useMoneyRecordStore } from '@/store/moneyRecord' +import { toDateString, toDateTimeString } from '@/utils/time' + +export default function MoneyRecordDetail() { + const router = useRouter() + const moneyRecordStore = useMoneyRecordStore() + + const { id: detailId } = useLocalSearchParams<{ id: string }>() + + const moneryRecord = moneyRecordStore.getMoneyRecordById(Number(detailId)) + + const [showDeleteModal, setShowDeleteModal] = useState(false) + + if (moneryRecord === null) { + return null + } + + return ( + <> + + + + + + {moneryRecord.category_name} + + + + + + + + + + + + { + setShowDeleteModal(true) + }} + > + 删除 + + { + router.navigate({ + pathname: '/money/record/editor', + params: { id: detailId }, + }) + }} + > + 编辑 + + + + + + + { + setShowDeleteModal(false) + + moneyRecordStore.deleteMoneyRecord(Number(detailId)) + + router.back() + }} + onCancel={() => { + setShowDeleteModal(false) + }} + onClose={() => { + setShowDeleteModal(false) + }} + /> + + ) +} diff --git a/code/base-project/base-app/src/app/money/record/editor/components/CategoryGrid.tsx b/code/base-project/base-app/src/app/money/record/editor/components/CategoryGrid.tsx index 889c5684..e34e8205 100644 --- a/code/base-project/base-app/src/app/money/record/editor/components/CategoryGrid.tsx +++ b/code/base-project/base-app/src/app/money/record/editor/components/CategoryGrid.tsx @@ -2,7 +2,7 @@ import type { ViewStyle } from 'react-native' import type { MoneyCategoryItem } from '@/types/money/category' import React, { useMemo } from 'react' import { FlatList, Pressable, Text, View } from 'react-native' -import MoneyCategoryIcon from './CategoryIcon' +import MoneyCategoryIcon from '@/components/common/MoneyCategoryIcon' interface CategoryGridProps { data: MoneyCategoryItem[] diff --git a/code/base-project/base-app/src/app/money/record/editor/components/CategoryIcon.tsx b/code/base-project/base-app/src/app/money/record/editor/components/CategoryIcon.tsx deleted file mode 100644 index 9fafc0f9..00000000 --- a/code/base-project/base-app/src/app/money/record/editor/components/CategoryIcon.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react' -import IconFont from '@/components/iconfont' - -interface CategoryIconProps { - name?: string - size?: number - color?: string - type?: 0 | 1 -} - -/** - * 分类图标组件 - */ -export default function MoneyCategoryIcon({ - name, - size = 24, - color, -}: CategoryIconProps) { - return -} 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 e84fbc19..e86cbc13 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 @@ -6,7 +6,7 @@ import { Text, TouchableOpacity, View } from 'react-native' import { LucideIcon } from '@/components/common/ao' import CustomHeader from '@/components/custom/Header' import PagerTabs from '@/components/custom/PagerTabs' -import { insertMoneyRecordApi } from '@/request/api/money/record' +import { insertMoneyRecordApi, updateMoneyRecordApi } from '@/request/api/money/record' import { useMoneyCategoryStore } from '@/store/moneyCategory' import { useMoneyRecordStore } from '@/store/moneyRecord' import { getCurrentDate, toDateString } from '@/utils/time' @@ -23,7 +23,6 @@ export default function MoenyRecordEditor() { const income = moneyCategoryStore.getIncomeCategories() const { id: updateId } = useLocalSearchParams<{ id: string }>() - // 自定义键盘 const [showKeydown, setShowKeydown] = useState(false) // 选中分类 @@ -78,34 +77,61 @@ export default function MoenyRecordEditor() { const amount = moneyRecord.amount.replace(/^-+/, '') - insertMoneyRecordApi({ - ...moneyRecord, - amount, - category_id: selectedCategory.id, - }) - .then((res) => { + if (updateId) { + updateMoneyRecordApi({ + id: moneyRecord.id, + category_id: selectedCategory.id, + amount, + remark: moneyRecord.remark, + record_time: moneyRecord.record_time, + }).then((res) => { if (res.isSuccess) { const result: MoneyRecordItem = { - ...res.data, + ...moneyRecord, + category_id: selectedCategory.id, category_name: selectedCategory.name, category_icon: selectedCategory.icon, category_type: selectedCategory.type, } - moneyRecordStore.addMoneyRecord(result) + moneyRecordStore.updateMoneyRecord(result) router.back() } - else { - toast.show({ - title: res.message || '保存失败', - }) - } - }) - .catch((err) => { - console.log('发生异常啦') + }).catch((err) => { console.log(err) }) + } + else { + insertMoneyRecordApi({ + ...moneyRecord, + amount, + category_id: selectedCategory.id, + }) + .then((res) => { + if (res.isSuccess) { + const result: MoneyRecordItem = { + ...res.data, + category_name: selectedCategory.name, + category_icon: selectedCategory.icon, + category_type: selectedCategory.type, + } + + moneyRecordStore.addMoneyRecord(result) + + router.back() + } + else { + toast.show({ + title: res.message || '保存失败', + }) + } + }) + .catch((err) => { + console.log('发生异常啦') + console.log(err) + }) + } } return ( diff --git a/code/base-project/base-app/src/app/system/share/index.tsx b/code/base-project/base-app/src/app/system/share/index.tsx index 0edafdef..9a4710eb 100644 --- a/code/base-project/base-app/src/app/system/share/index.tsx +++ b/code/base-project/base-app/src/app/system/share/index.tsx @@ -1,127 +1,11 @@ import * as React from 'react' -import { Animated, StyleSheet, Text, TouchableOpacity, useWindowDimensions, View } from 'react-native' -import PagerView from 'react-native-pager-view' +import { View } from 'react-native' +import CustomHeader from '@/components/custom/Header' export default function PagerWithTab() { - const layout = useWindowDimensions() - const pagerRef = React.useRef(null) - const [index, setIndex] = React.useState(0) - const [tabWidth, setTabWidth] = React.useState(0) - - // 用 Animated.Value 记录滑动位置(0 ~ 1 ~ 2) - const scrollX = React.useRef(new Animated.Value(0)).current - - const routes = [ - { key: 'first', title: 'Tab1' }, - { key: 'second', title: 'Tab2' }, - ] - - const onTabPress = (i: number) => { - pagerRef.current?.setPage(i) - } - - // 页面滑动回调,把 offset 和 position 转换成 scrollX - const onPageScroll = (e: any) => { - const { position, offset } = e.nativeEvent - scrollX.setValue(position + offset) - } - - // 下划线平滑动画 - const indicatorTranslateX = scrollX.interpolate({ - inputRange: [0, routes.length - 1], - outputRange: [0, tabWidth - tabWidth / routes.length], - extrapolate: 'clamp', - }) - return ( - - {/* Header */} - - Left - - {/* TabBar */} - setTabWidth(e.nativeEvent.layout.width)} - > - {routes.map((r, i) => ( - onTabPress(i)} - > - - {r.title} - - - ))} - - {tabWidth > 0 && ( - - )} - - - Right - - - {/* PagerView */} - setIndex(e.nativeEvent.position)} - > - - 第一个页面内容 - - - 第二个页面内容 - - + + ) } - -const styles = StyleSheet.create({ - header: { - flexDirection: 'row', - alignItems: 'center', - }, - headerSide: { - textAlign: 'center', - }, - tabBar: { - flex: 1, - flexDirection: 'row', - position: 'relative', - }, - tabItem: { - flex: 1, - alignItems: 'center', - paddingVertical: 8, - }, - tabText: { - fontSize: 16, - color: '#333', - }, - indicator: { - height: 2, - backgroundColor: 'blue', - position: 'absolute', - bottom: 0, - left: 0, - }, - page: { - justifyContent: 'center', - alignItems: 'center', - }, -}) diff --git a/code/base-project/base-app/src/app/(tabs)/money/components/MoneyCategoryIcon.tsx b/code/base-project/base-app/src/components/common/MoneyCategoryIcon.tsx similarity index 100% rename from code/base-project/base-app/src/app/(tabs)/money/components/MoneyCategoryIcon.tsx rename to code/base-project/base-app/src/components/common/MoneyCategoryIcon.tsx diff --git a/code/base-project/base-app/src/components/common/SortableFlatList.tsx b/code/base-project/base-app/src/components/common/SortableFlatList.tsx new file mode 100644 index 00000000..e69de29b diff --git a/code/base-project/base-app/src/components/common/ao/Button.tsx b/code/base-project/base-app/src/components/common/ao/Button.tsx index 2fae814e..a43bed0f 100644 --- a/code/base-project/base-app/src/components/common/ao/Button.tsx +++ b/code/base-project/base-app/src/components/common/ao/Button.tsx @@ -51,7 +51,6 @@ const Button: React.FC = ({ return {leftIcon} } - // 只剩下 LucideIconProps & { icon } const { icon, ...props } = leftIcon as LucideIconProps & { icon: keyof typeof icons } return } diff --git a/code/base-project/base-app/src/components/common/ao/Cell.tsx b/code/base-project/base-app/src/components/common/ao/Cell.tsx new file mode 100644 index 00000000..2c618344 --- /dev/null +++ b/code/base-project/base-app/src/components/common/ao/Cell.tsx @@ -0,0 +1,105 @@ +import type { icons } from 'lucide-react-native' +import type { ReactElement, ReactNode } from 'react' +import type { ViewStyle } from 'react-native' +import { Children, cloneElement, isValidElement } from 'react' +import { Text, TouchableOpacity, View } from 'react-native' +import LucideIcon from './LucideIcon' + +/* ================= Cell ================= */ +export interface CellProps { + left?: ReactNode + right?: ReactNode + + title?: string + leftIcon?: keyof typeof icons + rightIcon?: keyof typeof icons + rightText?: string + + isLink?: boolean + className?: string + style?: ViewStyle + + showDivider?: boolean // 由 CellGroup 注入 + + onPress?: () => void +} + +export function Cell({ + left, + right, + title, + leftIcon, + rightIcon = 'ChevronRight', + rightText, + isLink = false, + className, + style, + showDivider = false, + onPress, +}: CellProps) { + return ( + + + {left || ( + + {leftIcon && } + {title && {title}} + + )} + + {right || ( + + {rightText && {rightText}} + {isLink && rightIcon && ( + + )} + + )} + + + {showDivider && ( + + )} + + ) +} + +/* ================= CellGroup ================= */ +export interface CellGroupProps { + className?: string + style?: ViewStyle + children?: ReactNode + showDivider?: boolean // 整组是否显示下划线 +} + +export function CellGroup({ + className, + style, + children, + showDivider = true, +}: CellGroupProps) { + const elements = Children.toArray(children) + + return ( + + {elements.map((child, index) => { + if (!isValidElement(child)) + return child + + const isLast = index === elements.length - 1 + + return cloneElement(child as ReactElement, { + showDivider: showDivider && !isLast, + }) + })} + + ) +} diff --git a/code/base-project/base-app/src/components/common/ao/Modal.tsx b/code/base-project/base-app/src/components/common/ao/Modal.tsx new file mode 100644 index 00000000..8516e420 --- /dev/null +++ b/code/base-project/base-app/src/components/common/ao/Modal.tsx @@ -0,0 +1,94 @@ +import type { ViewStyle } from 'react-native' +import { Text } from 'react-native' +import { CloseIcon, Icon } from '@/components/ui/icon' + +import { + Modal, + ModalBackdrop, + ModalBody, + ModalCloseButton, + ModalContent, + ModalFooter, + ModalHeader, +} from '@/components/ui/modal' +import AButton from '../ao/Button' + +interface AModalProps { + isOpen?: boolean + title?: string | React.ReactNode + body?: string | React.ReactNode + footer?: React.ReactNode + onClose?: () => void + onConfirm?: () => void + onCancel?: () => void + showBackdrop?: boolean + showCloseIcon?: boolean + + className?: string + style?: ViewStyle +} + +export default function AModal({ + isOpen, + title, + body, + footer, + onClose, + onConfirm, + showBackdrop = true, + showCloseIcon = false, + + className, + style, +}: AModalProps) { + return ( + + { showBackdrop && } + + + {title && (typeof title === 'string' ? {title} : title)} + { + showCloseIcon && ( + + + + ) + } + + + {body && (typeof body === 'string' ? {body} : body)} + + + { + footer || ( + <> + + 取消 + + + 确认 + + + ) + } + + + + ) +} 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 b4a5fdce..9332a187 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 @@ -1,18 +1,23 @@ import Button from './Button' +import { Cell, CellGroup } from './Cell' import Checkbox from './Checkbox' import DatePicker from './DatePicker' import Form, { FormRef } from './Form' import FormItem from './FormItem' import Input from './Input' import LucideIcon from './LucideIcon' +import Modal from './Modal' export { Button as AButton, + Cell as ACell, + CellGroup as ACellGroup, Checkbox as ACheckbox, DatePicker as ADatePicker, Form as AForm, FormItem as AFormItem, FormRef as AFormRef, Input as AInput, + Modal as AModal, LucideIcon, } diff --git a/code/base-project/base-app/src/components/custom/PagerTabs.tsx b/code/base-project/base-app/src/components/custom/PagerTabs.tsx index c2f8e535..cc88ba02 100644 --- a/code/base-project/base-app/src/components/custom/PagerTabs.tsx +++ b/code/base-project/base-app/src/components/custom/PagerTabs.tsx @@ -1,5 +1,5 @@ import type { ViewStyle } from 'react-native' -import * as React from 'react' +import { useEffect, useRef, useState } from 'react' import { Animated, Text, TouchableOpacity, View } from 'react-native' import PagerView from 'react-native-pager-view' @@ -31,16 +31,20 @@ export default function PagerTabs({ className, style, }: PagerTabsProps) { - const pagerRef = React.useRef(null) - const [index, setIndex] = React.useState(selectedIndex ?? 0) - const [tabWidth, setTabWidth] = React.useState(0) - const scrollX = React.useRef(new Animated.Value(selectedIndex ?? 0)).current + const pagerRef = useRef(null) + const [index, setIndex] = useState(selectedIndex ?? 0) + const [tabWidth, setTabWidth] = useState(0) + const scrollX = useRef(new Animated.Value(selectedIndex ?? 0)).current - // 外部 selectedIndex 更新时同步内部 index - React.useEffect(() => { + useEffect(() => { if (typeof selectedIndex === 'number' && selectedIndex !== index) { setIndex(selectedIndex) - pagerRef.current?.setPage(selectedIndex) + + requestAnimationFrame(() => { + if (pagerRef.current) { + pagerRef.current.setPage(selectedIndex) + } + }) } }, [selectedIndex]) diff --git a/code/base-project/base-app/src/components/ui/modal/index.tsx b/code/base-project/base-app/src/components/ui/modal/index.tsx new file mode 100644 index 00000000..1130686a --- /dev/null +++ b/code/base-project/base-app/src/components/ui/modal/index.tsx @@ -0,0 +1,276 @@ +'use client' +import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils' +import type { + MotionComponentProps, +} from '@legendapp/motion' +import type { ViewStyle } from 'react-native' +import { createModal } from '@gluestack-ui/core/modal/creator' +import { tva, useStyleContext, withStyleContext } from '@gluestack-ui/utils/nativewind-utils' +import { + AnimatePresence, + createMotionAnimatedComponent, + Motion, +} from '@legendapp/motion' + +import { cssInterop } from 'nativewind' +import React from 'react' +import { Pressable, ScrollView, View } from 'react-native' + +type IAnimatedPressableProps = React.ComponentProps + & MotionComponentProps + +const AnimatedPressable = createMotionAnimatedComponent( + Pressable, +) as React.ComponentType +const SCOPE = 'MODAL' + +type IMotionViewProps = React.ComponentProps + & MotionComponentProps + +const MotionView = Motion.View as React.ComponentType + +const UIModal = createModal({ + Root: withStyleContext(View, SCOPE), + Backdrop: AnimatedPressable, + Content: MotionView, + Body: ScrollView, + CloseButton: Pressable, + Footer: View, + Header: View, + AnimatePresence, +}) + +cssInterop(AnimatedPressable, { className: 'style' }) +cssInterop(MotionView, { className: 'style' }) + +const modalStyle = tva({ + base: 'group/modal w-full h-full justify-center items-center web:pointer-events-none', + variants: { + size: { + xs: '', + sm: '', + md: '', + lg: '', + full: '', + }, + }, +}) + +const modalBackdropStyle = tva({ + base: 'absolute left-0 top-0 right-0 bottom-0 bg-background-dark web:cursor-default', +}) + +const modalContentStyle = tva({ + base: 'bg-background-0 rounded-md overflow-hidden border border-outline-100 shadow-hard-2 p-6', + parentVariants: { + size: { + xs: 'w-[60%] max-w-[360px]', + sm: 'w-[70%] max-w-[420px]', + md: 'w-[80%] max-w-[510px]', + lg: 'w-[90%] max-w-[640px]', + full: 'w-full', + }, + }, +}) + +const modalBodyStyle = tva({ + base: 'mt-2 mb-6', +}) + +const modalCloseButtonStyle = tva({ + base: 'group/modal-close-button z-10 rounded data-[focus-visible=true]:web:bg-background-100 web:outline-0 cursor-pointer', +}) + +const modalHeaderStyle = tva({ + base: 'justify-between items-center flex-row', +}) + +const modalFooterStyle = tva({ + base: 'flex-row justify-end items-center gap-2', +}) + +type IModalProps = React.ComponentProps + & VariantProps & { className?: string } + +type IModalBackdropProps = React.ComponentProps + & VariantProps & { className?: string } + +type IModalContentProps = React.ComponentProps + & VariantProps & { className?: string } + +type IModalHeaderProps = React.ComponentProps + & VariantProps & { className?: string } + +type IModalBodyProps = React.ComponentProps + & VariantProps & { className?: string } + +type IModalFooterProps = React.ComponentProps + & VariantProps & { className?: string } + +type IModalCloseButtonProps = React.ComponentProps + & VariantProps & { className?: string } + +const Modal = React.forwardRef, IModalProps>( + ({ className, size = 'md', ...props }, ref) => ( + + ), +) + +const ModalBackdrop = React.forwardRef< + React.ComponentRef, + IModalBackdropProps +>(({ className, ...props }, ref) => { + return ( + + ) +}) + +const ModalContent = React.forwardRef< + React.ComponentRef, + IModalContentProps +>(({ className, size, ...props }, ref) => { + const { size: parentSize } = useStyleContext(SCOPE) + + return ( + + ) +}) + +const ModalHeader = React.forwardRef< + React.ComponentRef, + IModalHeaderProps +>(({ className, ...props }, ref) => { + return ( + + ) +}) + +const ModalBody = React.forwardRef< + React.ComponentRef, + IModalBodyProps +>(({ className, ...props }, ref) => { + return ( + + ) +}) + +const ModalFooter = React.forwardRef< + React.ComponentRef, + IModalFooterProps +>(({ className, ...props }, ref) => { + return ( + + ) +}) + +const ModalCloseButton = React.forwardRef< + React.ComponentRef, + IModalCloseButtonProps +>(({ className, ...props }, ref) => { + return ( + + ) +}) + +Modal.displayName = 'Modal' +ModalBackdrop.displayName = 'ModalBackdrop' +ModalContent.displayName = 'ModalContent' +ModalHeader.displayName = 'ModalHeader' +ModalBody.displayName = 'ModalBody' +ModalFooter.displayName = 'ModalFooter' +ModalCloseButton.displayName = 'ModalCloseButton' + +export { + Modal, + ModalBackdrop, + ModalBody, + ModalCloseButton, + ModalContent, + ModalFooter, + ModalHeader, +} diff --git a/code/base-project/base-app/src/request/index.ts b/code/base-project/base-app/src/request/index.ts index a70533ab..89b78e6c 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:8085/app', + baseURL: 'http://192.168.1.136:8085/app', requestAdapter: axiosRequestAdapter(), timeout: 5000, cacheFor: null, diff --git a/code/base-project/base-app/src/store/moneyCategory.ts b/code/base-project/base-app/src/store/moneyCategory.ts index e41e48c6..6a06f1a6 100644 --- a/code/base-project/base-app/src/store/moneyCategory.ts +++ b/code/base-project/base-app/src/store/moneyCategory.ts @@ -32,6 +32,12 @@ export const useMoneyCategoryStore = create()( set({ categories: get().categories.filter(c => c.id !== id) }), init: async () => { + const local = get().categories + + if (local && local.length > 0) { + return + } + try { const res = await queryMoneyCategoryApi() if (res.isSuccess) { 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 4d5483de..2461993a 100644 --- a/code/base-project/base-app/src/types/money/record.ts +++ b/code/base-project/base-app/src/types/money/record.ts @@ -9,6 +9,7 @@ export interface MoneyRecordBase { remark: string record_time: string create_time: string + update_time: string } // 查询条件(请求参数) diff --git a/code/base-project/base-app/test2.html b/code/base-project/base-app/test2.html index 6e5c1b85..accdad56 100644 --- a/code/base-project/base-app/test2.html +++ b/code/base-project/base-app/test2.html @@ -1,232 +1,337 @@ - + - - - 共享空间 - + /* Footer hint */ + .hint{font-size:12px;color:var(--muted);text-align:center;margin-top:12px} + + /* small screens */ + @media (max-width:360px){ + .btn{padding:10px} + .space-avatar{width:48px;height:48px;font-size:18px} + } + +
+
+
+
+
共享空间
+
邀请别人加入你的空间,或输入邀请码加入别人空间
+
+
-
-

共享空间(预览)

+ +
+
+
A
+
+
工作协作空间
+
成员 12 • 权限 管理者
+
+
+
-
- - +
+
+
AB12-CD34
+
+ + +
+
+ +
+ + + +
+
+
邀请码有效期:24小时 · 点击分享可选择渠道
+
+ + +
+
+
加入别人的空间
+
通过邀请码或扫码加入
+
+ +
+ + +
+
若对方使用链接邀请,可直接粘贴链接;扫码请使用 App 内相机。
+ +
+
最近加入的空间
+
+
+
P
+
+
产品设计空间
+
邀请人:小李 · 2 天前
+
+
+
+
成员 8
+
+
+ +
+
+
M
+
+
市场推广
+
邀请人:小王 · 1 周前
+
+
+
+
成员 20
+
+
+
+
+ + +
+ +
- -
-

你尚未加入共享空间,你可以创建自己的空间或加入他人的。

+
+
-
生成邀请码
-
扫码加入
+ + - - - - - + + - diff --git a/code/base-project/base-go-v2/internal/api/auth/index.go b/code/base-project/base-go-v2/internal/api/auth/index.go index 3b1d2645..7d6be2cf 100644 --- a/code/base-project/base-go-v2/internal/api/auth/index.go +++ b/code/base-project/base-go-v2/internal/api/auth/index.go @@ -106,7 +106,7 @@ func loginDefault(c *router.Context) error { routinex.Go(func() { kv := mapx.New().SetKV( - "last_login_time", timex.NowString(), + "last_login_time", timex.DateTimeString(), ) _, _ = db.UpdateOne("user_info", "id", userId, kv) }) diff --git a/code/base-project/base-go-v2/internal/api/money/index.go b/code/base-project/base-go-v2/internal/api/money/index.go index 14e20645..90007ec6 100644 --- a/code/base-project/base-go-v2/internal/api/money/index.go +++ b/code/base-project/base-go-v2/internal/api/money/index.go @@ -6,6 +6,7 @@ import ( "base-go-v2/internal/router" "base-go-v2/internal/utils/idgen" "base-go-v2/internal/utils/mapx" + "base-go-v2/internal/utils/timex" "fmt" "sort" "time" @@ -272,6 +273,8 @@ func updateMoneyRecord(c *router.Context) error { updateRecord.SetKV("remark", remark) } + updateRecord.SetKV("update_time", timex.DateTimeString()) + updateOne, err := db.UpdateOne("money_record", "id", recordId, updateRecord) if err != nil { return c.AddError(err) diff --git a/code/base-project/base-go-v2/internal/utils/timex/index.go b/code/base-project/base-go-v2/internal/utils/timex/index.go index d7d547b2..ec7822a4 100644 --- a/code/base-project/base-go-v2/internal/utils/timex/index.go +++ b/code/base-project/base-go-v2/internal/utils/timex/index.go @@ -7,8 +7,8 @@ func NowTime() time.Time { return time.Now() } -// NowString 返回当前时间的字符串格式:YYYY-MM-DD HH:mm:ss -func NowString() string { +// DateTimeString 返回当前时间的字符串格式:YYYY-MM-DD HH:mm:ss +func DateTimeString() string { return time.Now().Format("2006-01-02 15:04:05") }