diff --git a/code/app/base-app/package.json b/code/app/base-app/package.json
index fd085342..74d86f3f 100644
--- a/code/app/base-app/package.json
+++ b/code/app/base-app/package.json
@@ -24,10 +24,11 @@
"expo-splash-screen": "~31.0.10",
"expo-status-bar": "~3.0.8",
"lucide-react-native": "^0.548.0",
+ "moti": "^0.30.0",
"nativewind": "^4.2.1",
"react": "19.1.0",
"react-native": "0.81.5",
- "react-native-curved-bottom-bar": "^3.5.1",
+ "react-native-gesture-handler": "~2.28.0",
"react-native-reanimated": "~3.17.5",
"react-native-safe-area-context": "5.4.0",
"react-native-screens": "~4.16.0",
diff --git a/code/app/base-app/pnpm-lock.yaml b/code/app/base-app/pnpm-lock.yaml
index 7c282d66..5d378711 100644
--- a/code/app/base-app/pnpm-lock.yaml
+++ b/code/app/base-app/pnpm-lock.yaml
@@ -40,7 +40,7 @@ importers:
version: 8.0.8(expo@54.0.20)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
expo-router:
specifier: ~6.0.13
- version: 6.0.13(@expo/metro-runtime@6.1.2)(@types/react@19.1.17)(expo-constants@18.0.10)(expo-linking@8.0.8)(expo@54.0.20)(react-dom@19.2.0(react@19.1.0))(react-native-reanimated@3.17.5(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.4.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-web@0.21.2(react-dom@19.2.0(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
+ version: 6.0.13(@expo/metro-runtime@6.1.2)(@types/react@19.1.17)(expo-constants@18.0.10)(expo-linking@8.0.8)(expo@54.0.20)(react-dom@19.2.0(react@19.1.0))(react-native-gesture-handler@2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-reanimated@3.17.5(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.4.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-web@0.21.2(react-dom@19.2.0(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
expo-splash-screen:
specifier: ~31.0.10
version: 31.0.10(expo@54.0.20)
@@ -50,6 +50,9 @@ importers:
lucide-react-native:
specifier: ^0.548.0
version: 0.548.0(react-native-svg@15.12.1(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
+ moti:
+ specifier: ^0.30.0
+ version: 0.30.0(react-dom@19.2.0(react@19.1.0))(react-native-reanimated@3.17.5(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react@19.1.0)
nativewind:
specifier: ^4.2.1
version: 4.2.1(react-native-reanimated@3.17.5(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.4.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-svg@15.12.1(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)(tailwindcss@3.4.17)
@@ -59,9 +62,9 @@ importers:
react-native:
specifier: 0.81.5
version: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0)
- react-native-curved-bottom-bar:
- specifier: ^3.5.1
- version: 3.5.1(@react-navigation/bottom-tabs@7.5.0(@react-navigation/native@7.1.18(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.4.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-svg@15.12.1(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
+ react-native-gesture-handler:
+ specifier: ~2.28.0
+ version: 2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
react-native-reanimated:
specifier: ~3.17.5
version: 3.17.5(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
@@ -678,6 +681,16 @@ packages:
'@clack/prompts@0.11.0':
resolution: {integrity: sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw==}
+ '@egjs/hammerjs@2.0.17':
+ resolution: {integrity: sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==}
+ engines: {node: '>=0.8.0'}
+
+ '@emotion/is-prop-valid@0.8.8':
+ resolution: {integrity: sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==}
+
+ '@emotion/memoize@0.7.4':
+ resolution: {integrity: sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==}
+
'@es-joy/jsdoccomment@0.50.2':
resolution: {integrity: sha512-YAdE/IJSpwbOTiaURNCKECdAwqrJuFiZhylmesBcIRawtYKnBR2wxPhoIewMg+Yu+QuYvHfJNReWpoxGBKOChA==}
engines: {node: '>=18'}
@@ -1009,6 +1022,24 @@ packages:
'@jridgewell/trace-mapping@0.3.31':
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
+ '@motionone/animation@10.18.0':
+ resolution: {integrity: sha512-9z2p5GFGCm0gBsZbi8rVMOAJCtw1WqBTIPw3ozk06gDvZInBPIsQcHgYogEJ4yuHJ+akuW8g1SEIOpTOvYs8hw==}
+
+ '@motionone/dom@10.12.0':
+ resolution: {integrity: sha512-UdPTtLMAktHiqV0atOczNYyDd/d8Cf5fFsd1tua03PqTwwCe/6lwhLSQ8a7TbnQ5SN0gm44N1slBfj+ORIhrqw==}
+
+ '@motionone/easing@10.18.0':
+ resolution: {integrity: sha512-VcjByo7XpdLS4o9T8t99JtgxkdMcNWD3yHU/n6CLEz3bkmKDRZyYQ/wmSf6daum8ZXqfUAgFeCZSpJZIMxaCzg==}
+
+ '@motionone/generators@10.18.0':
+ resolution: {integrity: sha512-+qfkC2DtkDj4tHPu+AFKVfR/C30O1vYdvsGYaR13W/1cczPrrcjdvYCj0VLFuRMN+lP1xvpNZHCRNM4fBzn1jg==}
+
+ '@motionone/types@10.17.1':
+ resolution: {integrity: sha512-KaC4kgiODDz8hswCrS0btrVrzyU2CSQKO7Ps90ibBVSQmjkrt2teqta6/sOG59v7+dPnKMAg13jyqtMKV2yJ7A==}
+
+ '@motionone/utils@10.18.0':
+ resolution: {integrity: sha512-3XVF7sgyTSI2KWvTf6uLlBJ5iAgRgmvp3bpuOiQJvInd4nZ19ET8lX5unn30SlmRH7hXbBbH+Gxd0m0klJ3Xtw==}
+
'@nodelib/fs.scandir@2.1.5':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'}
@@ -1955,6 +1986,9 @@ packages:
'@types/graceful-fs@4.1.9':
resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==}
+ '@types/hammerjs@2.0.46':
+ resolution: {integrity: sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw==}
+
'@types/istanbul-lib-coverage@2.0.6':
resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==}
@@ -2522,14 +2556,6 @@ packages:
csstype@3.1.3:
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
- d3-path@3.1.0:
- resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==}
- engines: {node: '>=12'}
-
- d3-shape@3.2.0:
- resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==}
- engines: {node: '>=12'}
-
debug@2.6.9:
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
peerDependencies:
@@ -3138,6 +3164,15 @@ packages:
resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==}
engines: {node: '>=0.4.x'}
+ framer-motion@6.5.1:
+ resolution: {integrity: sha512-o1BGqqposwi7cgDrtg0dNONhkmPsUFDaLcKXigzuTFC5x58mE8iyTazxSudFzmT6MEyJKfjjU8ItoMe3W+3fiw==}
+ peerDependencies:
+ react: '>=16.8 || ^17.0.0 || ^18.0.0'
+ react-dom: '>=16.8 || ^17.0.0 || ^18.0.0'
+
+ framesync@6.0.1:
+ resolution: {integrity: sha512-fUY88kXvGiIItgNC7wcTOl0SNRCVXMKSWW2Yzfmn7EKNc+MpCzcz9DhdHcdjbrtN3c6R4H5dTY2jiCpPdysEjA==}
+
freeport-async@2.0.0:
resolution: {integrity: sha512-K7od3Uw45AJg00XUmy15+Hae2hOcgKcmN3/EF6Y7i01O0gaqiRx8sUSpsb9+BRNL8RPBrhzPsVfy8q9ADlJuWQ==}
engines: {node: '>=8'}
@@ -3252,6 +3287,12 @@ packages:
hermes-parser@0.32.0:
resolution: {integrity: sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw==}
+ hey-listen@1.0.8:
+ resolution: {integrity: sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==}
+
+ hoist-non-react-statics@3.3.2:
+ resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==}
+
hosted-git-info@7.0.2:
resolution: {integrity: sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==}
engines: {node: ^16.14.0 || >=18.0.0}
@@ -3556,48 +3597,56 @@ packages:
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
lightningcss-linux-arm64-gnu@1.30.2:
resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
lightningcss-linux-arm64-musl@1.27.0:
resolution: {integrity: sha512-rCGBm2ax7kQ9pBSeITfCW9XSVF69VX+fm5DIpvDZQl4NnQoMQyRwhZQm9pd59m8leZ1IesRqWk2v/DntMo26lg==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
+ libc: [musl]
lightningcss-linux-arm64-musl@1.30.2:
resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
+ libc: [musl]
lightningcss-linux-x64-gnu@1.27.0:
resolution: {integrity: sha512-Dk/jovSI7qqhJDiUibvaikNKI2x6kWPN79AQiD/E/KeQWMjdGe9kw51RAgoWFDi0coP4jinaH14Nrt/J8z3U4A==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
+ libc: [glibc]
lightningcss-linux-x64-gnu@1.30.2:
resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
+ libc: [glibc]
lightningcss-linux-x64-musl@1.27.0:
resolution: {integrity: sha512-QKjTxXm8A9s6v9Tg3Fk0gscCQA1t/HMoF7Woy1u68wCk5kS4fR+q3vXa1p3++REW784cRAtkYKrPy6JKibrEZA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
+ libc: [musl]
lightningcss-linux-x64-musl@1.30.2:
resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
+ libc: [musl]
lightningcss-win32-arm64-msvc@1.27.0:
resolution: {integrity: sha512-/wXegPS1hnhkeG4OXQKEMQeJd48RDC3qdh+OA8pCuOPCyvnm/yEayrJdJVqzBsqpy1aJklRCVxscpFur80o6iQ==}
@@ -4008,6 +4057,11 @@ packages:
mlly@1.8.0:
resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==}
+ moti@0.30.0:
+ resolution: {integrity: sha512-YN78mcefo8kvJaL+TZNyusq6YA2aMFvBPl8WiLPy4eb4wqgOFggJOjP9bUr2YO8PrAt0uusmRG8K4RPL4OhCsA==}
+ peerDependencies:
+ react-native-reanimated: '*'
+
ms@2.0.0:
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
@@ -4247,6 +4301,9 @@ packages:
pnpm-workspace-yaml@1.3.0:
resolution: {integrity: sha512-Krb5q8Totd5mVuLx7we+EFHq/AfxA75nbfTm25Q1pIf606+RlaKUG+PXH8SDihfe5b5k4H09gE+sL47L1t5lbw==}
+ popmotion@11.0.3:
+ resolution: {integrity: sha512-Y55FLdj3UxkR7Vl3s7Qr4e9m0onSnP8W7d/xQLsoJM40vs6UKHFdygs6SWryasTZYqugMjm3BepCF4CWXDiHgA==}
+
postcss-import@15.1.0:
resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==}
engines: {node: '>=14.0.0'}
@@ -4374,6 +4431,9 @@ packages:
peerDependencies:
react: '>=17.0.0'
+ react-is@16.13.1:
+ resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
+
react-is@18.3.1:
resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
@@ -4396,14 +4456,11 @@ packages:
react-native-svg:
optional: true
- react-native-curved-bottom-bar@3.5.1:
- resolution: {integrity: sha512-WG7ZTs+JYXCmGrnXzsYB12DD/UvVdSC4pgem7X5BxXR/bnSgPEHMc0lmGhuRoPS7WgH75w8kncUJFDcwlA75fQ==}
- engines: {node: '>= 16.0.0'}
+ react-native-gesture-handler@2.28.0:
+ resolution: {integrity: sha512-0msfJ1vRxXKVgTgvL+1ZOoYw3/0z1R+Ked0+udoJhyplC2jbVKIJ8Z1bzWdpQRCV3QcQ87Op0zJVE5DhKK2A0A==}
peerDependencies:
- '@react-navigation/bottom-tabs': '*'
react: '*'
react-native: '*'
- react-native-svg: '*'
react-native-is-edge-to-edge@1.1.7:
resolution: {integrity: sha512-EH6i7E8epJGIcu7KpfXYXiV2JFIYITtq+rVS8uEb+92naMRBdxhTuS8Wn2Q7j9sqyO0B+Xbaaf9VdipIAmGW4w==}
@@ -4436,12 +4493,6 @@ packages:
react: '*'
react-native: '*'
- react-native-size-scaling@0.5.4:
- resolution: {integrity: sha512-GtoE7qcw4ftOigtq6hRTV34nosGw9z6WXNq2l06Uj+yFfvrz6teGjD/E22Jo4raegdTIQmPcMU4/nRRhRLZpAA==}
- peerDependencies:
- react: '*'
- react-native: '*'
-
react-native-svg@15.12.1:
resolution: {integrity: sha512-vCuZJDf8a5aNC2dlMovEv4Z0jjEUET53lm/iILFnFewa15b4atjVxU6Wirm6O9y6dEsdjDZVD7Q3QM4T1wlI8g==}
peerDependencies:
@@ -4817,6 +4868,9 @@ packages:
structured-headers@0.4.1:
resolution: {integrity: sha512-0MP/Cxx5SzeeZ10p/bZI0S6MpgD+yxAhi1BOQ34jgnMXsCq3j1t6tQnZu+KdlL7dvJTLT3g9xN8tl10TqgFMcg==}
+ style-value-types@5.0.0:
+ resolution: {integrity: sha512-08yq36Ikn4kx4YU6RD7jWEv27v4V+PUsOGa4n/as8Et3CuODMJQ00ENeAVXAeydX4Z2j1XHZF1K2sX4mGl18fA==}
+
styleq@0.1.3:
resolution: {integrity: sha512-3ZUifmCDCQanjeej1f6kyl/BeP/Vae5EYkQ9iJfUm/QwZvlgnZzyflqAsAWYURdtea8Vkvswu2GrC57h3qffcA==}
@@ -5939,6 +5993,18 @@ snapshots:
picocolors: 1.1.1
sisteransi: 1.0.5
+ '@egjs/hammerjs@2.0.17':
+ dependencies:
+ '@types/hammerjs': 2.0.46
+
+ '@emotion/is-prop-valid@0.8.8':
+ dependencies:
+ '@emotion/memoize': 0.7.4
+ optional: true
+
+ '@emotion/memoize@0.7.4':
+ optional: true
+
'@es-joy/jsdoccomment@0.50.2':
dependencies:
'@types/estree': 1.0.8
@@ -6106,7 +6172,7 @@ snapshots:
wrap-ansi: 7.0.0
ws: 8.18.3
optionalDependencies:
- expo-router: 6.0.13(@expo/metro-runtime@6.1.2)(@types/react@19.1.17)(expo-constants@18.0.10)(expo-linking@8.0.8)(expo@54.0.20)(react-dom@19.2.0(react@19.1.0))(react-native-reanimated@3.17.5(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.4.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-web@0.21.2(react-dom@19.2.0(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
+ expo-router: 6.0.13(@expo/metro-runtime@6.1.2)(@types/react@19.1.17)(expo-constants@18.0.10)(expo-linking@8.0.8)(expo@54.0.20)(react-dom@19.2.0(react@19.1.0))(react-native-gesture-handler@2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-reanimated@3.17.5(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.4.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-web@0.21.2(react-dom@19.2.0(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0)
transitivePeerDependencies:
- '@modelcontextprotocol/sdk'
@@ -6532,6 +6598,41 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5
+ '@motionone/animation@10.18.0':
+ dependencies:
+ '@motionone/easing': 10.18.0
+ '@motionone/types': 10.17.1
+ '@motionone/utils': 10.18.0
+ tslib: 2.8.1
+
+ '@motionone/dom@10.12.0':
+ dependencies:
+ '@motionone/animation': 10.18.0
+ '@motionone/generators': 10.18.0
+ '@motionone/types': 10.17.1
+ '@motionone/utils': 10.18.0
+ hey-listen: 1.0.8
+ tslib: 2.8.1
+
+ '@motionone/easing@10.18.0':
+ dependencies:
+ '@motionone/utils': 10.18.0
+ tslib: 2.8.1
+
+ '@motionone/generators@10.18.0':
+ dependencies:
+ '@motionone/types': 10.17.1
+ '@motionone/utils': 10.18.0
+ tslib: 2.8.1
+
+ '@motionone/types@10.17.1': {}
+
+ '@motionone/utils@10.18.0':
+ dependencies:
+ '@motionone/types': 10.17.1
+ hey-listen: 1.0.8
+ tslib: 2.8.1
+
'@nodelib/fs.scandir@2.1.5':
dependencies:
'@nodelib/fs.stat': 2.0.5
@@ -7948,6 +8049,8 @@ snapshots:
dependencies:
'@types/node': 24.9.1
+ '@types/hammerjs@2.0.46': {}
+
'@types/istanbul-lib-coverage@2.0.6': {}
'@types/istanbul-lib-report@3.0.3':
@@ -8608,12 +8711,6 @@ snapshots:
csstype@3.1.3: {}
- d3-path@3.1.0: {}
-
- d3-shape@3.2.0:
- dependencies:
- d3-path: 3.1.0
-
debug@2.6.9:
dependencies:
ms: 2.0.0
@@ -9094,7 +9191,7 @@ snapshots:
react: 19.1.0
react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0)
- expo-router@6.0.13(@expo/metro-runtime@6.1.2)(@types/react@19.1.17)(expo-constants@18.0.10)(expo-linking@8.0.8)(expo@54.0.20)(react-dom@19.2.0(react@19.1.0))(react-native-reanimated@3.17.5(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.4.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-web@0.21.2(react-dom@19.2.0(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0):
+ expo-router@6.0.13(@expo/metro-runtime@6.1.2)(@types/react@19.1.17)(expo-constants@18.0.10)(expo-linking@8.0.8)(expo@54.0.20)(react-dom@19.2.0(react@19.1.0))(react-native-gesture-handler@2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-reanimated@3.17.5(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.4.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-web@0.21.2(react-dom@19.2.0(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0):
dependencies:
'@expo/metro-runtime': 6.1.2(expo@54.0.20)(react-dom@19.2.0(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
'@expo/schema-utils': 0.1.7
@@ -9128,6 +9225,7 @@ snapshots:
vaul: 1.1.2(@types/react@19.1.17)(react-dom@19.2.0(react@19.1.0))(react@19.1.0)
optionalDependencies:
react-dom: 19.2.0(react@19.1.0)
+ react-native-gesture-handler: 2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
react-native-reanimated: 3.17.5(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
react-native-web: 0.21.2(react-dom@19.2.0(react@19.1.0))(react@19.1.0)
transitivePeerDependencies:
@@ -9291,6 +9389,23 @@ snapshots:
format@0.2.2: {}
+ framer-motion@6.5.1(react-dom@19.2.0(react@19.1.0))(react@19.1.0):
+ dependencies:
+ '@motionone/dom': 10.12.0
+ framesync: 6.0.1
+ hey-listen: 1.0.8
+ popmotion: 11.0.3
+ react: 19.1.0
+ react-dom: 19.2.0(react@19.1.0)
+ style-value-types: 5.0.0
+ tslib: 2.8.1
+ optionalDependencies:
+ '@emotion/is-prop-valid': 0.8.8
+
+ framesync@6.0.1:
+ dependencies:
+ tslib: 2.8.1
+
freeport-async@2.0.0: {}
fresh@0.5.2: {}
@@ -9386,6 +9501,12 @@ snapshots:
dependencies:
hermes-estree: 0.32.0
+ hey-listen@1.0.8: {}
+
+ hoist-non-react-statics@3.3.2:
+ dependencies:
+ react-is: 16.13.1
+
hosted-git-info@7.0.2:
dependencies:
lru-cache: 10.4.3
@@ -10517,6 +10638,14 @@ snapshots:
pkg-types: 1.3.1
ufo: 1.6.1
+ moti@0.30.0(react-dom@19.2.0(react@19.1.0))(react-native-reanimated@3.17.5(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react@19.1.0):
+ dependencies:
+ framer-motion: 6.5.1(react-dom@19.2.0(react@19.1.0))(react@19.1.0)
+ react-native-reanimated: 3.17.5(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
+ transitivePeerDependencies:
+ - react
+ - react-dom
+
ms@2.0.0: {}
ms@2.1.3: {}
@@ -10732,6 +10861,13 @@ snapshots:
dependencies:
yaml: 2.8.1
+ popmotion@11.0.3:
+ dependencies:
+ framesync: 6.0.1
+ hey-listen: 1.0.8
+ style-value-types: 5.0.0
+ tslib: 2.8.1
+
postcss-import@15.1.0(postcss@8.5.6):
dependencies:
postcss: 8.5.6
@@ -10896,6 +11032,8 @@ snapshots:
dependencies:
react: 19.1.0
+ react-is@16.13.1: {}
+
react-is@18.3.1: {}
react-is@19.2.0: {}
@@ -10918,14 +11056,13 @@ snapshots:
transitivePeerDependencies:
- supports-color
- react-native-curved-bottom-bar@3.5.1(@react-navigation/bottom-tabs@7.5.0(@react-navigation/native@7.1.18(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.4.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-svg@15.12.1(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0):
+ react-native-gesture-handler@2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0):
dependencies:
- '@react-navigation/bottom-tabs': 7.5.0(@react-navigation/native@7.1.18(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.4.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
- d3-shape: 3.2.0
+ '@egjs/hammerjs': 2.0.17
+ hoist-non-react-statics: 3.3.2
+ invariant: 2.2.4
react: 19.1.0
react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0)
- react-native-size-scaling: 0.5.4(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
- react-native-svg: 15.12.1(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
react-native-is-edge-to-edge@1.1.7(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0):
dependencies:
@@ -10970,11 +11107,6 @@ snapshots:
react-native-is-edge-to-edge: 1.2.1(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
warn-once: 0.1.1
- react-native-size-scaling@0.5.4(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0):
- dependencies:
- react: 19.1.0
- react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0)
-
react-native-svg@15.12.1(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0):
dependencies:
css-select: 5.2.2
@@ -11403,6 +11535,11 @@ snapshots:
structured-headers@0.4.1: {}
+ style-value-types@5.0.0:
+ dependencies:
+ hey-listen: 1.0.8
+ tslib: 2.8.1
+
styleq@0.1.3: {}
sucrase@3.35.0:
diff --git a/code/app/base-app/src/app/(tabs)/_layout.tsx b/code/app/base-app/src/app/(tabs)/_layout.tsx
index 7167ecb7..5c59f244 100644
--- a/code/app/base-app/src/app/(tabs)/_layout.tsx
+++ b/code/app/base-app/src/app/(tabs)/_layout.tsx
@@ -1,70 +1,22 @@
-import { Tabs } from 'expo-router'
-import { CircleDollarSign, CircleUserRound, Heart, Layers, MessageSquare } from 'lucide-react-native'
-import React, { useRef } from 'react'
-import { Animated, Pressable, View } from 'react-native'
-
-const tabItems = [
- { name: 'finance', icon: },
- { name: 'dating', icon: },
- { name: 'app-center', icon: },
- { name: 'chat', icon: },
- { name: 'profile', icon: },
-]
-
-// 自定义 TabBar
-function CustomTabBar({ state, descriptors, navigation }: any) {
- const scales = useRef(tabItems.map(() => new Animated.Value(1))).current
+import { Tabs, TabSlot } from 'expo-router/ui' // ✅ 从 expo-router/ui 导入
+import { CustomTabBar } from '@/components/CustomTabBar'
+export default function TabsLayout() {
return (
-
- {state.routes.map((route: any, index: number) => {
- const item = tabItems.find(i => i.name === route.name)
- if (!item)
- return null
+
+ {/* 渲染当前激活的页面内容 */}
+
- const isFocused = state.index === index
- const color = isFocused ? '#333333' : '#9ca3af'
-
- const onPressIn = () =>
- Animated.spring(scales[index], { toValue: 0.8, useNativeDriver: true }).start()
- const onPressOut = () =>
- Animated.spring(scales[index], { toValue: 1, useNativeDriver: true }).start()
-
- const onPress = () => {
- const event = navigation.emit({
- type: 'tabPress',
- target: route.key,
- canPreventDefault: true,
- })
- if (!isFocused && !event.defaultPrevented) {
- navigation.navigate(route.name)
- }
- }
-
- return (
-
-
- {React.cloneElement(item.icon, { color })}
-
-
- )
- })}
-
- )
-}
-
-export default function TabLayout() {
- return (
- }>
- {tabItems.map(item => (
- null }} />
- ))}
+ {/* 您的自定义 TabBar */}
+
)
}
diff --git a/code/app/base-app/src/app/_layout.tsx b/code/app/base-app/src/app/_layout.tsx
index 67127a41..3242a551 100644
--- a/code/app/base-app/src/app/_layout.tsx
+++ b/code/app/base-app/src/app/_layout.tsx
@@ -8,6 +8,8 @@ import { useEffect } from 'react'
import { EmptyState } from '@/components/empty-status'
import { GluestackUIProvider } from '@/components/ui/gluestack-ui-provider'
import '@/styles/global.css'
+import 'react-native-reanimated'
+import 'react-native-gesture-handler'
SplashScreen.preventAutoHideAsync()
@@ -25,7 +27,8 @@ export function ErrorBoundary({ error, retry }: ErrorBoundaryProps) {
)
}
-export default function Layout() {
+
+export default function RootLayout() {
const [loaded, error] = useFonts({
SpaceMono: require('@/static/fonts/SpaceMono-Regular.ttf'),
})
@@ -48,6 +51,7 @@ export default function Layout() {
name="(tabs)"
options={{ headerShown: false }}
/>
+
diff --git a/code/app/base-app/src/components/CustomTabBar.tsx b/code/app/base-app/src/components/CustomTabBar.tsx
new file mode 100644
index 00000000..d6430250
--- /dev/null
+++ b/code/app/base-app/src/components/CustomTabBar.tsx
@@ -0,0 +1,67 @@
+import type { MotiProps } from 'moti'
+import { TabList, TabTrigger } from 'expo-router/ui'
+import { icons } from 'lucide-react-native'
+import { motifySvg } from 'moti/svg'
+import { Text, View } from 'react-native'
+
+type IconNames = keyof typeof icons
+
+type IconProp = {
+ name: IconNames
+ size?: number
+} & MotiProps
+
+interface TabbarItem {
+ icon: IconNames
+ name: string
+ href: string
+ label: string
+}
+
+interface CustomTabBarProps {
+ data: TabbarItem[]
+}
+
+function Icon({ name, size = 24, ...rest }: IconProp) {
+ const IconComponent = motifySvg(icons[name] as any)()
+ return
+}
+
+export function CustomTabBar({ data }: CustomTabBarProps) {
+ return (
+
+
+ {data.map(item => (
+
+
+
+ {item.label}
+
+
+ ))}
+
+
+ )
+}
diff --git a/code/base-project/base-go-v2/cmd/app/main.go b/code/base-project/base-go-v2/cmd/app/main.go
index 43a3412f..fc9edb80 100644
--- a/code/base-project/base-go-v2/cmd/app/main.go
+++ b/code/base-project/base-go-v2/cmd/app/main.go
@@ -1,25 +1,37 @@
package main
import (
+ "base-go-v2/internal/api"
"base-go-v2/internal/config"
"base-go-v2/internal/database"
"base-go-v2/internal/logx"
+ "fmt"
+ "github.com/valyala/fasthttp"
"log"
)
func main() {
// 初始化配置
- err := config.InitAppConfig("./configs/app.yaml")
- if err != nil {
+ if err := config.InitAppConfig("./configs/app.yaml"); err != nil {
log.Fatalf("加载配置失败: %v", err)
- return
}
+
// 初始化数据库
- err = database.InitDb()
- if err != nil {
+ if err := database.InitDb(); err != nil {
log.Fatalf("加载数据库配置失败: %v", err)
- return
}
+
// 初始化日志
logx.InitLogger()
+
+ // 初始化路由
+ r := api.InitAppRouter()
+ addr := fmt.Sprintf(":%d", config.App.Server.Port)
+
+ log.Printf("✅ 服务器启动成功,端口%s", addr)
+
+ // 启动服务器(简洁写法)
+ if err := fasthttp.ListenAndServe(addr, r.Handler); err != nil {
+ log.Fatalf("❌ 服务器启动失败: %v", err)
+ }
}
diff --git a/code/base-project/base-go-v2/configs/app.yaml b/code/base-project/base-go-v2/configs/app.yaml
index eb10e709..af4a7b03 100644
--- a/code/base-project/base-go-v2/configs/app.yaml
+++ b/code/base-project/base-go-v2/configs/app.yaml
@@ -1,5 +1,6 @@
server:
port: 8085
+ baseUrl: '/app'
snowflake:
datacenterID: 0
@@ -23,5 +24,5 @@ log:
daily: true # 是否按天滚动
maxDays: 30 # 保留天数,0表示无限
caller: true # 是否记录调用文件行号
- levelDirs: false # 是否按级别目录
+ levelDirs: true # 是否按级别目录
console: true # 是否打印到控制台
diff --git a/code/base-project/base-go-v2/go.mod b/code/base-project/base-go-v2/go.mod
index 3758b50a..72048674 100644
--- a/code/base-project/base-go-v2/go.mod
+++ b/code/base-project/base-go-v2/go.mod
@@ -6,6 +6,7 @@ require (
github.com/golang-jwt/jwt/v5 v5.3.0
github.com/lib/pq v1.10.9
github.com/spf13/viper v1.21.0
+ go.uber.org/zap v1.27.0
)
require (
@@ -23,7 +24,6 @@ require (
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.68.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
- go.uber.org/zap v1.27.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/sys v0.37.0 // indirect
golang.org/x/text v0.30.0 // indirect
diff --git a/code/base-project/base-go-v2/internal/api/auth/index.go b/code/base-project/base-go-v2/internal/api/auth/index.go
new file mode 100644
index 00000000..eaf3abd8
--- /dev/null
+++ b/code/base-project/base-go-v2/internal/api/auth/index.go
@@ -0,0 +1,5 @@
+package auth
+
+func InitAuthRouter() {
+
+}
diff --git a/code/base-project/base-go-v2/internal/api/middleware/cors.go b/code/base-project/base-go-v2/internal/api/middleware/cors.go
new file mode 100644
index 00000000..063f9bc6
--- /dev/null
+++ b/code/base-project/base-go-v2/internal/api/middleware/cors.go
@@ -0,0 +1,24 @@
+package middleware
+
+import "base-go-v2/internal/router"
+
+func CORSMiddleware() router.HandlerFunc {
+ return func(c *router.Context) error {
+ origin := string(c.RequestCtx.Request.Header.Peek("Origin"))
+ if origin == "" {
+ origin = "*"
+ }
+
+ c.RequestCtx.Response.Header.Set("Access-Control-Allow-Origin", origin)
+ c.RequestCtx.Response.Header.Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
+ c.RequestCtx.Response.Header.Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
+ c.RequestCtx.Response.Header.Set("Access-Control-Allow-Credentials", "true")
+
+ if string(c.RequestCtx.Method()) == "OPTIONS" {
+ c.RequestCtx.SetStatusCode(200)
+ return nil
+ }
+
+ return c.Next()
+ }
+}
diff --git a/code/base-project/base-go-v2/internal/api/middleware/error.go b/code/base-project/base-go-v2/internal/api/middleware/error.go
new file mode 100644
index 00000000..90146122
--- /dev/null
+++ b/code/base-project/base-go-v2/internal/api/middleware/error.go
@@ -0,0 +1,29 @@
+package middleware
+
+import (
+ "base-go-v2/internal/logx"
+ "base-go-v2/internal/reponse"
+ "base-go-v2/internal/router"
+ "runtime/debug"
+)
+
+func ErrorMiddleware() router.HandlerFunc {
+ return func(c *router.Context) error {
+ err := c.Next()
+
+ if err != nil {
+ stack := string(debug.Stack())
+
+ logx.Logger.Error("请求错误",
+ logx.String("path", string(c.RequestCtx.Path())),
+ logx.String("method", string(c.RequestCtx.Method())),
+ logx.String("error", err.Error()),
+ logx.String("stack", stack),
+ )
+
+ return response.Error(c).Message(err.Error()).Send()
+ }
+
+ return nil
+ }
+}
diff --git a/code/base-project/base-go-v2/internal/api/middleware/logger.go b/code/base-project/base-go-v2/internal/api/middleware/logger.go
new file mode 100644
index 00000000..91013ad3
--- /dev/null
+++ b/code/base-project/base-go-v2/internal/api/middleware/logger.go
@@ -0,0 +1,43 @@
+package middleware
+
+import (
+ "base-go-v2/internal/logx"
+ "base-go-v2/internal/router"
+ "base-go-v2/internal/routinex"
+ "base-go-v2/internal/utils/uid"
+ "time"
+)
+
+// TrackTime 记录开始时间到现在时间的耗时
+func TrackTime(pre time.Time) time.Duration {
+ elapsed := time.Since(pre)
+
+ return elapsed
+}
+
+func LoggerMiddleware() router.HandlerFunc {
+ return func(c *router.Context) error {
+ start := time.Now() // 记录开始时间
+
+ // 生成 trackID 并存入 routinex
+ trackID := uid.NextID()
+ routinex.Set(logx.TrackID, trackID)
+
+ // 执行业务逻辑,并捕获错误
+ err := c.Next()
+
+ // 请求结束后计算耗时
+ elapsed := TrackTime(start)
+
+ ip := c.RequestCtx.RemoteIP().String()
+
+ logx.Logger.Info("接口请求",
+ logx.String("path", string(c.RequestCtx.Path())),
+ logx.String("method", string(c.RequestCtx.Method())),
+ logx.String("ip", ip),
+ logx.Duration("elapsed", elapsed),
+ )
+
+ return err
+ }
+}
diff --git a/code/base-project/base-go-v2/internal/api/middleware/recovery.go b/code/base-project/base-go-v2/internal/api/middleware/recovery.go
new file mode 100644
index 00000000..aa4ff01c
--- /dev/null
+++ b/code/base-project/base-go-v2/internal/api/middleware/recovery.go
@@ -0,0 +1,31 @@
+package middleware
+
+import (
+ "base-go-v2/internal/logx"
+ response "base-go-v2/internal/reponse"
+ "base-go-v2/internal/router"
+ "runtime/debug"
+)
+
+func RecoveryMiddleware() router.HandlerFunc {
+ return func(c *router.Context) error {
+ defer func() {
+ if r := recover(); r != nil {
+ stack := string(debug.Stack()) // 获取堆栈
+
+ logx.Logger.Error("请求 panic",
+ logx.String("path", string(c.RequestCtx.Path())),
+ logx.String("method", string(c.RequestCtx.Method())),
+ logx.String("panic", r.(error).Error()),
+ logx.String("stack", stack),
+ )
+
+ // 返回统一 JSON 响应
+ _ = response.Error(c).Message("系统 Panic").Send()
+ }
+ }()
+
+ // 调用后续 handler
+ return c.Next()
+ }
+}
diff --git a/code/base-project/base-go-v2/internal/api/router.go b/code/base-project/base-go-v2/internal/api/router.go
new file mode 100644
index 00000000..964f33ba
--- /dev/null
+++ b/code/base-project/base-go-v2/internal/api/router.go
@@ -0,0 +1,23 @@
+package api
+
+import (
+ "base-go-v2/internal/api/middleware"
+ "base-go-v2/internal/config"
+ response "base-go-v2/internal/reponse"
+ "base-go-v2/internal/router"
+)
+
+func InitAppRouter() *router.Router {
+ r := router.NewRouter(config.App.Server.BaseUrl)
+
+ r.Use(middleware.RecoveryMiddleware())
+ r.Use(middleware.CORSMiddleware())
+ r.Use(middleware.LoggerMiddleware())
+ r.Use(middleware.ErrorMiddleware())
+
+ r.GET("/test", func(c *router.Context) error {
+ return response.Success(c).Send()
+ })
+
+ return r
+}
diff --git a/code/base-project/base-go-v2/internal/logx/field.go b/code/base-project/base-go-v2/internal/logx/field.go
index f4000214..b4b3a69d 100644
--- a/code/base-project/base-go-v2/internal/logx/field.go
+++ b/code/base-project/base-go-v2/internal/logx/field.go
@@ -6,6 +6,8 @@ import (
"go.uber.org/zap"
)
+type Field = zap.Field
+
func String(key, val string) zap.Field { return zap.String(key, val) }
func Stringp(key string, val *string) zap.Field { return zap.Stringp(key, val) }
func Strings(key string, val []string) zap.Field { return zap.Strings(key, val) }
diff --git a/code/base-project/base-go-v2/internal/reponse/response.go b/code/base-project/base-go-v2/internal/reponse/response.go
new file mode 100644
index 00000000..e2f5fbc8
--- /dev/null
+++ b/code/base-project/base-go-v2/internal/reponse/response.go
@@ -0,0 +1,107 @@
+package response
+
+import (
+ "base-go-v2/internal/logx"
+ "base-go-v2/internal/router"
+ "base-go-v2/internal/routinex"
+ "base-go-v2/internal/utils/strutil"
+ "net/http"
+)
+
+type Result struct {
+ Code string `json:"code"`
+ Message string `json:"message"`
+ Data interface{} `json:"data,omitempty"`
+ TrackId string `json:"trackId"`
+}
+
+type Code struct {
+ Value string
+ Message string
+}
+
+var (
+ CodeSuccess = Code{"0000", "请求成功"}
+ CodeFail = Code{"0001", "请求失败"}
+ CodeNoLogin = Code{"1001", "未登录"}
+ CodeLoginExpired = Code{"1002", "登录已过期"}
+ CodeInvalidToken = Code{"1003", "Token 无效"}
+ CodeInvalidAccount = Code{"1005", "账号或密码错误"}
+)
+
+const defaultMessage = "未知错误"
+
+type Builder struct {
+ c *router.Context
+ result Result
+}
+
+func Success(c *router.Context) *Builder {
+ return &Builder{
+ c: c,
+ result: Result{
+ Code: CodeSuccess.Value,
+ Message: CodeSuccess.Message,
+ },
+ }
+}
+
+func Error(c *router.Context) *Builder {
+ return &Builder{
+ c: c,
+ result: Result{
+ Code: CodeFail.Value,
+ Message: CodeFail.Message,
+ },
+ }
+}
+
+func (b *Builder) Code(code string) *Builder {
+ b.result.Code = code
+ b.result.Message = GetMessage(code)
+ return b
+}
+
+func (b *Builder) CodeObj(code Code) *Builder {
+ b.result.Code = code.Value
+ b.result.Message = code.Message
+ return b
+}
+
+func (b *Builder) Message(msg string) *Builder {
+ b.result.Message = msg
+ return b
+}
+
+func (b *Builder) Data(data interface{}) *Builder {
+ b.result.Data = data
+ return b
+}
+
+func (b *Builder) Send() error {
+ // 默认 Data 为空
+ if b.result.Data == nil {
+ b.result.Data = ""
+ }
+
+ // 从 routinex 获取 trackId
+ trackId := routinex.Get(logx.TrackID)
+ if trackId != nil {
+ b.result.TrackId = strutil.ToString(trackId)
+ }
+
+ // 发送响应
+ return b.c.JSON(http.StatusOK, b.result)
+}
+
+func GetMessage(value string) string {
+ for _, c := range []Code{
+ CodeSuccess, CodeFail, CodeNoLogin,
+ CodeLoginExpired, CodeInvalidToken, CodeInvalidAccount,
+ } {
+ if c.Value == value {
+ return c.Message
+ }
+ }
+ return defaultMessage
+}
diff --git a/code/base-project/base-go-v2/internal/router/context.go b/code/base-project/base-go-v2/internal/router/context.go
new file mode 100644
index 00000000..89159d66
--- /dev/null
+++ b/code/base-project/base-go-v2/internal/router/context.go
@@ -0,0 +1,123 @@
+package router
+
+import (
+ "encoding/json"
+ "sync"
+
+ "github.com/valyala/fasthttp"
+)
+
+// BodyCache 缓存 fasthttp 请求体
+type BodyCache struct {
+ once sync.Once
+ Data []byte
+ Err error
+}
+
+// Load 读取并缓存请求体
+func (b *BodyCache) Load(ctx *fasthttp.RequestCtx) ([]byte, error) {
+ b.once.Do(func() {
+ b.Data = ctx.PostBody()
+ })
+ return b.Data, b.Err
+}
+
+// Context fasthttp 上下文封装
+type Context struct {
+ RequestCtx *fasthttp.RequestCtx
+ params map[string]string
+ index int
+ handlers []HandlerFunc
+
+ bodyCache BodyCache
+ keys map[string]interface{}
+ aborted bool
+}
+
+// Next 执行下一个中间件
+func (c *Context) Next() error {
+ c.index++
+ if c.index < len(c.handlers) {
+ return c.handlers[c.index](c)
+ }
+ return nil
+}
+
+// Abort 中止执行
+func (c *Context) Abort() {
+ c.aborted = true
+}
+
+// Param 获取路由参数
+func (c *Context) Param(key string) string {
+ return c.params[key]
+}
+
+// Header 获取请求头
+func (c *Context) Header(key string) string {
+ return string(c.RequestCtx.Request.Header.Peek(key))
+}
+
+// Body 获取请求体
+func (c *Context) Body() ([]byte, error) {
+ return c.bodyCache.Load(c.RequestCtx)
+}
+
+// BindJSON 反序列化 JSON 请求体
+func (c *Context) BindJSON(obj interface{}) error {
+ body, err := c.Body()
+ if err != nil {
+ return err
+ }
+ return json.Unmarshal(body, obj)
+}
+
+// PostForm 获取表单参数
+func (c *Context) PostForm(key string) string {
+ return string(c.RequestCtx.FormValue(key))
+}
+
+// Set 存储键值
+func (c *Context) Set(key string, value interface{}) {
+ if c.keys == nil {
+ c.keys = make(map[string]interface{})
+ }
+ c.keys[key] = value
+}
+
+// Get 获取键值
+func (c *Context) Get(key string) (interface{}, bool) {
+ if c.keys == nil {
+ return nil, false
+ }
+ val, ok := c.keys[key]
+ return val, ok
+}
+
+// JSON 返回 JSON 响应,支持返回 error
+func (c *Context) JSON(statusCode int, data interface{}) error {
+ c.RequestCtx.SetStatusCode(statusCode)
+ c.RequestCtx.SetContentType("application/json; charset=utf-8")
+
+ if err := json.NewEncoder(c.RequestCtx).Encode(data); err != nil {
+ c.Abort()
+ return err
+ }
+
+ c.Abort()
+ return nil
+}
+
+// Text 返回纯文本,支持返回 error
+func (c *Context) Text(statusCode int, msg string) error {
+ c.RequestCtx.SetStatusCode(statusCode)
+ c.RequestCtx.SetContentType("text/plain; charset=utf-8")
+
+ if _, err := c.RequestCtx.WriteString(msg); err != nil {
+ c.Abort()
+ return err
+ }
+
+ c.Abort()
+ return nil
+}
diff --git a/code/base-project/base-go-v2/internal/router/router.go b/code/base-project/base-go-v2/internal/router/router.go
new file mode 100644
index 00000000..edaba7a7
--- /dev/null
+++ b/code/base-project/base-go-v2/internal/router/router.go
@@ -0,0 +1,222 @@
+package router
+
+import (
+ "strings"
+
+ "github.com/valyala/fasthttp"
+)
+
+func (n *node) matchChild(part string) *node {
+ for _, child := range n.children {
+ if child.part == part || child.isParam {
+ return child
+ }
+ }
+ return nil
+}
+
+func (n *node) matchChildren(part string) []*node {
+ nodes := make([]*node, 0)
+ for _, child := range n.children {
+ if child.part == part || child.isParam {
+ nodes = append(nodes, child)
+ }
+ }
+ return nodes
+}
+
+func (n *node) insert(pattern string, parts []string, height int, handler HandlerFunc) {
+ if len(parts) == height {
+ n.pattern = pattern
+ n.handler = handler
+ return
+ }
+ part := parts[height]
+ child := n.matchChild(part)
+ if child == nil {
+ child = &node{
+ part: part,
+ isParam: len(part) > 0 && part[0] == ':',
+ }
+ n.children = append(n.children, child)
+ }
+ child.insert(pattern, parts, height+1, handler)
+}
+
+func (n *node) search(parts []string, height int, params map[string]string) *node {
+ if len(parts) == height || n.part == "*" {
+ if n.pattern == "" {
+ return nil
+ }
+ return n
+ }
+ part := parts[height]
+ children := n.matchChildren(part)
+
+ for _, child := range children {
+ if child.isParam {
+ params[child.part[1:]] = part
+ }
+ res := child.search(parts, height+1, params)
+ if res != nil {
+ return res
+ }
+ }
+ return nil
+}
+
+// 中间件条目
+type middlewareEntry struct {
+ handler HandlerFunc
+ excludePaths map[string]struct{}
+}
+
+// Router 主结构
+type Router struct {
+ roots map[string]*node
+ middleware []middlewareEntry
+ basePath string
+}
+
+// NewRouter 创建
+func NewRouter(basePath ...string) *Router {
+ path := "/"
+ if len(basePath) > 0 && basePath[0] != "" {
+ path = basePath[0]
+ }
+ return &Router{
+ roots: make(map[string]*node),
+ basePath: path,
+ }
+}
+
+type MiddlewareHandle struct {
+ router *Router
+ entryIdx int
+}
+
+func (r *Router) Use(handler HandlerFunc) *MiddlewareHandle {
+ entry := middlewareEntry{handler: handler}
+ r.middleware = append(r.middleware, entry)
+ return &MiddlewareHandle{
+ router: r,
+ entryIdx: len(r.middleware) - 1,
+ }
+}
+
+func (mh *MiddlewareHandle) ExcludePaths(paths ...string) *Router {
+ excludeMap := make(map[string]struct{}, len(paths))
+ for _, p := range paths {
+ excludeMap[p] = struct{}{}
+ }
+ mh.router.middleware[mh.entryIdx].excludePaths = excludeMap
+ return mh.router
+}
+
+func (r *Router) Group(prefix string, m ...HandlerFunc) *Router {
+ newMiddleware := make([]middlewareEntry, len(r.middleware))
+ copy(newMiddleware, r.middleware)
+ for _, handler := range m {
+ entry := middlewareEntry{handler: handler}
+ newMiddleware = append(newMiddleware, entry)
+ }
+ return &Router{
+ roots: r.roots,
+ middleware: newMiddleware,
+ basePath: joinPaths(r.basePath, prefix),
+ }
+}
+
+func (r *Router) GET(path string, handler HandlerFunc) *Router {
+ return r.handle("GET", path, handler)
+}
+
+func (r *Router) POST(path string, handler HandlerFunc) *Router {
+ return r.handle("POST", path, handler)
+}
+
+func (r *Router) handle(method, path string, handler HandlerFunc) *Router {
+ fullPath := joinPaths(r.basePath, path)
+ parts := parsePattern(fullPath)
+ if r.roots[method] == nil {
+ r.roots[method] = &node{}
+ }
+ r.roots[method].insert(fullPath, parts, 0, handler)
+ return r
+}
+
+// Handler 入口函数(fasthttp)
+func (r *Router) Handler(ctx *fasthttp.RequestCtx) {
+ method := string(ctx.Method())
+ root := r.roots[method]
+ if root == nil {
+ ctx.Error("404 Not Found", fasthttp.StatusNotFound)
+ return
+ }
+
+ searchParts := parsePattern(string(ctx.Path()))
+ params := make(map[string]string)
+ n := root.search(searchParts, 0, params)
+ if n == nil || n.handler == nil {
+ ctx.Error("404 Not Found", fasthttp.StatusNotFound)
+ return
+ }
+
+ // 组装中间件链
+ handlers := make([]HandlerFunc, 0)
+ path := string(ctx.Path())
+ for _, m := range r.middleware {
+ if m.excludePaths != nil {
+ if _, excluded := m.excludePaths[path]; excluded {
+ continue
+ }
+ }
+ handlers = append(handlers, m.handler)
+ }
+ handlers = append(handlers, n.handler) // 最终 handler
+
+ c := &Context{
+ RequestCtx: ctx,
+ params: params,
+ index: -1,
+ handlers: handlers,
+ }
+
+ _ = c.Next()
+}
+
+func parsePattern(pattern string) []string {
+ vs := strings.Split(strings.Trim(pattern, "/"), "/")
+ parts := make([]string, 0, len(vs))
+ for _, item := range vs {
+ if item != "" {
+ parts = append(parts, item)
+ }
+ }
+ return parts
+}
+
+func joinPaths(a, b string) string {
+ if a == "" {
+ if b == "" {
+ return "/"
+ }
+ if !strings.HasPrefix(b, "/") {
+ return "/" + b
+ }
+ return b
+ }
+ if b == "" {
+ return a
+ }
+ aslash := strings.HasSuffix(a, "/")
+ bslash := strings.HasPrefix(b, "/")
+ switch {
+ case aslash && bslash:
+ return a + b[1:]
+ case !aslash && !bslash:
+ return a + "/" + b
+ default:
+ return a + b
+ }
+}
diff --git a/code/base-project/base-go-v2/internal/router/types.go b/code/base-project/base-go-v2/internal/router/types.go
new file mode 100644
index 00000000..59ada547
--- /dev/null
+++ b/code/base-project/base-go-v2/internal/router/types.go
@@ -0,0 +1,12 @@
+package router
+
+type HandlerFunc func(c *Context) error
+
+// node 路由树节点
+type node struct {
+ pattern string
+ part string
+ children []*node
+ isParam bool
+ handler HandlerFunc
+}
diff --git a/code/base-project/base-go-v2/internal/types/appConfig.go b/code/base-project/base-go-v2/internal/types/appConfig.go
index 89e6f171..00ded23a 100644
--- a/code/base-project/base-go-v2/internal/types/appConfig.go
+++ b/code/base-project/base-go-v2/internal/types/appConfig.go
@@ -12,7 +12,8 @@ type AppConfig struct {
}
type ServerConfig struct {
- Port int
+ Port int
+ BaseUrl string
}
type JWTConfig struct {
diff --git a/code/base-project/base-go-v2/logs/error/error-2025-11-01.log b/code/base-project/base-go-v2/logs/error/error-2025-11-01.log
new file mode 100644
index 00000000..bf2f4101
--- /dev/null
+++ b/code/base-project/base-go-v2/logs/error/error-2025-11-01.log
@@ -0,0 +1 @@
+{"level":"ERROR","ts":"2025-11-01 13:46:05.593","caller":"middleware/recovery.go:16","msg":"请求 panic","path":"/app/test","method":"GET","panic":"interface conversion: interface {} is uint64, not string","stack":"goroutine 35 [running]:\nruntime/debug.Stack()\n\tD:/devtool/golang/go1.25.0/src/runtime/debug/stack.go:26 +0x5e\nbase-go-v2/internal/api.InitAppRouter.RecoveryMiddleware.func2.1()\n\tD:/workspace/code/base-project/base-go-v2/internal/api/middleware/recovery.go:14 +0x4b\npanic({0x7ff7c009b840?, 0xc000094240?})\n\tD:/devtool/golang/go1.25.0/src/runtime/panic.go:783 +0x132\nbase-go-v2/internal/reponse.(*Builder).Send(0xc0001b7640)\n\tD:/workspace/code/base-project/base-go-v2/internal/reponse/response.go:89 +0xeb\nbase-go-v2/internal/api.InitAppRouter.func1(0xa0?)\n\tD:/workspace/code/base-project/base-go-v2/internal/api/router.go:19 +0x6b\nbase-go-v2/internal/router.(*Context).Next(0x7ff7bfd30a85?)\n\tD:/workspace/code/base-project/base-go-v2/internal/router/context.go:41 +0x35\nbase-go-v2/internal/api.InitAppRouter.ErrorMiddleware.func5(0xc0000a4080)\n\tD:/workspace/code/base-project/base-go-v2/internal/api/middleware/error.go:12 +0x2a\nbase-go-v2/internal/router.(*Context).Next(0x7ff7c010426d?)\n\tD:/workspace/code/base-project/base-go-v2/internal/router/context.go:41 +0x35\nbase-go-v2/internal/api.InitAppRouter.LoggerMiddleware.func4(0xc0000a4080)\n\tD:/workspace/code/base-project/base-go-v2/internal/api/middleware/logger.go:27 +0x77\nbase-go-v2/internal/router.(*Context).Next(0xc0000a2660?)\n\tD:/workspace/code/base-project/base-go-v2/internal/router/context.go:41 +0x35\nbase-go-v2/internal/api.InitAppRouter.CORSMiddleware.func3(0xc0000a4080)\n\tD:/workspace/code/base-project/base-go-v2/internal/api/middleware/cors.go:22 +0x159\nbase-go-v2/internal/router.(*Context).Next(0xc0001840a0?)\n\tD:/workspace/code/base-project/base-go-v2/internal/router/context.go:41 +0x35\nbase-go-v2/internal/api.InitAppRouter.RecoveryMiddleware.func2(0xc0000900a0?)\n\tD:/workspace/code/base-project/base-go-v2/internal/api/middleware/recovery.go:29 +0x45\nbase-go-v2/internal/router.(*Context).Next(...)\n\tD:/workspace/code/base-project/base-go-v2/internal/router/context.go:41\nbase-go-v2/internal/router.(*Router).Handler(0xc0001a81e0, 0xc0000a2608)\n\tD:/workspace/code/base-project/base-go-v2/internal/router/router.go:185 +0x402\ngithub.com/valyala/fasthttp.(*Server).serveConn(0xc0001d0008, {0x7ff7c01a5358, 0xc000096008})\n\tD:/devtool/golang/gopath/pkg/mod/github.com/valyala/fasthttp@v1.68.0/server.go:2465 +0x124b\ngithub.com/valyala/fasthttp.(*workerPool).workerFunc(0xc0001d2000, 0xc000090040)\n\tD:/devtool/golang/gopath/pkg/mod/github.com/valyala/fasthttp@v1.68.0/workerpool.go:225 +0x92\ngithub.com/valyala/fasthttp.(*workerPool).getCh.func1()\n\tD:/devtool/golang/gopath/pkg/mod/github.com/valyala/fasthttp@v1.68.0/workerpool.go:197 +0x32\ncreated by github.com/valyala/fasthttp.(*workerPool).getCh in goroutine 1\n\tD:/devtool/golang/gopath/pkg/mod/github.com/valyala/fasthttp@v1.68.0/workerpool.go:196 +0x194\n","trackId":"375158538101587968"}
diff --git a/code/base-project/base-go-v2/logs/info/info-2025-11-01.log b/code/base-project/base-go-v2/logs/info/info-2025-11-01.log
new file mode 100644
index 00000000..7aa85053
--- /dev/null
+++ b/code/base-project/base-go-v2/logs/info/info-2025-11-01.log
@@ -0,0 +1,9 @@
+{"level":"INFO","ts":"2025-11-01 11:02:11.221","caller":"middleware/logger.go:31","msg":"请求结束","logType":"request","trackID":375117289755770880,"path":"/ping","method":"GET","elapsed":0.0009988,"trackId":"375117289755770880"}
+{"level":"INFO","ts":"2025-11-01 11:03:01.633","caller":"middleware/logger.go:31","msg":"请求结束","logType":"request","path":"/ping","method":"GET","elapsed":0.0008635,"trackId":"375117501199024128"}
+{"level":"INFO","ts":"2025-11-01 11:06:32.290","caller":"middleware/logger.go:34","msg":"请求结束","logType":"request","path":"/ping","method":"GET","clientIP":"\u0000\u0000\u0001","elapsed":0.0005278,"trackId":"375118384762716160"}
+{"level":"INFO","ts":"2025-11-01 11:07:13.255","caller":"middleware/logger.go:34","msg":"请求结束","logType":"request","path":"/ping","method":"GET","clientIP":"127.0.0.1","elapsed":0.0006027,"trackId":"375118556582379520"}
+{"level":"INFO","ts":"2025-11-01 11:07:55.599","caller":"middleware/logger.go:41","msg":"请求结束","logType":"request","path":"/ping","method":"GET","ip":"127.0.0.1","elapsed":0.0010004,"trackId":"375118734181793792"}
+{"level":"INFO","ts":"2025-11-01 13:19:29.451","caller":"middleware/logger.go:34","msg":"请求结束","path":"/app/app/ping","method":"GET","ip":"127.0.0.1","elapsed":0.0005252,"trackId":"375151843401007104"}
+{"level":"INFO","ts":"2025-11-01 13:26:31.817","caller":"middleware/logger.go:34","msg":"请求结束","path":"/app/test","method":"GET","ip":"127.0.0.1","elapsed":0.0019991,"trackId":"375153614924021760"}
+{"level":"INFO","ts":"2025-11-01 13:43:51.649","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/test","method":"GET","ip":"127.0.0.1","elapsed":0.0005048,"trackId":"375157976299732992"}
+{"level":"INFO","ts":"2025-11-01 13:46:59.502","caller":"middleware/logger.go:34","msg":"接口请求","path":"/app/test","method":"GET","ip":"127.0.0.1","elapsed":0.001,"trackId":"375158764212322304"}
diff --git a/code/base-project/base-go-v2/logs/warn/warn-2025-11-01.log b/code/base-project/base-go-v2/logs/warn/warn-2025-11-01.log
new file mode 100644
index 00000000..e69de29b