20260623212917
This commit is contained in:
1 parent
e9e3371be7
commit
056cd4970f
30 files changed
+2093
-636
No files matched your search
@@ -49,6 +49,7 @@
|
||||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0",
|
||||
"react-native": "0.83.4",
|
||||
"react-native-dates-picker": "^0.2.4",
|
||||
"react-native-gesture-handler": "~2.30.0",
|
||||
"react-native-image-crop-picker": "^0.51.1",
|
||||
"react-native-keyboard-controller": "1.20.7",
|
||||
|
||||
Generated
+51
@@ -116,6 +116,9 @@ importers:
|
||||
react-native:
|
||||
specifier: 0.83.4
|
||||
version: 0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0)
|
||||
react-native-dates-picker:
|
||||
specifier: ^0.2.4
|
||||
version: 0.2.4(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
|
||||
react-native-gesture-handler:
|
||||
specifier: ~2.30.0
|
||||
version: 2.30.1(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
|
||||
@@ -1028,48 +1031,56 @@ packages:
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxfmt/binding-linux-arm64-musl@0.44.0':
|
||||
resolution: {integrity: sha512-AlObQIXyVRZ96LbtVljtFq0JqH5B92NU+BQeDFrXWBUWlCKAM0wF5GLfIhCLT5kQ3Sl+U0YjRJ7Alqj5hGQaCg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxfmt/binding-linux-ppc64-gnu@0.44.0':
|
||||
resolution: {integrity: sha512-YcFE8/q/BbrCiIiM5piwbkA6GwJc5QqhMQp2yDrqQ2fuVkZ7CInb1aIijZ/k8EXc72qXMSwKpVlBv1w/MsGO/A==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxfmt/binding-linux-riscv64-gnu@0.44.0':
|
||||
resolution: {integrity: sha512-eOdzs6RqkRzuqNHUX5C8ISN5xfGh4xDww8OEd9YAmc3OWN8oAe5bmlIqQ+rrHLpv58/0BuU48bxkhnIGjA/ATQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxfmt/binding-linux-riscv64-musl@0.44.0':
|
||||
resolution: {integrity: sha512-YBgNTxntD/QvlFUfgvh8bEdwOhXiquX8gaofZJAwYa/Xp1S1DQrFVZEeck7GFktr24DztsSp8N8WtWCBwxs0Hw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxfmt/binding-linux-s390x-gnu@0.44.0':
|
||||
resolution: {integrity: sha512-GLIh1R6WHWshl/i4QQDNgj0WtT25aRO4HNUWEoitxiywyRdhTFmFEYT2rXlcl9U6/26vhmOqG5cRlMLG3ocaIA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxfmt/binding-linux-x64-gnu@0.44.0':
|
||||
resolution: {integrity: sha512-gZOpgTlOsLcLfAF9qgpTr7FIIFSKnQN3hDf/0JvQ4CIwMY7h+eilNjxq/CorqvYcEOu+LRt1W4ZS7KccEHLOdA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxfmt/binding-linux-x64-musl@0.44.0':
|
||||
resolution: {integrity: sha512-1CyS9JTB+pCUFYFI6pkQGGZaT/AY5gnhHVrQQLhFba6idP9AzVYm1xbdWfywoldTYvjxQJV6x4SuduCIfP3W+A==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxfmt/binding-openharmony-arm64@0.44.0':
|
||||
resolution: {integrity: sha512-bmEv70Ak6jLr1xotCbF5TxIKjsmQaiX+jFRtnGtfA03tJPf6VG3cKh96S21boAt3JZc+Vjx8PYcDuLj39vM2Pw==}
|
||||
@@ -1142,48 +1153,56 @@ packages:
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxlint/binding-linux-arm64-musl@1.60.0':
|
||||
resolution: {integrity: sha512-eDYDXZGhQAXyn6GwtwiX/qcLS0HlOLPJ/+iiIY8RYr+3P8oKBmgKxADLlniL6FtWfE7pPk7IGN9/xvDEvDvFeg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxlint/binding-linux-ppc64-gnu@1.60.0':
|
||||
resolution: {integrity: sha512-nxehly5XYBHUWI9VJX1bqCf9j/B43DaK/aS/T1fcxCpX3PA4Rm9BB54nPD1CKayT8xg6REN1ao+01hSRNgy8OA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxlint/binding-linux-riscv64-gnu@1.60.0':
|
||||
resolution: {integrity: sha512-j1qf/NaUfOWQutjeoooNG1Q0zsK0XGmSu1uDLq3cctquRF3j7t9Hxqf/76ehCc5GEUAanth2W4Fa+XT1RFg/nw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxlint/binding-linux-riscv64-musl@1.60.0':
|
||||
resolution: {integrity: sha512-YELKPRefQ/q/h3RUmeRfPCUhh2wBvgV1RyZ/F9M9u8cDyXsQW2ojv1DeWQTt466yczDITjZnIOg/s05pk7Ve2A==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxlint/binding-linux-s390x-gnu@1.60.0':
|
||||
resolution: {integrity: sha512-JkO3C6Gki7Y6h/MiIkFKvHFOz98/YWvQ4WYbK9DLXACMP2rjULzkeGyAzorJE5S1dzLQGFgeqvN779kSFwoV1g==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxlint/binding-linux-x64-gnu@1.60.0':
|
||||
resolution: {integrity: sha512-XjKHdFVCpZZZSWBCKyyqCq65s2AKXykMXkjLoKYODrD+f5toLhlwsMESscu8FbgnJQ4Y/dpR/zdazsahmgBJIA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxlint/binding-linux-x64-musl@1.60.0':
|
||||
resolution: {integrity: sha512-js29ZWIuPhNWzY8NC7KoffEMEeWG105vbmm+8EOJsC+T/jHBiKIJEUF78+F/IrgEWMMP9N0kRND4Pp75+xAhKg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxlint/binding-openharmony-arm64@1.60.0':
|
||||
resolution: {integrity: sha512-H+PUITKHk04stFpWj3x3Kg08Afp/bcXSBi0EhasR5a0Vw7StXHTzdl655PUI0fB4qdh2Wsu6Dsi+3ACxPoyQnA==}
|
||||
@@ -1605,24 +1624,28 @@ packages:
|
||||
engines: {node: '>= 20'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tailwindcss/oxide-linux-arm64-musl@4.2.1':
|
||||
resolution: {integrity: sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==}
|
||||
engines: {node: '>= 20'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@tailwindcss/oxide-linux-x64-gnu@4.2.1':
|
||||
resolution: {integrity: sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==}
|
||||
engines: {node: '>= 20'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tailwindcss/oxide-linux-x64-musl@4.2.1':
|
||||
resolution: {integrity: sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==}
|
||||
engines: {node: '>= 20'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@tailwindcss/oxide-wasm32-wasi@4.2.1':
|
||||
resolution: {integrity: sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==}
|
||||
@@ -1696,10 +1719,12 @@ packages:
|
||||
|
||||
'@ungap/structured-clone@1.3.0':
|
||||
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
|
||||
deprecated: Potential CWE-502 - Update to 1.3.1 or higher
|
||||
|
||||
'@xmldom/xmldom@0.8.12':
|
||||
resolution: {integrity: sha512-9k/gHF6n/pAi/9tqr3m3aqkuiNosYTurLLUtc7xQ9sxB/wm7WPygCv8GYa6mS0fLJEHhqMC1ATYhz++U/lRHqg==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
deprecated: this version has critical issues, please update to the latest version
|
||||
|
||||
'@yz1311/react-native-wheel-picker@0.4.5':
|
||||
resolution: {integrity: sha512-07ga/fYdgHYI3JTAd+ntaXcJ1D3VxbgaqCPxABjxigehmFYD3VpBbMMVRhxmZC3oVnpedc+SKJXHTNwGtLriCQ==}
|
||||
@@ -2907,72 +2932,84 @@ packages:
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
lightningcss-linux-arm64-gnu@1.31.1:
|
||||
resolution: {integrity: sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
lightningcss-linux-arm64-gnu@1.32.0:
|
||||
resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
lightningcss-linux-arm64-musl@1.30.1:
|
||||
resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
lightningcss-linux-arm64-musl@1.31.1:
|
||||
resolution: {integrity: sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
lightningcss-linux-arm64-musl@1.32.0:
|
||||
resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
lightningcss-linux-x64-gnu@1.30.1:
|
||||
resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
lightningcss-linux-x64-gnu@1.31.1:
|
||||
resolution: {integrity: sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
lightningcss-linux-x64-gnu@1.32.0:
|
||||
resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
lightningcss-linux-x64-musl@1.30.1:
|
||||
resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
lightningcss-linux-x64-musl@1.31.1:
|
||||
resolution: {integrity: sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
lightningcss-linux-x64-musl@1.32.0:
|
||||
resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
lightningcss-win32-arm64-msvc@1.30.1:
|
||||
resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==}
|
||||
@@ -3518,6 +3555,13 @@ packages:
|
||||
react-native-animatable@1.4.0:
|
||||
resolution: {integrity: sha512-DZwaDVWm2NBvBxf7I0wXKXLKb/TxDnkV53sWhCvei1pRyTX3MVFpkvdYBknNBqPrxYuAIlPxEp7gJOidIauUkw==}
|
||||
|
||||
react-native-dates-picker@0.2.4:
|
||||
resolution: {integrity: sha512-zKNri33Ci8ozvWuZW9ErlYcrs66CUy3ZyVvLqqaYz9quE+sRyu2AMouLl1LQrwq4Fsk0hUjoSDuK2PPc0zUE/g==}
|
||||
engines: {node: '>= 16.0.0'}
|
||||
peerDependencies:
|
||||
react: '>=17.0.0'
|
||||
react-native: '*'
|
||||
|
||||
react-native-gesture-handler@2.30.1:
|
||||
resolution: {integrity: sha512-xIUBDo5ktmJs++0fZlavQNvDEE4PsihWhSeJsJtoz4Q6p0MiTM9TgrTgfEgzRR36qGPytFoeq+ShLrVwGdpUdA==}
|
||||
peerDependencies:
|
||||
@@ -4076,6 +4120,7 @@ packages:
|
||||
|
||||
uuid@7.0.3:
|
||||
resolution: {integrity: sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==}
|
||||
deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).
|
||||
hasBin: true
|
||||
|
||||
validate-npm-package-name@5.0.1:
|
||||
@@ -8017,6 +8062,12 @@ snapshots:
|
||||
dependencies:
|
||||
prop-types: 15.8.1
|
||||
|
||||
react-native-dates-picker@0.2.4(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0):
|
||||
dependencies:
|
||||
dayjs: 1.11.20
|
||||
react: 19.2.0
|
||||
react-native: 0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0)
|
||||
|
||||
react-native-gesture-handler@2.30.1(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0):
|
||||
dependencies:
|
||||
'@egjs/hammerjs': 2.0.17
|
||||
|
||||
File renamed without changes.
@@ -0,0 +1,104 @@
|
||||
import React, { useState } from "react";
|
||||
import { View, Text, Button, ScrollView } from "react-native";
|
||||
import LayoutView from "@/components/layout/view";
|
||||
import DateTime from "@/components/DateTime";
|
||||
import DateTimeRange from "@/components/DateTimeRange";
|
||||
|
||||
export default function ChatTest() {
|
||||
// --- DateTime (单选) ---
|
||||
const [showDate, setShowDate] = useState(false);
|
||||
const [dateValue, setDateValue] = useState("2026-06-23");
|
||||
|
||||
const [showTime, setShowTime] = useState(false);
|
||||
const [timeValue, setTimeValue] = useState("20:00:00");
|
||||
|
||||
const [showDatetime, setShowDatetime] = useState(false);
|
||||
const [datetimeValue, setDatetimeValue] = useState("2026-06-23 20:00");
|
||||
|
||||
const [showMonth, setShowMonth] = useState(false);
|
||||
const [monthValue, setMonthValue] = useState("2026-06");
|
||||
|
||||
const [showYear, setShowYear] = useState(false);
|
||||
const [yearValue, setYearValue] = useState("2026");
|
||||
|
||||
// --- DateTimeRange (范围) ---
|
||||
const [showRange, setShowRange] = useState(false);
|
||||
const [rangeStart, setRangeStart] = useState("");
|
||||
const [rangeEnd, setRangeEnd] = useState("");
|
||||
|
||||
return (
|
||||
<LayoutView>
|
||||
<LayoutView.ScrollView>
|
||||
<View style={{ padding: 16, gap: 16, marginTop: 30 }}>
|
||||
|
||||
{/* DateTime */}
|
||||
<Text style={{ fontSize: 18, fontWeight: "bold" }}>DateTime(单选)</Text>
|
||||
|
||||
<Button title={`日期: ${dateValue}`} onPress={() => setShowDate(true)} />
|
||||
<DateTime
|
||||
show={showDate}
|
||||
value={dateValue}
|
||||
mode="date"
|
||||
onConfirm={(v) => { setDateValue(v); setShowDate(false); }}
|
||||
onClose={() => setShowDate(false)}
|
||||
/>
|
||||
|
||||
<Button title={`时间: ${timeValue}`} onPress={() => setShowTime(true)} />
|
||||
<DateTime
|
||||
show={showTime}
|
||||
value={timeValue}
|
||||
mode="time"
|
||||
onConfirm={(v) => { setTimeValue(v); setShowTime(false); }}
|
||||
onClose={() => setShowTime(false)}
|
||||
/>
|
||||
|
||||
<Button title={`日期时间: ${datetimeValue}`} onPress={() => setShowDatetime(true)} />
|
||||
<DateTime
|
||||
show={showDatetime}
|
||||
value={datetimeValue}
|
||||
mode="datetime"
|
||||
onConfirm={(v) => { setDatetimeValue(v); setShowDatetime(false); }}
|
||||
onClose={() => setShowDatetime(false)}
|
||||
/>
|
||||
|
||||
<Button title={`月份: ${monthValue}`} onPress={() => setShowMonth(true)} />
|
||||
<DateTime
|
||||
show={showMonth}
|
||||
value={monthValue}
|
||||
mode="month"
|
||||
onConfirm={(v) => { setMonthValue(v); setShowMonth(false); }}
|
||||
onClose={() => setShowMonth(false)}
|
||||
/>
|
||||
|
||||
<Button title={`年份: ${yearValue}`} onPress={() => setShowYear(true)} />
|
||||
<DateTime
|
||||
show={showYear}
|
||||
value={yearValue}
|
||||
mode="year"
|
||||
onConfirm={(v) => { setYearValue(v); setShowYear(false); }}
|
||||
onClose={() => setShowYear(false)}
|
||||
/>
|
||||
|
||||
{/* DateTimeRange */}
|
||||
<Text style={{ fontSize: 18, fontWeight: "bold", marginTop: 16 }}>DateTimeRange(范围)</Text>
|
||||
|
||||
<Button
|
||||
title={rangeStart ? `${rangeStart} ~ ${rangeEnd || "..."}` : "选择日期范围"}
|
||||
onPress={() => setShowRange(true)}
|
||||
/>
|
||||
<DateTimeRange
|
||||
show={showRange}
|
||||
startDate={rangeStart || undefined}
|
||||
endDate={rangeEnd || undefined}
|
||||
onConfirm={(start, end) => {
|
||||
setRangeStart(start);
|
||||
setRangeEnd(end);
|
||||
setShowRange(false);
|
||||
}}
|
||||
onClose={() => setShowRange(false)}
|
||||
/>
|
||||
</View>
|
||||
</LayoutView.ScrollView>
|
||||
</LayoutView>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import LayoutView from "@/components/layout/view";
|
||||
import { loadDataApi } from "@/request/api";
|
||||
import { View, Text } from "react-native";
|
||||
import { useFamilyStore, useFavoriteStore } from "@/store";
|
||||
import {
|
||||
ChevronRight,
|
||||
Grid3X3,
|
||||
UserPlus,
|
||||
User,
|
||||
Receipt,
|
||||
Heart,
|
||||
Clock3,
|
||||
} from "lucide-react-native";
|
||||
import { PressableFeedback } from "heroui-native";
|
||||
import { SUB_APPS } from "@/configs/pages";
|
||||
import { useRouter } from "expo-router";
|
||||
import { loadFeeds, type FeedRecord } from "@/helpers/feed";
|
||||
|
||||
/* ─── 问候 ─── */
|
||||
function getGreeting() {
|
||||
const h = new Date().getHours();
|
||||
if (h < 6) return "夜深了";
|
||||
if (h < 9) return "早上好";
|
||||
if (h < 12) return "上午好";
|
||||
if (h < 14) return "中午好";
|
||||
if (h < 18) return "下午好";
|
||||
return "晚上好";
|
||||
}
|
||||
|
||||
/* ─── 类型 → 图标 ─── */
|
||||
const TYPE_ICONS: Record<string, React.ComponentType<any>> = {
|
||||
join: UserPlus,
|
||||
rename: User,
|
||||
bill: Receipt,
|
||||
fav: Heart,
|
||||
};
|
||||
|
||||
/* ─── 时间格式化 ─── */
|
||||
function formatFeedTime(dateStr: string) {
|
||||
const date = new Date(dateStr);
|
||||
const now = new Date();
|
||||
const diff = now.getTime() - date.getTime();
|
||||
const mins = Math.floor(diff / 60000);
|
||||
const hours = Math.floor(diff / 3600000);
|
||||
const days = Math.floor(diff / 86400000);
|
||||
|
||||
if (mins < 1) return "刚刚";
|
||||
if (mins < 60) return `${mins} 分钟前`;
|
||||
if (hours < 24) return `${hours} 小时前`;
|
||||
if (days < 7) return `${days} 天前`;
|
||||
return date.toLocaleDateString("zh-CN", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
});
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
const router = useRouter();
|
||||
const familyStore = useFamilyStore();
|
||||
const favoriteStore = useFavoriteStore();
|
||||
|
||||
const [family, setFamily] = useState<any>({});
|
||||
const [duration, setDuration] = useState({
|
||||
days: 0,
|
||||
hours: 0,
|
||||
minutes: 0,
|
||||
seconds: 0,
|
||||
});
|
||||
const [feeds, setFeeds] = useState<FeedRecord[]>([]);
|
||||
|
||||
async function loadFamilyData() {
|
||||
const res = await loadDataApi({
|
||||
view_name: "b_family",
|
||||
search_condition: "id = ?",
|
||||
search_columns: [],
|
||||
order_by: "",
|
||||
args: [familyStore.currentFamily?.family_id],
|
||||
});
|
||||
if (res.isSuccess && res.data.length > 0) setFamily(res.data[0]);
|
||||
}
|
||||
|
||||
function calcDuration(startTime: string) {
|
||||
const start = new Date(startTime).getTime();
|
||||
const now = Date.now();
|
||||
let diff = Math.floor((now - start) / 1000);
|
||||
const days = Math.floor(diff / (3600 * 24));
|
||||
diff %= 3600 * 24;
|
||||
const hours = Math.floor(diff / 3600);
|
||||
diff %= 3600;
|
||||
const minutes = Math.floor(diff / 60);
|
||||
const seconds = diff % 60;
|
||||
return { days, hours, minutes, seconds };
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadFamilyData();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!family.create_time) return;
|
||||
const timer = setInterval(
|
||||
() => setDuration(calcDuration(family.create_time)),
|
||||
1000,
|
||||
);
|
||||
return () => clearInterval(timer);
|
||||
}, [family.create_time]);
|
||||
|
||||
useEffect(() => {
|
||||
const fid = familyStore.currentFamily.family_id;
|
||||
if (fid) {
|
||||
favoriteStore.fetchFavorites(fid);
|
||||
loadFeeds(fid).then(setFeeds).catch(console.error);
|
||||
}
|
||||
}, [familyStore.currentFamily.family_id]);
|
||||
|
||||
return (
|
||||
<LayoutView>
|
||||
<LayoutView.ScrollView
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
{/* ═══════════ 头部 ═══════════ */}
|
||||
<View className="pt-2 pb-8">
|
||||
<Text className="text-[13px] text-gray-400 tracking-wide">
|
||||
{getGreeting()}
|
||||
</Text>
|
||||
<Text className="text-[28px] font-bold text-gray-900 mt-1 tracking-tight">
|
||||
{family.name || "我的空间"}
|
||||
</Text>
|
||||
|
||||
{/* 计时 — 开放式,无卡片 */}
|
||||
<View className="mt-6">
|
||||
<View className="flex-row items-baseline">
|
||||
<Text className="text-[44px] font-bold text-accent tracking-tighter">
|
||||
{duration.days}
|
||||
</Text>
|
||||
<Text className="text-[16px] text-accent/60 font-medium ml-1.5">
|
||||
天
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<Text className="text-sm text-gray-500 mt-2">
|
||||
{String(duration.hours).padStart(2, "0")} 小时{" "}
|
||||
{String(duration.minutes).padStart(2, "0")} 分钟{" "}
|
||||
{String(duration.seconds).padStart(2, "0")} 秒
|
||||
</Text>
|
||||
|
||||
<Text className="text-[13px] text-gray-400/70 mt-1.5">
|
||||
已经在一起这么久啦
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* ═══════════ 快捷工具 ═══════════ */}
|
||||
<View className="flex-row items-center mb-3">
|
||||
<View className="w-1.5 h-4 rounded-full bg-accent mr-1.5" />
|
||||
<Text className="text-[11px] text-gray-500 font-semibold tracking-wider uppercase">
|
||||
快捷工具
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{SUB_APPS.map((app) => {
|
||||
const Icon = app.icon;
|
||||
return (
|
||||
<PressableFeedback
|
||||
key={app.id}
|
||||
onPress={() => router.navigate(app.id as any)}
|
||||
className="flex-row items-center bg-white rounded-xl shadow-sm mb-2 px-4 py-3.5"
|
||||
>
|
||||
<View className="w-11 h-11 rounded-xl bg-accent/10 items-center justify-center mr-3.5">
|
||||
<Icon size={22} />
|
||||
</View>
|
||||
|
||||
<View className="flex-1">
|
||||
<Text className="text-[15px] font-semibold text-gray-800">
|
||||
{app.title}
|
||||
</Text>
|
||||
<Text className="text-[13px] text-gray-400 mt-0.5">
|
||||
{app.description}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<ChevronRight size={18} color="#d1d5db" />
|
||||
<PressableFeedback.Ripple />
|
||||
</PressableFeedback>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* 更多工具 */}
|
||||
<PressableFeedback
|
||||
onPress={() => router.navigate("/tools")}
|
||||
className="flex-row items-center bg-white rounded-xl shadow-sm mb-1 mt-1 px-4 py-3.5"
|
||||
>
|
||||
<View className="w-11 h-11 rounded-xl bg-gray-100 items-center justify-center mr-3.5">
|
||||
<Grid3X3 size={20} color="#9ca3af" />
|
||||
</View>
|
||||
|
||||
<Text className="flex-1 text-[15px] font-medium text-gray-500">
|
||||
更多工具
|
||||
</Text>
|
||||
|
||||
<ChevronRight size={18} color="#d1d5db" />
|
||||
<PressableFeedback.Ripple />
|
||||
</PressableFeedback>
|
||||
|
||||
{/* ═══════════ 最近动态 ═══════════ */}
|
||||
<View className="flex-row items-center justify-between mb-4 mt-7">
|
||||
<View className="flex-row items-center">
|
||||
<View className="w-1.5 h-4 rounded-full bg-accent mr-1.5" />
|
||||
<Text className="text-[11px] text-gray-500 font-semibold tracking-wider uppercase">
|
||||
最近动态
|
||||
</Text>
|
||||
</View>
|
||||
<PressableFeedback
|
||||
onPress={() => router.navigate("/activities")}
|
||||
className="flex-row items-center"
|
||||
>
|
||||
<Text className="text-[12px] text-gray-400">更多</Text>
|
||||
<ChevronRight size={14} color="#d1d5db" />
|
||||
<PressableFeedback.Ripple />
|
||||
</PressableFeedback>
|
||||
</View>
|
||||
|
||||
{feeds.length === 0 ? (
|
||||
<Text className="text-sm text-gray-400 mb-6">
|
||||
暂无动态
|
||||
</Text>
|
||||
) : (
|
||||
feeds.slice(0, 3).map((item, idx) => {
|
||||
const isLast = idx === Math.min(feeds.length, 3) - 1;
|
||||
const Icon = TYPE_ICONS[item.type] || Clock3;
|
||||
const meta = item.meta ? JSON.parse(item.meta) : null;
|
||||
|
||||
return (
|
||||
<View key={item.id} className="flex-row">
|
||||
{/* 时间轴列 */}
|
||||
<View className="items-center mr-3" style={{ width: 16 }}>
|
||||
<View className="w-2 h-2 rounded-full bg-accent mt-1.5" />
|
||||
{!isLast && <View className="flex-1 w-px bg-gray-200" />}
|
||||
</View>
|
||||
|
||||
{/* 内容 */}
|
||||
<View className={`flex-1 ${isLast ? "" : "pb-5"}`}>
|
||||
<Text className="text-xs text-gray-400">
|
||||
{formatFeedTime(item.create_time)}
|
||||
</Text>
|
||||
<Text className="text-[15px] text-gray-800 leading-5 mt-0.5">
|
||||
{item.content}
|
||||
</Text>
|
||||
{meta?.amount ? (
|
||||
<Text className="text-[13px] font-semibold text-accent mt-0.5">
|
||||
¥{meta.amount}
|
||||
</Text>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
})
|
||||
)}
|
||||
|
||||
{/* 底部留白 */}
|
||||
<View className="h-4" />
|
||||
</LayoutView.ScrollView>
|
||||
</LayoutView>
|
||||
);
|
||||
}
|
||||
File renamed without changes.
@@ -0,0 +1,117 @@
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import LayoutView from "@/components/layout/view";
|
||||
import { View, Text } from "react-native";
|
||||
import { Stack, useRouter } from "expo-router";
|
||||
import { CloseButton } from "heroui-native";
|
||||
import { useFamilyStore } from "@/store";
|
||||
import { loadFeeds, type FeedRecord } from "@/helpers/feed";
|
||||
import { UserPlus, User, Receipt, Heart } from "lucide-react-native";
|
||||
|
||||
/* ─── 类型 → 图标 ─── */
|
||||
const TYPE_ICONS: Record<string, React.ComponentType<any>> = {
|
||||
join: UserPlus,
|
||||
rename: User,
|
||||
bill: Receipt,
|
||||
fav: Heart,
|
||||
};
|
||||
|
||||
/* ─── 时间格式化 ─── */
|
||||
function formatTime(dateStr: string) {
|
||||
const date = new Date(dateStr);
|
||||
const now = new Date();
|
||||
const diff = now.getTime() - date.getTime();
|
||||
const mins = Math.floor(diff / 60000);
|
||||
const hours = Math.floor(diff / 3600000);
|
||||
const days = Math.floor(diff / 86400000);
|
||||
|
||||
if (mins < 1) return "刚刚";
|
||||
if (mins < 60) return `${mins} 分钟前`;
|
||||
if (hours < 24) return `${hours} 小时前`;
|
||||
if (days < 7) return `${days} 天前`;
|
||||
return date.toLocaleDateString("zh-CN", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
});
|
||||
}
|
||||
|
||||
export default function Activities() {
|
||||
const router = useRouter();
|
||||
const familyStore = useFamilyStore();
|
||||
|
||||
const [feeds, setFeeds] = useState<FeedRecord[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const fetchFeeds = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await loadFeeds(familyStore.currentFamily.family_id);
|
||||
setFeeds(data);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [familyStore.currentFamily.family_id]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchFeeds();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<LayoutView>
|
||||
<Stack.Screen options={{ animation: "slide_from_bottom" }} />
|
||||
|
||||
<LayoutView.ScrollView
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
{loading ? (
|
||||
<View className="flex-1 items-center justify-center py-20">
|
||||
<Text className="text-sm text-gray-400">加载中...</Text>
|
||||
</View>
|
||||
) : feeds.length === 0 ? (
|
||||
<View className="flex-1 items-center justify-center py-20">
|
||||
<Text className="text-sm text-gray-400">暂无动态</Text>
|
||||
</View>
|
||||
) : (
|
||||
feeds.map((item, idx) => {
|
||||
const isLast = idx === feeds.length - 1;
|
||||
const Icon = TYPE_ICONS[item.type] || Receipt;
|
||||
|
||||
return (
|
||||
<View key={item.id} className="flex-row">
|
||||
{/* 时间轴列 */}
|
||||
<View className="items-center mr-3" style={{ width: 24 }}>
|
||||
<View className="w-7 h-7 rounded-full bg-accent/10 items-center justify-center mt-0.5">
|
||||
<Icon size={14} color="#0d9488" />
|
||||
</View>
|
||||
{!isLast && <View className="flex-1 w-px bg-gray-200 mt-1" />}
|
||||
</View>
|
||||
|
||||
{/* 内容 */}
|
||||
<View className={`flex-1 ${isLast ? "" : "pb-6"}`}>
|
||||
<Text className="text-xs text-gray-400">
|
||||
{formatTime(item.create_time)}
|
||||
</Text>
|
||||
<Text className="text-[15px] text-gray-800 leading-5 mt-0.5">
|
||||
{item.content}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
})
|
||||
)}
|
||||
|
||||
<View className="h-4" />
|
||||
</LayoutView.ScrollView>
|
||||
|
||||
{/* 关闭按钮 */}
|
||||
<View className="absolute bottom-6 left-0 right-0 items-center">
|
||||
<CloseButton
|
||||
onPress={() => router.back()}
|
||||
iconProps={{ size: 28 }}
|
||||
style={{ padding: 24 }}
|
||||
/>
|
||||
</View>
|
||||
</LayoutView>
|
||||
);
|
||||
}
|
||||
+1
-1
@@ -130,7 +130,7 @@ export default function Login() {
|
||||
family_id: user.family_id,
|
||||
});
|
||||
|
||||
router.replace("/(tabs)/home");
|
||||
router.replace("/home");
|
||||
}
|
||||
});
|
||||
};
|
||||
+8
-1
@@ -21,6 +21,7 @@ import { generateCode } from "@/utils/common";
|
||||
import { FadeIn } from "react-native-reanimated";
|
||||
import { Keyboard } from "react-native";
|
||||
import { useRouter } from "expo-router";
|
||||
import { recordFeed } from "@/helpers/feed";
|
||||
|
||||
const BottomSheetTextInput = forwardRef<any, React.ComponentProps<typeof Input>>((props, ref) => {
|
||||
const { onFocus, onBlur } = useBottomSheetAwareHandlers();
|
||||
@@ -98,7 +99,7 @@ export default function EntryFamily() {
|
||||
toast.success("你已经有家庭了");
|
||||
|
||||
familyStore.setCurrentFamily({ family_id: res.data[0].family_id });
|
||||
router.replace("/(tabs)/home");
|
||||
router.replace("/home");
|
||||
}
|
||||
// 否则保持在 entry 页面
|
||||
};
|
||||
@@ -271,6 +272,9 @@ export default function EntryFamily() {
|
||||
setShowCreate(false);
|
||||
setCreateForm({ name: "" });
|
||||
|
||||
// 记录动态
|
||||
recordFeed(familyId, userStore.userInfo.id, "join", `{nickname} 创建了家庭「${createForm.name}」`);
|
||||
|
||||
enterFamily(familyId);
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -347,6 +351,9 @@ export default function EntryFamily() {
|
||||
setShowJoin(false);
|
||||
setJoinForm({ invite_code: "" });
|
||||
|
||||
// 记录动态
|
||||
recordFeed(family.id, userStore.userInfo.id, "join", "{nickname} 加入了家庭");
|
||||
|
||||
enterFamily(family.id);
|
||||
}
|
||||
} catch (e) {
|
||||
File renamed without changes.
File renamed without changes.
+29
@@ -35,6 +35,7 @@ import { forwardRef, useRef } from "react";
|
||||
import { FadeIn } from "react-native-reanimated";
|
||||
import DateTime from "@/components/DateTime";
|
||||
import { toDateString } from "@/utils/time";
|
||||
import { recordFeed } from "@/helpers/feed";
|
||||
|
||||
const StyledIonicons = withUniwind(Ionicons);
|
||||
|
||||
@@ -384,6 +385,26 @@ export default function UserSetting() {
|
||||
deletes: [],
|
||||
});
|
||||
|
||||
// 同步更新家庭成员昵称
|
||||
const memberRes = await loadDataApi({
|
||||
view_name: "b_family_member",
|
||||
search_condition: "family_id = ? AND user_id = ?",
|
||||
search_columns: ["id", "nickname"],
|
||||
order_by: "",
|
||||
args: [familyStore.currentFamily.family_id, userStore.userInfo.id],
|
||||
});
|
||||
|
||||
if (memberRes.isSuccess && memberRes.data?.length > 0) {
|
||||
const memberId = memberRes.data[0].id;
|
||||
requestData.push({
|
||||
table_name: "b_family_member",
|
||||
key_field: "id",
|
||||
inserts: [],
|
||||
updates: [{ id: memberId, nickname: renameForm.name }],
|
||||
deletes: [],
|
||||
});
|
||||
}
|
||||
|
||||
const saveRes = await saveDataApi(requestData);
|
||||
|
||||
if (saveRes.isSuccess) {
|
||||
@@ -397,6 +418,14 @@ export default function UserSetting() {
|
||||
name: "",
|
||||
});
|
||||
|
||||
// 记录动态
|
||||
recordFeed(
|
||||
familyStore.currentFamily.family_id,
|
||||
userStore.userInfo.id,
|
||||
"rename",
|
||||
`{nickname} 修改了昵称为「${renameForm.name}」`,
|
||||
);
|
||||
|
||||
toast.success("名称修改成功");
|
||||
}
|
||||
} catch (e) {
|
||||
+71
-74
@@ -1,22 +1,20 @@
|
||||
import LayoutView from "@/components/layout/view";
|
||||
import { SearchField, CloseButton, PressableFeedback } from "heroui-native";
|
||||
import { View, Text, useWindowDimensions } from "react-native";
|
||||
import { toast } from "sonner-native";
|
||||
import { View, Text } from "react-native";
|
||||
import { Stack, useRouter } from "expo-router";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { getGroupedApps, type SubAppItem } from "@/configs/pages";
|
||||
import { useState, useMemo, useRef, useCallback, useEffect } from "react";
|
||||
import EmptyState from "@/components/EmptyStatus";
|
||||
import AZIndex from "@/components/AZIndex";
|
||||
import { useFavoriteStore, useFamilyStore } from "@/store";
|
||||
import { Star } from "lucide-react-native";
|
||||
|
||||
const NUM_COLUMNS = 3;
|
||||
const GAP = 10;
|
||||
import { useFavoriteStore, useFamilyStore, useUserStore } from "@/store";
|
||||
import { Heart } from "lucide-react-native";
|
||||
import { recordFeed } from "@/helpers/feed";
|
||||
|
||||
export default function Tools() {
|
||||
const router = useRouter();
|
||||
const safeAreaInsets = useSafeAreaInsets();
|
||||
const { width: screenWidth } = useWindowDimensions();
|
||||
const scrollRef = useRef<any>(null);
|
||||
const headerPositions = useRef<Record<string, number>>({});
|
||||
|
||||
@@ -24,6 +22,7 @@ export default function Tools() {
|
||||
|
||||
const familyStore = useFamilyStore();
|
||||
const favoriteStore = useFavoriteStore();
|
||||
const userStore = useUserStore();
|
||||
|
||||
// 进入页面时拉取收藏
|
||||
useEffect(() => {
|
||||
@@ -81,12 +80,6 @@ export default function Tools() {
|
||||
return result;
|
||||
}, [keyword, favoriteStore.favorites]);
|
||||
|
||||
// 16 是 LayoutView.ScrollView 内置的 paddingHorizontal
|
||||
const contentPadding = 16;
|
||||
const itemWidth = Math.floor(
|
||||
(screenWidth - contentPadding * 2 - GAP * (NUM_COLUMNS - 1)) / NUM_COLUMNS
|
||||
);
|
||||
|
||||
const handleLetterPress = useCallback(
|
||||
(letter: string) => {
|
||||
const y = headerPositions.current[letter];
|
||||
@@ -104,8 +97,6 @@ export default function Tools() {
|
||||
<LayoutView.ScrollView
|
||||
ref={scrollRef}
|
||||
showsVerticalScrollIndicator={false}
|
||||
topExtraPadding={70}
|
||||
bottomExtraPadding={100}
|
||||
>
|
||||
{sortedApps.length === 0 ? (
|
||||
<EmptyState />
|
||||
@@ -126,77 +117,82 @@ export default function Tools() {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Text className="text-[11px] text-gray-400/70 font-medium mb-2.5 ml-0.5 mt-3 first:mt-0 tracking-wider">
|
||||
{item.title}
|
||||
</Text>
|
||||
<View className="flex-row items-center mb-3 mt-5 first:mt-0">
|
||||
{/* 收藏分组特殊标记 */}
|
||||
{item.title === "收藏" && (
|
||||
<View className="w-5 h-5 rounded-full bg-accent/15 items-center justify-center mr-1.5">
|
||||
<Heart size={10} fill="#0d9488" color="#0d9488" />
|
||||
</View>
|
||||
)}
|
||||
{/* 普通分组标记 */}
|
||||
{item.title !== "收藏" && (
|
||||
<View className="w-1.5 h-4 rounded-full bg-accent mr-1.5" />
|
||||
)}
|
||||
<Text className="text-[11px] text-gray-500 font-semibold tracking-wider uppercase">
|
||||
{item.title}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const rows: (typeof item.data)[] = [];
|
||||
for (let i = 0; i < item.data.length; i += NUM_COLUMNS) {
|
||||
rows.push(item.data.slice(i, i + NUM_COLUMNS));
|
||||
}
|
||||
|
||||
return (
|
||||
<View key={`g-${idx}`} className="mb-4">
|
||||
{rows.map((row, rowIdx) => (
|
||||
<View
|
||||
key={rowIdx}
|
||||
className="flex-row mb-2.5 last:mb-0"
|
||||
style={{ gap: GAP }}
|
||||
>
|
||||
{row.map((subItem) => {
|
||||
const Icon = subItem.icon;
|
||||
const isFav = favoriteStore.isFavorite(subItem.id);
|
||||
{item.data.map((subItem) => {
|
||||
const Icon = subItem.icon;
|
||||
const isFav = favoriteStore.isFavorite(subItem.id);
|
||||
|
||||
return (
|
||||
<View
|
||||
key={subItem.id}
|
||||
style={{ width: itemWidth, height: 80 }}
|
||||
>
|
||||
<PressableFeedback
|
||||
onPress={() => router.navigate(subItem.id as any)}
|
||||
className="absolute inset-0 items-center justify-center bg-white rounded-xl shadow-lg"
|
||||
>
|
||||
<Icon size={26} />
|
||||
<Text className="text-xs text-gray-700 mt-2 font-medium text-center">
|
||||
{subItem.title}
|
||||
return (
|
||||
<View key={subItem.id} className="bg-white rounded-xl shadow-sm mx-3 mb-2 overflow-hidden relative">
|
||||
<PressableFeedback
|
||||
onPress={() => router.navigate(subItem.id as any)}
|
||||
className="flex-row items-center py-3.5 px-4"
|
||||
>
|
||||
<Icon size={28} />
|
||||
<View className="flex-1 ml-3">
|
||||
<Text className="text-[15px] text-gray-700 font-medium leading-5">
|
||||
{subItem.title}
|
||||
</Text>
|
||||
{subItem.description ? (
|
||||
<Text className="text-[12px] text-gray-400 leading-4 mt-0.5" numberOfLines={1}>
|
||||
{subItem.description}
|
||||
</Text>
|
||||
<PressableFeedback.Ripple />
|
||||
</PressableFeedback>
|
||||
) : null}
|
||||
</View>
|
||||
<PressableFeedback.Ripple />
|
||||
</PressableFeedback>
|
||||
|
||||
{/* 星标按钮 — 独立于导航主体,避免事件冲突 */}
|
||||
<PressableFeedback
|
||||
onPress={() =>
|
||||
favoriteStore.toggleFavorite(
|
||||
{/* 收藏 — 行右侧,独立触摸 */}
|
||||
<PressableFeedback
|
||||
onPress={async () => {
|
||||
const result =
|
||||
await favoriteStore.toggleFavorite(
|
||||
familyStore.currentFamily.family_id,
|
||||
subItem.id
|
||||
)
|
||||
);
|
||||
toast(result ? "已收藏" : "已取消收藏");
|
||||
// 记录收藏动态(仅收藏时)
|
||||
if (result) {
|
||||
recordFeed(
|
||||
familyStore.currentFamily.family_id,
|
||||
userStore.userInfo?.id ?? 0,
|
||||
"fav",
|
||||
`{nickname} 收藏了「${subItem.title}」`,
|
||||
);
|
||||
}
|
||||
className="absolute top-1 right-1 p-1 z-10"
|
||||
>
|
||||
<Star
|
||||
size={14}
|
||||
fill={isFav ? "#fbbf24" : "none"}
|
||||
color={isFav ? "#fbbf24" : "#d1d5db"}
|
||||
/>
|
||||
<PressableFeedback.Ripple />
|
||||
</PressableFeedback>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
{row.length < NUM_COLUMNS &&
|
||||
Array.from({ length: NUM_COLUMNS - row.length }).map(
|
||||
(_, emptyIdx) => (
|
||||
<View
|
||||
key={`empty-${emptyIdx}`}
|
||||
style={{ width: itemWidth }}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</View>
|
||||
))}
|
||||
}}
|
||||
className="absolute right-4 top-0 bottom-0 justify-center px-2 z-10"
|
||||
>
|
||||
<Heart
|
||||
size={18}
|
||||
fill={isFav ? "#0d9488" : "none"}
|
||||
color={isFav ? "#0d9488" : "#9ca3af"}
|
||||
/>
|
||||
<PressableFeedback.Ripple />
|
||||
</PressableFeedback>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
);
|
||||
})
|
||||
@@ -227,6 +223,7 @@ export default function Tools() {
|
||||
style={{ padding: 24 }}
|
||||
/>
|
||||
</View>
|
||||
|
||||
</LayoutView>
|
||||
);
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
import React, { useState } from "react";
|
||||
import { View, Button, Image, ScrollView, StyleSheet } from "react-native";
|
||||
import ImagePicker from "react-native-image-crop-picker";
|
||||
|
||||
export default function ImageCropPickerDemo() {
|
||||
const [images, setImages] = useState<any[]>([]);
|
||||
|
||||
// 选择单张 + 裁剪
|
||||
const pickSingle = async () => {
|
||||
try {
|
||||
const image = await ImagePicker.openPicker({
|
||||
freeStyleCropEnabled: true,
|
||||
cropping: true,
|
||||
compressImageQuality: 0.7,
|
||||
});
|
||||
|
||||
setImages([image]);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
};
|
||||
|
||||
// 拍照 + 裁剪
|
||||
const takePhoto = async () => {
|
||||
try {
|
||||
const image = await ImagePicker.openCamera({
|
||||
width: 300,
|
||||
height: 300,
|
||||
cropping: true,
|
||||
compressImageQuality: 0.7,
|
||||
});
|
||||
|
||||
setImages([image]);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
};
|
||||
|
||||
// 多选(不裁剪)
|
||||
const pickMultiple = async () => {
|
||||
try {
|
||||
const result = await ImagePicker.openPicker({
|
||||
multiple: true,
|
||||
compressImageQuality: 0.7,
|
||||
});
|
||||
|
||||
setImages(result);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Button title="选择图片(裁剪)" onPress={pickSingle} />
|
||||
<Button title="拍照(裁剪)" onPress={takePhoto} />
|
||||
<Button title="多选图片" onPress={pickMultiple} />
|
||||
|
||||
<ScrollView style={styles.preview}>
|
||||
{images.map((img, index) => (
|
||||
<Image key={index} source={{ uri: img.path }} style={styles.image} />
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
padding: 20,
|
||||
marginTop: 60,
|
||||
},
|
||||
preview: {
|
||||
marginTop: 20,
|
||||
},
|
||||
image: {
|
||||
width: "100%",
|
||||
height: 200,
|
||||
marginBottom: 10,
|
||||
borderRadius: 10,
|
||||
},
|
||||
});
|
||||
@@ -1,207 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import LayoutView from "@/components/layout/view";
|
||||
import { loadDataApi } from "@/request/api";
|
||||
import { View, Text, useWindowDimensions } from "react-native";
|
||||
import { useFamilyStore, useFavoriteStore } from "@/store";
|
||||
import { LayoutGridIcon, LayersIcon } from "lucide-react-native";
|
||||
import { PressableFeedback } from "heroui-native";
|
||||
import { SUB_APPS, type SubAppItem } from "@/configs/pages";
|
||||
import { useRouter } from "expo-router";
|
||||
|
||||
const NUM_COLUMNS = 3;
|
||||
const GAP = 10;
|
||||
|
||||
export default function Home() {
|
||||
const router = useRouter();
|
||||
const familyStore = useFamilyStore();
|
||||
const favoriteStore = useFavoriteStore();
|
||||
const { width: screenWidth } = useWindowDimensions();
|
||||
|
||||
const [family, setFamily] = useState<any>({});
|
||||
|
||||
const [duration, setDuration] = useState({
|
||||
days: 0,
|
||||
hours: 0,
|
||||
minutes: 0,
|
||||
seconds: 0,
|
||||
});
|
||||
|
||||
// 16 是 LayoutView.ScrollView 内置的 paddingHorizontal
|
||||
const contentPadding = 16;
|
||||
const itemWidth = Math.floor(
|
||||
(screenWidth - contentPadding * 2 - GAP * (NUM_COLUMNS - 1)) / NUM_COLUMNS
|
||||
);
|
||||
|
||||
async function loadFamilyData() {
|
||||
const res = await loadDataApi({
|
||||
view_name: "b_family",
|
||||
search_condition: "id = ?",
|
||||
search_columns: [],
|
||||
order_by: "",
|
||||
args: [familyStore.currentFamily?.family_id],
|
||||
});
|
||||
|
||||
if (res.isSuccess && res.data.length > 0) {
|
||||
setFamily(res.data[0]);
|
||||
}
|
||||
}
|
||||
|
||||
function calcDuration(startTime: string) {
|
||||
const start = new Date(startTime).getTime();
|
||||
const now = Date.now();
|
||||
let diff = Math.floor((now - start) / 1000);
|
||||
|
||||
const days = Math.floor(diff / (3600 * 24));
|
||||
diff %= 3600 * 24;
|
||||
|
||||
const hours = Math.floor(diff / 3600);
|
||||
diff %= 3600;
|
||||
|
||||
const minutes = Math.floor(diff / 60);
|
||||
const seconds = diff % 60;
|
||||
|
||||
return { days, hours, minutes, seconds };
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadFamilyData();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!family.create_time) return;
|
||||
|
||||
const timer = setInterval(() => {
|
||||
setDuration(calcDuration(family.create_time));
|
||||
}, 1000);
|
||||
|
||||
return () => clearInterval(timer);
|
||||
}, [family.create_time]);
|
||||
|
||||
// 监听家庭变化,拉取收藏
|
||||
useEffect(() => {
|
||||
const fid = familyStore.currentFamily.family_id;
|
||||
if (fid) favoriteStore.fetchFavorites(fid);
|
||||
}, [familyStore.currentFamily.family_id]);
|
||||
|
||||
// 首页展示:收藏在前,其余在后 + "更多" 铺满行
|
||||
const displayItems: SubAppItem[] = (() => {
|
||||
const favIds = favoriteStore.favorites;
|
||||
const starred = SUB_APPS.filter((a) => favIds.has(a.id));
|
||||
const unstarred = SUB_APPS.filter((a) => !favIds.has(a.id));
|
||||
return [...starred, ...unstarred];
|
||||
})();
|
||||
|
||||
// 按 3 列分行使"更多"自然填充
|
||||
const cardRows: { items: SubAppItem[]; hasMore: boolean }[] = (() => {
|
||||
const rows: { items: SubAppItem[]; hasMore: boolean }[] = [];
|
||||
const all = [...displayItems];
|
||||
const hasMore = true; // 永远显示"更多"
|
||||
|
||||
for (let i = 0; i < all.length; i += NUM_COLUMNS) {
|
||||
rows.push({ items: all.slice(i, i + NUM_COLUMNS), hasMore: false });
|
||||
}
|
||||
|
||||
// 最后一行若不满 3 个,"更多" 补在末尾
|
||||
const last = rows[rows.length - 1];
|
||||
if (last && last.items.length < NUM_COLUMNS) {
|
||||
last.hasMore = true;
|
||||
last.items.push({ id: "__more__", title: "更多", icon: LayersIcon, group: "" } as any);
|
||||
} else {
|
||||
// 最后一行已满或没有数据,新增一行
|
||||
rows.push({
|
||||
items: [{ id: "__more__", title: "更多", icon: LayersIcon, group: "" } as any],
|
||||
hasMore: true,
|
||||
});
|
||||
}
|
||||
|
||||
return rows;
|
||||
})();
|
||||
|
||||
return (
|
||||
<LayoutView>
|
||||
<LayoutView.ScrollView>
|
||||
<View className="items-center justify-center">
|
||||
<View className="flex-row items-center justify-center gap-2">
|
||||
<Text className="text-4xl font-bold tracking-tight text-gray-900">
|
||||
{family.name || "-"}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View className="items-center mt-3">
|
||||
<View className="flex-row items-center gap-2">
|
||||
<Text className="text-3xl font-bold text-accent">
|
||||
{duration.days}
|
||||
</Text>
|
||||
<Text className="text-gray-400">天</Text>
|
||||
|
||||
<Text className="text-3xl font-bold text-accent">
|
||||
{duration.hours}
|
||||
</Text>
|
||||
<Text className="text-gray-400">时</Text>
|
||||
|
||||
<Text className="text-3xl font-bold text-accent">
|
||||
{duration.minutes}
|
||||
</Text>
|
||||
<Text className="text-gray-400">分</Text>
|
||||
|
||||
<Text className="text-3xl font-bold text-accent">
|
||||
{duration.seconds}
|
||||
</Text>
|
||||
<Text className="text-gray-400">秒</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className="mt-6">
|
||||
<View className="flex-row items-center mb-2 gap-2">
|
||||
<LayoutGridIcon size={18} color="#6B7280" />
|
||||
<Text className="text-sm text-muted">快捷应用</Text>
|
||||
</View>
|
||||
|
||||
{cardRows.map((row, rowIdx) => (
|
||||
<View
|
||||
key={rowIdx}
|
||||
className="flex-row mb-2.5 last:mb-0"
|
||||
style={{ gap: GAP }}
|
||||
>
|
||||
{row.items.map((app) => {
|
||||
const Icon = app.icon;
|
||||
const isMoreBtn = app.id === "__more__";
|
||||
|
||||
return (
|
||||
<PressableFeedback
|
||||
key={app.id}
|
||||
onPress={() =>
|
||||
isMoreBtn
|
||||
? router.navigate("/tools")
|
||||
: router.navigate(app.id as any)
|
||||
}
|
||||
style={{ width: itemWidth }}
|
||||
className="items-center justify-center h-20 bg-white rounded-xl shadow-lg"
|
||||
>
|
||||
<Icon size={26} />
|
||||
<Text className="text-xs text-gray-700 mt-2 font-medium">
|
||||
{app.title}
|
||||
</Text>
|
||||
<PressableFeedback.Ripple />
|
||||
</PressableFeedback>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* 末尾空位占位,保持左对齐 */}
|
||||
{row.items.length < NUM_COLUMNS &&
|
||||
Array.from({ length: NUM_COLUMNS - row.items.length }).map(
|
||||
(_, emptyIdx) => (
|
||||
<View
|
||||
key={`empty-${emptyIdx}`}
|
||||
style={{ width: itemWidth }}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</LayoutView.ScrollView>
|
||||
</LayoutView>
|
||||
);
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import React from "react";
|
||||
import LayoutView from "@/components/layout/view";
|
||||
import { Text } from "react-native";
|
||||
|
||||
export default function Start() {
|
||||
return (
|
||||
<LayoutView>
|
||||
<LayoutView.ScrollView>
|
||||
<Text className="text-base text-black">Hello</Text>
|
||||
</LayoutView.ScrollView>
|
||||
</LayoutView>
|
||||
);
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
import { Stack } from "expo-router";
|
||||
import { Image, Text, View } from "react-native";
|
||||
import { Button, LinkButton, ControlField, Checkbox, Spinner } from "heroui-native";
|
||||
import { IconQq } from "@/components/iconfont";
|
||||
import { useState } from "react";
|
||||
import { FadeIn } from "react-native-reanimated";
|
||||
import { useUserStore, useFamilyStore } from "@/store";
|
||||
import { useRouter } from "expo-router";
|
||||
|
||||
export default function Login() {
|
||||
const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
const familyStore = useFamilyStore();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [isAgreed, setIsAgreed] = useState(true);
|
||||
|
||||
/**
|
||||
* {"code": 1000, "data": {"avatar": "http://thirdqq.qlogo.cn/ek_qqapp/AQHVialeUibE42oV8RvTTVZ4tyRlPYeJrUibWTwtCPfV68E4rSzn9cw00GkXQ3QDTiawwhjoqQb7/100", "family_id": 797225979793477, "nickname": "Zhang", "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7InVzZXJfaWQiOjc5NjU0MTkwMTM4OTg5M30sImlhdCI6MTc3Njc3MTcyOX0.JldxjA-0u2hza3qNx20xgUyVBVVV6hQuVuTW1GcJyr8", "userId": 796541901389893}, "isSuccess": true, "message": "success", "track_id": "hI5gEsPHu1LGwkQaJ6lf1dDQby0rzxGVJj0KcNC3Nxw"}
|
||||
*/
|
||||
|
||||
function handleTest() {
|
||||
if (loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
setTimeout(() => {
|
||||
requestAnimationFrame(() => {
|
||||
userStore.setUserInfo({
|
||||
id: 796541901389893,
|
||||
token:
|
||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7InVzZXJfaWQiOjc5NjU0MTkwMTM4OTg5M30sImlhdCI6MTc3Njc3MTcyOX0.JldxjA-0u2hza3qNx20xgUyVBVVV6hQuVuTW1GcJyr8",
|
||||
nickname: "Zhang",
|
||||
avatar:
|
||||
"http://thirdqq.qlogo.cn/ek_qqapp/AQHVialeUibE42oV8RvTTVZ4tyRlPYeJrUibWTwtCPfV68E4rSzn9cw00GkXQ3QDTiawwhjoqQb7/100",
|
||||
});
|
||||
userStore.setAuthExpired(false);
|
||||
console.log(familyStore.inFamily());
|
||||
|
||||
// if (familyStore.inFamily()) {
|
||||
// router.replace("/(tabs)/home");
|
||||
// } else {
|
||||
// router.replace("/family/entry");
|
||||
// return;
|
||||
// }
|
||||
|
||||
familyStore.setCurrentFamily({
|
||||
family_id: 797225979793477,
|
||||
});
|
||||
|
||||
router.replace("/(tabs)/home");
|
||||
});
|
||||
setLoading(false);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack.Screen
|
||||
options={{
|
||||
animation: "fade",
|
||||
}}
|
||||
/>
|
||||
<View className="flex-1 bg-white px-6 justify-center">
|
||||
<View className="items-center mb-12">
|
||||
<Image
|
||||
source={require("@/static/images/logo.png")}
|
||||
className="w-20 h-20 mb-4"
|
||||
resizeMode="contain"
|
||||
/>
|
||||
<Text className="text-xl font-semibold text-gray-900">Welcome to Allapp</Text>
|
||||
</View>
|
||||
|
||||
<View className="flex flex-col gap-4">
|
||||
{/* <Button onPress={handleLoginWechat}>
|
||||
{loading ? (
|
||||
<Spinner entering={FadeIn.delay(50)} color="white" />
|
||||
) : (
|
||||
<IconWeixin color="white" />
|
||||
)}
|
||||
<Button.Label>WeChat</Button.Label>
|
||||
</Button>
|
||||
|
||||
<Button variant="outline" onPress={handleLoginQQ}>
|
||||
{loading ? <Spinner entering={FadeIn.delay(50)} /> : <IconQq />}
|
||||
<Button.Label>QQ</Button.Label>
|
||||
</Button> */}
|
||||
|
||||
<Button variant="outline" onPress={handleTest}>
|
||||
{loading ? <Spinner entering={FadeIn.delay(50)} /> : <IconQq />}
|
||||
<Button.Label>Test</Button.Label>
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
<ControlField
|
||||
isSelected={isAgreed}
|
||||
onSelectedChange={setIsAgreed}
|
||||
className="items-start mt-10"
|
||||
>
|
||||
<ControlField.Indicator>
|
||||
<Checkbox className="w-5 h-5 rounded-sm" />
|
||||
</ControlField.Indicator>
|
||||
<View className="flex-row flex-wrap flex-1 gap-1">
|
||||
<Text className="text-sm text-muted">登录即表示你同意我们的</Text>
|
||||
<LinkButton size="sm">
|
||||
<LinkButton.Label className="text-accent">用户协议</LinkButton.Label>
|
||||
</LinkButton>
|
||||
<Text className="text-sm text-muted">和</Text>
|
||||
<LinkButton size="sm">
|
||||
<LinkButton.Label className="text-accent">隐私政策</LinkButton.Label>
|
||||
</LinkButton>
|
||||
</View>
|
||||
</ControlField>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -63,7 +63,7 @@ export default function Index() {
|
||||
family_id: res.data[0].family_id,
|
||||
});
|
||||
|
||||
router.replace("/(tabs)/home");
|
||||
router.replace("/home");
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
toast.error("初始化失败,请稍后重试");
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
import { DatePicker as YzDatePicker } from "@yz1311/react-native-wheel-picker";
|
||||
import DateTimePicker from "react-native-dates-picker";
|
||||
import dayjs from "dayjs";
|
||||
import { BottomSheet, Button, Spinner } from "heroui-native";
|
||||
import { useState, useEffect } from "react";
|
||||
import { toDate } from "@/utils/time";
|
||||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
import { View } from "react-native";
|
||||
import { CheckIcon } from "lucide-react-native";
|
||||
import { FadeIn } from "react-native-reanimated";
|
||||
|
||||
export type DatePickerMode = "year" | "month" | "date" | "time" | "datetime";
|
||||
|
||||
export type DateFormatType = "YYYY" | "YYYY-MM" | "YYYY-MM-DD" | "HH:mm:ss" | "YYYY-MM-DD HH:mm";
|
||||
export type DateFormatType =
|
||||
| "YYYY"
|
||||
| "YYYY-MM"
|
||||
| "YYYY-MM-DD"
|
||||
| "HH:mm:ss"
|
||||
| "YYYY-MM-DD HH:mm";
|
||||
|
||||
interface DatePickerProps {
|
||||
value: Date | string;
|
||||
@@ -28,6 +32,14 @@ const defaultFormatMap: Record<DatePickerMode, DateFormatType> = {
|
||||
datetime: "YYYY-MM-DD HH:mm",
|
||||
};
|
||||
|
||||
const modeViewMap: Record<DatePickerMode, "day" | "month" | "year"> = {
|
||||
year: "year",
|
||||
month: "month",
|
||||
date: "day",
|
||||
time: "day",
|
||||
datetime: "day",
|
||||
};
|
||||
|
||||
export default function DateTime({
|
||||
value,
|
||||
show = false,
|
||||
@@ -38,51 +50,60 @@ export default function DateTime({
|
||||
}: DatePickerProps) {
|
||||
const finalFormat = format ?? defaultFormatMap[mode];
|
||||
|
||||
const [tempDate, setTempDate] = useState<Date>(toDate(value) ?? new Date());
|
||||
const [open, setOpen] = useState(false);
|
||||
const [tempDate, setTempDate] = useState<dayjs.Dayjs | undefined>(dayjs());
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const onCloseRef = useRef(onClose);
|
||||
onCloseRef.current = onClose;
|
||||
|
||||
// 打开
|
||||
useEffect(() => {
|
||||
if (show) {
|
||||
setTempDate(toDate(value) ?? new Date());
|
||||
const d = dayjs(value);
|
||||
setTempDate(d.isValid() ? d : dayjs());
|
||||
setOpen(true);
|
||||
}
|
||||
}, [show, value]);
|
||||
|
||||
const handleClose = () => {
|
||||
if (loading) return;
|
||||
onClose?.();
|
||||
};
|
||||
// 关闭:BottomSheet 的 onOpenChange(false) 或 confirm 后 setOpen(false)
|
||||
// 立刻通知父组件,不延迟 — BottomSheet 自身驱动关闭动画
|
||||
useEffect(() => {
|
||||
if (!open && show) {
|
||||
onCloseRef.current?.();
|
||||
}
|
||||
}, [open, show]);
|
||||
|
||||
const handleConfirm = async () => {
|
||||
if (!tempDate || loading) return;
|
||||
const handleConfirm = useCallback(async () => {
|
||||
if (loading || !tempDate) return;
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
const result = dayjs(tempDate).format(finalFormat);
|
||||
|
||||
const result = tempDate.format(finalFormat);
|
||||
await onConfirm?.(result);
|
||||
|
||||
handleClose();
|
||||
setOpen(false);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
}, [loading, tempDate, finalFormat, onConfirm]);
|
||||
|
||||
return (
|
||||
<BottomSheet isOpen={show} onOpenChange={(v) => !v && handleClose()}>
|
||||
<BottomSheet isOpen={open} onOpenChange={setOpen}>
|
||||
<BottomSheet.Portal>
|
||||
<BottomSheet.Overlay />
|
||||
|
||||
<BottomSheet.Content>
|
||||
<View style={{ width: "100%", alignItems: "center" }}>
|
||||
<YzDatePicker
|
||||
showHeader={false}
|
||||
<DateTimePicker
|
||||
mode="single"
|
||||
date={tempDate}
|
||||
mode={mode}
|
||||
isModal={false}
|
||||
onDateChange={(d: Date) => setTempDate(d)}
|
||||
initialView={modeViewMap[mode]}
|
||||
timePicker={mode === "time" || mode === "datetime"}
|
||||
selectedItemColor="#0d9488"
|
||||
onChange={({ date }) => {
|
||||
if (date) setTempDate(dayjs(date));
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button className="w-full" onPress={handleConfirm} isDisabled={loading}>
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { DatePicker as YzDatePicker } from "@yz1311/react-native-wheel-picker";
|
||||
import { BottomSheet, Button } from "heroui-native";
|
||||
import { View, Text, TouchableOpacity } from "react-native";
|
||||
import DateTimePicker, { DateType } from "react-native-dates-picker";
|
||||
import dayjs from "dayjs";
|
||||
import { toDate } from "@/utils/time";
|
||||
import { BottomSheet, Button, Spinner } from "heroui-native";
|
||||
import { View } from "react-native";
|
||||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
import { CheckIcon } from "lucide-react-native";
|
||||
import { FadeIn } from "react-native-reanimated";
|
||||
|
||||
interface DateRangePickerProps {
|
||||
startDate?: Date | string;
|
||||
endDate?: Date | string;
|
||||
show?: boolean;
|
||||
onClose?: () => void;
|
||||
onConfirm?: (start: string, end: string) => void;
|
||||
onConfirm?: (start: string, end: string) => void | Promise<void>;
|
||||
}
|
||||
|
||||
export default function DateTimeRange({
|
||||
@@ -21,121 +21,78 @@ export default function DateTimeRange({
|
||||
onClose,
|
||||
onConfirm,
|
||||
}: DateRangePickerProps) {
|
||||
const [tempStart, setTempStart] = useState<Date | undefined>(undefined);
|
||||
const [tempEnd, setTempEnd] = useState<Date | undefined>(undefined);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [range, setRange] = useState<{
|
||||
startDate: DateType;
|
||||
endDate: DateType;
|
||||
}>({ startDate: undefined, endDate: undefined });
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const [active, setActive] = useState<"start" | "end">("start");
|
||||
const onCloseRef = useRef(onClose);
|
||||
onCloseRef.current = onClose;
|
||||
|
||||
// ✅ 每次打开重置
|
||||
useEffect(() => {
|
||||
if (show) {
|
||||
setTempStart(toDate(startDate) ?? new Date());
|
||||
setTempEnd(toDate(endDate) ?? undefined);
|
||||
setActive("start");
|
||||
setRange({
|
||||
startDate: startDate ? dayjs(startDate) : undefined,
|
||||
endDate: endDate ? dayjs(endDate) : undefined,
|
||||
});
|
||||
setOpen(true);
|
||||
}
|
||||
}, [show, startDate, endDate]);
|
||||
|
||||
const handleClose = () => {
|
||||
onClose?.();
|
||||
};
|
||||
useEffect(() => {
|
||||
if (!open && show) {
|
||||
onCloseRef.current?.();
|
||||
}
|
||||
}, [open, show]);
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (!tempStart) return;
|
||||
const handleConfirm = useCallback(async () => {
|
||||
if (loading) return;
|
||||
if (!range.startDate) return;
|
||||
|
||||
onConfirm?.(
|
||||
dayjs(tempStart).format("YYYY-MM-DD"),
|
||||
tempEnd ? dayjs(tempEnd).format("YYYY-MM-DD") : "",
|
||||
);
|
||||
|
||||
handleClose();
|
||||
};
|
||||
|
||||
// ❗ start 修改 → 清空 end
|
||||
const handleStartChange = (date: Date) => {
|
||||
setTempStart(date);
|
||||
setTempEnd(undefined);
|
||||
};
|
||||
|
||||
// ❗ end 修改
|
||||
const handleEndChange = (date: Date) => {
|
||||
if (!tempStart) return;
|
||||
setTempEnd(date);
|
||||
};
|
||||
|
||||
const formatDate = (date?: Date) => (date ? dayjs(date).format("YYYY-MM-DD") : "请选择");
|
||||
try {
|
||||
setLoading(true);
|
||||
await onConfirm?.(
|
||||
dayjs(range.startDate).format("YYYY-MM-DD"),
|
||||
range.endDate ? dayjs(range.endDate).format("YYYY-MM-DD") : "",
|
||||
);
|
||||
setOpen(false);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [loading, range, onConfirm]);
|
||||
|
||||
return (
|
||||
<BottomSheet isOpen={show} onOpenChange={(v) => !v && handleClose()}>
|
||||
<BottomSheet isOpen={open} onOpenChange={setOpen}>
|
||||
<BottomSheet.Portal>
|
||||
<BottomSheet.Overlay />
|
||||
|
||||
<BottomSheet.Content>
|
||||
<View className="w-full items-center">
|
||||
{/* 顶部选择区 */}
|
||||
<View className="flex-row items-center justify-between w-full">
|
||||
{/* start */}
|
||||
<TouchableOpacity className="flex-1 items-center" onPress={() => setActive("start")}>
|
||||
<Text>开始时间</Text>
|
||||
<Text
|
||||
className={`text-base ${
|
||||
active === "start" ? "text-accent font-bold" : "text-gray-400"
|
||||
}`}
|
||||
>
|
||||
{formatDate(tempStart)}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<View style={{ width: "100%", alignItems: "center" }}>
|
||||
<DateTimePicker
|
||||
mode="range"
|
||||
startDate={range.startDate}
|
||||
endDate={range.endDate}
|
||||
selectedItemColor="#0d9488"
|
||||
selectedRangeBackgroundColor="rgba(13,148,136,0.15)"
|
||||
onChange={(params) => setRange(params)}
|
||||
/>
|
||||
|
||||
<View className="w-px h-10 bg-gray-300 mx-2" />
|
||||
|
||||
{/* end */}
|
||||
<TouchableOpacity
|
||||
className="flex-1 items-center"
|
||||
style={{ opacity: tempStart ? 1 : 0.4 }}
|
||||
disabled={!tempStart}
|
||||
onPress={() => {
|
||||
if (!tempStart) return;
|
||||
|
||||
// ✅ 核心优化:未选 end → 自动补 start
|
||||
if (!tempEnd) {
|
||||
setTempEnd(tempStart);
|
||||
}
|
||||
|
||||
setActive("end");
|
||||
}}
|
||||
>
|
||||
<Text>结束时间</Text>
|
||||
<Text
|
||||
className={`text-base ${
|
||||
active === "end" ? "text-accent font-bold" : "text-gray-400"
|
||||
}`}
|
||||
>
|
||||
{formatDate(tempEnd)}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Picker */}
|
||||
{active === "start" ? (
|
||||
<YzDatePicker
|
||||
showHeader={false}
|
||||
mode="date"
|
||||
date={tempStart ?? new Date()}
|
||||
onDateChange={handleStartChange}
|
||||
/>
|
||||
) : (
|
||||
<YzDatePicker
|
||||
showHeader={false}
|
||||
mode="date"
|
||||
date={tempEnd ?? tempStart ?? new Date()}
|
||||
minDate={tempStart}
|
||||
onDateChange={handleEndChange}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* confirm */}
|
||||
<Button className="w-full" onPress={handleConfirm}>
|
||||
<CheckIcon color="#fff" />
|
||||
<Button.Label>确认</Button.Label>
|
||||
<Button
|
||||
className="w-full"
|
||||
onPress={handleConfirm}
|
||||
isDisabled={loading || !range.startDate}
|
||||
>
|
||||
{loading ? (
|
||||
<Spinner entering={FadeIn.delay(50)} color="white" />
|
||||
) : (
|
||||
<>
|
||||
<CheckIcon color="#fff" />
|
||||
<Button.Label>确认</Button.Label>
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</View>
|
||||
</BottomSheet.Content>
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import { loadDataApi, saveDataApi, getUniqueIdApi } from "@/request/api";
|
||||
|
||||
export type FeedType = "join" | "rename" | "bill" | "fav";
|
||||
|
||||
export interface FeedRecord {
|
||||
id: number;
|
||||
family_id: number;
|
||||
user_id: number;
|
||||
type: FeedType;
|
||||
content: string;
|
||||
meta: string | null;
|
||||
create_time: string;
|
||||
}
|
||||
|
||||
/** 获取用户在家庭中的昵称(来自 b_family_member,非 b_user) */
|
||||
export async function getFamilyMemberNickname(
|
||||
familyId: number,
|
||||
userId: number,
|
||||
): Promise<string> {
|
||||
const res = await loadDataApi({
|
||||
view_name: "b_family_member",
|
||||
search_condition: "family_id = ? AND user_id = ?",
|
||||
search_columns: ["nickname"],
|
||||
order_by: "",
|
||||
args: [familyId, userId],
|
||||
});
|
||||
if (res.isSuccess && res.data?.length > 0) {
|
||||
return res.data[0].nickname || "成员";
|
||||
}
|
||||
return "成员";
|
||||
}
|
||||
|
||||
/** 写入一条动态 — content 直接包含所有展示信息,用 {nickname} 占位 */
|
||||
export async function recordFeed(
|
||||
familyId: number,
|
||||
userId: number,
|
||||
type: FeedType,
|
||||
content: string,
|
||||
) {
|
||||
const idRes = await getUniqueIdApi(1);
|
||||
const feedId = idRes.data?.[0];
|
||||
if (!feedId) return;
|
||||
|
||||
await saveDataApi([
|
||||
{
|
||||
table_name: "b_family_feed",
|
||||
key_field: "id",
|
||||
inserts: [
|
||||
{
|
||||
id: feedId,
|
||||
family_id: familyId,
|
||||
user_id: userId,
|
||||
type,
|
||||
content,
|
||||
},
|
||||
],
|
||||
updates: [],
|
||||
deletes: [],
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
/** 加载家庭动态列表 — 自动替换 {nickname} 占位符 */
|
||||
export async function loadFeeds(familyId: number): Promise<FeedRecord[]> {
|
||||
const res = await loadDataApi({
|
||||
view_name: "b_family_feed",
|
||||
search_condition: "family_id = ?",
|
||||
order_by: "create_time DESC",
|
||||
search_columns: ["id", "family_id", "user_id", "type", "content", "meta", "create_time"],
|
||||
args: [familyId],
|
||||
});
|
||||
const feeds = (res.data || []) as FeedRecord[];
|
||||
|
||||
// 批量加载昵称:收集唯一 user_id,逐个查 b_family_member
|
||||
const uniqueUserIds = [...new Set(feeds.map((f) => f.user_id))];
|
||||
const nickMap = new Map<number, string>();
|
||||
for (const uid of uniqueUserIds) {
|
||||
nickMap.set(uid, await getFamilyMemberNickname(familyId, uid));
|
||||
}
|
||||
|
||||
// 替换 {nickname} 占位符,保证每次展示都是最新昵称
|
||||
return feeds.map((f) => ({
|
||||
...f,
|
||||
content: f.content.replace(/\{nickname\}/g, nickMap.get(f.user_id) || "成员"),
|
||||
}));
|
||||
}
|
||||
@@ -21,7 +21,7 @@ export default function PageLayout({ title, titleIcon, children, renderTitle }:
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
router.navigate("/(tabs)/home");
|
||||
router.navigate("/home");
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
Reference in new issue
Block a user