lv 2 days ago
parent
commit
5cef6e9cfc
11 changed files with 593 additions and 9 deletions
  1. 19 5
      app.json
  2. 8 4
      package.json
  3. 208 0
      pnpm-lock.yaml
  4. 9 0
      src/config.json
  5. 8 0
      src/index.d.ts
  6. 171 0
      src/utils/api.ts
  7. 9 0
      src/utils/auth.ts
  8. 112 0
      src/utils/cache.ts
  9. 6 0
      src/utils/size.ts
  10. 8 0
      src/utils/size.web.ts
  11. 35 0
      src/utils/storage.ts

+ 19 - 5
app.json

@@ -1,14 +1,21 @@
 {
   "expo": {
-    "name": "assistant",
+    "name": "Loan Assistant",
     "slug": "assistant",
     "version": "1.0.0",
     "orientation": "portrait",
     "icon": "./assets/images/icon.png",
-    "scheme": "assistant",
-    "userInterfaceStyle": "automatic",
+    "scheme": "loanassistant",
+    "userInterfaceStyle": "light",
     "ios": {
-      "icon": "./assets/expo.icon"
+      "icon": "./assets/expo.icon",
+      "bundleIdentifier": "com.cdloan.assistant",
+      "buildNumber": "1",
+      "infoPlist": {
+        "CFBundleDisplayName": "借贷助手",
+        "NSCameraUsageDescription": "需要访问相机权限以扫描二维码",
+        "NSPhotoLibraryUsageDescription": "需要访问照片权限以选择图片"
+      }
     },
     "android": {
       "adaptiveIcon": {
@@ -17,7 +24,14 @@
         "backgroundImage": "./assets/images/android-icon-background.png",
         "monochromeImage": "./assets/images/android-icon-monochrome.png"
       },
-      "predictiveBackGestureEnabled": false
+      "predictiveBackGestureEnabled": false,
+      "package": "com.cdloan.assistant",
+      "versionCode": 1,
+      "permissions": [
+        "CAMERA",
+        "READ_MEDIA_IMAGES",
+        "READ_MEDIA_VIDEO"
+      ]
     },
     "web": {
       "output": "static",

+ 8 - 4
package.json

@@ -5,8 +5,8 @@
   "scripts": {
     "start": "expo start",
     "reset-project": "node ./scripts/reset-project.js",
-    "android": "expo start --android",
-    "ios": "expo start --ios",
+    "android": "expo run:android",
+    "ios": "expo run:ios",
     "web": "expo start --web",
     "lint": "expo lint"
   },
@@ -14,6 +14,7 @@
     "@react-navigation/bottom-tabs": "^7.15.5",
     "@react-navigation/elements": "^2.9.10",
     "@react-navigation/native": "^7.1.33",
+    "axios": "^1.14.0",
     "expo": "~55.0.12",
     "expo-constants": "~55.0.12",
     "expo-device": "~55.0.13",
@@ -30,12 +31,15 @@
     "react": "19.2.0",
     "react-dom": "19.2.0",
     "react-native": "0.83.4",
+    "react-native-fs-turbo": "^0.5.1",
     "react-native-gesture-handler": "~2.30.0",
-    "react-native-worklets": "0.7.2",
+    "react-native-mmkv": "^4.3.1",
+    "react-native-nitro-modules": "^0.35.3",
     "react-native-reanimated": "4.2.1",
     "react-native-safe-area-context": "~5.6.2",
     "react-native-screens": "~4.23.0",
-    "react-native-web": "~0.21.0"
+    "react-native-web": "~0.21.0",
+    "react-native-worklets": "0.7.2"
   },
   "devDependencies": {
     "@types/react": "~19.2.2",

+ 208 - 0
pnpm-lock.yaml

@@ -17,6 +17,9 @@ importers:
       '@react-navigation/native':
         specifier: ^7.1.33
         version: 7.2.2(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+      axios:
+        specifier: ^1.14.0
+        version: 1.14.0
       expo:
         specifier: ~55.0.12
         version: 55.0.12(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-router@55.0.11)(react-dom@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)(typescript@5.9.3)
@@ -65,9 +68,18 @@ 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-fs-turbo:
+        specifier: ^0.5.1
+        version: 0.5.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-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-mmkv:
+        specifier: ^4.3.1
+        version: 4.3.1(react-native-nitro-modules@0.35.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)
+      react-native-nitro-modules:
+        specifier: ^0.35.3
+        version: 0.35.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-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)
@@ -1313,6 +1325,12 @@ packages:
   asap@2.0.6:
     resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==}
 
+  asynckit@0.4.0:
+    resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
+
+  axios@1.14.0:
+    resolution: {integrity: sha512-3Y8yrqLSwjuzpXuZ0oIYZ/XGgLwUIBU3uLvbcpb0pidD9ctpShJd43KSlEEkVQg6DS0G9NKyzOvBfUtDKEyHvQ==}
+
   babel-jest@29.7.0:
     resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==}
     engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@@ -1443,6 +1461,10 @@ packages:
     resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
     engines: {node: '>= 0.8'}
 
+  call-bind-apply-helpers@1.0.2:
+    resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
+    engines: {node: '>= 0.4'}
+
   camelcase@5.3.1:
     resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==}
     engines: {node: '>=6'}
