u
This commit is contained in:
1 parent
173eb2dd37
commit
26ff2381b7
17 files changed
+596
-305
No files matched your search
@@ -51,14 +51,22 @@ func ListFinanceRecords(c fiber.Ctx) error {
|
|||||||
endTime = &t
|
endTime = &t
|
||||||
}
|
}
|
||||||
|
|
||||||
categoryId := dto.CategoryId
|
var categoryId int64 = -1
|
||||||
|
if dto.CategoryId != nil {
|
||||||
|
categoryId = *dto.CategoryId
|
||||||
|
}
|
||||||
|
|
||||||
|
var categoryType int16 = -1
|
||||||
|
if dto.CategoryType != nil {
|
||||||
|
categoryType = *dto.CategoryType
|
||||||
|
}
|
||||||
|
|
||||||
records, err := db.Queries.ListFinanceRecords(c.Context(), repo.ListFinanceRecordsParams{
|
records, err := db.Queries.ListFinanceRecords(c.Context(), repo.ListFinanceRecordsParams{
|
||||||
SpaceID: spaceID,
|
SpaceID: spaceID,
|
||||||
RecordTime: pgtypex.TimePtrToPgDate(startTime),
|
RecordTime: pgtypex.TimePtrToPgDate(startTime),
|
||||||
RecordTime_2: pgtypex.TimePtrToPgDate(endTime),
|
RecordTime_2: pgtypex.TimePtrToPgDate(endTime),
|
||||||
Column4: categoryId,
|
Column4: categoryId,
|
||||||
Column5: dto.CategoryType,
|
Column5: categoryType,
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -65,8 +65,8 @@ type FinanceRecord struct {
|
|||||||
type ListFinanceRecordsDTO struct {
|
type ListFinanceRecordsDTO struct {
|
||||||
StartTime string `json:"start_time"`
|
StartTime string `json:"start_time"`
|
||||||
EndTime string `json:"end_time"`
|
EndTime string `json:"end_time"`
|
||||||
CategoryId int64 `json:"category_id"`
|
CategoryId *int64 `json:"category_id"`
|
||||||
CategoryType int16 `json:"category_type"`
|
CategoryType *int16 `json:"category_type"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ListFinanceRecordsVO struct {
|
type ListFinanceRecordsVO struct {
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ FROM finance_record fr
|
|||||||
WHERE
|
WHERE
|
||||||
fr.space_id = $1
|
fr.space_id = $1
|
||||||
AND fr.record_time >= $2
|
AND fr.record_time >= $2
|
||||||
AND fr.record_time < $3
|
AND fr.record_time <= $3
|
||||||
AND fr.category_id = COALESCE(NULLIF($4::bigint, 0), fr.category_id)
|
AND ($4::bigint = -1 OR fr.category_id = $4)
|
||||||
AND fcs.type = COALESCE(NULLIF($5::smallint, 0), fcs.type)
|
AND ($5::smallint = -1 OR fcs.type = $5)
|
||||||
ORDER BY
|
ORDER BY
|
||||||
fr.record_time DESC,
|
fr.record_time DESC,
|
||||||
fr.create_time DESC;
|
fr.create_time DESC;
|
||||||
|
|||||||
@@ -123,9 +123,9 @@ FROM finance_record fr
|
|||||||
WHERE
|
WHERE
|
||||||
fr.space_id = $1
|
fr.space_id = $1
|
||||||
AND fr.record_time >= $2
|
AND fr.record_time >= $2
|
||||||
AND fr.record_time < $3
|
AND fr.record_time <= $3
|
||||||
AND fr.category_id = COALESCE(NULLIF($4::bigint, 0), fr.category_id)
|
AND ($4::bigint = -1 OR fr.category_id = $4)
|
||||||
AND fcs.type = COALESCE(NULLIF($5::smallint, 0), fcs.type)
|
AND ($5::smallint = -1 OR fcs.type = $5)
|
||||||
ORDER BY
|
ORDER BY
|
||||||
fr.record_time DESC,
|
fr.record_time DESC,
|
||||||
fr.create_time DESC
|
fr.create_time DESC
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
"web": {
|
"web": {
|
||||||
"favicon": "./assets/favicon.png"
|
"favicon": "./assets/favicon.png"
|
||||||
},
|
},
|
||||||
|
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"./plugins/withAndroidSignature",
|
"./plugins/withAndroidSignature",
|
||||||
"expo-router",
|
"expo-router",
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"main": "expo-router/entry",
|
"main": "expo-router/entry",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "expo start",
|
"start": "expo start --port 8088",
|
||||||
"android": "expo run:android",
|
"android": "expo run:android --port 8088",
|
||||||
"ios": "expo run:ios",
|
"ios": "expo run:ios",
|
||||||
"web": "expo start --web"
|
"web": "expo start --web"
|
||||||
},
|
},
|
||||||
@@ -46,6 +46,7 @@
|
|||||||
"react-native": "0.81.5",
|
"react-native": "0.81.5",
|
||||||
"react-native-css-interop": "^0.2.1",
|
"react-native-css-interop": "^0.2.1",
|
||||||
"react-native-drawer-layout": "^4.2.0",
|
"react-native-drawer-layout": "^4.2.0",
|
||||||
|
"react-native-enriched": "^0.5.1",
|
||||||
"react-native-gesture-handler": "~2.28.0",
|
"react-native-gesture-handler": "~2.28.0",
|
||||||
"react-native-keyboard-controller": "1.18.5",
|
"react-native-keyboard-controller": "1.18.5",
|
||||||
"react-native-pager-view": "6.9.1",
|
"react-native-pager-view": "6.9.1",
|
||||||
|
|||||||
Generated
+14
-8
@@ -116,6 +116,9 @@ importers:
|
|||||||
react-native-drawer-layout:
|
react-native-drawer-layout:
|
||||||
specifier: ^4.2.0
|
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)
|
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-enriched:
|
||||||
|
specifier: ^0.5.1
|
||||||
|
version: 0.5.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-gesture-handler:
|
react-native-gesture-handler:
|
||||||
specifier: ~2.28.0
|
specifier: ~2.28.0
|
||||||
version: 2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
|
version: 2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
|
||||||
@@ -4129,56 +4132,48 @@ packages:
|
|||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
lightningcss-linux-arm64-gnu@1.30.2:
|
lightningcss-linux-arm64-gnu@1.30.2:
|
||||||
resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==}
|
resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
lightningcss-linux-arm64-musl@1.27.0:
|
lightningcss-linux-arm64-musl@1.27.0:
|
||||||
resolution: {integrity: sha512-rCGBm2ax7kQ9pBSeITfCW9XSVF69VX+fm5DIpvDZQl4NnQoMQyRwhZQm9pd59m8leZ1IesRqWk2v/DntMo26lg==}
|
resolution: {integrity: sha512-rCGBm2ax7kQ9pBSeITfCW9XSVF69VX+fm5DIpvDZQl4NnQoMQyRwhZQm9pd59m8leZ1IesRqWk2v/DntMo26lg==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
lightningcss-linux-arm64-musl@1.30.2:
|
lightningcss-linux-arm64-musl@1.30.2:
|
||||||
resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==}
|
resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
lightningcss-linux-x64-gnu@1.27.0:
|
lightningcss-linux-x64-gnu@1.27.0:
|
||||||
resolution: {integrity: sha512-Dk/jovSI7qqhJDiUibvaikNKI2x6kWPN79AQiD/E/KeQWMjdGe9kw51RAgoWFDi0coP4jinaH14Nrt/J8z3U4A==}
|
resolution: {integrity: sha512-Dk/jovSI7qqhJDiUibvaikNKI2x6kWPN79AQiD/E/KeQWMjdGe9kw51RAgoWFDi0coP4jinaH14Nrt/J8z3U4A==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
lightningcss-linux-x64-gnu@1.30.2:
|
lightningcss-linux-x64-gnu@1.30.2:
|
||||||
resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==}
|
resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
lightningcss-linux-x64-musl@1.27.0:
|
lightningcss-linux-x64-musl@1.27.0:
|
||||||
resolution: {integrity: sha512-QKjTxXm8A9s6v9Tg3Fk0gscCQA1t/HMoF7Woy1u68wCk5kS4fR+q3vXa1p3++REW784cRAtkYKrPy6JKibrEZA==}
|
resolution: {integrity: sha512-QKjTxXm8A9s6v9Tg3Fk0gscCQA1t/HMoF7Woy1u68wCk5kS4fR+q3vXa1p3++REW784cRAtkYKrPy6JKibrEZA==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
lightningcss-linux-x64-musl@1.30.2:
|
lightningcss-linux-x64-musl@1.30.2:
|
||||||
resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==}
|
resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
lightningcss-win32-arm64-msvc@1.27.0:
|
lightningcss-win32-arm64-msvc@1.27.0:
|
||||||
resolution: {integrity: sha512-/wXegPS1hnhkeG4OXQKEMQeJd48RDC3qdh+OA8pCuOPCyvnm/yEayrJdJVqzBsqpy1aJklRCVxscpFur80o6iQ==}
|
resolution: {integrity: sha512-/wXegPS1hnhkeG4OXQKEMQeJd48RDC3qdh+OA8pCuOPCyvnm/yEayrJdJVqzBsqpy1aJklRCVxscpFur80o6iQ==}
|
||||||
@@ -5023,6 +5018,12 @@ packages:
|
|||||||
react-native-gesture-handler: '>= 2.0.0'
|
react-native-gesture-handler: '>= 2.0.0'
|
||||||
react-native-reanimated: '>= 2.0.0'
|
react-native-reanimated: '>= 2.0.0'
|
||||||
|
|
||||||
|
react-native-enriched@0.5.1:
|
||||||
|
resolution: {integrity: sha512-MYwTVRBIkAl6fOosCBE26wFgNgk/2sOX7r+qnvDirsuecJ6H+6Zz4rHP5LnLFLDPZIon+lEQyZoWIdoEsGCZtw==}
|
||||||
|
peerDependencies:
|
||||||
|
react: '*'
|
||||||
|
react-native: '*'
|
||||||
|
|
||||||
react-native-gesture-handler@2.28.0:
|
react-native-gesture-handler@2.28.0:
|
||||||
resolution: {integrity: sha512-0msfJ1vRxXKVgTgvL+1ZOoYw3/0z1R+Ked0+udoJhyplC2jbVKIJ8Z1bzWdpQRCV3QcQ87Op0zJVE5DhKK2A0A==}
|
resolution: {integrity: sha512-0msfJ1vRxXKVgTgvL+1ZOoYw3/0z1R+Ked0+udoJhyplC2jbVKIJ8Z1bzWdpQRCV3QcQ87Op0zJVE5DhKK2A0A==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -12233,6 +12234,11 @@ snapshots:
|
|||||||
react-native-reanimated: 4.1.5(@babel/core@7.28.5)(react-native-worklets@0.5.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
|
react-native-reanimated: 4.1.5(@babel/core@7.28.5)(react-native-worklets@0.5.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
|
||||||
use-latest-callback: 0.2.6(react@19.1.0)
|
use-latest-callback: 0.2.6(react@19.1.0)
|
||||||
|
|
||||||
|
react-native-enriched@0.5.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-gesture-handler@2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0):
|
react-native-gesture-handler@2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@egjs/hammerjs': 2.0.17
|
'@egjs/hammerjs': 2.0.17
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import type { DateMorePickerMode } from '@/components/common/ao/DateMorePicker'
|
import type { DateMorePickerMode } from '@/components/common/ao/DateMorePicker'
|
||||||
import type { FinanceCategory } from '@/types/finance/category'
|
import type { FinanceCategory } from '@/types/finance/category'
|
||||||
import { useFocusEffect, useRouter } from 'expo-router'
|
import { useFocusEffect, useRouter } from 'expo-router'
|
||||||
import React, { useCallback, useEffect, useState } from 'react'
|
import React, { useCallback, useState } from 'react'
|
||||||
import { Image, Text, TouchableOpacity, View } from 'react-native'
|
import { Image, Text, TouchableOpacity, View } from 'react-native'
|
||||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'
|
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||||
import { toast } from 'sonner-native'
|
import { toast } from 'sonner-native'
|
||||||
import { ADateMorePicker, ADatePicker, ADrawer, AListItem, LucideIcon } from '@/components/common/ao'
|
import { ADateMorePicker, ADrawer, AListItem, LucideIcon } from '@/components/common/ao'
|
||||||
import { resolveDateRange } from '@/components/common/ao/DateMorePicker'
|
import { resolveDateRange } from '@/components/common/ao/DateMorePicker'
|
||||||
import { Fab, FabIcon } from '@/components/ui/fab'
|
import { Fab, FabIcon } from '@/components/ui/fab'
|
||||||
import { AddIcon } from '@/components/ui/icon'
|
import { AddIcon } from '@/components/ui/icon'
|
||||||
@@ -45,18 +45,32 @@ export default function Money() {
|
|||||||
function fetchFinanceRecords() {
|
function fetchFinanceRecords() {
|
||||||
const resolveDate = resolveDateRange(dateMode, searchParams.startTime)
|
const resolveDate = resolveDateRange(dateMode, searchParams.startTime)
|
||||||
|
|
||||||
let searchType = 0
|
let searchType: number | null = 0
|
||||||
|
let searchId: number | null = 0
|
||||||
|
|
||||||
if (searchParams.category) {
|
if (searchParams.category) {
|
||||||
if (searchParams.category.id === -1 || searchParams.category.id === -2) {
|
if (searchParams.category.id === 0) {
|
||||||
|
searchId = null
|
||||||
|
searchType = null
|
||||||
|
}
|
||||||
|
else if (searchParams.category.id === -1 || searchParams.category.id === -2) {
|
||||||
|
searchId = null
|
||||||
searchType = searchParams.category.type
|
searchType = searchParams.category.type
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
searchId = searchParams.category.id
|
||||||
|
searchType = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
searchId = null
|
||||||
|
searchType = null
|
||||||
}
|
}
|
||||||
|
|
||||||
listFinanceRecordsApi({
|
listFinanceRecordsApi({
|
||||||
start_time: resolveDate.startTime,
|
start_time: resolveDate.startTime,
|
||||||
end_time: resolveDate.endTime,
|
end_time: resolveDate.endTime,
|
||||||
category_id: searchParams.category ? searchParams.category.id : 0,
|
category_id: searchId,
|
||||||
category_type: searchType,
|
category_type: searchType,
|
||||||
}).then((res) => {
|
}).then((res) => {
|
||||||
if (res.isSuccess) {
|
if (res.isSuccess) {
|
||||||
@@ -92,7 +106,7 @@ export default function Money() {
|
|||||||
|
|
||||||
function timeModeTip() {
|
function timeModeTip() {
|
||||||
if (dateMode === 'unlimited') {
|
if (dateMode === 'unlimited') {
|
||||||
return '全部'
|
return '全部时间'
|
||||||
}
|
}
|
||||||
else if (dateMode === 'range') {
|
else if (dateMode === 'range') {
|
||||||
return `${searchParams.startTime} - ${searchParams.endTime}`
|
return `${searchParams.startTime} - ${searchParams.endTime}`
|
||||||
@@ -135,21 +149,21 @@ export default function Money() {
|
|||||||
>
|
>
|
||||||
<Text className="font-semibold text-lg">
|
<Text className="font-semibold text-lg">
|
||||||
{
|
{
|
||||||
searchParams.category ? searchParams.category.name : '分类'
|
searchParams.category ? searchParams.category.name : '全部分类'
|
||||||
}
|
}
|
||||||
</Text>
|
</Text>
|
||||||
<LucideIcon icon="ChevronDown" size={16} color="#666" clickable={false} />
|
<LucideIcon icon="ChevronDown" size={16} color="#666" clickable={false} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<TouchableOpacity
|
{/* <TouchableOpacity
|
||||||
className="flex flex-row items-center gap-2"
|
className="flex flex-row items-center gap-2"
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setShowActionsMenu(true)
|
setShowActionsMenu(true)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<LucideIcon icon="Menu" size={24} clickable={false} />
|
<LucideIcon icon="Menu" size={24} clickable={false} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity> */}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View
|
<View
|
||||||
@@ -249,21 +263,6 @@ export default function Money() {
|
|||||||
}))
|
}))
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/* <ADatePicker
|
|
||||||
show={showDatePicker}
|
|
||||||
mode="month"
|
|
||||||
date={searchParams.record_time}
|
|
||||||
onConfirm={(val) => {
|
|
||||||
setSearchParams({
|
|
||||||
...searchParams,
|
|
||||||
record_time: val,
|
|
||||||
})
|
|
||||||
setShowDatePicker(false)
|
|
||||||
}}
|
|
||||||
onClose={() => {
|
|
||||||
setShowDatePicker(false)
|
|
||||||
}}
|
|
||||||
/> */}
|
|
||||||
|
|
||||||
<CategorySheet
|
<CategorySheet
|
||||||
open={showCategorySheet}
|
open={showCategorySheet}
|
||||||
@@ -280,7 +279,7 @@ export default function Money() {
|
|||||||
}))
|
}))
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{/*
|
||||||
<ADrawer
|
<ADrawer
|
||||||
visible={showActionsMenu}
|
visible={showActionsMenu}
|
||||||
hideModal={() => {
|
hideModal={() => {
|
||||||
@@ -328,7 +327,7 @@ export default function Money() {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
</ADrawer>
|
</ADrawer> */}
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -2,9 +2,10 @@ import { useRouter } from 'expo-router'
|
|||||||
import { Image, Text, View } from 'react-native'
|
import { Image, Text, View } from 'react-native'
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context'
|
import { SafeAreaView } from 'react-native-safe-area-context'
|
||||||
import { AButton } from '@/components/common/ao'
|
import { AButton } from '@/components/common/ao'
|
||||||
|
import { useNavStore } from '@/store/navigation'
|
||||||
|
|
||||||
export default function Index() {
|
export default function Index() {
|
||||||
const router = useRouter()
|
const navStore = useNavStore()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView className="flex-1 bg-white">
|
<SafeAreaView className="flex-1 bg-white">
|
||||||
@@ -24,28 +25,14 @@ export default function Index() {
|
|||||||
记录生活,记录情绪
|
记录生活,记录情绪
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{/* <AButton
|
<AButton
|
||||||
className="w-full"
|
className="w-full"
|
||||||
onPress={() => ('/finance')}
|
onPress={() => {
|
||||||
|
navStore.navigateTo('finance/index')
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
开始使用
|
开始使用
|
||||||
</AButton> */}
|
</AButton>
|
||||||
<View className="flex flex-row gap-3 mt-4">
|
|
||||||
<AButton
|
|
||||||
className="flex-1"
|
|
||||||
onPress={() => ('/finance')}
|
|
||||||
>
|
|
||||||
创建空间
|
|
||||||
</AButton>
|
|
||||||
<AButton
|
|
||||||
className="flex-1"
|
|
||||||
variant="outline"
|
|
||||||
onPress={() => ('/finance')}
|
|
||||||
>
|
|
||||||
加入空间
|
|
||||||
</AButton>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
</View>
|
</View>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export default function NotFound() {
|
|||||||
title="(´・ω・`) 页面走丢了~"
|
title="(´・ω・`) 页面走丢了~"
|
||||||
description="回首页看看吧!"
|
description="回首页看看吧!"
|
||||||
footer={(
|
footer={(
|
||||||
<AButton onPress={() => router.replace('/')}>
|
<AButton onPress={() => router.navigate('/finance')}>
|
||||||
返回应用首页
|
返回应用首页
|
||||||
</AButton>
|
</AButton>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -0,0 +1,285 @@
|
|||||||
|
import type { AnalysisMoneyAmount } from '@/types/money/analysis'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { ScrollView, Text, TouchableOpacity, View } from 'react-native'
|
||||||
|
import { VictoryChart, VictoryLabel, VictoryLine, VictoryPie, VictoryTheme, VictoryTooltip } from 'victory-native'
|
||||||
|
import { ADatePicker, ADateRangePicker, ASegmentedTabs, LucideIcon } from '@/components/common/ao'
|
||||||
|
import EmptyState from '@/components/common/EmptyStatus'
|
||||||
|
import CustomHeader from '@/components/custom/Header'
|
||||||
|
import PagerTabs from '@/components/custom/PagerTabs'
|
||||||
|
import { analysisMoneyLineApi, analysisMoneyPieApi } from '@/request/api/money/analysis'
|
||||||
|
import { MoneyCategoryEnum } from '@/types/money/category'
|
||||||
|
import { add } from '@/utils/math'
|
||||||
|
import { getCurrentYear, getCurrentYearMonth, getEndOfMonth, getStartOfMonth } from '@/utils/time'
|
||||||
|
|
||||||
|
interface AnalysisContentProps {
|
||||||
|
analysisType: MoneyCategoryEnum
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Analysis() {
|
||||||
|
function AnalysisContent({ analysisType }: AnalysisContentProps) {
|
||||||
|
// 按月0 按年1 自定义2
|
||||||
|
const [timeType, setTimeType] = useState<number>(0)
|
||||||
|
|
||||||
|
// 开始结束时间
|
||||||
|
const [startDate, setStartDate] = useState(getCurrentYearMonth())
|
||||||
|
const [endDate, setEndDate] = useState('')
|
||||||
|
|
||||||
|
// 显示
|
||||||
|
const [showDatePicker, setShowDatePicker] = useState(false)
|
||||||
|
|
||||||
|
// 数据
|
||||||
|
const [lineRecords, setLineRecords] = useState<AnalysisMoneyAmount[]>([])
|
||||||
|
const [pieRecords, setPieRecords] = useState<AnalysisMoneyAmount[]>([])
|
||||||
|
|
||||||
|
// 请求
|
||||||
|
function fetchChartsData() {
|
||||||
|
let start = ''
|
||||||
|
let end = ''
|
||||||
|
|
||||||
|
const selectedStartDate = dayjs(startDate)
|
||||||
|
const selectedEndDate = dayjs(endDate)
|
||||||
|
|
||||||
|
let periodType = 0
|
||||||
|
|
||||||
|
if (timeType === 0) {
|
||||||
|
start = selectedStartDate.startOf('month').format('YYYY-MM-DD')
|
||||||
|
end = selectedStartDate.endOf('month').format('YYYY-MM-DD')
|
||||||
|
|
||||||
|
periodType = 0
|
||||||
|
}
|
||||||
|
else if (timeType === 1) {
|
||||||
|
start = selectedStartDate.startOf('year').format('YYYY-MM-DD')
|
||||||
|
end = selectedStartDate.endOf('year').format('YYYY-MM-DD')
|
||||||
|
|
||||||
|
periodType = 1
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (selectedStartDate.isSame(selectedEndDate, 'month')) {
|
||||||
|
periodType = 0
|
||||||
|
start = selectedStartDate.startOf('month').format('YYYY-MM-DD')
|
||||||
|
end = selectedEndDate.endOf('month').format('YYYY-MM-DD')
|
||||||
|
}
|
||||||
|
else if (selectedStartDate.isSame(selectedEndDate, 'year')) {
|
||||||
|
periodType = 1
|
||||||
|
start = selectedStartDate.startOf('year').format('YYYY-MM-DD')
|
||||||
|
end = selectedEndDate.endOf('year').format('YYYY-MM-DD')
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
periodType = 2
|
||||||
|
start = selectedStartDate.format('YYYY-MM-DD')
|
||||||
|
end = selectedEndDate.format('YYYY-MM-DD')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
analysisMoneyLineApi({
|
||||||
|
start_date: start,
|
||||||
|
end_date: end,
|
||||||
|
type: analysisType,
|
||||||
|
period_type: periodType,
|
||||||
|
}).then((res) => {
|
||||||
|
if (res.isSuccess) {
|
||||||
|
setLineRecords(res.data.records)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
analysisMoneyPieApi({
|
||||||
|
start_date: start,
|
||||||
|
end_date: end,
|
||||||
|
type: analysisType,
|
||||||
|
period_type: periodType,
|
||||||
|
}).then((res) => {
|
||||||
|
if (res.isSuccess) {
|
||||||
|
setPieRecords(res.data.records)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const totalAmount: number = pieRecords.reduce(
|
||||||
|
(sum, item) => Number(add(sum, Number(item.y) || 0)),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchChartsData()
|
||||||
|
}, [timeType, startDate, endDate])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScrollView
|
||||||
|
className="p-4"
|
||||||
|
>
|
||||||
|
<ASegmentedTabs
|
||||||
|
value={timeType}
|
||||||
|
onChange={(index) => {
|
||||||
|
setTimeType(index)
|
||||||
|
if (index === 2) {
|
||||||
|
// 自定义类型,本月的 1号 - 终止号
|
||||||
|
setStartDate(getStartOfMonth())
|
||||||
|
setEndDate(getEndOfMonth())
|
||||||
|
}
|
||||||
|
else if (index === 1) {
|
||||||
|
// 按年
|
||||||
|
setStartDate(getCurrentYear())
|
||||||
|
setEndDate('')
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// 按月
|
||||||
|
setStartDate(getCurrentYearMonth())
|
||||||
|
setEndDate('')
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
tabs={[
|
||||||
|
{ key: '1', label: '按月' },
|
||||||
|
{ key: '2', label: '按年' },
|
||||||
|
{ key: '3', label: '自定义' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
className="flex flex-row justify-center items-center"
|
||||||
|
>
|
||||||
|
<LucideIcon icon="CalendarDays" size={14} color="#666" clickable={false} />
|
||||||
|
<Text className="font-semibold text-lg mr-2 ml-1">{startDate}</Text>
|
||||||
|
{timeType === 2 && (
|
||||||
|
<>
|
||||||
|
<Text> - </Text>
|
||||||
|
<Text className="font-semibold text-lg mx-1">
|
||||||
|
{endDate}
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<LucideIcon icon="ChevronDown" size={16} color="#666" clickable={false} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<View className="mt-4">
|
||||||
|
<Text className="text-lg font-semibold text-gray-700">趋势分析</Text>
|
||||||
|
|
||||||
|
<VictoryChart
|
||||||
|
height={220}
|
||||||
|
theme={VictoryTheme.material}
|
||||||
|
>
|
||||||
|
<VictoryLine
|
||||||
|
interpolation="catmullRom"
|
||||||
|
data={lineRecords}
|
||||||
|
animate={{
|
||||||
|
duration: 1000,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</VictoryChart>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View
|
||||||
|
className="mt-4"
|
||||||
|
>
|
||||||
|
<Text className="text-lg font-semibold text-gray-700">分类构成</Text>
|
||||||
|
|
||||||
|
{
|
||||||
|
pieRecords.length > 0
|
||||||
|
? (
|
||||||
|
<VictoryPie
|
||||||
|
height={250}
|
||||||
|
data={pieRecords}
|
||||||
|
theme={VictoryTheme.material}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
: (
|
||||||
|
<EmptyState title="空" />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="mt-4" style={{ paddingBottom: 24 }}>
|
||||||
|
<View className="flex flex-row justify-between">
|
||||||
|
<Text className="text-lg font-semibold text-gray-700">分类详细</Text>
|
||||||
|
<Text>
|
||||||
|
总:
|
||||||
|
{totalAmount}
|
||||||
|
{' '}
|
||||||
|
元
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{
|
||||||
|
pieRecords.length > 0
|
||||||
|
? (
|
||||||
|
<View className="mt-4 flex flex-col gap-2">
|
||||||
|
{pieRecords.map((item, index) => (
|
||||||
|
<View
|
||||||
|
key={index}
|
||||||
|
className="flex flex-row justify-between items-center py-3 border-b border-gray-300"
|
||||||
|
>
|
||||||
|
<Text className="text-base font-medium text-gray-800">{item.x}</Text>
|
||||||
|
<Text className="text-base font-semibold text-gray-700">
|
||||||
|
{item.y}
|
||||||
|
{' '}
|
||||||
|
元
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
: (
|
||||||
|
<EmptyState title="空" />
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{
|
||||||
|
timeType === 2
|
||||||
|
? (
|
||||||
|
<ADateRangePicker
|
||||||
|
show={showDatePicker}
|
||||||
|
onConfirm={(startDate, endDate) => {
|
||||||
|
setStartDate(startDate)
|
||||||
|
setEndDate(endDate)
|
||||||
|
setShowDatePicker(false)
|
||||||
|
}}
|
||||||
|
onClose={() => {
|
||||||
|
setShowDatePicker(false)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
: (
|
||||||
|
<ADatePicker
|
||||||
|
show={showDatePicker}
|
||||||
|
mode={timeType === 0 ? 'month' : 'year'}
|
||||||
|
onConfirm={(val) => {
|
||||||
|
setStartDate(val)
|
||||||
|
setShowDatePicker(false)
|
||||||
|
}}
|
||||||
|
onClose={() => {
|
||||||
|
setShowDatePicker(false)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</ScrollView>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className="flex-1">
|
||||||
|
<CustomHeader
|
||||||
|
showHeader={false}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PagerTabs
|
||||||
|
showCustomHeader={true}
|
||||||
|
routes={[
|
||||||
|
{
|
||||||
|
key: 'expense',
|
||||||
|
title: '支出',
|
||||||
|
element: <AnalysisContent analysisType={MoneyCategoryEnum.Expense} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'income',
|
||||||
|
title: '收入',
|
||||||
|
element: <AnalysisContent analysisType={MoneyCategoryEnum.Income} />,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
|
import type { DateMorePickerMode } from '@/components/common/ao/DateMorePicker'
|
||||||
import type { FinanceRecord } from '@/types/finance/record'
|
import type { FinanceRecord } from '@/types/finance/record'
|
||||||
import Big from 'big.js'
|
import Big from 'big.js'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { Keyboard, Pressable, StyleSheet, Text, TouchableWithoutFeedback, View } from 'react-native'
|
import { Keyboard, Pressable, StyleSheet, Text, TouchableWithoutFeedback, View } from 'react-native'
|
||||||
|
import { ADateMorePicker } from '@/components/common/ao'
|
||||||
import { ADatePicker, AInput, LucideIcon } from '@/components/common/ao/index'
|
import { ADatePicker, AInput, LucideIcon } from '@/components/common/ao/index'
|
||||||
import { useKeyboardHeight } from '@/hooks/useKeyboardHeight'
|
import { useKeyboardHeight } from '@/hooks/useKeyboardHeight'
|
||||||
import { getCurrentDate } from '@/utils/time'
|
import { getCurrentDate } from '@/utils/time'
|
||||||
@@ -51,10 +53,11 @@ export default function AmountKeyboard({
|
|||||||
record,
|
record,
|
||||||
onDone,
|
onDone,
|
||||||
}: AmountKeyboardProps) {
|
}: AmountKeyboardProps) {
|
||||||
|
// 日期
|
||||||
|
const [showDatePicker, setShowDatePicker] = useState(false)
|
||||||
|
const [dateMode, setDateMode] = useState<DateMorePickerMode>('date')
|
||||||
// 自定义键盘计算过程
|
// 自定义键盘计算过程
|
||||||
const [keydownInput, setKeydownInput] = useState('')
|
const [keydownInput, setKeydownInput] = useState('')
|
||||||
// 日期选择
|
|
||||||
const [showDatePicker, setShowDatePicker] = useState(false)
|
|
||||||
// 备注聚焦
|
// 备注聚焦
|
||||||
const [remarkIsFocused, setRemarkIsFocused] = useState(false)
|
const [remarkIsFocused, setRemarkIsFocused] = useState(false)
|
||||||
// 键盘高度
|
// 键盘高度
|
||||||
@@ -342,7 +345,7 @@ export default function AmountKeyboard({
|
|||||||
<View style={{ height: keyboardHeight + 10 - 220 }} />
|
<View style={{ height: keyboardHeight + 10 - 220 }} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ADatePicker
|
{/* <ADatePicker
|
||||||
pickerTitle="记账时间"
|
pickerTitle="记账时间"
|
||||||
mode="date"
|
mode="date"
|
||||||
date={innerRecord.record_time}
|
date={innerRecord.record_time}
|
||||||
@@ -358,6 +361,24 @@ export default function AmountKeyboard({
|
|||||||
record_time: date,
|
record_time: date,
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
|
/> */}
|
||||||
|
|
||||||
|
<ADateMorePicker
|
||||||
|
visible={showDatePicker}
|
||||||
|
mode={dateMode}
|
||||||
|
onClose={() => {
|
||||||
|
setShowDatePicker(false)
|
||||||
|
}}
|
||||||
|
startDate={innerRecord.record_time}
|
||||||
|
oneMode="date"
|
||||||
|
onConfirm={(value) => {
|
||||||
|
setDateMode(value.mode)
|
||||||
|
|
||||||
|
setInnerRecord(val => ({
|
||||||
|
...val,
|
||||||
|
record_time: value.startTime,
|
||||||
|
}))
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useLocalSearchParams, useRouter } from 'expo-router'
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { Text, TouchableOpacity, View } from 'react-native'
|
import { Text, TouchableOpacity, View } from 'react-native'
|
||||||
import { toast } from 'sonner-native'
|
import { toast } from 'sonner-native'
|
||||||
import { LucideIcon } from '@/components/common/ao'
|
import { ADateMorePicker, LucideIcon } from '@/components/common/ao'
|
||||||
import CustomHeader from '@/components/custom/Header'
|
import CustomHeader from '@/components/custom/Header'
|
||||||
import PagerTabs from '@/components/custom/PagerTabs'
|
import PagerTabs from '@/components/custom/PagerTabs'
|
||||||
import { createFinanceRecordApi, updateFinanceRecordApi } from '@/request/api/finance/record'
|
import { createFinanceRecordApi, updateFinanceRecordApi } from '@/request/api/finance/record'
|
||||||
@@ -21,6 +21,7 @@ export default function MoenyRecordEditor() {
|
|||||||
const income = financeStore.getIncomeCategories()
|
const income = financeStore.getIncomeCategories()
|
||||||
|
|
||||||
const { id: updateId } = useLocalSearchParams<{ id: string }>()
|
const { id: updateId } = useLocalSearchParams<{ id: string }>()
|
||||||
|
|
||||||
// 自定义键盘
|
// 自定义键盘
|
||||||
const [showKeydown, setShowKeydown] = useState(false)
|
const [showKeydown, setShowKeydown] = useState(false)
|
||||||
// 选中分类
|
// 选中分类
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ interface DateMorePickerProps {
|
|||||||
endTime: string
|
endTime: string
|
||||||
}) => void
|
}) => void
|
||||||
hiddenModes?: DateMorePickerMode[]
|
hiddenModes?: DateMorePickerMode[]
|
||||||
|
oneMode?: DateMorePickerMode
|
||||||
onChange?: (mode: DateMorePickerMode) => void
|
onChange?: (mode: DateMorePickerMode) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,6 +109,7 @@ export default function DateMorePicker({
|
|||||||
onClose,
|
onClose,
|
||||||
onConfirm,
|
onConfirm,
|
||||||
onChange,
|
onChange,
|
||||||
|
oneMode,
|
||||||
hiddenModes = [],
|
hiddenModes = [],
|
||||||
}: DateMorePickerProps) {
|
}: DateMorePickerProps) {
|
||||||
const [localVisible, setLocalVisible] = useState(false)
|
const [localVisible, setLocalVisible] = useState(false)
|
||||||
@@ -249,6 +251,13 @@ export default function DateMorePicker({
|
|||||||
]
|
]
|
||||||
|
|
||||||
const visibleTabItems = tabItems.filter((item) => {
|
const visibleTabItems = tabItems.filter((item) => {
|
||||||
|
if (oneMode) {
|
||||||
|
if (oneMode === item.key) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
if (item.key === 'unlimited' && isHidden('unlimited'))
|
if (item.key === 'unlimited' && isHidden('unlimited'))
|
||||||
return false
|
return false
|
||||||
if (item.key === 'range' && isHidden('range'))
|
if (item.key === 'range' && isHidden('range'))
|
||||||
@@ -271,6 +280,12 @@ export default function DateMorePicker({
|
|||||||
]
|
]
|
||||||
|
|
||||||
const visibleCoreModeOptions = coreModeOptions.filter((item) => {
|
const visibleCoreModeOptions = coreModeOptions.filter((item) => {
|
||||||
|
if (oneMode) {
|
||||||
|
if (oneMode === item.mode) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
if (item.mode === 'date' && isHidden('date'))
|
if (item.mode === 'date' && isHidden('date'))
|
||||||
return false
|
return false
|
||||||
if (item.mode === 'month' && isHidden('month'))
|
if (item.mode === 'month' && isHidden('month'))
|
||||||
@@ -293,9 +308,6 @@ export default function DateMorePicker({
|
|||||||
const canConfirm = (() => {
|
const canConfirm = (() => {
|
||||||
if (internalMode !== 'range')
|
if (internalMode !== 'range')
|
||||||
return true
|
return true
|
||||||
console.log(internalMode)
|
|
||||||
console.log(internalStart)
|
|
||||||
console.log(internalEnd)
|
|
||||||
|
|
||||||
const startValid = dayjs(internalStart).isValid()
|
const startValid = dayjs(internalStart).isValid()
|
||||||
const endValid = dayjs(internalEnd).isValid()
|
const endValid = dayjs(internalEnd).isValid()
|
||||||
@@ -323,7 +335,7 @@ export default function DateMorePicker({
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* 模式选择 */}
|
{/* 模式选择 */}
|
||||||
{!['unlimited', 'range'].includes(internalMode) && (
|
{!oneMode && !['unlimited', 'range'].includes(internalMode) && (
|
||||||
<View className="mb-2">
|
<View className="mb-2">
|
||||||
<Text className="text-lg mb-1">选择模式</Text>
|
<Text className="text-lg mb-1">选择模式</Text>
|
||||||
{visibleCoreModeOptions.length > 0 && (
|
{visibleCoreModeOptions.length > 0 && (
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import type { IDatePickerProps } from '@yz1311/react-native-wheel-picker'
|
import type { IDatePickerProps } from '@yz1311/react-native-wheel-picker'
|
||||||
import SegmentedControl from '@react-native-segmented-control/segmented-control'
|
|
||||||
import { DatePicker } from '@yz1311/react-native-wheel-picker'
|
import { DatePicker } from '@yz1311/react-native-wheel-picker'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import React, { useEffect, useRef, useState } from 'react'
|
import React, { useEffect, useRef, useState } from 'react'
|
||||||
@@ -33,6 +32,7 @@ export default function ADatePicker({
|
|||||||
date,
|
date,
|
||||||
show = false,
|
show = false,
|
||||||
onClose,
|
onClose,
|
||||||
|
mode = 'date',
|
||||||
onConfirm,
|
onConfirm,
|
||||||
format,
|
format,
|
||||||
...restProps
|
...restProps
|
||||||
@@ -40,8 +40,6 @@ export default function ADatePicker({
|
|||||||
const fadeAnim = useRef(new Animated.Value(0)).current
|
const fadeAnim = useRef(new Animated.Value(0)).current
|
||||||
const slideAnim = useRef(new Animated.Value(300)).current
|
const slideAnim = useRef(new Animated.Value(300)).current
|
||||||
const [visible, setVisible] = useState(show)
|
const [visible, setVisible] = useState(show)
|
||||||
const [index, setIndex] = useState(0)
|
|
||||||
const [mode, setMode] = useState<DatePickerMode>('month')
|
|
||||||
|
|
||||||
// 使用传入 format 或默认 format
|
// 使用传入 format 或默认 format
|
||||||
const finalFormat = format || defaultFormatMap[mode]
|
const finalFormat = format || defaultFormatMap[mode]
|
||||||
@@ -83,58 +81,6 @@ export default function ADatePicker({
|
|||||||
<Animated.View
|
<Animated.View
|
||||||
style={[styles.pickerContainer, { transform: [{ translateY: slideAnim }] }]}
|
style={[styles.pickerContainer, { transform: [{ translateY: slideAnim }] }]}
|
||||||
>
|
>
|
||||||
<View className="px-3 pt-2 flex flex-col gap-2">
|
|
||||||
<View className="w-full">
|
|
||||||
<Text>快速选择</Text>
|
|
||||||
{/* <ATab
|
|
||||||
data={[
|
|
||||||
{
|
|
||||||
label: '自定义',
|
|
||||||
key: '自定义',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '不限',
|
|
||||||
key: '不限',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '今天',
|
|
||||||
key: '今天',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '本月',
|
|
||||||
key: '本月',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '本年',
|
|
||||||
key: '本年',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '范围',
|
|
||||||
key: '范围',
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
onChange={(key) => {
|
|
||||||
setMode('datetime')
|
|
||||||
}}
|
|
||||||
/> */}
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View className="w-full">
|
|
||||||
<Text>选择模式</Text>
|
|
||||||
<SegmentedControl
|
|
||||||
values={['日', '月', '年', '时间', '日期']}
|
|
||||||
selectedIndex={index}
|
|
||||||
style={{ borderRadius: 2, width: '100%' }}
|
|
||||||
sliderStyle={{
|
|
||||||
borderRadius: 2,
|
|
||||||
}}
|
|
||||||
|
|
||||||
onChange={(event: any) => setIndex(event.nativeEvent.selectedSegmentIndex)}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<DatePicker
|
<DatePicker
|
||||||
{...restProps}
|
{...restProps}
|
||||||
date={toDate(date)}
|
date={toDate(date)}
|
||||||
@@ -142,16 +88,10 @@ export default function ADatePicker({
|
|||||||
isModal={false}
|
isModal={false}
|
||||||
onPickerCancel={handleCancel}
|
onPickerCancel={handleCancel}
|
||||||
onPickerConfirm={handleConfirm}
|
onPickerConfirm={handleConfirm}
|
||||||
showHeader={false}
|
|
||||||
onDateChange={(value) => {
|
onDateChange={(value) => {
|
||||||
console.log(value)
|
handleConfirm(value)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<View className="flex flex-row gap-4 px-3 pb-1 v">
|
|
||||||
{/* <AButton variant="outline" className="flex-1">取消</AButton>
|
|
||||||
<AButton className="flex-1">确认</AButton> */}
|
|
||||||
</View>
|
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
</Modal>
|
</Modal>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ export interface FinanceRecord {
|
|||||||
export interface ListFinanceRecordsParams {
|
export interface ListFinanceRecordsParams {
|
||||||
start_time: string
|
start_time: string
|
||||||
end_time: string
|
end_time: string
|
||||||
category_id: number
|
category_id: number | null
|
||||||
category_type: number
|
category_type: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ListFinanceRecordsResult {
|
export interface ListFinanceRecordsResult {
|
||||||
|
|||||||
+196
-166
@@ -1,177 +1,207 @@
|
|||||||
import type { FinanceRecord } from '@/types/finance/record'
|
<!DOCTYPE html>
|
||||||
import Big from 'big.js'
|
<html>
|
||||||
import { useEffect, useState } from 'react'
|
<head>
|
||||||
import { Keyboard, Pressable, StyleSheet, Text, TouchableWithoutFeedback, View } from 'react-native'
|
<meta charset="UTF-8" />
|
||||||
import { ADatePicker, AInput, LucideIcon } from '@/components/common/ao/index'
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
import { useKeyboardHeight } from '@/hooks/useKeyboardHeight'
|
<title>家庭菜单</title>
|
||||||
import { getCurrentDate } from '@/utils/time'
|
<style>
|
||||||
|
body{
|
||||||
|
margin:0;
|
||||||
|
font-family: sans-serif;
|
||||||
|
background:#f2f2f2;
|
||||||
|
}
|
||||||
|
.header{
|
||||||
|
background:#4CAF50;
|
||||||
|
color:white;
|
||||||
|
padding:15px;
|
||||||
|
text-align:center;
|
||||||
|
font-size:18px;
|
||||||
|
}
|
||||||
|
.page{
|
||||||
|
display:none;
|
||||||
|
padding:15px;
|
||||||
|
padding-bottom:80px;
|
||||||
|
}
|
||||||
|
.page.active{
|
||||||
|
display:block;
|
||||||
|
}
|
||||||
|
.card{
|
||||||
|
background:white;
|
||||||
|
padding:12px;
|
||||||
|
margin-bottom:10px;
|
||||||
|
border-radius:8px;
|
||||||
|
box-shadow:0 2px 5px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
button{
|
||||||
|
background:#4CAF50;
|
||||||
|
border:none;
|
||||||
|
color:white;
|
||||||
|
padding:6px 10px;
|
||||||
|
border-radius:6px;
|
||||||
|
margin-top:5px;
|
||||||
|
}
|
||||||
|
.tabs{
|
||||||
|
position:fixed;
|
||||||
|
bottom:0;
|
||||||
|
width:100%;
|
||||||
|
display:flex;
|
||||||
|
background:white;
|
||||||
|
border-top:1px solid #ddd;
|
||||||
|
}
|
||||||
|
.tab{
|
||||||
|
flex:1;
|
||||||
|
padding:12px;
|
||||||
|
text-align:center;
|
||||||
|
}
|
||||||
|
.tab.active{
|
||||||
|
background:#dcedc8;
|
||||||
|
}
|
||||||
|
input,select{
|
||||||
|
width:100%;
|
||||||
|
padding:6px;
|
||||||
|
margin-top:5px;
|
||||||
|
}
|
||||||
|
.small{
|
||||||
|
font-size:12px;
|
||||||
|
color:#666;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
interface KeydownItem {
|
<div class="header">家庭做饭助手</div>
|
||||||
key: string | number
|
|
||||||
content: React.ReactNode
|
<!-- 菜谱 -->
|
||||||
|
<div class="page active" id="recipePage">
|
||||||
|
<h3>菜谱</h3>
|
||||||
|
<div id="recipeList"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 下单 -->
|
||||||
|
<div class="page" id="orderPage">
|
||||||
|
<h3>下单</h3>
|
||||||
|
<div id="orderList"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 库存 -->
|
||||||
|
<div class="page" id="inventoryPage">
|
||||||
|
<h3>库存</h3>
|
||||||
|
<input id="name" placeholder="食材名称">
|
||||||
|
<input id="qty" type="number" placeholder="数量">
|
||||||
|
<button onclick="addInventory()">添加主料</button>
|
||||||
|
<div id="inventoryList"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tabs">
|
||||||
|
<div class="tab active" onclick="switchTab(0)">菜谱</div>
|
||||||
|
<div class="tab" onclick="switchTab(1)">下单</div>
|
||||||
|
<div class="tab" onclick="switchTab(2)">库存</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
let inventory = [
|
||||||
|
{ id:1, name:"鸡蛋", qty:4 },
|
||||||
|
{ id:2, name:"番茄", qty:2 }
|
||||||
|
];
|
||||||
|
|
||||||
|
let recipes = [
|
||||||
|
{
|
||||||
|
id:1,
|
||||||
|
name:"番茄炒蛋",
|
||||||
|
ingredients:[
|
||||||
|
{ name:"鸡蛋", qty:2, type:"main" },
|
||||||
|
{ name:"番茄", qty:1, type:"main" },
|
||||||
|
{ name:"盐", type:"seasoning" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
function renderInventory(){
|
||||||
|
let div=document.getElementById("inventoryList");
|
||||||
|
div.innerHTML="";
|
||||||
|
inventory.forEach(i=>{
|
||||||
|
div.innerHTML+=`
|
||||||
|
<div class="card">
|
||||||
|
${i.name} - 剩余 ${i.qty}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AmountKeyboardProps {
|
function renderRecipes(){
|
||||||
show?: boolean
|
let div=document.getElementById("recipeList");
|
||||||
record: FinanceRecord
|
div.innerHTML="";
|
||||||
|
recipes.forEach(r=>{
|
||||||
// 可选:点击完成按钮触发
|
div.innerHTML+=`
|
||||||
onDone?: (record: FinanceRecord) => void
|
<div class="card">
|
||||||
|
<b>${r.name}</b>
|
||||||
|
<div class="small">
|
||||||
|
${r.ingredients.map(i=> i.type==="main" ? i.name+" "+i.qty : i.name+"(调料)").join(", ")}
|
||||||
|
</div>
|
||||||
|
<button onclick="order(${r.id})">做这道菜</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AmountKeyboard({
|
function order(id){
|
||||||
show = false,
|
let recipe=recipes.find(r=>r.id===id);
|
||||||
record,
|
let canCook=true;
|
||||||
onDone,
|
|
||||||
}: AmountKeyboardProps) {
|
|
||||||
// 自定义键盘计算过程
|
|
||||||
const [keydownInput, setKeydownInput] = useState('')
|
|
||||||
// 日期选择
|
|
||||||
const [showDatePicker, setShowDatePicker] = useState(false)
|
|
||||||
// 备注聚焦
|
|
||||||
const [remarkIsFocused, setRemarkIsFocused] = useState(false)
|
|
||||||
// 键盘高度
|
|
||||||
const keyboardHeight = useKeyboardHeight()
|
|
||||||
|
|
||||||
const [innerRecord, setInnerRecord] = useState<FinanceRecord>({
|
recipe.ingredients.forEach(i=>{
|
||||||
id: 0,
|
if(i.type==="main"){
|
||||||
category_id: 0,
|
let stock=inventory.find(s=>s.name===i.name);
|
||||||
amount: '',
|
if(!stock || stock.qty<i.qty){
|
||||||
remark: '',
|
canCook=false;
|
||||||
record_time: '',
|
}
|
||||||
member_id: 0,
|
|
||||||
member_name: '',
|
|
||||||
})
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setInnerRecord(record)
|
|
||||||
setKeydownInput(record.amount || '0.00')
|
|
||||||
}, [record])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!keydownInput) {
|
|
||||||
setInnerRecord({
|
|
||||||
...innerRecord,
|
|
||||||
amount: '0',
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
if(!canCook){
|
||||||
setInnerRecord({
|
alert("主料不足,做不了 😢");
|
||||||
...innerRecord,
|
return;
|
||||||
amount: calculateWithBig(keydownInput),
|
}
|
||||||
})
|
|
||||||
|
recipe.ingredients.forEach(i=>{
|
||||||
|
if(i.type==="main"){
|
||||||
|
let stock=inventory.find(s=>s.name===i.name);
|
||||||
|
stock.qty-=i.qty;
|
||||||
}
|
}
|
||||||
catch {
|
});
|
||||||
setInnerRecord({
|
|
||||||
...innerRecord,
|
|
||||||
amount: '0',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, [keydownInput])
|
|
||||||
|
|
||||||
return (
|
alert("开做!🍳");
|
||||||
<View style={{
|
renderInventory();
|
||||||
backgroundColor: '#fff',
|
|
||||||
elevation: 20,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
|
|
||||||
<View className="h-28 flex pl-4 pr-4 justify-center">
|
|
||||||
<View className="flex flex-row items-center justify-between">
|
|
||||||
<Pressable
|
|
||||||
className="flex flex-row items-center gap-1.5 px-2 py-1.5 rounded-md bg-gray-50 active:opacity-70"
|
|
||||||
onPress={() => setShowDatePicker(true)}
|
|
||||||
>
|
|
||||||
<LucideIcon icon="Calendar" size={16} color="#60a5fa" clickable={false} />
|
|
||||||
<Text className="text-gray-600 text-sm">{innerRecord.record_time}</Text>
|
|
||||||
<LucideIcon icon="ChevronDown" size={12} color="#aaa" clickable={false} />
|
|
||||||
</Pressable>
|
|
||||||
<Text className="text-3xl text-blue-700 text-right">{innerRecord.amount}</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<Text className="text-gray-400 text-right">{keydownInput}</Text>
|
|
||||||
<AInput
|
|
||||||
value={innerRecord.remark}
|
|
||||||
placeholder="备注"
|
|
||||||
className="bg-[#f2f3f5] border-0"
|
|
||||||
size="sm"
|
|
||||||
onChangeText={(val) => {
|
|
||||||
setInnerRecord({
|
|
||||||
...innerRecord,
|
|
||||||
remark: val,
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
onFocus={() => setRemarkIsFocused(true)}
|
|
||||||
onBlur={() => setRemarkIsFocused(false)}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</TouchableWithoutFeedback>
|
|
||||||
<View className="flex flex-row flex-wrap" style={{ height: 220 }}>
|
|
||||||
{keys.map((item, idx) => {
|
|
||||||
const isFirstCol = idx % 4 === 0
|
|
||||||
const isFirstRow = idx < 4
|
|
||||||
const isLastCol = (idx + 1) % 4 === 0
|
|
||||||
const isLastRow = idx >= 12
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Pressable
|
|
||||||
key={item.key}
|
|
||||||
onPress={() => 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 (
|
|
||||||
<>
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
...StyleSheet.absoluteFillObject,
|
|
||||||
backgroundColor: pressed ? pressedBg : defaultBg,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{item.content}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</Pressable>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</View>
|
|
||||||
{remarkIsFocused && keyboardHeight > 220 && (
|
|
||||||
<View style={{ height: keyboardHeight + 10 - 220 }} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
<ADatePicker
|
|
||||||
pickerTitle="记账时间"
|
|
||||||
mode="date"
|
|
||||||
date={innerRecord.record_time}
|
|
||||||
show={showDatePicker}
|
|
||||||
onClose={() => {
|
|
||||||
setShowDatePicker(false)
|
|
||||||
}}
|
|
||||||
onConfirm={(date) => {
|
|
||||||
setShowDatePicker(false)
|
|
||||||
|
|
||||||
setInnerRecord({
|
|
||||||
...innerRecord,
|
|
||||||
record_time: date,
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function addInventory(){
|
||||||
|
let name=document.getElementById("name").value;
|
||||||
|
let qty=parseInt(document.getElementById("qty").value);
|
||||||
|
|
||||||
|
if(!name||!qty) return;
|
||||||
|
|
||||||
|
let existing=inventory.find(i=>i.name===name);
|
||||||
|
if(existing){
|
||||||
|
existing.qty+=qty;
|
||||||
|
}else{
|
||||||
|
inventory.push({id:inventory.length+1,name,qty});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderInventory();
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchTab(i){
|
||||||
|
document.querySelectorAll(".page").forEach((p,index)=>{
|
||||||
|
p.classList.toggle("active",index===i);
|
||||||
|
});
|
||||||
|
document.querySelectorAll(".tab").forEach((t,index)=>{
|
||||||
|
t.classList.toggle("active",index===i);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderInventory();
|
||||||
|
renderRecipes();
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in new issue
Block a user