This commit is contained in:
oneao committed 2026-04-17 17:25:15 +08:00
1 parent e893ef2e48
commit b6415c0d65
25 files changed
+1495 -112

No files matched your search

+2
View File
@@ -46,10 +46,12 @@
"react-dom": "19.2.0",
"react-native": "0.83.4",
"react-native-gesture-handler": "~2.30.0",
"react-native-keyboard-controller": "1.20.7",
"react-native-reanimated": "4.2.1",
"react-native-safe-area-context": "~5.6.2",
"react-native-screens": "~4.23.0",
"react-native-shadow-2": "^7.1.2",
"react-native-sortables": "^1.9.4",
"react-native-svg": "15.15.3",
"react-native-web": "~0.21.0",
"react-native-worklets": "0.7.2",
+86 -1
View File
@@ -107,6 +107,9 @@ importers:
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)
react-native-keyboard-controller:
specifier: 1.20.7
version: 1.20.7(react-native-reanimated@4.2.1(react-native-worklets@0.7.2(@babel/core@7.29.0)(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@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(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-reanimated:
specifier: 4.2.1
version: 4.2.1(react-native-worklets@0.7.2(@babel/core@7.29.0)(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@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
@@ -119,6 +122,9 @@ importers:
react-native-shadow-2:
specifier: ^7.1.2
version: 7.1.2(react-native-svg@15.15.3(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@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))
react-native-sortables:
specifier: ^1.9.4
version: 1.9.4(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))(react-native-reanimated@4.2.1(react-native-worklets@0.7.2(@babel/core@7.29.0)(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@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(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-svg:
specifier: 15.15.3
version: 15.15.3(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
@@ -1004,48 +1010,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==}
@@ -1118,48 +1132,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==}
@@ -1578,24 +1600,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==}
@@ -2513,7 +2539,7 @@ packages:
glob@7.2.3:
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
deprecated: Glob versions prior to v9 are no longer supported
gopd@1.2.0:
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
@@ -2846,72 +2872,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==}
@@ -3454,6 +3492,15 @@ packages:
react: '*'
react-native: '*'
react-native-haptic-feedback@3.0.0:
resolution: {integrity: sha512-yxBQFHhXU8S0xupGtzDRToGw5yMWezUdLzxtlx2Oi9YpjPwprWZWCw8foPt0S2t84B0ZikzZYyOHiMSXIelUxg==}
peerDependencies:
react: '>=18.0.0'
react-native: '>=0.71.0'
peerDependenciesMeta:
react:
optional: true
react-native-iconfont-cli@2.2.4:
resolution: {integrity: sha512-qTjfHG2f/sc3/YKWZJvIJk/SwiTu8C9/SvtoLxRX+GM/6pvOJq0ge6rcV3vTh4s3QJDuNDOD6+a4hGD6vv2vSg==}
hasBin: true
@@ -3474,6 +3521,13 @@ packages:
react: '*'
react-native: '*'
react-native-keyboard-controller@1.20.7:
resolution: {integrity: sha512-G8S5jz1FufPrcL1vPtReATx+jJhT/j+sTqxMIb30b1z7cYEfMlkIzOCyaHgf6IMB2KA9uBmnA5M6ve2A9Ou4kw==}
peerDependencies:
react: '*'
react-native: '*'
react-native-reanimated: '>=3.0.0'
react-native-reanimated@4.2.1:
resolution: {integrity: sha512-/NcHnZMyOvsD/wYXug/YqSKw90P9edN0kEPL5lP4PFf1aQ4F1V7MKe/E0tvfkXKIajy3Qocp5EiEnlcrK/+BZg==}
peerDependencies:
@@ -3499,6 +3553,14 @@ packages:
react-native: '*'
react-native-svg: '>=12.1.0'
react-native-sortables@1.9.4:
resolution: {integrity: sha512-a6hxT+gl14HA5Sm8UiLXJqF8KMEQVa+mUJd75OnzoVsmrxUDtjAatlMdV0kI9qTQDT/ZSFLPRmdUhOR762IA4g==}
peerDependencies:
react: '*'
react-native: '*'
react-native-gesture-handler: '>=2.0.0'
react-native-reanimated: '>=3.0.0'
react-native-svg@15.15.3:
resolution: {integrity: sha512-/k4KYwPBLGcx2f5d4FjE+vCScK7QOX14cl2lIASJ28u4slHHtIhL0SZKU7u9qmRBHxTCKPoPBtN6haT1NENJNA==}
peerDependencies:
@@ -7868,6 +7930,13 @@ snapshots:
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-haptic-feedback@3.0.0(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0):
dependencies:
react-native: 0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0)
optionalDependencies:
react: 19.2.0
optional: true
react-native-iconfont-cli@2.2.4(react-native-svg@15.15.3(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@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0):
dependencies:
colors: 1.4.0
@@ -7893,6 +7962,13 @@ snapshots:
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-keyboard-controller@1.20.7(react-native-reanimated@4.2.1(react-native-worklets@0.7.2(@babel/core@7.29.0)(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@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0):
dependencies:
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-is-edge-to-edge: 1.3.1(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-reanimated: 4.2.1(react-native-worklets@0.7.2(@babel/core@7.29.0)(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@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
react-native-reanimated@4.2.1(react-native-worklets@0.7.2(@babel/core@7.29.0)(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@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0):
dependencies:
react: 19.2.0
@@ -7919,6 +7995,15 @@ snapshots:
react-native: 0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0)
react-native-svg: 15.15.3(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-sortables@1.9.4(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))(react-native-reanimated@4.2.1(react-native-worklets@0.7.2(@babel/core@7.29.0)(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@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0):
dependencies:
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)
react-native-reanimated: 4.2.1(react-native-worklets@0.7.2(@babel/core@7.29.0)(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@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
optionalDependencies:
react-native-haptic-feedback: 3.0.0(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-svg@15.15.3(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0):
dependencies:
css-select: 5.2.2
+6 -6
View File
@@ -20,9 +20,9 @@ const tabs: TabbarItem[] = [
icon: HomeIcon,
},
{
id: "/chat",
title: "聊天",
icon: MessageCircleIcon,
id: "/space",
title: "空间",
icon: EarthIcon,
},
{
id: "/tools",
@@ -31,9 +31,9 @@ const tabs: TabbarItem[] = [
isCenter: true,
},
{
id: "/space",
title: "空间",
icon: EarthIcon,
id: "/chat",
title: "聊天",
icon: MessageCircleIcon,
},
{
id: "/my",
+36 -29
View File
@@ -1,36 +1,43 @@
import { Button, Dialog } from "heroui-native";
import { Button, Toast, useToast, useThemeColor } from "heroui-native";
import { View } from "react-native";
import { useState } from "react";
export default function DialogExample() {
const [isOpen, setIsOpen] = useState(false);
export default function ToastExample() {
const { toast } = useToast();
const themeColorForeground = useThemeColor("foreground");
return (
<>
<Button variant="primary" onPress={() => {
setIsOpen(true)
}}>Open Dialog</Button>
<View className="gap-4 p-4">
<Button
onPress={() =>
toast.show({
variant: "success",
label: "You have upgraded your plan",
description: "You can continue using HeroUI Chat",
actionLabel: "Close",
onActionPress: ({ hide }) => hide(),
})
}
>
Show Success Toast
</Button>
<Dialog isOpen={isOpen} onOpenChange={setIsOpen}>
<Dialog.Portal>
<Dialog.Overlay />
<Dialog.Content>
<Dialog.Close variant="ghost" />
<View className="mb-5 gap-1.5">
<Dialog.Title>Confirm Action</Dialog.Title>
<Dialog.Description>
Are you sure you want to proceed with this action? This cannot be undone.
</Dialog.Description>
</View>
<View className="flex-row justify-end gap-3">
<Button variant="ghost" size="sm" onPress={() => setIsOpen(false)}>
Cancel
</Button>
<Button size="sm">Confirm</Button>
</View>
</Dialog.Content>
</Dialog.Portal>
</Dialog>
</>
<Button
onPress={() =>
toast.show({
duration: 0,
component: (props) => (
<Toast variant="accent" {...props}>
<Toast.Title>Custom Toast</Toast.Title>
<Toast.Description>This uses a custom component</Toast.Description>
<Toast.Action onPress={() => props.hide()}>Undo</Toast.Action>
<Toast.Close className="absolute top-0 right-0" />
</Toast>
),
})
}
>
Show Custom Toast
</Button>
</View>
);
}
+556 -3
View File
@@ -1,13 +1,566 @@
import React from "react";
import React, { useEffect, useState, useRef } from "react";
import LayoutView from "@/components/layout/view";
import { Text } from "react-native";
import { View, Text } from "react-native";
import {
Button,
Separator,
Spinner,
Chip,
BottomSheet,
Label,
TextField,
Input,
InputOTP,
type InputOTPRef,
PressableFeedback,
useBottomSheetAwareHandlers,
} from "heroui-native";
import { PlusIcon, HousePlusIcon, GripIcon } from "lucide-react-native";
import { useUserStore } from "@/store";
import { getUniqueIdApi, loadDataApi, loadDataBySqlApi, saveDataApi } from "@/request/api";
import EmptyState from "@/components/EmptyStatus";
import Sortable from "react-native-sortables";
import { useSpaceStore } from "@/store";
import { useRouter } from "expo-router";
import { UserRoundIcon, UsersRoundIcon } from "lucide-react-native";
import { toast } from "sonner-native";
import { FadeIn } from "react-native-reanimated";
import { generateCode } from "@/utils/common";
export default function Space() {
const router = useRouter();
const userStore = useUserStore();
const spaceStore = useSpaceStore();
const ITEM_HEIGHT = 64;
const [spaces, setSpaces] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
// 加入
const [showJoin, setShowJoin] = useState(false);
const [joinLoading, setJoinLoading] = useState(false);
const [joinForm, setJoinForm] = useState({
invite_code: "",
});
const joinInputRef = useRef<InputOTPRef>(null);
// 新建
const [showCreate, setShowCreate] = useState(false);
const [createForm, setCreateForm] = useState({
name: "",
});
const [createLoading, setCreateLoading] = useState(false);
const createInputRef = useRef<any>(null);
const { onFocus, onBlur } = useBottomSheetAwareHandlers();
useEffect(() => {
if (showCreate) {
setTimeout(() => {
createInputRef.current?.focus();
}, 0);
}
}, [showCreate]);
useEffect(() => {
if (showJoin) {
setTimeout(() => {
joinInputRef.current?.focus();
}, 0);
}
}, [showJoin]);
useEffect(() => {
listUserSpaces();
}, []);
function listUserSpaces(loading = true) {
if (loading) {
setLoading(true);
}
loadDataApi({
view_name: "v_user_spaces",
search_condition: `v_user_id = ?`,
search_columns: [],
order_by: "",
args: [userStore.userInfo.id],
}).then((res) => {
if (res.isSuccess) {
setSpaces(res.data);
if (loading) {
setLoading(false);
}
}
});
}
async function handleCreate() {
if (createLoading) return;
if (createForm.name === "") {
toast.warning("空间名称不能为空");
return;
}
setCreateLoading(true);
try {
let inviteCode = "";
let isExist = true;
// 循环生成直到不存在
while (isExist) {
inviteCode = generateCode(6);
const res = await loadDataBySqlApi({
sql: `SELECT 1 FROM space WHERE invite_code = ? LIMIT 1`,
args: [inviteCode],
});
if (!res.isSuccess) {
throw new Error("校验邀请码失败");
}
isExist = res.data.length > 0;
}
// 设置是否是默认空间
let isDefault = false;
const defaultRes = await loadDataApi({
view_name: "v_user_spaces",
search_condition: `v_user_id = ?`,
search_columns: ["v_is_default"],
order_by: "",
args: [userStore.userInfo.id],
});
if (!defaultRes.isSuccess) {
return;
}
if (defaultRes.data.length == 0) {
isDefault = true;
}
// 创建
const autoRes = await getUniqueIdApi(2);
if (!autoRes.isSuccess) {
return;
}
const spaceId = autoRes.data[0];
const spaceMemberId = autoRes.data[1];
// 插入空间
const requestData = [];
requestData.push({
table_name: "space",
key_field: "id",
inserts: [
{
id: spaceId,
name: createForm.name,
invite_code: inviteCode,
owner_id: userStore.userInfo.id,
status: 0,
},
],
updates: [],
deletes: [],
});
// 插入空间用户
requestData.push({
table_name: "space_member",
key_field: "id",
inserts: [
{
id: spaceMemberId,
space_id: spaceId,
user_id: userStore.userInfo.id,
role: 0,
is_default: isDefault ? 1 : 0,
sort_number: defaultRes.data.length,
member_name: userStore.userInfo.nickname,
},
],
updates: [],
deletes: [],
});
const saveRes = await saveDataApi(requestData);
if (saveRes.isSuccess) {
toast.success("创建成功");
setShowCreate(false);
setCreateForm({
name: "",
});
listUserSpaces();
}
} catch (e) {
console.log(e);
} finally {
setCreateLoading(false);
}
}
async function handleJoin() {
if (joinLoading) return;
const code = joinForm.invite_code.trim();
if (!code) {
toast.warning("邀请码不能为空");
return;
}
if (!/^[A-Za-z0-9]{6}$/.test(code)) {
toast.warning("邀请码必须为6位字母或数字");
return;
}
setJoinLoading(true);
try {
const res = await loadDataBySqlApi({
sql: ` SELECT
s.id,
s.name,
EXISTS (
SELECT 1
FROM space_member sm
WHERE sm.space_id = s.id
AND sm.user_id = ?
) AS is_joined
FROM space s
WHERE s.invite_code = ?
LIMIT 1;
`,
args: [userStore.userInfo.id, code],
});
if (!res.isSuccess) return;
const row = res.data?.[0];
if (!row) {
toast.warning("邀请码无效");
return;
}
if (row.is_joined) {
toast.warning("你已经在该空间了");
return;
}
const autoRes = await getUniqueIdApi(1);
if (!autoRes.isSuccess) {
return;
}
// 设置是否是默认空间
let isDefault = false;
const defaultRes = await loadDataApi({
view_name: "v_user_spaces",
search_condition: `v_user_id = ?`,
search_columns: ["v_is_default"],
order_by: "",
args: [userStore.userInfo.id],
});
if (!defaultRes.isSuccess) {
return;
}
if (defaultRes.data.length == 0) {
isDefault = true;
}
// 加入
const requestData = [];
requestData.push({
table_name: "space_member",
key_field: "id",
inserts: [
{
id: autoRes.data[0],
space_id: row.id,
user_id: userStore.userInfo.id,
role: 1,
is_default: isDefault ? 1 : 0,
sort_number: defaultRes.data.length,
member_name: userStore.userInfo.nickname,
},
],
updates: [],
deletes: [],
});
const saveRes = await saveDataApi(requestData);
if (saveRes.isSuccess) {
toast.success("加入成功");
setShowJoin(false);
setJoinForm({
invite_code: "",
});
listUserSpaces();
}
} catch (e) {
console.error(e);
toast.error("加入失败,请稍后重试");
} finally {
setJoinLoading(false);
}
}
const renderItem = ({ item, index }: { item: any; index: number }) => (
<PressableFeedback
key={item.space_id}
onPress={() => {
router.navigate({
pathname: "/tab-space" as any,
params: {
spaceId: item.space_id,
},
});
}}
style={{
height: ITEM_HEIGHT,
}}
className={`rounded-xl flex-row items-center px-4 py-3 bg-white ${
index === spaces.length - 1 ? "mb-4" : ""
}`}
>
<View className="w-full flex-row items-center justify-between">
<View>
<View className="flex-row items-center gap-2">
<Text className="font-semibold text-gray-900">{item.name}</Text>
{item.owner_id === userStore.userInfo.id && (
<Chip size="sm" variant="soft" color="success">
创建者
</Chip>
)}
{item.v_is_default === 1 && (
<Chip size="sm" variant="secondary" color="default">
默认
</Chip>
)}
{item.space_id === spaceStore?.currentSpace?.space_id && (
<Chip size="sm" variant="primary" color="accent">
当前空间
</Chip>
)}
</View>
<View className="flex-row items-center gap-3 mt-3">
<View className="flex-row gap-1 items-center">
<UserRoundIcon size={12} />
<Text className="text-xs">{item.v_member_name}</Text>
</View>
<View className="flex-row gap-1 items-center">
<UsersRoundIcon size={12} />
<Text className={`text-xs `}>{item.v_member_count}</Text>
</View>
</View>
</View>
<Sortable.Handle>
<GripIcon size={20} />
</Sortable.Handle>
</View>
<PressableFeedback.Ripple />
</PressableFeedback>
);
return (
<LayoutView>
<LayoutView.ScrollView>
<Text className="text-base text-black">我的空间</Text>
<View className="flex-row gap-4">
<Button
className="flex-1"
onPress={() => {
setShowCreate(true);
createInputRef.current?.focus();
}}
>
<PlusIcon color="#fff" size={20} />
<Button.Label>创建新空间</Button.Label>
</Button>
<Button
className="flex-1"
variant="outline"
onPress={() => {
setShowJoin(true);
}}
>
<HousePlusIcon size={20} />
<Button.Label>加入其它空间</Button.Label>
</Button>
</View>
<View className="flex-row my-3 items-center gap-3">
<Separator className="flex-1" />
<Text className="text-xs text-gray-400">我的空间</Text>
<Separator className="flex-1" />
</View>
{loading ? (
<View className="flex-1 items-center justify-center">
<Spinner size="lg" />
</View>
) : spaces.length === 0 ? (
<EmptyState />
) : (
<Sortable.Grid
keyExtractor={(item: any) => {
return String(item.space_id);
}}
activeItemScale={1}
columns={1}
data={spaces}
overDrag="vertical"
renderItem={renderItem}
rowGap={10}
highlight-next-line
customHandle
autoScrollActivationOffset={200}
onDragEnd={(dragData: any) => {
const list = dragData.data;
const updates = list.map((item: any, index: number) => ({
id: item.v_space_member_id,
sort_number: index,
}));
saveDataApi([
{
table_name: "space_member",
key_field: "id",
inserts: [],
updates,
deletes: [],
},
]).then((_res) => {
// console.log(res);
});
}}
/>
)}
</LayoutView.ScrollView>
<BottomSheet
isOpen={showCreate}
onOpenChange={(val) => {
if (!val) {
setCreateForm({
name: "",
});
setCreateLoading(false);
}
setShowCreate(val);
}}
>
<BottomSheet.Portal>
<BottomSheet.Overlay />
<BottomSheet.Content>
<View>
<TextField>
<Label>空间名称</Label>
<Input
ref={createInputRef}
placeholder="请输入空间名称"
onFocus={onFocus}
onBlur={onBlur}
value={createForm.name}
onChangeText={(val) =>
setCreateForm({
name: val,
})
}
/>
</TextField>
<Button className="mt-3" onPress={handleCreate}>
{createLoading && <Spinner entering={FadeIn.delay(50)} color="white" />}
<Button.Label>创建</Button.Label>
</Button>
</View>
</BottomSheet.Content>
</BottomSheet.Portal>
</BottomSheet>
<BottomSheet
isOpen={showJoin}
onOpenChange={(val) => {
if (!val) {
setJoinForm({
invite_code: "",
});
setJoinLoading(false);
}
setShowJoin(val);
}}
>
<BottomSheet.Portal>
<BottomSheet.Overlay />
<BottomSheet.Content>
<View>
<TextField>
<Label>邀请码</Label>
<InputOTP
ref={joinInputRef}
maxLength={6}
onFocus={onFocus}
onBlur={onBlur}
value={joinForm.invite_code}
onChange={(val) => {
setJoinForm({
invite_code: val,
});
}}
>
<InputOTP.Group>
<InputOTP.Slot index={0} />
<InputOTP.Slot index={1} />
<InputOTP.Slot index={2} />
</InputOTP.Group>
<InputOTP.Separator />
<InputOTP.Group>
<InputOTP.Slot index={3} />
<InputOTP.Slot index={4} />
<InputOTP.Slot index={5} />
</InputOTP.Group>
</InputOTP>
</TextField>
<Button className="mt-3" onPress={handleJoin}>
{joinLoading && <Spinner entering={FadeIn.delay(50)} color="white" />}
<Button.Label>加入</Button.Label>
</Button>
</View>
</BottomSheet.Content>
</BottomSheet.Portal>
</BottomSheet>
</LayoutView>
);
}
+2 -2
View File
@@ -18,11 +18,11 @@ export default function NotFound() {
className="mt-4"
onPress={() => {
requestAnimationFrame(() => {
router.replace("/home");
router.back();
});
}}
>
返回首页
返回
</Button>
}
/>
+33 -19
View File
@@ -8,10 +8,15 @@ import "@/global.css";
import { useEffect, useState } from "react";
import { Stack } from "expo-router";
import { Toaster } from "sonner-native";
import { KeyboardProvider } from "react-native-keyboard-controller";
import { useUserStore } from "@/store";
import { useRouter } from "expo-router";
SplashScreen.preventAutoHideAsync();
export default function RootLayout() {
const router = useRouter();
const userStore = useUserStore();
const isDark = useIsDark();
const isDev = __DEV__;
@@ -29,31 +34,40 @@ export default function RootLayout() {
init();
}, []);
useEffect(() => {
if (userStore.authExpired) {
requestAnimationFrame(() => {
router.replace("/auth/login");
});
}
}, [userStore.authExpired]);
if (!isReady) return null;
return (
<GestureHandlerRootView className="flex-1">
<ThemeProvider value={isDark ? DarkTheme : DefaultTheme}>
<HeroUINativeProvider
config={{
devInfo: {
stylingPrinciples: false,
},
}}
>
<Stack
screenOptions={{
animation: "slide_from_right",
contentStyle: {
backgroundColor: "#fff",
<KeyboardProvider>
<ThemeProvider value={isDark ? DarkTheme : DefaultTheme}>
<HeroUINativeProvider
config={{
devInfo: {
stylingPrinciples: false,
},
headerShown: false,
}}
/>
{!isDev && <StatusBar style={isDark ? "light" : "dark"} />}
</HeroUINativeProvider>
</ThemeProvider>
>
<Stack
screenOptions={{
animation: "slide_from_right",
contentStyle: {
backgroundColor: "#fff",
},
headerShown: false,
}}
/>
{!isDev && <StatusBar style={isDark ? "light" : "dark"} />}
</HeroUINativeProvider>
</ThemeProvider>
</KeyboardProvider>
<Toaster duration={1000} />
</GestureHandlerRootView>
);
+3 -1
View File
@@ -33,12 +33,14 @@ export default function Login() {
setTimeout(() => {
requestAnimationFrame(() => {
userStore.setUserInfo({
id: 1,
id: 779895419121733,
token: "123",
nickname: "Zhang",
avatar: "",
});
userStore.setAuthExpired(false);
router.replace("/(tabs)/home");
});
@@ -0,0 +1,43 @@
import React from "react";
import LayoutView from "@/components/layout/view";
import { View, Text } from "react-native";
import Navbar from "@/components/layout/navbar";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { EarthIcon } from "lucide-react-native";
import { useRouter } from "expo-router";
export default function Space() {
const insets = useSafeAreaInsets();
const router = useRouter();
return (
<View style={{ flex: 1 }}>
<View style={{ flex: 1, zIndex: 0 }}>
<LayoutView>
<LayoutView.ScrollView>
<Text className="text-base text-black">Space</Text>
</LayoutView.ScrollView>
</LayoutView>
</View>
<View
style={{
position: "absolute",
top: 0,
left: 0,
right: 0,
zIndex: 10,
}}
>
<View style={{ paddingTop: insets.top }}>
<Navbar
title={"空间"}
titleIcon={EarthIcon}
onBack={() => {
router.back();
}}
/>
</View>
</View>
</View>
);
}
+18 -1
View File
@@ -9,6 +9,12 @@ export function loginWxApi(params: LoginWxParams) {
return alovaInstance.Post<ResponseData<LoginResult>>(`/auth/login/wechat`, params);
}
export function getUniqueIdApi(count = 1) {
return alovaInstance.Post<ResponseData<number[]>>(`/data/getUniqueId`, {
count: count,
});
}
interface LoadDataBySqlParams {
sql: string;
args: any[];
@@ -23,10 +29,21 @@ interface LoadDataParams {
search_condition: string;
order_by: string;
search_columns: string[];
args: string[];
args: any[];
}
export function loadDataApi(params: LoadDataParams) {
return alovaInstance.Post<ResponseData<any[]>>(`/data/loadData`, params);
}
interface SaveDataParams {
table_name: string;
key_field: string;
inserts: any[];
updates: any[];
deletes: any[];
}
export function saveDataApi(params: SaveDataParams[]) {
return alovaInstance.Post<ResponseData<null>>(`/data/saveData`, params);
}
+1
View File
@@ -1,2 +1,3 @@
export * from "./app";
export * from "./user";
export * from "./space";
+32
View File
@@ -0,0 +1,32 @@
import AsyncStorage from "@react-native-async-storage/async-storage";
import { create } from "zustand";
import { createJSONStorage, persist } from "zustand/middleware";
interface Space {
space_id: number;
}
interface SpaceState {
currentSpace: Space | null;
setCurrentSpace: (space: Space | null) => void;
clear: () => void;
inSpace: () => boolean;
}
export const useSpaceStore = create<SpaceState>()(
persist(
(set, get) => ({
currentSpace: null,
setCurrentSpace: (space) => set({ currentSpace: space }),
clear: () => set({ currentSpace: null }),
inSpace: () => {
const currentSpace = get().currentSpace;
return currentSpace !== null && currentSpace.space_id !== 0;
},
}),
{
name: "space-storage",
storage: createJSONStorage(() => AsyncStorage),
},
),
);
+13
View File
@@ -0,0 +1,13 @@
export function generateCode(
length: number = 6,
chars: string = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789",
): string {
let result = "";
const max = chars.length;
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * max));
}
return result;
}