This commit is contained in:
oneao committed 2025-11-09 20:59:22 +08:00
1 parent 8a6d29f5d0
commit 7ed98a04c1
11 files changed
+1127 -55

No files matched your search

@@ -5,5 +5,16 @@ module.exports = function (api) {
['babel-preset-expo', { jsxImportSource: 'nativewind' }],
'nativewind/babel',
],
plugins: [
[
'module-resolver',
{
root: ['./src'],
alias: {
'@': './src',
},
},
],
],
}
}
+3
View File
@@ -16,6 +16,7 @@
"@gluestack/ui-next-adapter": "^3.0.3",
"@legendapp/motion": "^2.4.0",
"@react-native-async-storage/async-storage": "2.2.0",
"@react-native-segmented-control/segmented-control": "2.5.7",
"alova": "^3.3.4",
"axios": "^1.13.2",
"expo": "~54.0.20",
@@ -35,12 +36,14 @@
"react-native-reanimated": "~4.1.3",
"react-native-safe-area-context": "~5.6.1",
"react-native-screens": "~4.16.0",
"react-native-segmented-control-2": "^2.1.1",
"react-native-svg": "15.12.0",
"zustand": "^5.0.8"
},
"devDependencies": {
"@antfu/eslint-config": "^6.2.0",
"@types/react": "~19.1.0",
"babel-plugin-module-resolver": "^5.0.2",
"eslint": "^9.38.0",
"react-native-iconfont-cli": "^2.2.4",
"tailwindcss": "^3.4.17",
+118 -8
View File
@@ -26,6 +26,9 @@ importers:
'@react-native-async-storage/async-storage':
specifier: 2.2.0
version: 2.2.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))
'@react-native-segmented-control/segmented-control':
specifier: 2.5.7
version: 2.5.7(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
alova:
specifier: ^3.3.4
version: 3.3.4
@@ -83,6 +86,9 @@ 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-segmented-control-2:
specifier: ^2.1.1
version: 2.1.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-svg:
specifier: 15.12.0
version: 15.12.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
@@ -96,6 +102,9 @@ importers:
'@types/react':
specifier: ~19.1.0
version: 19.1.17
babel-plugin-module-resolver:
specifier: ^5.0.2
version: 5.0.2
eslint:
specifier: ^9.38.0
version: 9.39.0(jiti@1.21.7)
@@ -1571,6 +1580,12 @@ packages:
peerDependencies:
react-native: ^0.0.0-0 || >=0.65 <1.0
'@react-native-segmented-control/segmented-control@2.5.7':
resolution: {integrity: sha512-l84YeVX8xAU3lvOJSvV4nK/NbGhIm2gBfveYolwaoCbRp+/SLXtc6mYrQmM9ScXNwU14mnzjQTpTHWl5YPnkzQ==}
peerDependencies:
react: '>=16.0'
react-native: '>=0.62'
'@react-native/assets-registry@0.81.5':
resolution: {integrity: sha512-705B6x/5Kxm1RKRvSv0ADYWm5JOnoiQ1ufW7h8uu2E6G9Of/eE6hP/Ivw3U5jI16ERqZxiKQwk34VJbB0niX9w==}
engines: {node: '>= 20.19.4'}
@@ -2257,6 +2272,9 @@ packages:
resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
babel-plugin-module-resolver@5.0.2:
resolution: {integrity: sha512-9KtaCazHee2xc0ibfqsDeamwDps6FZNo5S0Q81dUqEuFzVwPhcT4J5jOqIVvgCA3Q/wO9hKYxN/Ds3tIsp5ygg==}
babel-plugin-polyfill-corejs2@0.4.14:
resolution: {integrity: sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==}
peerDependencies:
@@ -3198,10 +3216,17 @@ packages:
resolution: {integrity: sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==}
engines: {node: '>= 0.8'}
find-babel-config@2.1.2:
resolution: {integrity: sha512-ZfZp1rQyp4gyuxqt1ZqjFGVeVBvmpURMqdIWXbPRfB97Bf6BzdK/xSIbylEINzQ0kB5tlDQfn9HkNXXWsqTqLg==}
find-up-simple@1.0.1:
resolution: {integrity: sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==}
engines: {node: '>=18'}
find-up@3.0.0:
resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==}
engines: {node: '>=6'}
find-up@4.1.0:
resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==}
engines: {node: '>=8'}
@@ -3324,6 +3349,10 @@ packages:
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
deprecated: Glob versions prior to v9 are no longer supported
glob@9.3.5:
resolution: {integrity: sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==}
engines: {node: '>=16 || 14 >=14.17'}
global-dirs@0.1.1:
resolution: {integrity: sha512-NknMLn7F2J7aflwFOlGdNIuCDpN3VGoSoB+aap3KABFWbHVn1TCgFC+np23J8W2BiZbjfEw3BFBycSMv1AFblg==}
engines: {node: '>=4'}
@@ -3690,56 +3719,48 @@ 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==}
@@ -3784,6 +3805,10 @@ packages:
resolution: {integrity: sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==}
engines: {node: '>=14'}
locate-path@3.0.0:
resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==}
engines: {node: '>=6'}
locate-path@5.0.0:
resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
engines: {node: '>=8'}
@@ -4131,6 +4156,10 @@ packages:
minimatch@3.1.2:
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
minimatch@8.0.4:
resolution: {integrity: sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA==}
engines: {node: '>=16 || 14 >=14.17'}
minimatch@9.0.5:
resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
engines: {node: '>=16 || 14 >=14.17'}
@@ -4138,6 +4167,10 @@ packages:
minimist@1.2.8:
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
minipass@4.2.8:
resolution: {integrity: sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==}
engines: {node: '>=8'}
minipass@7.1.2:
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
engines: {node: '>=16 || 14 >=14.17'}
@@ -4291,6 +4324,10 @@ packages:
resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
engines: {node: '>=10'}
p-locate@3.0.0:
resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==}
engines: {node: '>=6'}
p-locate@4.1.0:
resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==}
engines: {node: '>=8'}
@@ -4331,6 +4368,10 @@ packages:
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
engines: {node: '>= 0.8'}
path-exists@3.0.0:
resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==}
engines: {node: '>=4'}
path-exists@4.0.0:
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
engines: {node: '>=8'}
@@ -4382,6 +4423,10 @@ packages:
pkg-types@2.3.0:
resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==}
pkg-up@3.1.0:
resolution: {integrity: sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==}
engines: {node: '>=8'}
plist@3.1.0:
resolution: {integrity: sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==}
engines: {node: '>=10.4.0'}
@@ -4601,6 +4646,12 @@ packages:
react: '*'
react-native: '*'
react-native-segmented-control-2@2.1.1:
resolution: {integrity: sha512-qtDM4KhPMb/V3R6vVGWkhPVmUNLu+Vt+ypuYbsErFTCXLKMTmYfT+xH1rtvXn9cfzBM+lzjTiI5ij3hgs9/Tug==}
peerDependencies:
react: '>=17 || >=18'
react-native: '>=0.71'
react-native-svg@15.12.0:
resolution: {integrity: sha512-iE25PxIJ6V0C6krReLquVw6R0QTsRTmEQc4K2Co3P6zsimU/jltcDBKYDy1h/5j9S/fqmMeXnpM+9LEWKJKI6A==}
peerDependencies:
@@ -4726,6 +4777,9 @@ packages:
resolution: {integrity: sha512-nYzyjnFcPNGR3lx9lwPPPnuQxv6JWEZd2Ci0u9opN7N5zUEPIhY/GbL3vMGOr2UXwEg9WwSyV9X9Y/kLFgPsOg==}
engines: {node: '>= 4.0.0'}
reselect@4.1.8:
resolution: {integrity: sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==}
reserved-identifiers@1.2.0:
resolution: {integrity: sha512-yE7KUfFvaBFzGPs5H3Ops1RevfUEsDc5Iz65rOwWg4lE8HJSYtle77uul3+573457oHvBKuHYDl/xqUkKpEEdw==}
engines: {node: '>=18'}
@@ -7517,6 +7571,11 @@ snapshots:
merge-options: 3.0.4
react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0)
'@react-native-segmented-control/segmented-control@2.5.7(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/assets-registry@0.81.5': {}
'@react-native/babel-plugin-codegen@0.81.5(@babel/core@7.28.5)':
@@ -8447,6 +8506,14 @@ snapshots:
'@types/babel__core': 7.20.5
'@types/babel__traverse': 7.28.0
babel-plugin-module-resolver@5.0.2:
dependencies:
find-babel-config: 2.1.2
glob: 9.3.5
pkg-up: 3.1.0
reselect: 4.1.8
resolve: 1.22.11
babel-plugin-polyfill-corejs2@0.4.14(@babel/core@7.28.5):
dependencies:
'@babel/compat-data': 7.28.5
@@ -9501,8 +9568,16 @@ snapshots:
transitivePeerDependencies:
- supports-color
find-babel-config@2.1.2:
dependencies:
json5: 2.2.3
find-up-simple@1.0.1: {}
find-up@3.0.0:
dependencies:
locate-path: 3.0.0
find-up@4.1.0:
dependencies:
locate-path: 5.0.0
@@ -9628,6 +9703,13 @@ snapshots:
once: 1.4.0
path-is-absolute: 1.0.1
glob@9.3.5:
dependencies:
fs.realpath: 1.0.0
minimatch: 8.0.4
minipass: 4.2.8
path-scurry: 1.11.1
global-dirs@0.1.1:
dependencies:
ini: 1.3.8
@@ -10047,6 +10129,11 @@ snapshots:
pkg-types: 2.3.0
quansync: 0.2.11
locate-path@3.0.0:
dependencies:
p-locate: 3.0.0
path-exists: 3.0.0
locate-path@5.0.0:
dependencies:
p-locate: 4.1.0
@@ -10795,12 +10882,18 @@ snapshots:
dependencies:
brace-expansion: 1.1.12
minimatch@8.0.4:
dependencies:
brace-expansion: 2.0.2
minimatch@9.0.5:
dependencies:
brace-expansion: 2.0.2
minimist@1.2.8: {}
minipass@4.2.8: {}
minipass@7.1.2: {}
minizlib@3.1.0:
@@ -10950,6 +11043,10 @@ snapshots:
dependencies:
yocto-queue: 0.1.0
p-locate@3.0.0:
dependencies:
p-limit: 2.3.0
p-locate@4.1.0:
dependencies:
p-limit: 2.3.0
@@ -10982,6 +11079,8 @@ snapshots:
parseurl@1.3.3: {}
path-exists@3.0.0: {}
path-exists@4.0.0: {}
path-is-absolute@1.0.1: {}
@@ -11021,6 +11120,10 @@ snapshots:
exsolve: 1.0.7
pathe: 2.0.3
pkg-up@3.1.0:
dependencies:
find-up: 3.0.0
plist@3.1.0:
dependencies:
'@xmldom/xmldom': 0.8.11
@@ -11278,6 +11381,11 @@ 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-segmented-control-2@2.1.1(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-svg@15.12.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:
css-select: 5.2.2
@@ -11480,6 +11588,8 @@ snapshots:
rc: 1.2.8
resolve: 1.7.1
reselect@4.1.8: {}
reserved-identifiers@1.2.0: {}
resolve-from@4.0.0: {}
@@ -1,53 +1,159 @@
import { LinearGradient } from 'expo-linear-gradient'
import { useState } from 'react'
import type { SegmentedTabItem } from '@/components/common/ao/SegmentedTabs'
import React, { useEffect, useState } from 'react'
import { Pressable, Text, View } from 'react-native'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import Animated, { useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'
import { SafeAreaView } from 'react-native-safe-area-context'
import { LucideIcon } from '@/components/common/ao'
import SegmentedTabs from '@/components/common/ao/SegmentedTabs'
import { Button, ButtonText } from '@/components/ui/button'
export default function Money() {
const insets = useSafeAreaInsets()
const [active, setActive] = useState<'明细' | '视图'>('明细')
import {
Drawer,
DrawerBackdrop,
DrawerBody,
DrawerCloseButton,
DrawerContent,
DrawerFooter,
DrawerHeader,
} from '@/components/ui/drawer'
import { CloseIcon, Icon } from '@/components/ui/icon'
function Detail() {
const [dateSelected, setDateSelected] = useState(false)
const [filterOpen, setFilterOpen] = useState(false)
useEffect(() => {
console.log('Detail加载了')
}, [])
// 动画角度
const dateRotation = useSharedValue(0)
const filterRotation = useSharedValue(0)
const toggleDate = () => {
setDateSelected(prev => !prev)
dateRotation.value = withTiming(dateRotation.value === 0 ? 180 : 0, { duration: 200 })
}
const toggleFilter = () => {
setFilterOpen(prev => !prev)
filterRotation.value = withTiming(filterRotation.value === 0 ? 180 : 0, { duration: 200 })
}
const dateAnimatedStyle = useAnimatedStyle(() => ({
transform: [{ rotate: `${dateRotation.value}deg` }],
}))
const filterAnimatedStyle = useAnimatedStyle(() => ({
transform: [{ rotate: `${filterRotation.value}deg` }],
}))
return (
<LinearGradient
colors={['#fffaf0', '#fefefe', '#f0fff4']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={{
flex: 1,
paddingTop: insets.top,
paddingBottom: insets.bottom,
paddingLeft: insets.left,
paddingRight: insets.right,
}}
>
<View style={{ flexDirection: 'row' }}>
{['明细', '视图'].map((item) => {
const isActive = active === item
return (
<Pressable
key={item}
onPress={() => setActive(item as '明细' | '视图')}
style={{ alignItems: 'center', paddingVertical: 12 }}
>
<Text style={{ fontSize: isActive ? 22 : 16, color: isActive ? '#333' : '#888', fontWeight: isActive ? '600' : '400' }}>
{item}
</Text>
{/* 底部指示线 */}
{isActive && (
<View
style={{
marginTop: 4,
height: 2,
width: 24,
backgroundColor: '#007AFF',
borderRadius: 1,
}}
/>
)}
</Pressable>
)
})}
<View>
<View className="flex-row justify-between">
<Pressable
className="flex-row items-center gap-1"
onPress={toggleDate}
>
<Text style={{ fontSize: 16 }}>今天</Text>
<Animated.View style={dateAnimatedStyle}>
<LucideIcon icon="ChevronDown" size={18} />
</Animated.View>
</Pressable>
<Pressable
className="flex-row items-center gap-1"
onPress={toggleFilter}
>
<Text style={{ fontSize: 16 }}>筛选</Text>
<Animated.View style={filterAnimatedStyle}>
<LucideIcon icon="ChevronDown" size={18} />
</Animated.View>
</Pressable>
</View>
</LinearGradient>
<Drawer
isOpen={dateSelected}
anchor="bottom"
onClose={() => {
setDateSelected(false)
}}
size="md"
>
<DrawerBackdrop />
<DrawerContent style={{ padding: 12 }}>
<DrawerBody style={{ padding: 0, marginTop: 0, marginBottom: 0 }}>
<Text>This is the basic drawer component.</Text>
<Text>This is the basic drawer component.</Text>
<Text>This is the basic drawer component.</Text>
<Text>This is the basic drawer component.</Text>
<Text>This is the basic drawer component.</Text>
<Text>This is the basic drawer component.</Text>
<Text>This is the basic drawer component.</Text>
<Text>This is the basic drawer component.</Text>
<Text>This is the basic drawer component.</Text>
<Text>This is the basic drawer component.</Text>
<Text>This is the basic drawer component.</Text>
<Text>This is the basic drawer component.</Text>
<Text>This is the basic drawer component.</Text>
<Text>This is the basic drawer component.</Text>
<Text>This is the basic drawer component.</Text>
<Text>This is the basic drawer component.</Text>
<Text>This is the basic drawer component.</Text>
<Text>This is the basic drawer component.</Text>
<Text>This is the basic drawer component.</Text>
</DrawerBody>
</DrawerContent>
</Drawer>
</View>
)
}
function Chart() {
useEffect(() => {
console.log('Chart记载了')
}, [])
return (
<View>
<Text>这里是视图内这里是视图内容这里是视图内容这里是视图内容这里是视图内容这里是视图内容这里是视图内容这里是视图内容这里是视图内容这里是视图内容容</Text>
</View>
)
}
export default function MoneyTest() {
const [selectedIndex, setSelectedIndex] = useState(0)
const tabs: SegmentedTabItem[] = [
{
label: '明细',
icon: 'ScrollText',
content: <Detail />, // ✅ 注意这里不要调用 Detail()
},
{
label: '视图',
icon: 'ChartPie',
content: <Chart />, // ✅ 注意这里不要调用 Chart()
},
{
label: '视图2',
icon: 'ChartPie',
content: (
<View>
<Text>这里是视图内容2</Text>
</View>
),
},
]
return (
<SafeAreaView style={{ flex: 1, paddingTop: 4, paddingLeft: 8, paddingRight: 8 }}>
<SegmentedTabs
headerWidth={220}
tabs={tabs}
value={selectedIndex}
onChange={setSelectedIndex}
cacheContent={true}
/>
</SafeAreaView>
)
}
@@ -0,0 +1,33 @@
import type { GestureResponderEvent, ViewStyle } from 'react-native'
import { icons } from 'lucide-react-native'
import React from 'react'
import { Pressable } from 'react-native'
interface LucideIconProps {
icon: keyof typeof icons
size?: number
color?: string
strokeWidth?: number
onPress?: (event: GestureResponderEvent) => void
style?: ViewStyle
}
export default function LucideIcon({
icon,
size = 16,
color = '#000',
strokeWidth = 2,
onPress,
style,
}: LucideIconProps) {
const IconComponent = icons[icon]
if (!IconComponent)
return null
return (
<Pressable onPress={onPress} style={style}>
<IconComponent size={size} color={color} strokeWidth={strokeWidth} />
</Pressable>
)
}
@@ -0,0 +1,247 @@
import type { icons } from 'lucide-react-native'
import type { LayoutChangeEvent, StyleProp, TextStyle, ViewStyle } from 'react-native'
import React, { useEffect, useRef, useState } from 'react'
import { Platform, Pressable, Text, View } from 'react-native'
import Animated, { useAnimatedStyle, useSharedValue, withSpring, withTiming } from 'react-native-reanimated'
import LucideIcon from './LucideIcon'
export interface SegmentedTabItem {
label: string
icon?: keyof typeof icons
content?: React.ReactNode
}
interface SegmentedTabsProps {
tabs: SegmentedTabItem[]
value?: number
defaultIndex?: number
onChange?: (index: number) => void
className?: string
style?: StyleProp<ViewStyle>
tabHeight?: number
borderRadius?: number
spacing?: number
sliderColor?: string
sliderStyle?: StyleProp<ViewStyle>
selectedTextStyle?: StyleProp<TextStyle>
unselectedTextStyle?: StyleProp<TextStyle>
selectedColor?: string
unselectedColor?: string
fontSize?: number
iconSize?: number
contentContainerStyle?: StyleProp<ViewStyle>
animateContent?: boolean
cacheContent?: boolean
headerWidth?: number
contentWidth?: number
}
export default function SegmentedTabs({
tabs,
value,
defaultIndex = 0,
onChange,
className,
style,
tabHeight = 36,
borderRadius = 2,
spacing = 4,
sliderColor = '#fff',
sliderStyle,
selectedTextStyle,
unselectedTextStyle,
selectedColor = '#000',
unselectedColor = '#666',
fontSize = 14,
iconSize = 14,
contentContainerStyle,
animateContent = true,
cacheContent = true,
headerWidth,
contentWidth,
}: SegmentedTabsProps) {
const [selectedIndex, setSelectedIndex] = useState(value ?? defaultIndex)
const [headerMeasuredWidth, setHeaderMeasuredWidth] = useState(0)
const [contentMeasuredWidth, setContentMeasuredWidth] = useState(0)
const [ready, setReady] = useState(false)
const sliderX = useSharedValue(0)
const contentX = useSharedValue(0)
const initialized = useRef(false)
const actualHeaderWidth = headerWidth ?? headerMeasuredWidth
const actualContentWidth = contentWidth ?? contentMeasuredWidth
const tabWidth = actualHeaderWidth ? actualHeaderWidth / tabs.length : 0
const sliderWidth = tabWidth - spacing * 2
const sliderHeight = tabHeight - spacing * 2
// 外部 value 改变
useEffect(() => {
if (value !== undefined)
setSelectedIndex(value)
}, [value])
// 滑块和内容动画
useEffect(() => {
if (!ready || tabWidth === 0 || actualContentWidth === 0)
return
const target = selectedIndex * tabWidth + spacing
if (!initialized.current) {
sliderX.value = target
contentX.value = -selectedIndex * actualContentWidth
initialized.current = true
}
else {
sliderX.value = withSpring(target, { damping: 18, stiffness: 150, mass: 1 })
contentX.value = animateContent
? withTiming(-selectedIndex * actualContentWidth, { duration: 250 })
: -selectedIndex * actualContentWidth
}
}, [selectedIndex, tabWidth, actualContentWidth, ready])
const animatedSliderStyle = useAnimatedStyle(() => ({
transform: [{ translateX: sliderX.value }],
}))
const animatedContentStyle = useAnimatedStyle(() => ({
transform: [{ translateX: contentX.value }],
}))
const handlePress = (index: number) => {
if (value === undefined)
setSelectedIndex(index)
onChange?.(index)
}
const handleHeaderLayout = (event: LayoutChangeEvent) => {
const { width } = event.nativeEvent.layout
setHeaderMeasuredWidth(width)
if (width > 0 && actualContentWidth > 0 && !ready)
setReady(true)
}
const handleContentLayout = (event: LayoutChangeEvent) => {
const { width } = event.nativeEvent.layout
setContentMeasuredWidth(width)
if (width > 0 && actualHeaderWidth > 0 && !ready)
setReady(true)
}
return (
<View className="w-full h-full">
{/* Header */}
<View
className={className}
style={[
{
width: headerWidth,
flexDirection: 'row',
backgroundColor: '#f2f2f2',
borderRadius,
height: tabHeight,
overflow: 'hidden',
},
style,
]}
onLayout={handleHeaderLayout}
>
{/* 滑块 */}
{ready && (
<Animated.View
style={[
{
position: 'absolute',
top: spacing,
left: 0,
width: sliderWidth,
height: sliderHeight,
borderRadius,
backgroundColor: sliderColor,
...Platform.select({ android: { elevation: 6 } }),
},
sliderStyle,
animatedSliderStyle,
]}
/>
)}
{tabs.map((tab, i) => {
const isActive = i === selectedIndex
return (
<Pressable
key={i}
onPress={() => handlePress(i)}
style={{
width: tabWidth,
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'row',
gap: spacing,
}}
>
{tab.icon && (
<LucideIcon
icon={tab.icon}
size={iconSize}
color={isActive ? selectedColor : unselectedColor}
/>
)}
<Text
style={[
{ color: isActive ? selectedColor : unselectedColor, fontSize, fontWeight: 'bold' },
isActive ? selectedTextStyle : unselectedTextStyle,
]}
>
{tab.label}
</Text>
</Pressable>
)
})}
</View>
{/* Content */}
<View
style={[
{ width: contentWidth ?? '100%', flex: 1, overflow: 'hidden', marginTop: 16 },
contentContainerStyle,
]}
onLayout={handleContentLayout}
>
<Animated.View
style={[
{ flexDirection: 'row', width: actualContentWidth * tabs.length },
animatedContentStyle,
]}
>
{tabs.map((tab, i) => {
const isActive = i === selectedIndex
const shouldRender = cacheContent || isActive
if (!shouldRender) {
// 占位保持宽度
return <View key={`placeholder-${i}`} style={{ width: actualContentWidth }} />
}
return (
<View
key={isActive ? `tab-${i}-${selectedIndex}` : i} // ✅ 动态 key,确保重新挂载
style={{ width: actualContentWidth }}
>
{tab.content}
</View>
)
})}
</Animated.View>
</View>
</View>
)
}
@@ -3,6 +3,7 @@ import Checkbox from './Checkbox'
import Form, { FormRef } from './Form'
import FormItem from './FormItem'
import Input from './Input'
import LucideIcon from './LucideIcon'
export {
Button as AButton,
@@ -11,4 +12,5 @@ export {
FormItem as AFormItem,
FormRef as AFormRef,
Input as AInput,
LucideIcon,
}
@@ -0,0 +1,355 @@
'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 as createDrawer } 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 {
Dimensions,
Pressable,
ScrollView,
View,
} from 'react-native'
type IAnimatedPressableProps = React.ComponentProps<typeof Pressable>
& MotionComponentProps<typeof Pressable, ViewStyle, unknown, unknown, unknown>
const AnimatedPressable = createMotionAnimatedComponent(
Pressable,
) as React.ComponentType<IAnimatedPressableProps>
const SCOPE = 'MODAL'
const screenWidth = Dimensions.get('window').width
const screenHeight = Dimensions.get('window').height
const sizes: { [key: string]: number } = {
sm: 0.25,
md: 0.5,
lg: 0.75,
full: 1,
}
type IMotionViewProps = React.ComponentProps<typeof View>
& MotionComponentProps<typeof View, ViewStyle, unknown, unknown, unknown>
const MotionView = Motion.View as React.ComponentType<IMotionViewProps>
const UIDrawer = createDrawer({
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 drawerStyle = tva({
base: 'w-full h-full web:pointer-events-none relative',
variants: {
size: {
sm: '',
md: '',
lg: '',
full: '',
},
anchor: {
left: 'items-start',
right: 'items-end',
top: 'justify-start',
bottom: 'justify-end',
},
},
})
const drawerBackdropStyle = tva({
base: 'absolute left-0 top-0 right-0 bottom-0 bg-background-dark web:cursor-default',
})
const drawerContentStyle = tva({
base: 'bg-background-0 overflow-scroll border-outline-100 p-6 absolute',
parentVariants: {
size: {
sm: 'w-1/4',
md: 'w-1/2',
lg: 'w-3/4',
full: 'w-full',
},
anchor: {
left: 'h-full border-r',
right: 'h-full border-l',
top: 'w-full border-b',
bottom: 'w-full border-t',
},
},
parentCompoundVariants: [
{
anchor: 'top',
size: 'sm',
class: 'h-1/4',
},
{
anchor: 'top',
size: 'md',
class: 'h-1/2',
},
{
anchor: 'top',
size: 'lg',
class: 'h-3/4',
},
{
anchor: 'top',
size: 'full',
class: 'h-full',
},
{
anchor: 'bottom',
size: 'sm',
class: 'h-1/4',
},
{
anchor: 'bottom',
size: 'md',
class: 'h-1/2',
},
{
anchor: 'bottom',
size: 'lg',
class: 'h-3/4',
},
{
anchor: 'bottom',
size: 'full',
class: 'h-full',
},
],
})
const drawerCloseButtonStyle = tva({
base: 'z-10 rounded data-[focus-visible=true]:web:bg-background-100 web:outline-0 cursor-pointer',
})
const drawerHeaderStyle = tva({
base: 'justify-between items-center flex-row',
})
const drawerBodyStyle = tva({
base: 'mt-4 mb-6 shrink-0',
})
const drawerFooterStyle = tva({
base: 'flex-row justify-end items-center',
})
type IDrawerProps = React.ComponentProps<typeof UIDrawer>
& VariantProps<typeof drawerStyle> & { className?: string }
type IDrawerBackdropProps = React.ComponentProps<typeof UIDrawer.Backdrop>
& VariantProps<typeof drawerBackdropStyle> & { className?: string }
type IDrawerContentProps = React.ComponentProps<typeof UIDrawer.Content>
& VariantProps<typeof drawerContentStyle> & { className?: string }
type IDrawerHeaderProps = React.ComponentProps<typeof UIDrawer.Header>
& VariantProps<typeof drawerHeaderStyle> & { className?: string }
type IDrawerBodyProps = React.ComponentProps<typeof UIDrawer.Body>
& VariantProps<typeof drawerBodyStyle> & { className?: string }
type IDrawerFooterProps = React.ComponentProps<typeof UIDrawer.Footer>
& VariantProps<typeof drawerFooterStyle> & { className?: string }
type IDrawerCloseButtonProps = React.ComponentProps<
typeof UIDrawer.CloseButton
>
& VariantProps<typeof drawerCloseButtonStyle> & { className?: string }
const Drawer = React.forwardRef<
React.ComponentRef<typeof UIDrawer>,
IDrawerProps
>(({ className, size = 'sm', anchor = 'left', ...props }, ref) => {
return (
<UIDrawer
ref={ref}
{...props}
pointerEvents="box-none"
className={drawerStyle({ size, anchor, class: className })}
context={{ size, anchor }}
/>
)
})
const DrawerBackdrop = React.forwardRef<
React.ComponentRef<typeof UIDrawer.Backdrop>,
IDrawerBackdropProps
>(({ className, ...props }, ref) => {
return (
<UIDrawer.Backdrop
ref={ref}
initial={{
opacity: 0,
}}
animate={{
opacity: 0.5,
}}
exit={{
opacity: 0,
}}
transition={{
type: 'spring',
damping: 18,
stiffness: 250,
opacity: {
type: 'timing',
duration: 250,
},
}}
{...props}
className={drawerBackdropStyle({
class: className,
})}
/>
)
})
const DrawerContent = React.forwardRef<
React.ComponentRef<typeof UIDrawer.Content>,
IDrawerContentProps
>(({ className, ...props }, ref) => {
const { size: parentSize, anchor: parentAnchor } = useStyleContext(SCOPE)
const drawerHeight = screenHeight * (sizes[parentSize] || sizes.md)
const drawerWidth = screenWidth * (sizes[parentSize] || sizes.md)
const isHorizontal = parentAnchor === 'left' || parentAnchor === 'right'
const initialObj = isHorizontal
? { x: parentAnchor === 'left' ? -drawerWidth : drawerWidth }
: { y: parentAnchor === 'top' ? -drawerHeight : drawerHeight }
const animateObj = isHorizontal ? { x: 0 } : { y: 0 }
const exitObj = isHorizontal
? { x: parentAnchor === 'left' ? -drawerWidth : drawerWidth }
: { y: parentAnchor === 'top' ? -drawerHeight : drawerHeight }
const customClass = isHorizontal
? `top-0 ${parentAnchor === 'left' ? 'left-0' : 'right-0'}`
: `left-0 ${parentAnchor === 'top' ? 'top-0' : 'bottom-0'}`
return (
<UIDrawer.Content
ref={ref}
initial={initialObj}
animate={animateObj}
exit={exitObj}
transition={{
type: 'timing',
duration: 300,
}}
{...props}
className={drawerContentStyle({
parentVariants: {
size: parentSize,
anchor: parentAnchor,
},
class: `${className} ${customClass}`,
})}
pointerEvents="auto"
/>
)
})
const DrawerHeader = React.forwardRef<
React.ComponentRef<typeof UIDrawer.Header>,
IDrawerHeaderProps
>(({ className, ...props }, ref) => {
return (
<UIDrawer.Header
ref={ref}
{...props}
className={drawerHeaderStyle({
class: className,
})}
/>
)
})
const DrawerBody = React.forwardRef<
React.ComponentRef<typeof UIDrawer.Body>,
IDrawerBodyProps
>(({ className, ...props }, ref) => {
return (
<UIDrawer.Body
ref={ref}
{...props}
className={drawerBodyStyle({
class: className,
})}
/>
)
})
const DrawerFooter = React.forwardRef<
React.ComponentRef<typeof UIDrawer.Footer>,
IDrawerFooterProps
>(({ className, ...props }, ref) => {
return (
<UIDrawer.Footer
ref={ref}
{...props}
className={drawerFooterStyle({
class: className,
})}
/>
)
})
const DrawerCloseButton = React.forwardRef<
React.ComponentRef<typeof UIDrawer.CloseButton>,
IDrawerCloseButtonProps
>(({ className, ...props }, ref) => {
return (
<UIDrawer.CloseButton
ref={ref}
{...props}
className={drawerCloseButtonStyle({
class: className,
})}
/>
)
})
Drawer.displayName = 'Drawer'
DrawerBackdrop.displayName = 'DrawerBackdrop'
DrawerContent.displayName = 'DrawerContent'
DrawerHeader.displayName = 'DrawerHeader'
DrawerBody.displayName = 'DrawerBody'
DrawerFooter.displayName = 'DrawerFooter'
DrawerCloseButton.displayName = 'DrawerCloseButton'
export {
Drawer,
DrawerBackdrop,
DrawerBody,
DrawerCloseButton,
DrawerContent,
DrawerFooter,
DrawerHeader,
}
@@ -30,7 +30,7 @@ function isWhiteListed(url: string) {
}
const alovaInstance = createAlova({
baseURL: 'http://192.168.1.136:8085/app',
baseURL: 'http://192.168.5.6:8085/app',
requestAdapter: axiosRequestAdapter(),
timeout: 5000,
responded: {
+205
View File
@@ -0,0 +1,205 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>账单卡片 + 日期筛选</title>
<style>
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: #f7f8fa;
padding: 12px;
}
/* 顶部搜索 + 筛选 */
.filter-bar {
display: flex;
align-items: center;
background: #fff;
border-radius: 12px;
box-shadow: 0 2px 6px rgba(0,0,0,0.05);
padding: 8px 10px;
margin-bottom: 12px;
}
.search-box {
flex: 1;
display: flex;
align-items: center;
background: #f2f3f7;
border-radius: 8px;
padding: 6px 10px;
}
.search-box svg {
width: 16px;
height: 16px;
fill: #999;
}
.search-box input {
flex: 1;
border: none;
outline: none;
background: transparent;
margin-left: 6px;
font-size: 14px;
}
.filter-btn {
background: #007aff;
color: #fff;
padding: 7px 14px;
border-radius: 8px;
margin-left: 8px;
font-size: 13px;
}
/* 卡片 */
.card {
background: #fff;
border-radius: 16px;
padding: 16px;
box-shadow: 0 3px 8px rgba(0,0,0,0.08);
margin-bottom: 12px;
}
.card-date {
font-size: 12px;
color: #888;
margin-bottom: 10px;
}
.card-amount {
font-size: 24px;
font-weight: bold;
color: #222;
margin-bottom: 12px;
}
.card-summary {
display: flex;
justify-content: space-between;
font-size: 14px;
color: #555;
}
/* 折叠日期筛选面板 */
.filter-panel {
background: #fff;
border-radius: 12px;
padding: 12px 16px;
box-shadow: 0 3px 8px rgba(0,0,0,0.08);
max-height: 0;
overflow: hidden;
transition: max-height 0.35s ease;
margin-bottom: 12px;
}
.filter-panel.show {
max-height: 220px;
}
.filter-panel .panel-title {
font-size: 13px;
color: #666;
margin-bottom: 6px;
margin-top: 6px;
}
.filter-panel .date-input {
width: 100%;
padding: 8px 12px;
font-size: 14px;
border-radius: 8px;
border: 1px solid #ddd;
}
.filter-panel .panel-actions {
display: flex;
gap: 8px;
margin-top: 12px;
}
.filter-panel .btn {
flex: 1;
text-align: center;
padding: 10px 0;
border-radius: 10px;
font-size: 14px;
}
.btn.reset {
background-color: #f2f3f7;
color: #333;
}
.btn.confirm {
background-color: #007aff;
color: #fff;
}
</style>
</head>
<body>
<!-- 搜索 + 筛选 -->
<div class="filter-bar">
<div class="search-box">
<svg viewBox="0 0 24 24"><path d="M10,2A8,8 0 0,1 18,10C18,11.85 17.37,13.55 16.28,14.86L22,20.59L20.59,22L14.86,16.28C13.55,17.37 11.85,18 10,18A8,8 0 0,1 2,10A8,8 0 0,1 10,2Z"/></svg>
<input type="text" placeholder="搜索备注、分类...">
</div>
<div class="filter-btn" onclick="togglePanel()">筛选</div>
</div>
<!-- 日期筛选面板 -->
<div id="filterPanel" class="filter-panel">
<div class="panel-title">选择日期</div>
<input type="date" id="dateInput" class="date-input">
<div class="panel-actions">
<div class="btn reset" onclick="resetDate()">重置</div>
<div class="btn confirm" onclick="applyDate()">确定</div>
</div>
</div>
<!-- 卡片 -->
<div class="card">
<div id="cardDate" class="card-date">当前周期:本月</div>
<div id="cardRemaining" class="card-amount">剩余:¥2500</div>
<div class="card-summary">
<div>收入:¥5000</div>
<div>支出:¥2500</div>
</div>
</div>
<script>
const panel = document.getElementById('filterPanel');
const cardDate = document.getElementById('cardDate');
const cardRemaining = document.getElementById('cardRemaining');
function togglePanel() {
panel.classList.toggle('show');
}
function resetDate() {
document.getElementById('dateInput').value = '';
cardDate.innerText = "当前周期:本月";
cardRemaining.innerText = "剩余:¥2500";
panel.classList.remove('show');
}
function applyDate() {
const dateVal = document.getElementById('dateInput').value;
if(dateVal) {
// 模拟根据日期计算的金额,这里可替换为真实数据逻辑
cardDate.innerText = `当前周期:${dateVal}`;
cardRemaining.innerText = `剩余:¥${Math.floor(Math.random()*5000)}`;
}
panel.classList.remove('show');
}
</script>
</body>
</html>