This commit is contained in:
oneao committed 2025-11-15 17:31:36 +08:00
1 parent 33113f279c
commit c2c5cc92af
16 files changed
+732 -220

No files matched your search

+3 -2
View File
@@ -16,6 +16,7 @@
"@gluestack/ui-next-adapter": "^3.0.3", "@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-async-storage/async-storage": "2.2.0",
"@react-native-community/datetimepicker": "8.4.4",
"@react-native-segmented-control/segmented-control": "2.5.7", "@react-native-segmented-control/segmented-control": "2.5.7",
"alova": "^3.3.4", "alova": "^3.3.4",
"axios": "^1.13.2", "axios": "^1.13.2",
@@ -37,14 +38,14 @@
"react-native": "0.81.5", "react-native": "0.81.5",
"react-native-css-interop": "^0.2.1", "react-native-css-interop": "^0.2.1",
"react-native-dates-picker": "^0.2.0", "react-native-dates-picker": "^0.2.0",
"react-native-drawer-layout": "^4.2.0",
"react-native-gesture-handler": "~2.28.0", "react-native-gesture-handler": "~2.28.0",
"react-native-keyboard-aware-scroll-view": "^0.9.5",
"react-native-keyboard-controller": "1.18.5", "react-native-keyboard-controller": "1.18.5",
"react-native-modal-datetime-picker": "^18.0.0",
"react-native-reanimated": "~4.1.3", "react-native-reanimated": "~4.1.3",
"react-native-safe-area-context": "~5.6.1", "react-native-safe-area-context": "~5.6.1",
"react-native-screens": "~4.16.0", "react-native-screens": "~4.16.0",
"react-native-svg": "15.12.1", "react-native-svg": "15.12.1",
"react-native-ui-datepicker": "^3.1.2",
"react-native-web": "^0.21.0", "react-native-web": "^0.21.0",
"react-native-worklets": "0.5.1", "react-native-worklets": "0.5.1",
"zustand": "^5.0.8" "zustand": "^5.0.8"
+67 -53
View File
@@ -26,6 +26,9 @@ importers:
'@react-native-async-storage/async-storage': '@react-native-async-storage/async-storage':
specifier: 2.2.0 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)) 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-community/datetimepicker':
specifier: 8.4.4
version: 8.4.4(expo@54.0.23)(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/segmented-control': '@react-native-segmented-control/segmented-control':
specifier: 2.5.7 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) 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)
@@ -89,15 +92,18 @@ importers:
react-native-dates-picker: react-native-dates-picker:
specifier: ^0.2.0 specifier: ^0.2.0
version: 0.2.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) version: 0.2.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-drawer-layout:
specifier: ^4.2.0
version: 4.2.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))(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-gesture-handler: react-native-gesture-handler:
specifier: ~2.28.0 specifier: ~2.28.0
version: 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) version: 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-keyboard-aware-scroll-view:
specifier: ^0.9.5
version: 0.9.5(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))
react-native-keyboard-controller: react-native-keyboard-controller:
specifier: 1.18.5 specifier: 1.18.5
version: 1.18.5(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) version: 1.18.5(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-modal-datetime-picker:
specifier: ^18.0.0
version: 18.0.0(@react-native-community/datetimepicker@8.4.4(expo@54.0.23)(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-native-reanimated: react-native-reanimated:
specifier: ~4.1.3 specifier: ~4.1.3
version: 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) version: 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)
@@ -110,9 +116,6 @@ importers:
react-native-svg: react-native-svg:
specifier: 15.12.1 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) 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-ui-datepicker:
specifier: ^3.1.2
version: 3.1.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-web: react-native-web:
specifier: ^0.21.0 specifier: ^0.21.0
version: 0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) version: 0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@@ -1606,6 +1609,19 @@ packages:
peerDependencies: peerDependencies:
react-native: ^0.0.0-0 || >=0.65 <1.0 react-native: ^0.0.0-0 || >=0.65 <1.0
'@react-native-community/datetimepicker@8.4.4':
resolution: {integrity: sha512-bc4ZixEHxZC9/qf5gbdYvIJiLZ5CLmEsC3j+Yhe1D1KC/3QhaIfGDVdUcid0PdlSoGOSEq4VlB93AWyetEyBSQ==}
peerDependencies:
expo: '>=52.0.0'
react: '*'
react-native: '*'
react-native-windows: '*'
peerDependenciesMeta:
expo:
optional: true
react-native-windows:
optional: true
'@react-native-segmented-control/segmented-control@2.5.7': '@react-native-segmented-control/segmented-control@2.5.7':
resolution: {integrity: sha512-l84YeVX8xAU3lvOJSvV4nK/NbGhIm2gBfveYolwaoCbRp+/SLXtc6mYrQmM9ScXNwU14mnzjQTpTHWl5YPnkzQ==} resolution: {integrity: sha512-l84YeVX8xAU3lvOJSvV4nK/NbGhIm2gBfveYolwaoCbRp+/SLXtc6mYrQmM9ScXNwU14mnzjQTpTHWl5YPnkzQ==}
peerDependencies: peerDependencies:
@@ -3578,9 +3594,6 @@ packages:
jackspeak@3.4.3: jackspeak@3.4.3:
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
jalali-plugin-dayjs@1.1.4:
resolution: {integrity: sha512-d62QGMTufGQ1TSov4a85gBU58PA8aL6wVQs9BnC3+d/VA+v4Sq0OqM3o6RoY+/VZjliYJroGIPwlSVf7uLnZ3w==}
jest-environment-node@29.7.0: jest-environment-node@29.7.0:
resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==} resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@@ -3757,48 +3770,56 @@ packages:
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
lightningcss-linux-arm64-gnu@1.30.2: lightningcss-linux-arm64-gnu@1.30.2:
resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==} resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==}
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
lightningcss-linux-arm64-musl@1.27.0: lightningcss-linux-arm64-musl@1.27.0:
resolution: {integrity: sha512-rCGBm2ax7kQ9pBSeITfCW9XSVF69VX+fm5DIpvDZQl4NnQoMQyRwhZQm9pd59m8leZ1IesRqWk2v/DntMo26lg==} resolution: {integrity: sha512-rCGBm2ax7kQ9pBSeITfCW9XSVF69VX+fm5DIpvDZQl4NnQoMQyRwhZQm9pd59m8leZ1IesRqWk2v/DntMo26lg==}
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
lightningcss-linux-arm64-musl@1.30.2: lightningcss-linux-arm64-musl@1.30.2:
resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==}
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
lightningcss-linux-x64-gnu@1.27.0: lightningcss-linux-x64-gnu@1.27.0:
resolution: {integrity: sha512-Dk/jovSI7qqhJDiUibvaikNKI2x6kWPN79AQiD/E/KeQWMjdGe9kw51RAgoWFDi0coP4jinaH14Nrt/J8z3U4A==} resolution: {integrity: sha512-Dk/jovSI7qqhJDiUibvaikNKI2x6kWPN79AQiD/E/KeQWMjdGe9kw51RAgoWFDi0coP4jinaH14Nrt/J8z3U4A==}
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
lightningcss-linux-x64-gnu@1.30.2: lightningcss-linux-x64-gnu@1.30.2:
resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==}
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
lightningcss-linux-x64-musl@1.27.0: lightningcss-linux-x64-musl@1.27.0:
resolution: {integrity: sha512-QKjTxXm8A9s6v9Tg3Fk0gscCQA1t/HMoF7Woy1u68wCk5kS4fR+q3vXa1p3++REW784cRAtkYKrPy6JKibrEZA==} resolution: {integrity: sha512-QKjTxXm8A9s6v9Tg3Fk0gscCQA1t/HMoF7Woy1u68wCk5kS4fR+q3vXa1p3++REW784cRAtkYKrPy6JKibrEZA==}
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
lightningcss-linux-x64-musl@1.30.2: lightningcss-linux-x64-musl@1.30.2:
resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==}
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
lightningcss-win32-arm64-msvc@1.27.0: lightningcss-win32-arm64-msvc@1.27.0:
resolution: {integrity: sha512-/wXegPS1hnhkeG4OXQKEMQeJd48RDC3qdh+OA8pCuOPCyvnm/yEayrJdJVqzBsqpy1aJklRCVxscpFur80o6iQ==} resolution: {integrity: sha512-/wXegPS1hnhkeG4OXQKEMQeJd48RDC3qdh+OA8pCuOPCyvnm/yEayrJdJVqzBsqpy1aJklRCVxscpFur80o6iQ==}
@@ -4654,6 +4675,14 @@ packages:
react: '>=17.0.0' react: '>=17.0.0'
react-native: '*' react-native: '*'
react-native-drawer-layout@4.2.0:
resolution: {integrity: sha512-XFGK5RMcVhEqr2F4iH/cTXrcTm1uK3gIBNFPqHNO7rCXuXZTPBbQYA5yc9/Lqgw2KNQ3sAeHKvW6/WoTURo/eA==}
peerDependencies:
react: '>= 18.2.0'
react-native: '*'
react-native-gesture-handler: '>= 2.0.0'
react-native-reanimated: '>= 2.0.0'
react-native-gesture-handler@2.28.0: react-native-gesture-handler@2.28.0:
resolution: {integrity: sha512-0msfJ1vRxXKVgTgvL+1ZOoYw3/0z1R+Ked0+udoJhyplC2jbVKIJ8Z1bzWdpQRCV3QcQ87Op0zJVE5DhKK2A0A==} resolution: {integrity: sha512-0msfJ1vRxXKVgTgvL+1ZOoYw3/0z1R+Ked0+udoJhyplC2jbVKIJ8Z1bzWdpQRCV3QcQ87Op0zJVE5DhKK2A0A==}
peerDependencies: peerDependencies:
@@ -4668,22 +4697,12 @@ packages:
react-native: '*' react-native: '*'
react-native-svg: '>=9.7.0' react-native-svg: '>=9.7.0'
react-native-iphone-x-helper@1.3.1:
resolution: {integrity: sha512-HOf0jzRnq2/aFUcdCJ9w9JGzN3gdEg0zFE4FyYlp4jtidqU03D5X7ZegGKfT1EWteR0gPBGp9ye5T5FvSWi9Yg==}
peerDependencies:
react-native: '>=0.42.0'
react-native-is-edge-to-edge@1.2.1: react-native-is-edge-to-edge@1.2.1:
resolution: {integrity: sha512-FLbPWl/MyYQWz+KwqOZsSyj2JmLKglHatd3xLZWskXOpRaio4LfEDEz8E/A6uD8QoTHW6Aobw1jbEwK7KMgR7Q==} resolution: {integrity: sha512-FLbPWl/MyYQWz+KwqOZsSyj2JmLKglHatd3xLZWskXOpRaio4LfEDEz8E/A6uD8QoTHW6Aobw1jbEwK7KMgR7Q==}
peerDependencies: peerDependencies:
react: '*' react: '*'
react-native: '*' react-native: '*'
react-native-keyboard-aware-scroll-view@0.9.5:
resolution: {integrity: sha512-XwfRn+T/qBH9WjTWIBiJD2hPWg0yJvtaEw6RtPCa5/PYHabzBaWxYBOl0usXN/368BL1XktnZPh8C2lmTpOREA==}
peerDependencies:
react-native: '>=0.48.4'
react-native-keyboard-controller@1.18.5: react-native-keyboard-controller@1.18.5:
resolution: {integrity: sha512-wbYN6Tcu3G5a05dhRYBgjgd74KqoYWuUmroLpigRg9cXy5uYo7prTMIvMgvLtARQtUF7BOtFggUnzgoBOgk0TQ==} resolution: {integrity: sha512-wbYN6Tcu3G5a05dhRYBgjgd74KqoYWuUmroLpigRg9cXy5uYo7prTMIvMgvLtARQtUF7BOtFggUnzgoBOgk0TQ==}
peerDependencies: peerDependencies:
@@ -4691,6 +4710,12 @@ packages:
react-native: '*' react-native: '*'
react-native-reanimated: '>=3.0.0' react-native-reanimated: '>=3.0.0'
react-native-modal-datetime-picker@18.0.0:
resolution: {integrity: sha512-0jdvhhraZQlRACwr7pM6vmZ2kxgzJ4CpnmV6J3TVA6MrXMXK6Zo/upRBKkRp0+fTOiKuNblzesA2U59rYo6SGA==}
peerDependencies:
'@react-native-community/datetimepicker': '>=6.7.0'
react-native: '>=0.65.0'
react-native-reanimated@4.1.5: react-native-reanimated@4.1.5:
resolution: {integrity: sha512-UA6VUbxwhRjEw2gSNrvhkusUq3upfD3Cv+AnB07V+kC8kpvwRVI+ivwY95ePbWNFkFpP+Y2Sdw1WHpHWEV+P2Q==} resolution: {integrity: sha512-UA6VUbxwhRjEw2gSNrvhkusUq3upfD3Cv+AnB07V+kC8kpvwRVI+ivwY95ePbWNFkFpP+Y2Sdw1WHpHWEV+P2Q==}
peerDependencies: peerDependencies:
@@ -4717,13 +4742,6 @@ packages:
react: '*' react: '*'
react-native: '*' react-native: '*'
react-native-ui-datepicker@3.1.2:
resolution: {integrity: sha512-1yukCKMLUuVb5daYOgb4mLZqpynZ6GMQyNF2NPwpiDB5F2D/NGpeOmoYb7qFwDrTNwr+m4IemNUa0TlYIFktlg==}
engines: {node: '>= 18'}
peerDependencies:
react: '*'
react-native: '*'
react-native-web@0.21.2: react-native-web@0.21.2:
resolution: {integrity: sha512-SO2t9/17zM4iEnFvlu2DA9jqNbzNhoUP+AItkoCOyFmDMOhUnBBznBDCYN92fGdfAkfQlWzPoez6+zLxFNsZEg==} resolution: {integrity: sha512-SO2t9/17zM4iEnFvlu2DA9jqNbzNhoUP+AItkoCOyFmDMOhUnBBznBDCYN92fGdfAkfQlWzPoez6+zLxFNsZEg==}
peerDependencies: peerDependencies:
@@ -5124,9 +5142,6 @@ packages:
tailwind-merge@1.14.0: tailwind-merge@1.14.0:
resolution: {integrity: sha512-3mFKyCo/MBcgyOTlrY8T7odzZFx+w+qKSMAmdFzRvqBfLlSigU6TZnlFHK0lkMwj9Bj8OYU+9yW9lmGuS0QEnQ==} resolution: {integrity: sha512-3mFKyCo/MBcgyOTlrY8T7odzZFx+w+qKSMAmdFzRvqBfLlSigU6TZnlFHK0lkMwj9Bj8OYU+9yW9lmGuS0QEnQ==}
tailwind-merge@3.4.0:
resolution: {integrity: sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==}
tailwind-variants@0.1.20: tailwind-variants@0.1.20:
resolution: {integrity: sha512-AMh7x313t/V+eTySKB0Dal08RHY7ggYK0MSn/ad8wKWOrDUIzyiWNayRUm2PIJ4VRkvRnfNuyRuKbLV3EN+ewQ==} resolution: {integrity: sha512-AMh7x313t/V+eTySKB0Dal08RHY7ggYK0MSn/ad8wKWOrDUIzyiWNayRUm2PIJ4VRkvRnfNuyRuKbLV3EN+ewQ==}
engines: {node: '>=16.x', pnpm: '>=7.x'} engines: {node: '>=16.x', pnpm: '>=7.x'}
@@ -7634,6 +7649,14 @@ snapshots:
merge-options: 3.0.4 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: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0)
'@react-native-community/datetimepicker@8.4.4(expo@54.0.23)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)':
dependencies:
invariant: 2.2.4
react: 19.1.0
react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0)
optionalDependencies:
expo: 54.0.23(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.14)(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/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)': '@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: dependencies:
react: 19.1.0 react: 19.1.0
@@ -9953,8 +9976,6 @@ snapshots:
optionalDependencies: optionalDependencies:
'@pkgjs/parseargs': 0.11.0 '@pkgjs/parseargs': 0.11.0
jalali-plugin-dayjs@1.1.4: {}
jest-environment-node@29.7.0: jest-environment-node@29.7.0:
dependencies: dependencies:
'@jest/environment': 29.7.0 '@jest/environment': 29.7.0
@@ -11413,6 +11434,15 @@ snapshots:
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-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0)
react-native-drawer-layout@4.2.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))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0):
dependencies:
color: 4.2.3
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)
use-latest-callback: 0.2.6(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-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):
dependencies: dependencies:
'@egjs/hammerjs': 2.0.17 '@egjs/hammerjs': 2.0.17
@@ -11436,21 +11466,11 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
react-native-iphone-x-helper@1.3.1(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)
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): 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):
dependencies: dependencies:
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-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0)
react-native-keyboard-aware-scroll-view@0.9.5(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0)):
dependencies:
prop-types: 15.8.1
react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0)
react-native-iphone-x-helper: 1.3.1(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))
react-native-keyboard-controller@1.18.5(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-keyboard-controller@1.18.5(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: dependencies:
react: 19.1.0 react: 19.1.0
@@ -11458,6 +11478,12 @@ 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) 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)
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-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-modal-datetime-picker@18.0.0(@react-native-community/datetimepicker@8.4.4(expo@54.0.23)(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)):
dependencies:
'@react-native-community/datetimepicker': 8.4.4(expo@54.0.23)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
prop-types: 15.8.1
react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(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-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):
dependencies: dependencies:
'@babel/core': 7.28.5 '@babel/core': 7.28.5
@@ -11488,16 +11514,6 @@ snapshots:
react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0) react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0)
warn-once: 0.1.1 warn-once: 0.1.1
react-native-ui-datepicker@3.1.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0):
dependencies:
clsx: 2.1.1
dayjs: 1.11.19
jalali-plugin-dayjs: 1.1.4
lodash: 4.17.21
react: 19.1.0
react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0)
tailwind-merge: 3.4.0
react-native-web@0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0): react-native-web@0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
dependencies: dependencies:
'@babel/runtime': 7.28.4 '@babel/runtime': 7.28.4
@@ -11951,8 +11967,6 @@ snapshots:
tailwind-merge@1.14.0: {} tailwind-merge@1.14.0: {}
tailwind-merge@3.4.0: {}
tailwind-variants@0.1.20(tailwindcss@3.4.18(yaml@2.8.1)): tailwind-variants@0.1.20(tailwindcss@3.4.18(yaml@2.8.1)):
dependencies: dependencies:
tailwind-merge: 1.14.0 tailwind-merge: 1.14.0
@@ -1,42 +1,48 @@
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { useState } from 'react' // DemoADatePicker.tsx
import { StyleSheet, View } from 'react-native' import React, { useState } from 'react'
import DateTimePicker from 'react-native-dates-picker' import { Button, ScrollView, StyleSheet } from 'react-native'
import { ADateTimeDrawer } from '@/components/common/ao'
export default function App() { export default function DemoADatePicker() {
const [show, setShow] = useState(false)
const [date, setDate] = useState(dayjs()) const [date, setDate] = useState(dayjs())
return ( return (
<View style={styles.container}> <ScrollView contentContainerStyle={styles.container}>
<DateTimePicker <ADateTimeDrawer
mode="single" isOpen={show}
date={date} defaultDate={date}
selectedItemColor="#fff" modeType="year-month"
selectedTextStyle={{ onConfirm={(val) => {
fontWeight: 'bold', console.log(val)
color: '#00d279',
}} }}
headerTextStyle={{ onClose={() => {
fontSize: 18, setShow(false)
}} }}
headerButtonStyle={{
backgroundColor: '#ffffff',
borderRadius: 8,
}}
headerButtonColor="#00d279"
headerButtonSize={18}
headerButtonsPosition="right"
onChange={params => setDate(params.date)}
/> />
</View> <Button
title="测试"
onPress={() => {
setShow(!show)
}}
>
</Button>
</ScrollView>
) )
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
flex: 1, padding: 20,
backgroundColor: '#F5FCFF', },
marginTop: 128, label: {
padding: 16, marginTop: 20,
fontSize: 16,
fontWeight: 'bold',
},
result: {
marginVertical: 5,
fontSize: 14,
color: '#333',
}, },
}) })
@@ -1,10 +1,10 @@
import type { MoneyCategoryResult } from '@/request/api/money' import type { MoneyCategoryResult } from '@/request/api/money'
import type { MoneyCategory } from '@/types/money' import type { MoneyCategory, MoneyRecord } from '@/types/money'
import Big from 'big.js' import Big from 'big.js'
import { useEffect, useRef, useState } from 'react' import { useEffect, useState } from 'react'
import { Keyboard, Pressable, StyleSheet, Text, TouchableWithoutFeedback, View } from 'react-native' import { ActivityIndicator, Keyboard, Pressable, StyleSheet, Text, TouchableWithoutFeedback, View } from 'react-native'
import { useSafeAreaInsets } from 'react-native-safe-area-context' import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { AInput, LucideIcon } from '@/components/common/ao' import { ADatePicker, ADateTimeDrawer, AInput, LucideIcon } from '@/components/common/ao'
import SegmentedTabs from '@/components/common/ao/SegmentedTabs' import SegmentedTabs from '@/components/common/ao/SegmentedTabs'
import { import {
Drawer, Drawer,
@@ -15,7 +15,9 @@ import {
} from '@/components/ui/drawer' } from '@/components/ui/drawer'
import { Grid, GridItem } from '@/components/ui/grid' import { Grid, GridItem } from '@/components/ui/grid'
import { useKeyboardHeight } from '@/hooks/useKeyboardHeight' import { useKeyboardHeight } from '@/hooks/useKeyboardHeight'
import { getMoneyCategoryApi } from '@/request/api/money' import { getMoneyCategoryApi, insertMoneyRecordApi } from '@/request/api/money'
import { getCurrentDate } from '@/utils/time'
import { toast } from '@/utils/toast'
import MoneyCategoryIcon from './MoneyCategoryIcon' import MoneyCategoryIcon from './MoneyCategoryIcon'
interface AddMoneyDrawerProps { interface AddMoneyDrawerProps {
@@ -27,6 +29,7 @@ interface KeydownItem {
key: string | number key: string | number
content: React.ReactNode content: React.ReactNode
} }
const keys: KeydownItem[] = [ const keys: KeydownItem[] = [
{ key: '1', content: <Text className="text-2xl">1</Text> }, { key: '1', content: <Text className="text-2xl">1</Text> },
{ key: '2', content: <Text className="text-2xl">2</Text> }, { key: '2', content: <Text className="text-2xl">2</Text> },
@@ -40,9 +43,9 @@ const keys: KeydownItem[] = [
{ key: '8', content: <Text className="text-2xl">8</Text> }, { key: '8', content: <Text className="text-2xl">8</Text> },
{ key: '9', content: <Text className="text-2xl">9</Text> }, { key: '9', content: <Text className="text-2xl">9</Text> },
{ key: '-', content: <LucideIcon icon="Minus" size={22} strokeWidth={1.5} clickable={false} /> }, { key: '-', content: <LucideIcon icon="Minus" size={22} strokeWidth={1.5} clickable={false} /> },
{ key: 'repeat', content: ( { key: 'clear', content: (
<View className="w-full h-full flex items-center justify-center"> <View className="w-full h-full flex items-center justify-center">
<Text className="text-xl text-gray-600">再记</Text> <Text className="text-xl text-gray-600">重置</Text>
</View> </View>
) }, ) },
{ key: '0', content: <Text className="text-2xl">0</Text> }, { key: '0', content: <Text className="text-2xl">0</Text> },
@@ -59,21 +62,42 @@ export default function AddMoneyDrawer({
onClose, onClose,
}: AddMoneyDrawerProps) { }: AddMoneyDrawerProps) {
const insets = useSafeAreaInsets() const insets = useSafeAreaInsets()
// 选择顺序:0 支出 1收入
const [selectedIndex, setSelectedIndex] = useState(0) const [selectedIndex, setSelectedIndex] = useState(0)
const [selectedCategoryId, setSelectedCategoryId] = useState<number | null>(null) // 自定义键盘
const [showKeydown, setShowKeydown] = useState(false) const [showKeydown, setShowKeydown] = useState(false)
const [keydownInput, setKeydownInput] = useState<string>('') // 自定义键盘计算过程
const [result, setResult] = useState('0') const [keydownInput, setKeydownInput] = useState('')
// 日期选择
const [showDatePicker, setShowDatePicker] = useState(false)
// 备注聚焦
const [remarkIsFocused, setRemarkIsFocused] = useState(false)
// 分类
const [categories, setCategories] = useState<MoneyCategoryResult>({ const [categories, setCategories] = useState<MoneyCategoryResult>({
income: [], income: [],
expense: [], expense: [],
}) })
const keyboardHeight = useKeyboardHeight() const keyboardHeight = useKeyboardHeight()
console.log(keyboardHeight)
const defaultMoneyRecord: MoneyRecord = {
id: -1,
category_id: -1,
user_id: -1,
amount: '0.00',
remark: '',
record_time: getCurrentDate(),
create_time: '',
}
const [moneyRecord, setMoneyRecord] = useState<MoneyRecord>(defaultMoneyRecord)
function selectCategory(item: MoneyCategory) { function selectCategory(item: MoneyCategory) {
setSelectedCategoryId(item.id) setMoneyRecord({
...moneyRecord,
category_id: item.id,
})
setShowKeydown(true) setShowKeydown(true)
} }
@@ -94,11 +118,23 @@ export default function AddMoneyDrawer({
} }
}, [isOpen]) }, [isOpen])
// 关闭整个drawer处理
function onMainDrawerClose() { function onMainDrawerClose() {
setShowKeydown(false)
onClose() onClose()
// 清空键盘记录
setKeydownInput('')
// 关闭默认键盘
setShowKeydown(false)
// 设置默认值
setMoneyRecord(defaultMoneyRecord)
// 选中支出
setSelectedIndex(0)
// 关闭日期
setShowDatePicker(false)
} }
// 计算过程
const handlePress = (key: string | number) => { const handlePress = (key: string | number) => {
setKeydownInput((prev) => { setKeydownInput((prev) => {
let newInput = prev let newInput = prev
@@ -106,8 +142,17 @@ export default function AddMoneyDrawer({
if (key === 'del') { if (key === 'del') {
newInput = prev.slice(0, -1) newInput = prev.slice(0, -1)
} }
else if (key === 'clear') {
setMoneyRecord({
...moneyRecord,
amount: '0.00',
remark: '',
record_time: getCurrentDate(),
})
return ''
}
else if (key === '/') { else if (key === '/') {
// 完成操作,可以在这里提交或关闭键盘 saveRecord()
return prev return prev
} }
else { else {
@@ -115,6 +160,11 @@ export default function AddMoneyDrawer({
// 检查是否要输入运算符(+ 或 -) // 检查是否要输入运算符(+ 或 -)
if (keyStr === '+' || keyStr === '-') { if (keyStr === '+' || keyStr === '-') {
// 如果当前输入为空,不允许以运算符开头
if (prev === '') {
return prev
}
// 获取最后一个字符 // 获取最后一个字符
const lastChar = prev.slice(-1) const lastChar = prev.slice(-1)
@@ -122,31 +172,44 @@ export default function AddMoneyDrawer({
if (lastChar === '+' || lastChar === '-') { if (lastChar === '+' || lastChar === '-') {
newInput = prev.slice(0, -1) + keyStr newInput = prev.slice(0, -1) + keyStr
} }
// 如果最后一个字符是小数点,不允许在小数点后直接加运算符
else if (lastChar === '.') {
return prev
}
else { else {
newInput = prev + keyStr newInput = prev + keyStr
} }
} }
// 检查是否要输入小数点(.) // 检查是否要输入小数点(.)
else if (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 parts = prev.split(/[+-]/)
const currentNumber = parts[parts.length - 1] const currentNumber = parts[parts.length - 1]
// 如果当前数字中还没有小数点,可以添加 // 如果当前数字中还没有小数点,可以添加
if (!currentNumber.includes('.')) { if (!currentNumber.includes('.')) {
// 检查当前数字的小数位数(应该是0,因为还没有小数点)
if (currentNumber === '' || /[+-]/.test(prev.slice(-1))) {
newInput = `${prev}0.`
}
else {
newInput = `${prev}.` newInput = `${prev}.`
} }
}
else { else {
// 已经有小数点了,不重复添加 // 已经有小数点了,不重复添加
newInput = prev newInput = prev
} }
} }
}
}
// 处理数字输入 // 处理数字输入
else if (/^\d$/.test(keyStr)) { else if (/^\d$/.test(keyStr)) {
// 检查当前数字的小数位数 // 检查当前数字的小数位数
@@ -178,10 +241,16 @@ export default function AddMoneyDrawer({
// 计算结果 // 计算结果
try { try {
const computed = calculateWithBig(newInput) const computed = calculateWithBig(newInput)
setResult(computed) setMoneyRecord({
...moneyRecord,
amount: computed,
})
} }
catch (e) { catch (e) {
setResult('0') setMoneyRecord({
...moneyRecord,
amount: '0',
})
} }
return newInput return newInput
@@ -242,12 +311,39 @@ export default function AddMoneyDrawer({
return total.toFixed(2) return total.toFixed(2)
} }
} }
const inputRef = useRef<any>(null)
const [isFocused, setIsFocused] = useState(false) // 保存
function saveRecord() {
if (moneyRecord.category_id === -1) {
toast.show({
title: '请选择分类',
})
return
}
if (moneyRecord.amount === '0.00') {
toast.show({
title: '请输入金额',
})
}
insertMoneyRecordApi({
...moneyRecord,
}).then((res) => {
if (res.isSuccess) {
onMainDrawerClose()
}
else {
toast.show({
title: res.message || '保存失败',
})
}
}).catch((err) => {
console.log(err)
})
}
return ( return (
<>
<Drawer <Drawer
isOpen={isOpen} isOpen={isOpen}
size="full" size="full"
@@ -256,13 +352,7 @@ export default function AddMoneyDrawer({
> >
<DrawerBackdrop /> <DrawerBackdrop />
<DrawerContent <DrawerContent className="p-0 flex-1">
style={{
padding: 0,
flex: 1,
}}
>
<DrawerHeader <DrawerHeader
className="flex items-center justify-between" className="flex items-center justify-between"
style={{ paddingTop: insets.top + 8, paddingLeft: 16, paddingRight: 16 }} style={{ paddingTop: insets.top + 8, paddingLeft: 16, paddingRight: 16 }}
@@ -270,7 +360,7 @@ export default function AddMoneyDrawer({
<View className="w-1" /> <View className="w-1" />
<SegmentedTabs <SegmentedTabs
value={selectedIndex} value={selectedIndex}
tabs={[{ label: '收入' }, { label: '支出' }]} tabs={[{ label: '支出' }, { label: '收入' }]}
onChange={setSelectedIndex} onChange={setSelectedIndex}
style={{ width: 180 }} style={{ width: 180 }}
className="mx-auto" className="mx-auto"
@@ -279,14 +369,20 @@ export default function AddMoneyDrawer({
<Text className="text-gray-500 font-medium">取消</Text> <Text className="text-gray-500 font-medium">取消</Text>
</Pressable> </Pressable>
</DrawerHeader> </DrawerHeader>
<DrawerBody style={{ flex: 1, paddingLeft: 16, paddingRight: 16, marginBottom: 0, backgroundColor: '#fff', <DrawerBody
style={{
flex: 1,
paddingLeft: 16,
paddingRight: 16,
marginBottom: 0,
backgroundColor: '#fff',
}} }}
> >
<Grid className="gap-4 mb-8" _extra={{ className: 'grid-cols-4' }}> <Grid className="gap-4 mb-8" _extra={{ className: 'grid-cols-4' }}>
{(() => { {(() => {
const list = selectedIndex === 0 ? categories.income : categories.expense const list = selectedIndex === 1 ? categories.income : categories.expense
const items = list.map((item) => { const items = list.map((item) => {
const isSelected = selectedCategoryId === item.id const isSelected = moneyRecord.category_id === item.id
return ( return (
<GridItem key={item.id} _extra={{ className: 'col-span-1' }}> <GridItem key={item.id} _extra={{ className: 'col-span-1' }}>
<Pressable <Pressable
@@ -332,23 +428,29 @@ export default function AddMoneyDrawer({
<View className="flex flex-row items-center justify-between"> <View className="flex flex-row items-center justify-between">
<Pressable <Pressable
className="flex flex-row items-center gap-1.5 px-2 py-1.5 rounded-md bg-gray-50 active:opacity-70" className="flex flex-row items-center gap-1.5 px-2 py-1.5 rounded-md bg-gray-50 active:opacity-70"
onPress={() => setShowDatePicker(true)}
> >
<LucideIcon icon="Calendar" size={16} color="#60a5fa" /> <LucideIcon icon="Calendar" size={16} color="#60a5fa" clickable={false} />
<Text className="text-gray-600 text-sm">2024/11/05</Text> <Text className="text-gray-600 text-sm">{moneyRecord.record_time}</Text>
<LucideIcon icon="ChevronDown" size={12} color="#aaa" /> <LucideIcon icon="ChevronDown" size={12} color="#aaa" clickable={false} />
</Pressable> </Pressable>
<Text className="text-3xl text-blue-700 text-right">{result}</Text> <Text className="text-3xl text-blue-700 text-right">{moneyRecord.amount}</Text>
</View> </View>
<Text className="text-gray-400 text-right">{keydownInput}</Text> <Text className="text-gray-400 text-right">{keydownInput}</Text>
<AInput <AInput
ref={inputRef} value={moneyRecord.remark}
value={keydownInput}
placeholder="备注" placeholder="备注"
className="bg-[#f2f3f5] border-0" className="bg-[#f2f3f5] border-0"
size="sm" size="sm"
onFocus={() => setIsFocused(true)} onChangeText={(val) => {
onBlur={() => setIsFocused(false)} setMoneyRecord({
...moneyRecord,
remark: val,
})
}}
onFocus={() => setRemarkIsFocused(true)}
onBlur={() => setRemarkIsFocused(false)}
/> />
</View> </View>
</TouchableWithoutFeedback> </TouchableWithoutFeedback>
@@ -395,13 +497,27 @@ export default function AddMoneyDrawer({
) )
})} })}
</View> </View>
{isFocused && keyboardHeight > 220 && ( {remarkIsFocused && keyboardHeight > 220 && (
<View style={{ height: keyboardHeight - 220 }} /> <View style={{ height: keyboardHeight - 220 }} />
)} )}
</View> </View>
)} )}
</DrawerContent> </DrawerContent>
</Drawer> </Drawer>
<ADateTimeDrawer
isOpen={showDatePicker}
defaultDate={moneyRecord.record_time}
onConfirm={(val) => {
setMoneyRecord({
...moneyRecord,
record_time: val,
})
setShowDatePicker(false)
}}
onClose={() => {
setShowDatePicker((false))
}}
/>
</>
) )
} }
@@ -1,26 +1,29 @@
import type { LayoutChangeEvent } from 'react-native' import type { LayoutChangeEvent } from 'react-native'
import { BlurView } from 'expo-blur'
import { LinearGradient } from 'expo-linear-gradient' import { LinearGradient } from 'expo-linear-gradient'
import React, { useRef, useState } from 'react' import React, { useEffect, useRef, useState } from 'react'
import { ScrollView, Text, View } from 'react-native' import { ScrollView, Text, TouchableOpacity, View } from 'react-native'
import { useSafeAreaInsets } from 'react-native-safe-area-context' import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { LucideIcon } from '@/components/common/ao' import { ADateTimeDrawer, LucideIcon } from '@/components/common/ao'
import { IconJiaotong } from '@/components/iconfont' import { IconJiaotong } from '@/components/iconfont'
import { Fab, FabIcon } from '@/components/ui/fab' import { Fab, FabIcon } from '@/components/ui/fab'
import { Grid, GridItem } from '@/components/ui/grid' import { Grid, GridItem } from '@/components/ui/grid'
import { AddIcon } from '@/components/ui/icon' import { AddIcon } from '@/components/ui/icon'
import { Progress, ProgressFilledTrack } from '@/components/ui/progress' import { Progress, ProgressFilledTrack } from '@/components/ui/progress'
import { queryMoneyRecordsApi } from '@/request/api/money'
import { getCurrentYearMonth } from '@/utils/time'
import AddMoneyDrawer from './components/AddDrawer' import AddMoneyDrawer from './components/AddDrawer'
import TransactionsList from './components/TransactionsList' import TransactionsList from './components/TransactionsList'
export default function Money() { export default function Money() {
const [showDrawer, setShowDrawer] = useState(false) const [showAddDrawer, setShowAddDrawer] = useState(false)
// 安全高度
const insets = useSafeAreaInsets() const insets = useSafeAreaInsets()
const [gradientHeight, setGradientHeight] = useState(200) const [gradientHeight, setGradientHeight] = useState(200)
const gridLayout = useRef<{ y: number, height: number } | null>(null) const gridLayout = useRef<{ y: number, height: number } | null>(null)
const onGridLayout = (e: LayoutChangeEvent) => { const onGridLayout = (e: LayoutChangeEvent) => {
// 仅首次布局计算高度
if (!gridLayout.current) { if (!gridLayout.current) {
const { y, height } = e.nativeEvent.layout const { y, height } = e.nativeEvent.layout
gridLayout.current = { y, height } gridLayout.current = { y, height }
@@ -28,6 +31,20 @@ export default function Money() {
} }
} }
// 日期筛选
const [showDatePicker, setShowDatePicker] = useState(false)
const [searchParams, setSearchParams] = useState({
record_time: getCurrentYearMonth(),
})
useEffect(() => {
queryMoneyRecordsApi({
record_time: searchParams.record_time,
}).then((res) => {
console.log(res.data)
})
}, [searchParams])
return ( return (
<View <View
className="flex-1 relative bg-white" className="flex-1 relative bg-white"
@@ -52,10 +69,16 @@ export default function Money() {
<View style={{ zIndex: 10, paddingLeft: 12, paddingRight: 12 }}> <View style={{ zIndex: 10, paddingLeft: 12, paddingRight: 12 }}>
<View className="flex flex-row justify-between items-center py-2"> <View className="flex flex-row justify-between items-center py-2">
<View className="flex flex-row items-center gap-2"> <BlurView intensity={80} className="px-2 py-1">
<Text className="font-bold text-base">2025/11/10</Text> <TouchableOpacity
<LucideIcon icon="CircleChevronDown" size={14} /> onPress={() => setShowDatePicker(true)}
</View> className="flex flex-row items-center gap-2"
>
<Text className="font-bold text-base">{searchParams.record_time}</Text>
<LucideIcon icon="ArrowBigDownDash" size={16} />
</TouchableOpacity>
</BlurView>
<View className="flex flex-row items-center gap-2"> <View className="flex flex-row items-center gap-2">
<LucideIcon icon="SlidersHorizontal" /> <LucideIcon icon="SlidersHorizontal" />
</View> </View>
@@ -144,16 +167,31 @@ export default function Money() {
<Fab <Fab
size="lg" size="lg"
onPress={() => { onPress={() => {
setShowDrawer(true) setShowAddDrawer(true)
}} }}
> >
<FabIcon as={AddIcon} size="xl" /> <FabIcon as={AddIcon} size="xl" />
</Fab> </Fab>
<AddMoneyDrawer <AddMoneyDrawer
isOpen={showDrawer} isOpen={showAddDrawer}
onClose={() => { onClose={() => {
setShowDrawer(false) setShowAddDrawer(false)
}}
/>
<ADateTimeDrawer
isOpen={showDatePicker}
modeType="year-month"
onConfirm={(val) => {
setSearchParams({
...searchParams,
record_time: val,
})
setShowDatePicker(false)
}}
onClose={() => {
setShowDatePicker(false)
}} }}
/> />
</View> </View>
@@ -0,0 +1,81 @@
import type { DateTimePickerProps } from 'react-native-modal-datetime-picker'
import dayjs from 'dayjs'
import customParseFormat from 'dayjs/plugin/customParseFormat'
import React, { useMemo } from 'react'
import DateTimePickerModal from 'react-native-modal-datetime-picker'
dayjs.extend(customParseFormat)
interface ADatePickerProps extends Omit<DateTimePickerProps, 'onConfirm' | 'onCancel' | 'date'> {
format?: string
onConfirm?: (value: string) => void
date?: Date | string
is24Hour?: boolean
onCancel?: () => void
}
export default function DatePicker({
mode = 'date',
format,
date,
onConfirm,
is24Hour = true,
onCancel,
...restProps
}: ADatePickerProps) {
const parsedDate = useMemo(() => {
if (!date)
return new Date()
if (typeof date === 'string') {
// 支持多种常见日期格式
const formats = ['YYYY-MM-DD', 'YYYY/MM/DD', 'YYYY-MM-DDTHH:mm:ssZ', 'YYYY/MM/DD HH:mm:ss', 'YYYY-MM-DD HH:mm:ss']
for (const fmt of formats) {
const d = dayjs(date, fmt, true)
if (d.isValid())
return d.toDate()
}
// fallback: 尝试直接解析
const d = dayjs(date)
if (d.isValid())
return d.toDate()
// 仍然不合法就返回当前时间
return new Date()
}
return date
}, [date])
const handleConfirm = (selectedDate: Date) => {
const fmt = format || (() => {
switch (mode) {
case 'time':
return 'HH:mm:00'
case 'datetime':
return 'YYYY-MM-DD HH:mm:00'
case 'date':
default:
return 'YYYY-MM-DD'
}
})()
const localTimeStr = dayjs(selectedDate).format(fmt)
onConfirm?.(localTimeStr)
}
const handleCancel = () => {
onCancel?.()
}
return (
<DateTimePickerModal
{...restProps}
mode={mode}
date={parsedDate}
locale="zh-CN"
onConfirm={handleConfirm}
onCancel={handleCancel}
is24Hour={is24Hour}
/>
)
}
@@ -0,0 +1,106 @@
import type { DateType, OrderedDateParts } from 'react-native-dates-picker/lib/typescript/types'
import dayjs from 'dayjs'
import { useEffect, useRef, useState } from 'react'
import { View } from 'react-native'
import DateTimePicker from 'react-native-dates-picker'
import { Drawer, DrawerBackdrop, DrawerContent, DrawerFooter, DrawerHeader } from '@/components/ui/drawer'
import AButton from './Button'
type DateWheelMode
= | 'year-month'
| 'year-month-day'
| 'hour-minute-second'
| 'year-month-day-hour-minute-second'
interface DateDrawerProps {
isOpen?: boolean
defaultDate?: DateType
modeType?: DateWheelMode
onClose?: () => void
onConfirm?: (dateStr: string) => void
}
const modeColumnsMap: Record<DateWheelMode, OrderedDateParts[]> = {
'year-month': ['year', 'month'],
'year-month-day': ['year', 'month', 'day'],
'hour-minute-second': ['hour', 'minute', 'second'],
'year-month-day-hour-minute-second': ['year', 'month', 'day', 'hour', 'minute', 'second'],
}
const modeFormatMap: Record<DateWheelMode, string> = {
'year-month': 'YYYY-MM',
'year-month-day': 'YYYY-MM-DD',
'hour-minute-second': 'HH:mm:ss',
'year-month-day-hour-minute-second': 'YYYY-MM-DD HH:mm:ss',
}
export default function DateWheelDrawer({
isOpen,
defaultDate = dayjs(),
onClose,
onConfirm,
modeType = 'year-month-day',
}: DateDrawerProps) {
const columns = modeColumnsMap[modeType]
const format = modeFormatMap[modeType]
// 当前选择的值
const [date, setDate] = useState<DateType>(defaultDate)
// 保存上一次确认的值
const lastConfirmedRef = useRef<DateType>(defaultDate)
// 每次弹窗打开时,把滚轮显示初始化为上一次确认的值
useEffect(() => {
if (isOpen) {
setDate(lastConfirmedRef.current)
}
}, [isOpen])
// 点击取消
const handleCancel = () => {
onClose?.()
}
// 点击确认
const handleConfirm = () => {
console.log("确实")
onClose?.()
lastConfirmedRef.current = date // 保存为上一次确认的值
if (onConfirm) {
const formatted = dayjs(date).format(format)
onConfirm(formatted)
}
}
return (
<Drawer
isOpen={isOpen}
anchor="bottom"
closeOnOverlayClick
onClose={onClose}
size="md"
>
<DrawerBackdrop />
<DrawerContent className="p-0 flex-1">
<DrawerHeader className="flex items-center justify-between p-4">
<AButton action="secondary" onPress={handleCancel}>取消</AButton>
<AButton onPress={handleConfirm}>确认</AButton>
</DrawerHeader>
<View className="flex-1 flex items-center justify-center">
<DateTimePicker
mode="wheel"
date={date}
columns={columns}
wheelPickerSelectedIndicatorStyle={{ backgroundColor: '#eee' }}
onChange={params => setDate(params.date)}
/>
</View>
<DrawerFooter></DrawerFooter>
</DrawerContent>
</Drawer>
)
}
@@ -1,5 +1,7 @@
import Button from './Button' import Button from './Button'
import Checkbox from './Checkbox' import Checkbox from './Checkbox'
import DatePicker from './DatePicker'
import DateTimeDrawer from './DateTimeDrawer'
import Form, { FormRef } from './Form' import Form, { FormRef } from './Form'
import FormItem from './FormItem' import FormItem from './FormItem'
import Input from './Input' import Input from './Input'
@@ -8,6 +10,8 @@ import LucideIcon from './LucideIcon'
export { export {
Button as AButton, Button as AButton,
Checkbox as ACheckbox, Checkbox as ACheckbox,
DatePicker as ADatePicker,
DateTimeDrawer as ADateTimeDrawer,
Form as AForm, Form as AForm,
FormItem as AFormItem, FormItem as AFormItem,
FormRef as AFormRef, FormRef as AFormRef,
@@ -1,8 +1,19 @@
import type { MoneyCategory } from '@/types/money' import type { MoneyCategory, MoneyRecord } from '@/types/money'
import alovaInstance from '../index' import alovaInstance from '../index'
const API_PREFIX = '/money' const API_PREFIX = '/money'
export interface QueryMoneyRecordsParams {
record_time: string
}
export interface QueryMoneyRecordsResult {
[key: string]: MoneyRecord[]
}
export function queryMoneyRecordsApi(params: QueryMoneyRecordsParams) {
return alovaInstance.Post<ResponseData<QueryMoneyRecordsResult>>(`${API_PREFIX}/query`, params)
}
export interface MoneyCategoryResult { export interface MoneyCategoryResult {
income: MoneyCategory[] income: MoneyCategory[]
expense: MoneyCategory[] expense: MoneyCategory[]
@@ -30,8 +30,8 @@ function isWhiteListed(url: string) {
} }
const alovaInstance = createAlova({ const alovaInstance = createAlova({
baseURL: 'http://192.168.5.6:8085/app', // baseURL: 'http://192.168.5.6:8085/app',
// baseURL: 'http://192.168.1.136:8085/app', baseURL: 'http://192.168.1.136:8085/app',
requestAdapter: axiosRequestAdapter(), requestAdapter: axiosRequestAdapter(),
timeout: 5000, timeout: 5000,
cacheFor: null, cacheFor: null,
@@ -0,0 +1,29 @@
import dayjs from 'dayjs'
/**
* 获取当前日期,默认格式: YYYY-MM-DD
*/
export function getCurrentDate(format = 'YYYY-MM-DD'): string {
return dayjs().format(format)
}
/**
* 获取当前日期时间,默认格式: YYYY-MM-DD HH:mm:ss
*/
export function getCurrentDateTime(format = 'YYYY-MM-DD HH:mm:ss'): string {
return dayjs().format(format)
}
/**
* 获取当前时间,默认格式: HH:mm:ss
*/
export function getCurrentTime(format = 'HH:mm:ss'): string {
return dayjs().format(format)
}
/**
* 获取当前年月,默认格式: YYYY-MM
*/
export function getCurrentYearMonth(format = 'YYYY-MM'): string {
return dayjs().format(format)
}
@@ -69,7 +69,7 @@ func loginDefault(c *router.Context) error {
"account", account, "account", account,
"password", password, "password", password,
) )
user, err := db.FindOne("user_info", userQuery) user, err := db.SelectOne("user_info", db.BuildWhereString(userQuery), "")
if err != nil { if err != nil {
return response.Success(c).Message("查询用户失败,请稍后重试").Send() return response.Success(c).Message("查询用户失败,请稍后重试").Send()
} }
@@ -127,7 +127,7 @@ func registerDefault(c *router.Context) error {
// 构造查询条件,检查账号是否已存在 // 构造查询条件,检查账号是否已存在
accountQuery := mapx.New().SetKV("account", account) accountQuery := mapx.New().SetKV("account", account)
existingUsers, err := db.FindOne("user_info", accountQuery) existingUsers, err := db.SelectOne("user_info", db.BuildWhereString(accountQuery), "")
if err != nil { if err != nil {
return c.AddError(err) return c.AddError(err)
} }
@@ -8,10 +8,12 @@ import (
"base-go-v2/internal/utils/mapx" "base-go-v2/internal/utils/mapx"
"fmt" "fmt"
"github.com/shopspring/decimal" "github.com/shopspring/decimal"
"time"
) )
func InitMoneyRouter(r *router.Router) { func InitMoneyRouter(r *router.Router) {
group := r.Group("money") group := r.Group("money")
group.POST("/query", queryMoneyRecords)
group.GET("/category", getUserCategory) group.GET("/category", getUserCategory)
group.POST("/insert", insertMoneyRecord) group.POST("/insert", insertMoneyRecord)
group.POST("/update", updateMoneyRecord) group.POST("/update", updateMoneyRecord)
@@ -20,10 +22,8 @@ func InitMoneyRouter(r *router.Router) {
// InitDefaultCategories 初始化系统默认分类到用户分类表 // InitDefaultCategories 初始化系统默认分类到用户分类表
func InitDefaultCategories(userID int64) error { func InitDefaultCategories(userID int64) error {
fmt.Println("进入 InitDefaultCategories ===")
// 1. 查询系统分类表 // 1. 查询系统分类表
sysCategories, err := db.FindAll("money_sys_category", "sort_number") sysCategories, err := db.SelectList("money_sys_category", "", "sort_number")
if err != nil { if err != nil {
return err return err
} }
@@ -31,7 +31,6 @@ func InitDefaultCategories(userID int64) error {
fmt.Printf("系统分类数量: %d\n", len(sysCategories)) fmt.Printf("系统分类数量: %d\n", len(sysCategories))
// 2. 构建批量插入数据 // 2. 构建批量插入数据
//userCategories := make([]mapx.M, 0, len(sysCategories))
userCategories := mapx.NewArr(len(sysCategories)) userCategories := mapx.NewArr(len(sysCategories))
for i, row := range sysCategories { for i, row := range sysCategories {
@@ -52,15 +51,124 @@ func InitDefaultCategories(userID int64) error {
return err return err
} }
fmt.Println("初始化用户分类成功!")
return nil return nil
} }
// 根据
func queryMoneyRecords(c *router.Context) error {
bodyData, err := c.GetBodyWithRequired("record_time") // "YYYY-MM"
if err != nil {
return c.AddError(err)
}
recordMonth := bodyData.GetString("record_time")
// 解析年月
startTime, err := time.Parse("2006-01", recordMonth)
if err != nil {
return c.AddError(fmt.Errorf("invalid record_time format: %v", err))
}
// 获取当月最后一天
endTime := startTime.AddDate(0, 1, 0).Add(-time.Nanosecond)
// SQL: money_record LEFT JOIN money_user_category
sqlStr := fmt.Sprintf(`
SELECT
mr.*,
muc.name AS category_name,
muc.icon AS category_icon,
muc.type AS category_type
FROM money_record mr
LEFT JOIN money_user_category muc ON mr.category_id = muc.id
WHERE mr.create_time BETWEEN '%s' AND '%s'
ORDER BY mr.create_time DESC
`, startTime.Format("2006-01-02 15:04:05"), endTime.Format("2006-01-02 15:04:05"))
// 查询
list, err := db.SelectBySql(sqlStr)
if err != nil {
return c.AddError(err)
}
// 分组数据
grouped := mapx.New() // mapx.M
totalIncome := decimal.NewFromInt(0)
totalExpense := decimal.NewFromInt(0)
for _, r := range list {
row := mapx.M(r)
createTime, ok := row["create_time"].(time.Time)
if !ok {
continue
}
day := createTime.Format("2006-01-02")
// 获取当天数据
dayDataI := grouped.Get(day)
var dayData mapx.M
if dayDataI == nil {
dayData = mapx.New().
Set("records", mapx.NewArr()).
Set("income", decimal.NewFromInt(0).String()).
Set("expense", decimal.NewFromInt(0).String())
} else {
dayData = dayDataI.(mapx.M)
}
// 追加记录
records := dayData.GetArray("records")
records = append(records, row)
dayData.Set("records", records)
// 计算当天收入支出
categoryType := row.GetInt("category_type")
amount, err := decimal.NewFromString(row.GetString("amount"))
if err != nil {
continue
}
dayIncome, err := decimal.NewFromString(dayData.GetString("income"))
if err != nil {
dayIncome = decimal.NewFromInt(0)
}
dayExpense, err := decimal.NewFromString(dayData.GetString("expense"))
if err != nil {
dayExpense = decimal.NewFromInt(0)
}
if categoryType == 0 {
dayIncome = dayIncome.Add(amount)
totalIncome = totalIncome.Add(amount)
} else if categoryType == 1 {
dayExpense = dayExpense.Add(amount)
totalExpense = totalExpense.Add(amount)
}
dayData.Set("income", dayIncome.String())
dayData.Set("expense", dayExpense.String())
grouped.Set(day, dayData)
}
result := mapx.New().
Set("records", grouped).
Set("total_income", totalIncome.String()).
Set("total_expense", totalExpense.String())
return response.Success(c).Data(result).Send()
}
func getUserCategory(c *router.Context) error { func getUserCategory(c *router.Context) error {
userId := c.GetUserID() userId := c.GetUserID()
// 查询当前用户的分类 // 查询当前用户的分类
userCategories, err := db.Find("money_user_category", mapx.New().Set("user_id", userId), "sort_number ASC") userCategories, err := db.SelectList("money_user_category",
db.BuildWhereString(mapx.New().Set("user_id", userId)),
"sort_number ASC")
if err != nil { if err != nil {
return c.AddError(err) return c.AddError(err)
} }
@@ -126,7 +234,7 @@ func updateMoneyRecord(c *router.Context) error {
recordId := bodyData.GetInt64("id") recordId := bodyData.GetInt64("id")
record, err := db.GetOne("money_record", "id", recordId) record, err := db.SelectById("money_record", "id", recordId)
if err != nil { if err != nil {
return c.AddError(err) return c.AddError(err)
} }
@@ -24,9 +24,11 @@ var defaultPoolConfig = struct {
func InitDb() error { func InitDb() error {
d := config.App.Db d := config.App.Db
// 构造 DSN // 构造 DSN,指定时区为 Asia/Shanghai
dsn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", dsn := fmt.Sprintf(
d.Host, d.Port, d.User, d.Password, d.Dbname) "host=%s port=%d user=%s password=%s dbname=%s sslmode=disable TimeZone=Asia/Shanghai",
d.Host, d.Port, d.User, d.Password, d.Dbname,
)
db, err := sqlx.Connect("postgres", dsn) db, err := sqlx.Connect("postgres", dsn)
if err != nil { if err != nil {
@@ -8,7 +8,6 @@ import (
// ---------------- 通用查询方法 ---------------- // ---------------- 通用查询方法 ----------------
// 执行查询,返回多条记录
func queryMaps(query string, args []interface{}) ([]mapx.M, error) { func queryMaps(query string, args []interface{}) ([]mapx.M, error) {
rows, err := DB.Queryx(query, args...) rows, err := DB.Queryx(query, args...)
if err != nil { if err != nil {
@@ -22,13 +21,20 @@ func queryMaps(query string, args []interface{}) ([]mapx.M, error) {
if err := rows.MapScan(row); err != nil { if err := rows.MapScan(row); err != nil {
return nil, err return nil, err
} }
// 统一处理 []byte 类型,直接转字符串,保留原始小数点
for k, v := range row {
if b, ok := v.([]byte); ok {
row[k] = string(b)
}
}
result = append(result, row) result = append(result, row)
} }
return result, nil return result, nil
} }
// 执行查询,返回单条记录
func queryMap(query string, args []interface{}) (mapx.M, error) { func queryMap(query string, args []interface{}) (mapx.M, error) {
rows, err := DB.Queryx(query, args...) rows, err := DB.Queryx(query, args...)
if err != nil { if err != nil {
@@ -41,93 +47,73 @@ func queryMap(query string, args []interface{}) (mapx.M, error) {
if err := rows.MapScan(row); err != nil { if err := rows.MapScan(row); err != nil {
return nil, err return nil, err
} }
// 统一处理 []byte 类型,直接转字符串
for k, v := range row {
if b, ok := v.([]byte); ok {
row[k] = string(b)
}
}
return row, nil return row, nil
} }
return nil, nil return nil, nil
} }
// 构建 WHERE 条件 // BuildWhereString 将 map 条件转换为 SQL WHERE 字符串
func buildWhere(conditions map[string]interface{}) (string, []interface{}) { func BuildWhereString(conds mapx.M) string {
var whereParts []string if len(conds) == 0 {
var values []interface{} return ""
i := 1
for k, v := range conditions {
whereParts = append(whereParts, fmt.Sprintf("%s=$%d", k, i))
values = append(values, v)
i++
}
return strings.Join(whereParts, " AND "), values
} }
// ---------------- 公共查询方法 ---------------- var parts []string
for k, v := range conds {
parts = append(parts, fmt.Sprintf("%s='%v'", k, v))
}
// GetOne 根据主键查询单条记录 return strings.Join(parts, " AND ")
func GetOne(table string, pkColumn string, pkValue interface{}) (mapx.M, error) { }
query := fmt.Sprintf("SELECT * FROM %s WHERE %s=$1", table, pkColumn)
// ---------------- 统一 select 系列方法 ----------------
// SelectById 根据主键查询单条记录
func SelectById(tableName, pkColumn string, pkValue interface{}) (mapx.M, error) {
query := fmt.Sprintf("SELECT * FROM %s WHERE %s=$1", tableName, pkColumn)
return queryMap(query, []interface{}{pkValue}) return queryMap(query, []interface{}{pkValue})
} }
// GetBatch 根据主键批量查询 // SelectBySql 根据任意 SQL 查询多条记录
func GetBatch(table string, pkColumn string, pkValues []interface{}) ([]mapx.M, error) { func SelectBySql(sql string, args ...interface{}) ([]mapx.M, error) {
if len(pkValues) == 0 { return queryMaps(sql, args)
return nil, nil
} }
placeholders := make([]string, len(pkValues)) // SelectList 查询列表,可传 where 和 order,where 可以为空
for i := range pkValues { func SelectList(tableName string, where string, order string) ([]mapx.M, error) {
placeholders[i] = fmt.Sprintf("$%d", i+1) query := fmt.Sprintf("SELECT * FROM %s", tableName)
if strings.TrimSpace(where) != "" {
query += " WHERE " + where
} }
query := fmt.Sprintf("SELECT * FROM %s WHERE %s IN (%s)", table, pkColumn, strings.Join(placeholders, ",")) if strings.TrimSpace(order) == "" {
return queryMaps(query, pkValues) order = "id DESC"
}
// Find 根据条件查询多条记录,可选排序
func Find(table string, conditions map[string]interface{}, orderBy ...string) ([]mapx.M, error) {
if len(conditions) == 0 {
return nil, fmt.Errorf("查询条件不能为空")
}
where, values := buildWhere(conditions)
query := fmt.Sprintf("SELECT * FROM %s WHERE %s", table, where)
order := "id DESC"
if len(orderBy) > 0 && orderBy[0] != "" {
order = orderBy[0]
}
query += " ORDER BY " + order
return queryMaps(query, values)
}
// FindAll 查询整个表,可选排序
func FindAll(table string, orderBy ...string) ([]mapx.M, error) {
query := fmt.Sprintf("SELECT * FROM %s", table)
order := "id DESC"
if len(orderBy) > 0 && orderBy[0] != "" {
order = orderBy[0]
} }
query += " ORDER BY " + order query += " ORDER BY " + order
return queryMaps(query, nil) return queryMaps(query, nil)
} }
// FindOne 根据条件查询单条记录,可选排序 // SelectOne 查询单条记录,可传 where 和 order(order 可为空)
func FindOne(table string, conditions map[string]interface{}, orderBy ...string) (mapx.M, error) { func SelectOne(tableName string, where string, order string) (mapx.M, error) {
if len(conditions) == 0 { query := fmt.Sprintf("SELECT * FROM %s", tableName)
return nil, fmt.Errorf("查询条件不能为空") if strings.TrimSpace(where) != "" {
query += " WHERE " + where
} }
where, values := buildWhere(conditions) if strings.TrimSpace(order) == "" {
query := fmt.Sprintf("SELECT * FROM %s WHERE %s", table, where) order = "id DESC" // 默认排序
order := "id DESC"
if len(orderBy) > 0 && orderBy[0] != "" {
order = orderBy[0]
} }
query += " ORDER BY " + order + " LIMIT 1" query += " ORDER BY " + order + " LIMIT 1"
return queryMap(query, values) return queryMap(query, nil)
} }
@@ -86,6 +86,16 @@ func (m M) GetString(key string) string {
return "" return ""
} }
// GetArray 获取 map 中的 []M,如果不存在或类型不对返回空切片
func (m M) GetArray(key string) []M {
if v, ok := m[key]; ok {
if arr, ok := v.([]M); ok {
return arr
}
}
return NewArr()
}
// 核心获取整数的方法,返回 int64 // 核心获取整数的方法,返回 int64
func (m M) getIntValue(key string) int64 { func (m M) getIntValue(key string) int64 {
if v, ok := m[key]; ok && v != nil { if v, ok := m[key]; ok && v != nil {