@@ -1516,6 +1538,10 @@ packages:
     resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==}
     engines: {node: '>=12.5.0'}
 
+  combined-stream@1.0.8:
+    resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
+    engines: {node: '>= 0.8'}
+
   commander@12.1.0:
     resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==}
     engines: {node: '>=18'}
@@ -1601,6 +1627,10 @@ packages:
     resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==}
     engines: {node: '>=8'}
 
+  delayed-stream@1.0.0:
+    resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
+    engines: {node: '>=0.4.0'}
+
   depd@2.0.0:
     resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
     engines: {node: '>= 0.8'}
@@ -1619,6 +1649,10 @@ packages:
   dnssd-advertise@1.1.4:
     resolution: {integrity: sha512-AmGyK9WpNf06WeP5TjHZq/wNzP76OuEeaiTlKr9E/EEelYLczywUKoqRz+DPRq/ErssjT4lU+/W7wzJW+7K/ZA==}
 
+  dunder-proto@1.0.1:
+    resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
+    engines: {node: '>= 0.4'}
+
   ee-first@1.1.1:
     resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
 
@@ -1639,6 +1673,22 @@ packages:
   error-stack-parser@2.1.4:
     resolution: {integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==}
 
+  es-define-property@1.0.1:
+    resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
+    engines: {node: '>= 0.4'}
+
+  es-errors@1.3.0:
+    resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
+    engines: {node: '>= 0.4'}
+
+  es-object-atoms@1.1.1:
+    resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
+    engines: {node: '>= 0.4'}
+
+  es-set-tostringtag@2.1.0:
+    resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
+    engines: {node: '>= 0.4'}
+
   escalade@3.2.0:
     resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
     engines: {node: '>=6'}
@@ -1878,9 +1928,22 @@ packages:
   flow-enums-runtime@0.0.6:
     resolution: {integrity: sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw==}
 
+  follow-redirects@1.15.11:
+    resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==}
+    engines: {node: '>=4.0'}
+    peerDependencies:
+      debug: '*'
+    peerDependenciesMeta:
+      debug:
+        optional: true
+
   fontfaceobserver@2.3.0:
     resolution: {integrity: sha512-6FPvD/IVyT4ZlNe7Wcn5Fb/4ChigpucKYSvD6a+0iMoLn2inpo711eyIcKjmDtE5XNcgAkSH9uN/nfAeZzHEfg==}
 
+  form-data@4.0.5:
+    resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
+    engines: {node: '>= 6'}
+
   fresh@0.5.2:
     resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==}
     engines: {node: '>= 0.6'}
@@ -1904,6 +1967,10 @@ packages:
     resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
     engines: {node: 6.* || 8.* || >= 10.*}
 
