diff --git a/code/base-project/base-app/package.json b/code/base-project/base-app/package.json
index 130470c5..4893d85b 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-community/datetimepicker": "8.4.4",
"@react-native-segmented-control/segmented-control": "2.5.7",
"alova": "^3.3.4",
"axios": "^1.13.2",
@@ -37,14 +38,14 @@
"react-native": "0.81.5",
"react-native-css-interop": "^0.2.1",
"react-native-dates-picker": "^0.2.0",
+ "react-native-drawer-layout": "^4.2.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-modal-datetime-picker": "^18.0.0",
"react-native-reanimated": "~4.1.3",
"react-native-safe-area-context": "~5.6.1",
"react-native-screens": "~4.16.0",
"react-native-svg": "15.12.1",
- "react-native-ui-datepicker": "^3.1.2",
"react-native-web": "^0.21.0",
"react-native-worklets": "0.5.1",
"zustand": "^5.0.8"
diff --git a/code/base-project/base-app/pnpm-lock.yaml b/code/base-project/base-app/pnpm-lock.yaml
index b18dad8b..28b7893b 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-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':
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)
@@ -89,15 +92,18 @@ importers:
react-native-dates-picker:
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)
+ 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:
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)
- 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:
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)
+ 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:
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)
@@ -110,9 +116,6 @@ importers:
react-native-svg:
specifier: 15.12.1
version: 15.12.1(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
- react-native-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:
specifier: ^0.21.0
version: 0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@@ -1606,6 +1609,19 @@ packages:
peerDependencies:
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':
resolution: {integrity: sha512-l84YeVX8xAU3lvOJSvV4nK/NbGhIm2gBfveYolwaoCbRp+/SLXtc6mYrQmM9ScXNwU14mnzjQTpTHWl5YPnkzQ==}
peerDependencies:
@@ -3578,9 +3594,6 @@ packages:
jackspeak@3.4.3:
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:
resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@@ -3757,48 +3770,56 @@ packages:
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
lightningcss-linux-arm64-gnu@1.30.2:
resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
lightningcss-linux-arm64-musl@1.27.0:
resolution: {integrity: sha512-rCGBm2ax7kQ9pBSeITfCW9XSVF69VX+fm5DIpvDZQl4NnQoMQyRwhZQm9pd59m8leZ1IesRqWk2v/DntMo26lg==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
+ libc: [musl]
lightningcss-linux-arm64-musl@1.30.2:
resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
+ libc: [musl]
lightningcss-linux-x64-gnu@1.27.0:
resolution: {integrity: sha512-Dk/jovSI7qqhJDiUibvaikNKI2x6kWPN79AQiD/E/KeQWMjdGe9kw51RAgoWFDi0coP4jinaH14Nrt/J8z3U4A==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
+ libc: [glibc]
lightningcss-linux-x64-gnu@1.30.2:
resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
+ libc: [glibc]
lightningcss-linux-x64-musl@1.27.0:
resolution: {integrity: sha512-QKjTxXm8A9s6v9Tg3Fk0gscCQA1t/HMoF7Woy1u68wCk5kS4fR+q3vXa1p3++REW784cRAtkYKrPy6JKibrEZA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
+ libc: [musl]
lightningcss-linux-x64-musl@1.30.2:
resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
+ libc: [musl]
lightningcss-win32-arm64-msvc@1.27.0:
resolution: {integrity: sha512-/wXegPS1hnhkeG4OXQKEMQeJd48RDC3qdh+OA8pCuOPCyvnm/yEayrJdJVqzBsqpy1aJklRCVxscpFur80o6iQ==}
@@ -4654,6 +4675,14 @@ packages:
react: '>=17.0.0'
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:
resolution: {integrity: sha512-0msfJ1vRxXKVgTgvL+1ZOoYw3/0z1R+Ked0+udoJhyplC2jbVKIJ8Z1bzWdpQRCV3QcQ87Op0zJVE5DhKK2A0A==}
peerDependencies:
@@ -4668,22 +4697,12 @@ packages:
react-native: '*'
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:
resolution: {integrity: sha512-FLbPWl/MyYQWz+KwqOZsSyj2JmLKglHatd3xLZWskXOpRaio4LfEDEz8E/A6uD8QoTHW6Aobw1jbEwK7KMgR7Q==}
peerDependencies:
react: '*'
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:
resolution: {integrity: sha512-wbYN6Tcu3G5a05dhRYBgjgd74KqoYWuUmroLpigRg9cXy5uYo7prTMIvMgvLtARQtUF7BOtFggUnzgoBOgk0TQ==}
peerDependencies:
@@ -4691,6 +4710,12 @@ packages:
react-native: '*'
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:
resolution: {integrity: sha512-UA6VUbxwhRjEw2gSNrvhkusUq3upfD3Cv+AnB07V+kC8kpvwRVI+ivwY95ePbWNFkFpP+Y2Sdw1WHpHWEV+P2Q==}
peerDependencies:
@@ -4717,13 +4742,6 @@ packages:
react: '*'
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:
resolution: {integrity: sha512-SO2t9/17zM4iEnFvlu2DA9jqNbzNhoUP+AItkoCOyFmDMOhUnBBznBDCYN92fGdfAkfQlWzPoez6+zLxFNsZEg==}
peerDependencies:
@@ -5124,9 +5142,6 @@ packages:
tailwind-merge@1.14.0:
resolution: {integrity: sha512-3mFKyCo/MBcgyOTlrY8T7odzZFx+w+qKSMAmdFzRvqBfLlSigU6TZnlFHK0lkMwj9Bj8OYU+9yW9lmGuS0QEnQ==}
- tailwind-merge@3.4.0:
- resolution: {integrity: sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==}
-
tailwind-variants@0.1.20:
resolution: {integrity: sha512-AMh7x313t/V+eTySKB0Dal08RHY7ggYK0MSn/ad8wKWOrDUIzyiWNayRUm2PIJ4VRkvRnfNuyRuKbLV3EN+ewQ==}
engines: {node: '>=16.x', pnpm: '>=7.x'}
@@ -7634,6 +7649,14 @@ 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-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)':
dependencies:
react: 19.1.0
@@ -9953,8 +9976,6 @@ snapshots:
optionalDependencies:
'@pkgjs/parseargs': 0.11.0
- jalali-plugin-dayjs@1.1.4: {}
-
jest-environment-node@29.7.0:
dependencies:
'@jest/environment': 29.7.0
@@ -11413,6 +11434,15 @@ snapshots:
react: 19.1.0
react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0)
+ react-native-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):
dependencies:
'@egjs/hammerjs': 2.0.17
@@ -11436,21 +11466,11 @@ snapshots:
transitivePeerDependencies:
- 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):
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-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):
dependencies:
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-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):
dependencies:
'@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)
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):
dependencies:
'@babel/runtime': 7.28.4
@@ -11951,8 +11967,6 @@ snapshots:
tailwind-merge@1.14.0: {}
- tailwind-merge@3.4.0: {}
-
tailwind-variants@0.1.20(tailwindcss@3.4.18(yaml@2.8.1)):
dependencies:
tailwind-merge: 1.14.0
diff --git a/code/base-project/base-app/src/app/(tabs)/chat.tsx b/code/base-project/base-app/src/app/(tabs)/chat.tsx
index f0261775..d09b0503 100644
--- a/code/base-project/base-app/src/app/(tabs)/chat.tsx
+++ b/code/base-project/base-app/src/app/(tabs)/chat.tsx
@@ -1,42 +1,48 @@
import dayjs from 'dayjs'
-import { useState } from 'react'
-import { StyleSheet, View } from 'react-native'
-import DateTimePicker from 'react-native-dates-picker'
+// DemoADatePicker.tsx
+import React, { useState } from 'react'
+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())
-
return (
-
-
+ {
+ console.log(val)
}}
- headerTextStyle={{
- fontSize: 18,
+ onClose={() => {
+ setShow(false)
}}
- headerButtonStyle={{
- backgroundColor: '#ffffff',
- borderRadius: 8,
- }}
- headerButtonColor="#00d279"
- headerButtonSize={18}
- headerButtonsPosition="right"
- onChange={params => setDate(params.date)}
/>
-
+
+
)
}
const styles = StyleSheet.create({
container: {
- flex: 1,
- backgroundColor: '#F5FCFF',
- marginTop: 128,
- padding: 16,
+ padding: 20,
+ },
+ label: {
+ marginTop: 20,
+ fontSize: 16,
+ fontWeight: 'bold',
+ },
+ result: {
+ marginVertical: 5,
+ fontSize: 14,
+ color: '#333',
},
})
diff --git a/code/base-project/base-app/src/app/(tabs)/money/components/AddDrawer.tsx b/code/base-project/base-app/src/app/(tabs)/money/components/AddDrawer.tsx
index 938638e1..f7ebc183 100644
--- a/code/base-project/base-app/src/app/(tabs)/money/components/AddDrawer.tsx
+++ b/code/base-project/base-app/src/app/(tabs)/money/components/AddDrawer.tsx
@@ -1,10 +1,10 @@
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 { useEffect, useRef, useState } from 'react'
-import { Keyboard, Pressable, StyleSheet, Text, TouchableWithoutFeedback, View } from 'react-native'
+import { useEffect, useState } from 'react'
+import { ActivityIndicator, Keyboard, Pressable, StyleSheet, Text, TouchableWithoutFeedback, View } from 'react-native'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
-import { AInput, LucideIcon } from '@/components/common/ao'
+import { ADatePicker, ADateTimeDrawer, AInput, LucideIcon } from '@/components/common/ao'
import SegmentedTabs from '@/components/common/ao/SegmentedTabs'
import {
Drawer,
@@ -15,7 +15,9 @@ import {
} from '@/components/ui/drawer'
import { Grid, GridItem } from '@/components/ui/grid'
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'
interface AddMoneyDrawerProps {
@@ -27,6 +29,7 @@ interface KeydownItem {
key: string | number
content: React.ReactNode
}
+
const keys: KeydownItem[] = [
{ key: '1', content: 1 },
{ key: '2', content: 2 },
@@ -40,9 +43,9 @@ const keys: KeydownItem[] = [
{ key: '8', content: 8 },
{ key: '9', content: 9 },
{ key: '-', content: },
- { key: 'repeat', content: (
+ { key: 'clear', content: (
- 再记
+ 重置
) },
{ key: '0', content: 0 },
@@ -59,21 +62,42 @@ export default function AddMoneyDrawer({
onClose,
}: AddMoneyDrawerProps) {
const insets = useSafeAreaInsets()
+ // 选择顺序:0 支出 1收入
const [selectedIndex, setSelectedIndex] = useState(0)
- const [selectedCategoryId, setSelectedCategoryId] = useState(null)
+ // 自定义键盘
const [showKeydown, setShowKeydown] = useState(false)
- const [keydownInput, setKeydownInput] = useState('')
- const [result, setResult] = useState('0')
-
+ // 自定义键盘计算过程
+ const [keydownInput, setKeydownInput] = useState('')
+ // 日期选择
+ const [showDatePicker, setShowDatePicker] = useState(false)
+ // 备注聚焦
+ const [remarkIsFocused, setRemarkIsFocused] = useState(false)
+ // 分类
const [categories, setCategories] = useState({
income: [],
expense: [],
})
+
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(defaultMoneyRecord)
function selectCategory(item: MoneyCategory) {
- setSelectedCategoryId(item.id)
+ setMoneyRecord({
+ ...moneyRecord,
+ category_id: item.id,
+ })
+
setShowKeydown(true)
}
@@ -94,11 +118,23 @@ export default function AddMoneyDrawer({
}
}, [isOpen])
+ // 关闭整个drawer处理
function onMainDrawerClose() {
- setShowKeydown(false)
onClose()
+
+ // 清空键盘记录
+ setKeydownInput('')
+ // 关闭默认键盘
+ setShowKeydown(false)
+ // 设置默认值
+ setMoneyRecord(defaultMoneyRecord)
+ // 选中支出
+ setSelectedIndex(0)
+ // 关闭日期
+ setShowDatePicker(false)
}
+ // 计算过程
const handlePress = (key: string | number) => {
setKeydownInput((prev) => {
let newInput = prev
@@ -106,8 +142,17 @@ export default function AddMoneyDrawer({
if (key === 'del') {
newInput = prev.slice(0, -1)
}
+ else if (key === 'clear') {
+ setMoneyRecord({
+ ...moneyRecord,
+ amount: '0.00',
+ remark: '',
+ record_time: getCurrentDate(),
+ })
+ return ''
+ }
else if (key === '/') {
- // 完成操作,可以在这里提交或关闭键盘
+ saveRecord()
return prev
}
else {
@@ -115,6 +160,11 @@ export default function AddMoneyDrawer({
// 检查是否要输入运算符(+ 或 -)
if (keyStr === '+' || keyStr === '-') {
+ // 如果当前输入为空,不允许以运算符开头
+ if (prev === '') {
+ return prev
+ }
+
// 获取最后一个字符
const lastChar = prev.slice(-1)
@@ -122,30 +172,43 @@ export default function AddMoneyDrawer({
if (lastChar === '+' || lastChar === '-') {
newInput = prev.slice(0, -1) + keyStr
}
+ // 如果最后一个字符是小数点,不允许在小数点后直接加运算符
+ else if (lastChar === '.') {
+ return prev
+ }
else {
newInput = prev + keyStr
}
}
// 检查是否要输入小数点(.)
else if (keyStr === '.') {
- // 检查当前数字是否已经包含小数点
- const parts = prev.split(/[+-]/)
- const currentNumber = parts[parts.length - 1]
+ // 如果当前输入为空,不允许以小数点开头,改为输入"0."
+ if (prev === '') {
+ newInput = '0.'
+ }
+ else {
+ // 获取最后一个字符
+ const lastChar = prev.slice(-1)
- // 如果当前数字中还没有小数点,可以添加
- if (!currentNumber.includes('.')) {
- // 检查当前数字的小数位数(应该是0,因为还没有小数点)
- if (currentNumber === '' || /[+-]/.test(prev.slice(-1))) {
+ // 如果最后一个字符是运算符,则需要在运算符后添加"0."而不是单独的"."
+ if (lastChar === '+' || lastChar === '-') {
newInput = `${prev}0.`
}
else {
- newInput = `${prev}.`
+ // 检查当前数字是否已经包含小数点
+ const parts = prev.split(/[+-]/)
+ const currentNumber = parts[parts.length - 1]
+
+ // 如果当前数字中还没有小数点,可以添加
+ if (!currentNumber.includes('.')) {
+ newInput = `${prev}.`
+ }
+ else {
+ // 已经有小数点了,不重复添加
+ newInput = prev
+ }
}
}
- else {
- // 已经有小数点了,不重复添加
- newInput = prev
- }
}
// 处理数字输入
else if (/^\d$/.test(keyStr)) {
@@ -178,10 +241,16 @@ export default function AddMoneyDrawer({
// 计算结果
try {
const computed = calculateWithBig(newInput)
- setResult(computed)
+ setMoneyRecord({
+ ...moneyRecord,
+ amount: computed,
+ })
}
catch (e) {
- setResult('0')
+ setMoneyRecord({
+ ...moneyRecord,
+ amount: '0',
+ })
}
return newInput
@@ -242,166 +311,213 @@ export default function AddMoneyDrawer({
return total.toFixed(2)
}
}
- const inputRef = useRef(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 (
-
-
-
-
+
-
-
-
-
- 取消
-
-
-
-
- {(() => {
- const list = selectedIndex === 0 ? categories.income : categories.expense
- const items = list.map((item) => {
- const isSelected = selectedCategoryId === item.id
- return (
-
- selectCategory(item)}
- >
-
-
-
-
- {item.name}
-
-
-
- )
- })
+
- const remainder = list.length % 4
- if (remainder > 0) {
- const emptyCount = 4 - remainder
- for (let i = 0; i < emptyCount; i++) {
- items.push()
- }
- }
-
- return items
- })()}
-
-
-
- {showKeydown && (
-
+
-
-
-
-
-
- 2024/11/05
-
-
- {result}
+
+
+
+ 取消
+
+
+
+
+ {(() => {
+ const list = selectedIndex === 1 ? categories.income : categories.expense
+ const items = list.map((item) => {
+ const isSelected = moneyRecord.category_id === item.id
+ return (
+
+ selectCategory(item)}
+ >
+
+
+
+
+ {item.name}
+
+
+
+ )
+ })
+
+ const remainder = list.length % 4
+ if (remainder > 0) {
+ const emptyCount = 4 - remainder
+ for (let i = 0; i < emptyCount; i++) {
+ items.push()
+ }
+ }
+
+ return items
+ })()}
+
+
+
+ {showKeydown && (
+
+
+
+
+ setShowDatePicker(true)}
+ >
+
+ {moneyRecord.record_time}
+
+
+ {moneyRecord.amount}
+
+
+ {keydownInput}
+ {
+ setMoneyRecord({
+ ...moneyRecord,
+ remark: val,
+ })
+ }}
+ onFocus={() => setRemarkIsFocused(true)}
+ onBlur={() => setRemarkIsFocused(false)}
+ />
+
+
+ {keys.map((item, idx) => {
+ const isFirstCol = idx % 4 === 0
+ const isFirstRow = idx < 4
+ const isLastCol = (idx + 1) % 4 === 0
+ const isLastRow = idx >= 12
- {keydownInput}
- setIsFocused(true)}
- onBlur={() => setIsFocused(false)}
- />
+ return (
+ handlePress(item.key)}
+ style={{
+ width: '25%',
+ height: '25%',
+ justifyContent: 'center',
+ alignItems: 'center',
+ borderTopWidth: isFirstRow ? 0.3 : 0,
+ borderLeftWidth: isFirstCol ? 0.3 : 0,
+ borderRightWidth: !isLastCol ? 0.3 : 0,
+ borderBottomWidth: !isLastRow ? 0.3 : 0,
+ borderColor: '#ddd',
+ }}
+ >
+ {({ pressed }) => {
+ const defaultBg = item.key === '/' ? '#e6d7b8' : '#fff'
+ const pressedBg = item.key === '/' ? '#f5e9d4' : '#f2f2f2'
+
+ return (
+ <>
+
+ {item.content}
+ >
+ )
+ }}
+
+ )
+ })}
-
-
- {keys.map((item, idx) => {
- const isFirstCol = idx % 4 === 0
- const isFirstRow = idx < 4
- const isLastCol = (idx + 1) % 4 === 0
- const isLastRow = idx >= 12
-
- return (
- handlePress(item.key)}
- style={{
- width: '25%',
- height: '25%',
- justifyContent: 'center',
- alignItems: 'center',
- borderTopWidth: isFirstRow ? 0.3 : 0,
- borderLeftWidth: isFirstCol ? 0.3 : 0,
- borderRightWidth: !isLastCol ? 0.3 : 0,
- borderBottomWidth: !isLastRow ? 0.3 : 0,
- borderColor: '#ddd',
- }}
- >
- {({ pressed }) => {
- const defaultBg = item.key === '/' ? '#e6d7b8' : '#fff'
- const pressedBg = item.key === '/' ? '#f5e9d4' : '#f2f2f2'
-
- return (
- <>
-
- {item.content}
- >
- )
- }}
-
- )
- })}
+ {remarkIsFocused && keyboardHeight > 220 && (
+
+ )}
- {isFocused && keyboardHeight > 220 && (
-
- )}
-
-
- )}
-
-
+ )}
+
+
+ {
+ setMoneyRecord({
+ ...moneyRecord,
+ record_time: val,
+ })
+ setShowDatePicker(false)
+ }}
+ onClose={() => {
+ setShowDatePicker((false))
+ }}
+ />
+ >
)
}
diff --git a/code/base-project/base-app/src/app/(tabs)/money/index.tsx b/code/base-project/base-app/src/app/(tabs)/money/index.tsx
index 27ba5697..38bb68b5 100644
--- a/code/base-project/base-app/src/app/(tabs)/money/index.tsx
+++ b/code/base-project/base-app/src/app/(tabs)/money/index.tsx
@@ -1,26 +1,29 @@
import type { LayoutChangeEvent } from 'react-native'
+import { BlurView } from 'expo-blur'
import { LinearGradient } from 'expo-linear-gradient'
-import React, { useRef, useState } from 'react'
-import { ScrollView, Text, View } from 'react-native'
+import React, { useEffect, useRef, useState } from 'react'
+import { ScrollView, Text, TouchableOpacity, View } from 'react-native'
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 { Fab, FabIcon } from '@/components/ui/fab'
import { Grid, GridItem } from '@/components/ui/grid'
import { AddIcon } from '@/components/ui/icon'
import { Progress, ProgressFilledTrack } from '@/components/ui/progress'
+import { queryMoneyRecordsApi } from '@/request/api/money'
+import { getCurrentYearMonth } from '@/utils/time'
import AddMoneyDrawer from './components/AddDrawer'
import TransactionsList from './components/TransactionsList'
export default function Money() {
- const [showDrawer, setShowDrawer] = useState(false)
+ const [showAddDrawer, setShowAddDrawer] = useState(false)
+ // 安全高度
const insets = useSafeAreaInsets()
const [gradientHeight, setGradientHeight] = useState(200)
const gridLayout = useRef<{ y: number, height: number } | null>(null)
const onGridLayout = (e: LayoutChangeEvent) => {
- // 仅首次布局计算高度
if (!gridLayout.current) {
const { y, height } = e.nativeEvent.layout
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 (
-
- 2025/11/10
-
-
+
+ setShowDatePicker(true)}
+ className="flex flex-row items-center gap-2"
+ >
+ {searchParams.record_time}
+
+
+
+
@@ -144,16 +167,31 @@ export default function Money() {
{
- setShowDrawer(true)
+ setShowAddDrawer(true)
}}
>
{
- setShowDrawer(false)
+ setShowAddDrawer(false)
+ }}
+ />
+
+ {
+ setSearchParams({
+ ...searchParams,
+ record_time: val,
+ })
+ setShowDatePicker(false)
+ }}
+ onClose={() => {
+ setShowDatePicker(false)
}}
/>
diff --git a/code/base-project/base-app/src/components/common/ao/DatePicker.tsx b/code/base-project/base-app/src/components/common/ao/DatePicker.tsx
new file mode 100644
index 00000000..5a1b12cf
--- /dev/null
+++ b/code/base-project/base-app/src/components/common/ao/DatePicker.tsx
@@ -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 {
+ 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 (
+
+ )
+}
diff --git a/code/base-project/base-app/src/components/common/ao/DateTimeDrawer.tsx b/code/base-project/base-app/src/components/common/ao/DateTimeDrawer.tsx
new file mode 100644
index 00000000..b4807d83
--- /dev/null
+++ b/code/base-project/base-app/src/components/common/ao/DateTimeDrawer.tsx
@@ -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 = {
+ '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 = {
+ '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(defaultDate)
+ // 保存上一次确认的值
+ const lastConfirmedRef = useRef(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 (
+
+
+
+
+ 取消
+ 确认
+
+
+
+ setDate(params.date)}
+ />
+
+
+
+
+
+ )
+}
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 a7d71a6c..d78bfd82 100644
--- a/code/base-project/base-app/src/components/common/ao/index.ts
+++ b/code/base-project/base-app/src/components/common/ao/index.ts
@@ -1,5 +1,7 @@
import Button from './Button'
import Checkbox from './Checkbox'
+import DatePicker from './DatePicker'
+import DateTimeDrawer from './DateTimeDrawer'
import Form, { FormRef } from './Form'
import FormItem from './FormItem'
import Input from './Input'
@@ -8,6 +10,8 @@ import LucideIcon from './LucideIcon'
export {
Button as AButton,
Checkbox as ACheckbox,
+ DatePicker as ADatePicker,
+ DateTimeDrawer as ADateTimeDrawer,
Form as AForm,
FormItem as AFormItem,
FormRef as AFormRef,
diff --git a/code/base-project/base-app/src/request/api/money.ts b/code/base-project/base-app/src/request/api/money.ts
index c785b0aa..c7764b82 100644
--- a/code/base-project/base-app/src/request/api/money.ts
+++ b/code/base-project/base-app/src/request/api/money.ts
@@ -1,8 +1,19 @@
-import type { MoneyCategory } from '@/types/money'
+import type { MoneyCategory, MoneyRecord } from '@/types/money'
import alovaInstance from '../index'
const API_PREFIX = '/money'
+export interface QueryMoneyRecordsParams {
+ record_time: string
+}
+
+export interface QueryMoneyRecordsResult {
+ [key: string]: MoneyRecord[]
+}
+export function queryMoneyRecordsApi(params: QueryMoneyRecordsParams) {
+ return alovaInstance.Post>(`${API_PREFIX}/query`, params)
+}
+
export interface MoneyCategoryResult {
income: MoneyCategory[]
expense: MoneyCategory[]
diff --git a/code/base-project/base-app/src/request/index.ts b/code/base-project/base-app/src/request/index.ts
index 4085918d..6c068bd8 100644
--- a/code/base-project/base-app/src/request/index.ts
+++ b/code/base-project/base-app/src/request/index.ts
@@ -30,8 +30,8 @@ function isWhiteListed(url: string) {
}
const alovaInstance = createAlova({
- baseURL: 'http://192.168.5.6:8085/app',
- // baseURL: 'http://192.168.1.136:8085/app',
+ // baseURL: 'http://192.168.5.6:8085/app',
+ baseURL: 'http://192.168.1.136:8085/app',
requestAdapter: axiosRequestAdapter(),
timeout: 5000,
cacheFor: null,
diff --git a/code/base-project/base-app/src/utils/time.ts b/code/base-project/base-app/src/utils/time.ts
new file mode 100644
index 00000000..b76d0dae
--- /dev/null
+++ b/code/base-project/base-app/src/utils/time.ts
@@ -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)
+}
diff --git a/code/base-project/base-go-v2/internal/api/auth/index.go b/code/base-project/base-go-v2/internal/api/auth/index.go
index 4ff976f9..3b1d2645 100644
--- a/code/base-project/base-go-v2/internal/api/auth/index.go
+++ b/code/base-project/base-go-v2/internal/api/auth/index.go
@@ -69,7 +69,7 @@ func loginDefault(c *router.Context) error {
"account", account,
"password", password,
)
- user, err := db.FindOne("user_info", userQuery)
+ user, err := db.SelectOne("user_info", db.BuildWhereString(userQuery), "")
if err != nil {
return response.Success(c).Message("查询用户失败,请稍后重试").Send()
}
@@ -127,7 +127,7 @@ func registerDefault(c *router.Context) error {
// 构造查询条件,检查账号是否已存在
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 {
return c.AddError(err)
}
diff --git a/code/base-project/base-go-v2/internal/api/money/index.go b/code/base-project/base-go-v2/internal/api/money/index.go
index 20abb1da..7d22f070 100644
--- a/code/base-project/base-go-v2/internal/api/money/index.go
+++ b/code/base-project/base-go-v2/internal/api/money/index.go
@@ -8,10 +8,12 @@ import (
"base-go-v2/internal/utils/mapx"
"fmt"
"github.com/shopspring/decimal"
+ "time"
)
func InitMoneyRouter(r *router.Router) {
group := r.Group("money")
+ group.POST("/query", queryMoneyRecords)
group.GET("/category", getUserCategory)
group.POST("/insert", insertMoneyRecord)
group.POST("/update", updateMoneyRecord)
@@ -20,10 +22,8 @@ func InitMoneyRouter(r *router.Router) {
// InitDefaultCategories 初始化系统默认分类到用户分类表
func InitDefaultCategories(userID int64) error {
- fmt.Println("进入 InitDefaultCategories ===")
-
// 1. 查询系统分类表
- sysCategories, err := db.FindAll("money_sys_category", "sort_number")
+ sysCategories, err := db.SelectList("money_sys_category", "", "sort_number")
if err != nil {
return err
}
@@ -31,7 +31,6 @@ func InitDefaultCategories(userID int64) error {
fmt.Printf("系统分类数量: %d\n", len(sysCategories))
// 2. 构建批量插入数据
- //userCategories := make([]mapx.M, 0, len(sysCategories))
userCategories := mapx.NewArr(len(sysCategories))
for i, row := range sysCategories {
@@ -52,15 +51,124 @@ func InitDefaultCategories(userID int64) error {
return err
}
- fmt.Println("初始化用户分类成功!")
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 {
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 {
return c.AddError(err)
}
@@ -126,7 +234,7 @@ func updateMoneyRecord(c *router.Context) error {
recordId := bodyData.GetInt64("id")
- record, err := db.GetOne("money_record", "id", recordId)
+ record, err := db.SelectById("money_record", "id", recordId)
if err != nil {
return c.AddError(err)
}
diff --git a/code/base-project/base-go-v2/internal/db/db.go b/code/base-project/base-go-v2/internal/db/db.go
index 6f2bc0af..64ffe229 100644
--- a/code/base-project/base-go-v2/internal/db/db.go
+++ b/code/base-project/base-go-v2/internal/db/db.go
@@ -24,9 +24,11 @@ var defaultPoolConfig = struct {
func InitDb() error {
d := config.App.Db
- // 构造 DSN
- dsn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable",
- d.Host, d.Port, d.User, d.Password, d.Dbname)
+ // 构造 DSN,指定时区为 Asia/Shanghai
+ dsn := fmt.Sprintf(
+ "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)
if err != nil {
diff --git a/code/base-project/base-go-v2/internal/db/query.go b/code/base-project/base-go-v2/internal/db/query.go
index 904626ef..28abe0ad 100644
--- a/code/base-project/base-go-v2/internal/db/query.go
+++ b/code/base-project/base-go-v2/internal/db/query.go
@@ -8,7 +8,6 @@ import (
// ---------------- 通用查询方法 ----------------
-// 执行查询,返回多条记录
func queryMaps(query string, args []interface{}) ([]mapx.M, error) {
rows, err := DB.Queryx(query, args...)
if err != nil {
@@ -22,13 +21,20 @@ func queryMaps(query string, args []interface{}) ([]mapx.M, error) {
if err := rows.MapScan(row); err != nil {
return nil, err
}
+
+ // 统一处理 []byte 类型,直接转字符串,保留原始小数点
+ for k, v := range row {
+ if b, ok := v.([]byte); ok {
+ row[k] = string(b)
+ }
+ }
+
result = append(result, row)
}
return result, nil
}
-// 执行查询,返回单条记录
func queryMap(query string, args []interface{}) (mapx.M, error) {
rows, err := DB.Queryx(query, args...)
if err != nil {
@@ -41,93 +47,73 @@ func queryMap(query string, args []interface{}) (mapx.M, error) {
if err := rows.MapScan(row); err != nil {
return nil, err
}
+
+ // 统一处理 []byte 类型,直接转字符串
+ for k, v := range row {
+ if b, ok := v.([]byte); ok {
+ row[k] = string(b)
+ }
+ }
+
return row, nil
}
return nil, nil
}
-// 构建 WHERE 条件
-func buildWhere(conditions map[string]interface{}) (string, []interface{}) {
- var whereParts []string
- var values []interface{}
- i := 1
- for k, v := range conditions {
- whereParts = append(whereParts, fmt.Sprintf("%s=$%d", k, i))
- values = append(values, v)
- i++
+// BuildWhereString 将 map 条件转换为 SQL WHERE 字符串
+func BuildWhereString(conds mapx.M) string {
+ if len(conds) == 0 {
+ return ""
}
- return strings.Join(whereParts, " AND "), values
+
+ var parts []string
+ for k, v := range conds {
+ parts = append(parts, fmt.Sprintf("%s='%v'", k, v))
+ }
+
+ return strings.Join(parts, " AND ")
}
-// ---------------- 公共查询方法 ----------------
+// ---------------- 统一 select 系列方法 ----------------
-// GetOne 根据主键查询单条记录
-func GetOne(table string, pkColumn string, pkValue interface{}) (mapx.M, error) {
- query := fmt.Sprintf("SELECT * FROM %s WHERE %s=$1", table, pkColumn)
+// 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})
}
-// GetBatch 根据主键批量查询
-func GetBatch(table string, pkColumn string, pkValues []interface{}) ([]mapx.M, error) {
- if len(pkValues) == 0 {
- return nil, nil
- }
-
- placeholders := make([]string, len(pkValues))
- for i := range pkValues {
- placeholders[i] = fmt.Sprintf("$%d", i+1)
- }
-
- query := fmt.Sprintf("SELECT * FROM %s WHERE %s IN (%s)", table, pkColumn, strings.Join(placeholders, ","))
- return queryMaps(query, pkValues)
+// SelectBySql 根据任意 SQL 查询多条记录
+func SelectBySql(sql string, args ...interface{}) ([]mapx.M, error) {
+ return queryMaps(sql, args)
}
-// Find 根据条件查询多条记录,可选排序
-func Find(table string, conditions map[string]interface{}, orderBy ...string) ([]mapx.M, error) {
- if len(conditions) == 0 {
- return nil, fmt.Errorf("查询条件不能为空")
+// SelectList 查询列表,可传 where 和 order,where 可以为空
+func SelectList(tableName string, where string, order string) ([]mapx.M, error) {
+ query := fmt.Sprintf("SELECT * FROM %s", tableName)
+ if strings.TrimSpace(where) != "" {
+ query += " WHERE " + where
}
- 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]
+ if strings.TrimSpace(order) == "" {
+ order = "id DESC"
}
query += " ORDER BY " + order
return queryMaps(query, nil)
}
-// FindOne 根据条件查询单条记录,可选排序
-func FindOne(table string, conditions map[string]interface{}, orderBy ...string) (mapx.M, error) {
- if len(conditions) == 0 {
- return nil, fmt.Errorf("查询条件不能为空")
+// SelectOne 查询单条记录,可传 where 和 order(order 可为空)
+func SelectOne(tableName string, where string, order string) (mapx.M, error) {
+ query := fmt.Sprintf("SELECT * FROM %s", tableName)
+ if strings.TrimSpace(where) != "" {
+ query += " WHERE " + where
}
- 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]
+ if strings.TrimSpace(order) == "" {
+ order = "id DESC" // 默认排序
}
query += " ORDER BY " + order + " LIMIT 1"
- return queryMap(query, values)
+ return queryMap(query, nil)
}
diff --git a/code/base-project/base-go-v2/internal/utils/mapx/index.go b/code/base-project/base-go-v2/internal/utils/mapx/index.go
index 4206dccb..df79da4b 100644
--- a/code/base-project/base-go-v2/internal/utils/mapx/index.go
+++ b/code/base-project/base-go-v2/internal/utils/mapx/index.go
@@ -86,6 +86,16 @@ func (m M) GetString(key string) string {
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
func (m M) getIntValue(key string) int64 {
if v, ok := m[key]; ok && v != nil {