From c2c5cc92afc8d6c227899cbb90f93a5c9f87042c Mon Sep 17 00:00:00 2001 From: ZhangAo Date: Sat, 15 Nov 2025 17:31:36 +0800 Subject: [PATCH] u --- code/base-project/base-app/package.json | 5 +- code/base-project/base-app/pnpm-lock.yaml | 120 +++-- .../base-app/src/app/(tabs)/chat.tsx | 62 +-- .../app/(tabs)/money/components/AddDrawer.tsx | 474 +++++++++++------- .../base-app/src/app/(tabs)/money/index.tsx | 62 ++- .../src/components/common/ao/DatePicker.tsx | 81 +++ .../components/common/ao/DateTimeDrawer.tsx | 106 ++++ .../src/components/common/ao/index.ts | 4 + .../base-app/src/request/api/money.ts | 13 +- .../base-app/src/request/index.ts | 4 +- code/base-project/base-app/src/utils/time.ts | 29 ++ .../base-go-v2/internal/api/auth/index.go | 4 +- .../base-go-v2/internal/api/money/index.go | 122 ++++- .../base-project/base-go-v2/internal/db/db.go | 8 +- .../base-go-v2/internal/db/query.go | 112 ++--- .../base-go-v2/internal/utils/mapx/index.go | 10 + 16 files changed, 864 insertions(+), 352 deletions(-) create mode 100644 code/base-project/base-app/src/components/common/ao/DatePicker.tsx create mode 100644 code/base-project/base-app/src/components/common/ao/DateTimeDrawer.tsx create mode 100644 code/base-project/base-app/src/utils/time.ts 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 {