+  get-intrinsic@1.3.0:
+    resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
+    engines: {node: '>= 0.4'}
+
   get-nonce@1.0.1:
     resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==}
     engines: {node: '>=6'}
@@ -1912,6 +1979,10 @@ packages:
     resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==}
     engines: {node: '>=8.0.0'}
 
+  get-proto@1.0.1:
+    resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
+    engines: {node: '>= 0.4'}
+
   getenv@2.0.0:
     resolution: {integrity: sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ==}
     engines: {node: '>=6'}
@@ -1924,6 +1995,10 @@ packages:
     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
 
+  gopd@1.2.0:
+    resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
+    engines: {node: '>= 0.4'}
+
   graceful-fs@4.2.11:
     resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
 
@@ -1935,6 +2010,14 @@ packages:
     resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
     engines: {node: '>=8'}
 
+  has-symbols@1.1.0:
+    resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
+    engines: {node: '>= 0.4'}
+
+  has-tostringtag@1.0.2:
+    resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
+    engines: {node: '>= 0.4'}
+
   hasown@2.0.2:
     resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
     engines: {node: '>= 0.4'}
@@ -2221,6 +2304,10 @@ packages:
   marky@1.3.0:
     resolution: {integrity: sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==}
 
+  math-intrinsics@1.1.0:
+    resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
+    engines: {node: '>= 0.4'}
+
   memoize-one@5.2.1:
     resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==}
 
@@ -2519,6 +2606,10 @@ packages:
     resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==}
     engines: {node: '>= 6'}
 
+  proxy-from-env@2.1.0:
+    resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==}
+    engines: {node: '>=10'}
+
   query-string@7.1.3:
     resolution: {integrity: sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==}
     engines: {node: '>=6'}
@@ -2556,6 +2647,12 @@ packages:
   react-is@19.2.4:
     resolution: {integrity: sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==}
 
+  react-native-fs-turbo@0.5.1:
+    resolution: {integrity: sha512-r5r8S05E/lapqG+BEnCN9hhHiiSns4SH/zZO7HhZze5LF4Z6uH1tLi/nK/zgq2A9hXrEdBBwlrEg4ltbY4hbAQ==}
+    peerDependencies:
+      react: '*'
+      react-native: '*'
+
   react-native-gesture-handler@2.30.1:
     resolution: {integrity: sha512-xIUBDo5ktmJs++0fZlavQNvDEE4PsihWhSeJsJtoz4Q6p0MiTM9TgrTgfEgzRR36qGPytFoeq+ShLrVwGdpUdA==}
     peerDependencies:
@@ -2574,6 +2671,19 @@ packages:
       react: '*'
       react-native: '*'
 
+  react-native-mmkv@4.3.1:
+    resolution: {integrity: sha512-APyGGaaHtayVgT18dBM8QGGZKr9pGfSTiBwbbPNzhGGfJQSU7awLGRGq879OqYl31HmVks9hOBLCs+qfgacRZg==}
+    peerDependencies:
+      react: '*'
+      react-native: '*'
+      react-native-nitro-modules: '*'
+
+  react-native-nitro-modules@0.35.3:
+    resolution: {integrity: sha512-pfPfFtGeMfFpLo4HidTFs5zsAs1wRh9qaWGSiF5UeI2YQaAgRHPlson0UNxw25pTCLV9NBrKgSDaMVfPEXXbNw==}
+    peerDependencies:
+      react: '*'
+      react-native: '*'
+
   react-native-reanimated@4.2.1:
     resolution: {integrity: sha512-/NcHnZMyOvsD/wYXug/YqSKw90P9edN0kEPL5lP4PFf1aQ4F1V7MKe/E0tvfkXKIajy3Qocp5EiEnlcrK/+BZg==}
     peerDependencies:
@@ -4711,6 +4821,16 @@ snapshots:
 
   asap@2.0.6: {}
 
+  asynckit@0.4.0: {}
+
+  axios@1.14.0:
+    dependencies:
+      follow-redirects: 1.15.11
+      form-data: 4.0.5
+      proxy-from-env: 2.1.0
+    transitivePeerDependencies:
+      - debug
+
   babel-jest@29.7.0(@babel/core@7.29.0):
     dependencies:
       '@babel/core': 7.29.0
