diff --git a/code/base-project/base-app/babel.config.js b/code/base-project/base-app/babel.config.js index 39d20356..c771c108 100644 --- a/code/base-project/base-app/babel.config.js +++ b/code/base-project/base-app/babel.config.js @@ -5,5 +5,16 @@ module.exports = function (api) { ['babel-preset-expo', { jsxImportSource: 'nativewind' }], 'nativewind/babel', ], + plugins: [ + [ + 'module-resolver', + { + root: ['./src'], + alias: { + '@': './src', + }, + }, + ], + ], } } diff --git a/code/base-project/base-app/package.json b/code/base-project/base-app/package.json index 02f62c2b..84b09590 100644 --- a/code/base-project/base-app/package.json +++ b/code/base-project/base-app/package.json @@ -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", diff --git a/code/base-project/base-app/pnpm-lock.yaml b/code/base-project/base-app/pnpm-lock.yaml index fba97d33..c2266f2a 100644 --- a/code/base-project/base-app/pnpm-lock.yaml +++ b/code/base-project/base-app/pnpm-lock.yaml @@ -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: {} diff --git a/code/base-project/base-app/src/app/(tabs)/money.tsx b/code/base-project/base-app/src/app/(tabs)/money.tsx index 10f87ba7..8397177e 100644 --- a/code/base-project/base-app/src/app/(tabs)/money.tsx +++ b/code/base-project/base-app/src/app/(tabs)/money.tsx @@ -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 ( - - - {['明细', '视图'].map((item) => { - const isActive = active === item - return ( - setActive(item as '明细' | '视图')} - style={{ alignItems: 'center', paddingVertical: 12 }} - > - - {item} - - {/* 底部指示线 */} - {isActive && ( - - )} - - ) - })} + + + + 今天 + + + + + + + 筛选 + + + + - + + { + setDateSelected(false) + }} + size="md" + > + + + + This is the basic drawer component. + This is the basic drawer component. + This is the basic drawer component. + This is the basic drawer component. + This is the basic drawer component. + This is the basic drawer component. + This is the basic drawer component. + This is the basic drawer component. + This is the basic drawer component. + This is the basic drawer component. + This is the basic drawer component. + This is the basic drawer component. + This is the basic drawer component. + This is the basic drawer component. + This is the basic drawer component. + This is the basic drawer component. + This is the basic drawer component. + This is the basic drawer component. + This is the basic drawer component. + + + + + ) +} + +function Chart() { + useEffect(() => { + console.log('Chart记载了') + }, []) + return ( + + 这里是视图内这里是视图内容这里是视图内容这里是视图内容这里是视图内容这里是视图内容这里是视图内容这里是视图内容这里是视图内容这里是视图内容容 + + ) +} + +export default function MoneyTest() { + const [selectedIndex, setSelectedIndex] = useState(0) + const tabs: SegmentedTabItem[] = [ + { + label: '明细', + icon: 'ScrollText', + content: , // ✅ 注意这里不要调用 Detail() + }, + { + label: '视图', + icon: 'ChartPie', + content: , // ✅ 注意这里不要调用 Chart() + }, + { + label: '视图2', + icon: 'ChartPie', + content: ( + + 这里是视图内容2 + + ), + }, + ] + + return ( + + + ) } diff --git a/code/base-project/base-app/src/components/common/ao/LucideIcon.tsx b/code/base-project/base-app/src/components/common/ao/LucideIcon.tsx new file mode 100644 index 00000000..509f85d4 --- /dev/null +++ b/code/base-project/base-app/src/components/common/ao/LucideIcon.tsx @@ -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 ( + + + + ) +} diff --git a/code/base-project/base-app/src/components/common/ao/SegmentedTabs.tsx b/code/base-project/base-app/src/components/common/ao/SegmentedTabs.tsx new file mode 100644 index 00000000..5c5dcb67 --- /dev/null +++ b/code/base-project/base-app/src/components/common/ao/SegmentedTabs.tsx @@ -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 + + tabHeight?: number + borderRadius?: number + spacing?: number + + sliderColor?: string + sliderStyle?: StyleProp + + selectedTextStyle?: StyleProp + unselectedTextStyle?: StyleProp + selectedColor?: string + unselectedColor?: string + fontSize?: number + iconSize?: number + + contentContainerStyle?: StyleProp + 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 ( + + {/* Header */} + + {/* 滑块 */} + {ready && ( + + )} + + {tabs.map((tab, i) => { + const isActive = i === selectedIndex + return ( + handlePress(i)} + style={{ + width: tabWidth, + alignItems: 'center', + justifyContent: 'center', + flexDirection: 'row', + gap: spacing, + }} + > + {tab.icon && ( + + )} + + {tab.label} + + + ) + })} + + + {/* Content */} + + + {tabs.map((tab, i) => { + const isActive = i === selectedIndex + const shouldRender = cacheContent || isActive + + if (!shouldRender) { + // 占位保持宽度 + return + } + + return ( + + {tab.content} + + ) + })} + + + + ) +} 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 9f734aad..a7d71a6c 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 @@ -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, } diff --git a/code/base-project/base-app/src/components/ui/drawer/index.tsx b/code/base-project/base-app/src/components/ui/drawer/index.tsx new file mode 100644 index 00000000..229bc1a3 --- /dev/null +++ b/code/base-project/base-app/src/components/ui/drawer/index.tsx @@ -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 + & MotionComponentProps + +const AnimatedPressable = createMotionAnimatedComponent( + Pressable, +) as React.ComponentType + +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 + & MotionComponentProps + +const MotionView = Motion.View as React.ComponentType + +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 + & VariantProps & { className?: string } + +type IDrawerBackdropProps = React.ComponentProps + & VariantProps & { className?: string } + +type IDrawerContentProps = React.ComponentProps + & VariantProps & { className?: string } + +type IDrawerHeaderProps = React.ComponentProps + & VariantProps & { className?: string } + +type IDrawerBodyProps = React.ComponentProps + & VariantProps & { className?: string } + +type IDrawerFooterProps = React.ComponentProps + & VariantProps & { className?: string } + +type IDrawerCloseButtonProps = React.ComponentProps< + typeof UIDrawer.CloseButton +> +& VariantProps & { className?: string } + +const Drawer = React.forwardRef< + React.ComponentRef, + IDrawerProps +>(({ className, size = 'sm', anchor = 'left', ...props }, ref) => { + return ( + + ) +}) + +const DrawerBackdrop = React.forwardRef< + React.ComponentRef, + IDrawerBackdropProps +>(({ className, ...props }, ref) => { + return ( + + ) +}) + +const DrawerContent = React.forwardRef< + React.ComponentRef, + 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 ( + + ) +}) + +const DrawerHeader = React.forwardRef< + React.ComponentRef, + IDrawerHeaderProps +>(({ className, ...props }, ref) => { + return ( + + ) +}) + +const DrawerBody = React.forwardRef< + React.ComponentRef, + IDrawerBodyProps +>(({ className, ...props }, ref) => { + return ( + + ) +}) + +const DrawerFooter = React.forwardRef< + React.ComponentRef, + IDrawerFooterProps +>(({ className, ...props }, ref) => { + return ( + + ) +}) + +const DrawerCloseButton = React.forwardRef< + React.ComponentRef, + IDrawerCloseButtonProps +>(({ className, ...props }, ref) => { + return ( + + ) +}) + +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, +} diff --git a/code/base-project/base-app/src/request/index.ts b/code/base-project/base-app/src/request/index.ts index bed3e94e..c8463a29 100644 --- a/code/base-project/base-app/src/request/index.ts +++ b/code/base-project/base-app/src/request/index.ts @@ -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: { diff --git a/code/base-project/base-app/test.html b/code/base-project/base-app/test.html new file mode 100644 index 00000000..39e91f7d --- /dev/null +++ b/code/base-project/base-app/test.html @@ -0,0 +1,205 @@ + + + + + +账单卡片 + 日期筛选 + + + + + +
+ +
筛选
+
+ + +
+
选择日期
+ + +
+
重置
+
确定
+
+
+ + +
+
当前周期:本月
+
剩余:¥2500
+
+
收入:¥5000
+
支出:¥2500
+
+
+ + + + + diff --git a/原创UI—工具类记账APP分享_1_星河_来自小红书网页版.jpg b/原创UI—工具类记账APP分享_1_星河_来自小红书网页版.jpg new file mode 100644 index 00000000..0ad9d3ea Binary files /dev/null and b/原创UI—工具类记账APP分享_1_星河_来自小红书网页版.jpg differ