@@ -4898,6 +5018,11 @@ snapshots:
 
   bytes@3.1.2: {}
 
+  call-bind-apply-helpers@1.0.2:
+    dependencies:
+      es-errors: 1.3.0
+      function-bind: 1.1.2
+
   camelcase@5.3.1: {}
 
   camelcase@6.3.0: {}
@@ -4977,6 +5102,10 @@ snapshots:
       color-convert: 2.0.1
       color-string: 1.9.1
 
+  combined-stream@1.0.8:
+    dependencies:
+      delayed-stream: 1.0.0
+
   commander@12.1.0: {}
 
   commander@2.20.3: {}
@@ -5056,6 +5185,8 @@ snapshots:
 
   define-lazy-prop@2.0.0: {}
 
+  delayed-stream@1.0.0: {}
+
   depd@2.0.0: {}
 
   destroy@1.2.0: {}
@@ -5066,6 +5197,12 @@ snapshots:
 
   dnssd-advertise@1.1.4: {}
 
+  dunder-proto@1.0.1:
+    dependencies:
+      call-bind-apply-helpers: 1.0.2
+      es-errors: 1.3.0
+      gopd: 1.2.0
+
   ee-first@1.1.1: {}
 
   electron-to-chromium@1.5.333: {}
@@ -5080,6 +5217,21 @@ snapshots:
     dependencies:
       stackframe: 1.3.4
 
+  es-define-property@1.0.1: {}
+
+  es-errors@1.3.0: {}
+
+  es-object-atoms@1.1.1:
+    dependencies:
+      es-errors: 1.3.0
+
+  es-set-tostringtag@2.1.0:
+    dependencies:
+      es-errors: 1.3.0
+      get-intrinsic: 1.3.0
+      has-tostringtag: 1.0.2
+      hasown: 2.0.2
+
   escalade@3.2.0: {}
 
   escape-html@1.0.3: {}
@@ -5364,8 +5516,18 @@ snapshots:
 
   flow-enums-runtime@0.0.6: {}
 
+  follow-redirects@1.15.11: {}
+
   fontfaceobserver@2.3.0: {}
 
+  form-data@4.0.5:
+    dependencies:
+      asynckit: 0.4.0
+      combined-stream: 1.0.8
+      es-set-tostringtag: 2.1.0
+      hasown: 2.0.2
+      mime-types: 2.1.35
+
   fresh@0.5.2: {}
 
   fs.realpath@1.0.0: {}
@@ -5379,10 +5541,28 @@ snapshots:
 
   get-caller-file@2.0.5: {}
 
+  get-intrinsic@1.3.0:
+    dependencies:
+      call-bind-apply-helpers: 1.0.2
+      es-define-property: 1.0.1
+      es-errors: 1.3.0
+      es-object-atoms: 1.1.1
+      function-bind: 1.1.2
+      get-proto: 1.0.1
+      gopd: 1.2.0
+      has-symbols: 1.1.0
+      hasown: 2.0.2
+      math-intrinsics: 1.1.0
+
   get-nonce@1.0.1: {}
 
   get-package-type@0.1.0: {}
 
+  get-proto@1.0.1:
+    dependencies:
+      dunder-proto: 1.0.1
+      es-object-atoms: 1.1.1
+
   getenv@2.0.0: {}
 
   glob@13.0.6:
@@ -5400,12 +5580,20 @@ snapshots:
       once: 1.4.0
       path-is-absolute: 1.0.1
 
+  gopd@1.2.0: {}
+
   graceful-fs@4.2.11: {}
 
   has-flag@3.0.0: {}
 
   has-flag@4.0.0: {}
 
+  has-symbols@1.1.0: {}
+
+  has-tostringtag@1.0.2:
+    dependencies:
+      has-symbols: 1.1.0
+
   hasown@2.0.2:
     dependencies:
       function-bind: 1.1.2
@@ -5691,6 +5879,8 @@ snapshots:
 
   marky@1.3.0: {}
 
+  math-intrinsics@1.1.0: {}
+
   memoize-one@5.2.1: {}
 
   memoize-one@6.0.0: {}
@@ -6059,6 +6249,8 @@ snapshots:
       kleur: 3.0.3
       sisteransi: 1.0.5
 
+  proxy-from-env@2.1.0: {}
+
   query-string@7.1.3:
     dependencies:
       decode-uri-component: 0.2.2
@@ -6097,6 +6289,11 @@ snapshots:
 
   react-is@19.2.4: {}
 
+  react-native-fs-turbo@0.5.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:
+      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
@@ -6115,6 +6312,17 @@ 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-mmkv@4.3.1(react-native-nitro-modules@0.35.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:
+      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-nitro-modules: 0.35.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-nitro-modules@0.35.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:
+      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-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

+ 9 - 0
src/config.json

@@ -0,0 +1,9 @@
+
+{
+    "api": {
+        "url": "http://localhost:8080/v1/",
+        "timeout": 15000
+    },
+    
+    "jsVersion": "1"
+}

+ 8 - 0
src/index.d.ts

@@ -0,0 +1,8 @@
+interface ApiConfig {
+    url: string;
+    timeout?: number;
+}
+interface AppConfig {
+    api: ApiConfig;
+    jsVersion: string;
+}

+ 171 - 0
src/utils/api.ts

@@ -0,0 +1,171 @@
+// import { getAccessToken } from '@/apis/auth';
+const {api: apiConfig, jsVersion} = require('@/config') as AppConfig;
+import axios from 'axios';
+import Constants from 'expo-constants';
+import { Platform } from 'react-native';
+import { getAccessToken } from './auth';
+
+
+
+
+
+export class ApiError extends Error {
+    public get data() {
+        return this._data;
+    }
+    name: string = 'ApiError';
+    constructor(message: string, public code: number, private _data?: any) {
+        if (__DEV__) {
+            message = `${message} (code: ${code})`;
+        }
+        super(message);
+    }
+
+    static is(err: any) {
+        return err instanceof ApiError ? err as ApiError : null;
+    }
+    static is5xx(err: any) {
+        return err instanceof ApiError && err.code >= 500 && err.code < 600 ? err as ApiError : null;
+    }
+    static is4xx(err: any) {
+        return err instanceof ApiError && err.code >= 400 && err.code < 500 ? err as ApiError : null;
+    }
+    static is3xx(err: any) {
+        return err instanceof ApiError && err.code >= 300 && err.code < 400 ? err as ApiError : null;
+    }
+}
+
+
+interface ApiResponse<T> {
+    code?: number;
+    message?: string;
+    data?: T;
+}
+
+const verString = (() => {
+    const expoConfig = Constants.expoConfig;
+    const versionName = expoConfig?.version || '';
+    const platformSpecific = Platform.OS === 'android' ? expoConfig?.android?.versionCode || '' : expoConfig?.ios?.buildNumber || '';
+    return `${versionName}(${platformSpecific})-${jsVersion}`;
+})();
+const apiClient = axios.create({
+    baseURL: apiConfig.url,
+    timeout: apiConfig.timeout || 10000,
+    
+    headers: {
+        "x-app-name": Constants.expoConfig?.slug || 'unknown',
+        "x-app-version": verString,
+        "x-app-platform": Platform.OS,
+        'Content-Type': 'application/json'
+    },
+
+    validateStatus: (status) => {
+        return status > 199 && status < 500;
+    },
+    
+});
+
+// 开发模式下输出请求和响应数据
+if (__DEV__) {
+    // 请求拦截器:输出请求信息
+    apiClient.interceptors.request.use(
+        (config) => {
+            console.log('📤 API 请求:', {
+                url: config.url,
+                method: config.method?.toUpperCase(),
+                baseURL: config.baseURL,
+                headers: config.headers,
+                params: config.params,
+                data: config.data,
+            });
+            return config;
+        },
+        (error) => {
+            console.log('❌ API 请求配置错误:', error);
+            return Promise.reject(error);
+        }
+    );
+
+    // 响应拦截器:输出响应数据
+    apiClient.interceptors.response.use(
+        (response) => {
+            console.log('📡 API 响应:', {
+                url: response.config.url,
+                method: response.config.method?.toUpperCase(),
+                status: response.status,
+                statusText: response.statusText,
+                headers: response.headers,
+                data: response.data,
+            });
+            return response;
+        },
+        (error) => {
+            if (error.response) {
+                console.log('❌ API 错误响应:', {
+                    url: error.config?.url,
+                    method: error.config?.method?.toUpperCase(),
+                    status: error.response.status,
+                    statusText: error.response.statusText,
+                    headers: error.response.headers,
+                    data: error.response.data,
+                });
+            } else if (error.request) {
+                console.log('❌ API 请求错误:', {
+                    message: error.message,
+                    request: error.request,
+                });
+            } else {
+                console.log('❌ API 错误:', error.message);
+            }
+            return Promise.reject(error);
+        }
+    );
+}
+
+
+export async function request<T>(api: string, method: 'get' | 'post' | 'put' | 'delete', params: Record<string, any>, data: any): Promise<T> {
+    const headers: Record<string, string> = {};
+    let token = getAccessToken();
+    if (token?.accessToken) {
+        headers['Authorization'] = `Bearer ${token.accessToken}`;
+    }
+    try {
+    const response = await apiClient.request<ApiResponse<T>>({
+        url: api,
+        method,
+        params,
+        data,
+        headers,
+    });
+
+    const res = response.data;
+    if (`${res?.code}` !== '1') {
+        throw new ApiError(res?.message || response.statusText, res?.code || response.status, res?.data);
+    }
+    return res?.data as T;
+} catch (error) {
+    if (error instanceof ApiError) {
+        throw error;
+    }
+
+    // @ts-ignore
+    throw new ApiError(error?.message || error?.toString() || "unknown error", 500);
+}
+}
+
+
+export async function get<T>(api: string, params: Record<string, any>): Promise<T> {
+    return await request<T>(api, 'get', params, undefined);
+}
+
+export async function post<T>(api: string, data: any): Promise<T> {
+    return await request<T>(api, 'post', {}, data);
+}
+
+export async function put<T>(api: string, data: any): Promise<T> {
+    return await request<T>(api, 'put', {}, data);
+}
+
+export async function deleted<T>(api: string): Promise<T> {
+    return await request<T>(api, 'delete', {}, undefined);
+}

+ 9 - 0
src/utils/auth.ts

@@ -0,0 +1,9 @@
+interface AccessToken {
+    accessToken: string;
+    expiresIn: number;
+    tokenType: string;
+    scope: string;
+}
+export function getAccessToken(): AccessToken |null |undefined {
+    return ;
+}

+ 112 - 0
src/utils/cache.ts

@@ -0,0 +1,112 @@
+import { useEffect, useRef, useState } from "react";
+import { getApiCache, getGlobalStorage } from "./storage";
+
+
+
+interface UseSWCOPtions<T> {
+    onError?: (e: unknown) => void;
+    onLoad?: (data: T) => void;
+    cacheOnly?: boolean;
+    cacheTimeout?: number;
+}
+// 该hook用于在组件中通过key和异步action获取数据,并自动处理加载、错误和数据状态
+
+export function useSWC<T>(key: string, action: () => Promise<T>, options?: UseSWCOPtions<T>) {
+    const optionsRef = useRef(options);
+    optionsRef.current = options;
+    
+    const [data, setData] = useState<T | null | undefined>(() => {
+        try {
+            const data = JSON.parse(getApiCache().getString(`$swc-${key}`) || "null") as T;
+            if (!data) {
+                return undefined;
+            }
+            let ttl = optionsRef.current?.cacheTimeout || MAX_CACHE_TIME;
+            // @ts-ignore
+            let cacheTime = data.$__cacheTime;
+            if (Date.now() - cacheTime > ttl * 1000) {
+                return undefined;
+            }
+            return data;
+        // eslint-disable-next-line @typescript-eslint/no-unused-vars
+        } catch (_) {
+            return undefined;
+        }
+    });
+    const [loading, setLoading] = useState<boolean>(true);
+    const [error, setError] = useState<any>(null);
+
+    const actionRef = useRef(action);
+    actionRef.current = action;
+
+    const dataRef = useRef(data);
+    dataRef.current = data;
+    const keyRef = useRef(key);
+
+    useEffect(() => {
+        let isMounted = true;
+        if (dataRef.current && optionsRef.current?.cacheOnly) {
+            return;
+        }
+        setLoading(true);
+        setError(null);
+         
+        const options = optionsRef.current;
+        const key = keyRef.current;
+        actionRef.current()
+            .then((result) => {
+                // @ts-ignore
+                result.$__cacheTime = Date.now();
+                result && getApiCache().set(`$swc-${key}`, JSON.stringify(result));
+                if (isMounted) {
+                    options?.onLoad?.(result);
+                    setData(result);
+                    setLoading(false);
+                }
+            })
+            .catch((err) => {
+                if (isMounted) {
+                    options?.onError?.(err);
+                    setError(err);
+                    setLoading(false);
+                }
+            });
+
+        return () => {
+            isMounted = false;
+        };
+     
+    }, []);
+
+    return { data, loading, error };
+}
+
+
+
+export const MAX_CACHE_TIME = 99999999999999;
+
+let lastClearTime = parseInt(getGlobalStorage().getString("last_clear_time") || "0");
+const clearTimeout = 86400 * 3 * 1000;
+// 开启一直 30 分钏的定时器,用于清理缓存
+setInterval(() => {
+   
+    let now = Date.now();
+    if (now - lastClearTime < clearTimeout) {
+        lastClearTime = now;
+        const globalStorage = getGlobalStorage();
+        const caches = getApiCache();
+
+        caches.getAllKeys().forEach(key => {
+            try {
+                let {$__cacheTime} = JSON.parse(caches.getString(key) || `{"$__cacheTime": ${MAX_CACHE_TIME}}`);
+                    if (now - $__cacheTime > clearTimeout) {
+                        caches.remove(key);
+                    }
+                // eslint-disable-next-line @typescript-eslint/no-unused-vars
+                } catch (_e) {
+                }
+            });
+
+        globalStorage.set("last_clear_time", now+"");
+    }
+}, 60 * 15 * 1000);

+ 6 - 0
src/utils/size.ts

@@ -0,0 +1,6 @@
+import { Dimensions } from "react-native";
+
+const Window = Dimensions.get('window');
+const Screen = Dimensions.get('screen');
+
+export { Screen, Window };

+ 8 - 0
src/utils/size.web.ts

@@ -0,0 +1,8 @@
+import { Dimensions } from "react-native";
+const win = Dimensions.get('window');
+const Screen = Dimensions.get('screen');
+const Window = {
+    ...win,
+    width: win.width > 768 ? 768 : win.width
+}
+export { Screen, Window };

+ 35 - 0
src/utils/storage.ts

@@ -0,0 +1,35 @@
+// import RNDeviceInfo from 'react-native-device-info';
+import RNFS from 'react-native-fs-turbo';
+import { createMMKV, MMKV } from 'react-native-mmkv';
+
+let globalStorage: MMKV | null = null;
+export function getGlobalStorage() {
+    if (!globalStorage) {
+        globalStorage = createMMKV({
+            id: `global`,
+        });
+    }
+    return globalStorage;
+}
+
+let caches: MMKV | null = null;
+export function getCaches() {
+    if (!caches) {
+        caches = createMMKV({
+            id: `caches`,
+            path: `${RNFS.CachesDirectoryPath}`,
+        });
+    }
+    return caches;
+}
+
+let apiCache: MMKV | null = null;
+export function getApiCache() {
+    if (!apiCache) {
+        apiCache = createMMKV({
+            id: `api_cache`,
+            path: `${RNFS.CachesDirectoryPath}`,
+        });
+    }
+    return apiCache;
+}