yblunan@gmail.com hai 1 mes
pai
achega
65ed014724

+ 9 - 5
.vscode/settings.json

@@ -7,13 +7,17 @@
   "editor.quickSuggestions": {
   "editor.quickSuggestions": {
     "other": "on",
     "other": "on",
     "comments": "off",
     "comments": "off",
-    "strings": "on" 
+    "strings": "on"
   },
   },
-
   // 2. 确保在输入特定字符(如引号、等号)时立即触发提示
   // 2. 确保在输入特定字符(如引号、等号)时立即触发提示
   "editor.suggestOnTriggerCharacters": true,
   "editor.suggestOnTriggerCharacters": true,
-
   // 3. 如果你在写属性名后进入了 Snippet 模式(光标在引号内),
   // 3. 如果你在写属性名后进入了 Snippet 模式(光标在引号内),
   // 下面这一项设置为 false 可以防止 Snippet 模式阻塞建议列表
   // 下面这一项设置为 false 可以防止 Snippet 模式阻塞建议列表
-  "editor.suggest.snippetsPreventQuickSuggestions": false
-}
+  "editor.suggest.snippetsPreventQuickSuggestions": false,
+  "tailwindCSS.experimental.classRegex": [
+    [
+      "clsx\\(([^)]*)\\)",
+      "(?:'|\"|`)([^']*)(?:'|\"|`)"
+    ]
+  ]
+}

+ 1 - 0
android/app/src/main/AndroidManifest.xml

@@ -4,6 +4,7 @@
   <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" tools:replace="android:maxSdkVersion"/>
   <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" tools:replace="android:maxSdkVersion"/>
   <uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
   <uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
   <uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
   <uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
+  <uses-permission android:name="android.permission.RECORD_AUDIO"/>
   <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
   <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
   <uses-permission android:name="android.permission.VIBRATE"/>
   <uses-permission android:name="android.permission.VIBRATE"/>
   <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="32" tools:replace="android:maxSdkVersion"/>
   <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="32" tools:replace="android:maxSdkVersion"/>

+ 4 - 5
app.json

@@ -16,8 +16,8 @@
       "buildNumber": "1",
       "buildNumber": "1",
       "infoPlist": {
       "infoPlist": {
         "CFBundleDisplayName": "借贷助手",
         "CFBundleDisplayName": "借贷助手",
-        "NSCameraUsageDescription": "需要访问相机权限以扫描二维码",
-        "NSPhotoLibraryUsageDescription": "需要访问照片权限以选择图片"
+        "NSCameraUsageDescription": "需要从相机拍摄资料",
+        "NSPhotoLibraryUsageDescription": "需要从相册选择资料"
       }
       }
     },
     },
     "android": {
     "android": {
@@ -32,8 +32,7 @@
       "versionCode": 1,
       "versionCode": 1,
       "permissions": [
       "permissions": [
         "CAMERA",
         "CAMERA",
-        "READ_MEDIA_IMAGES",
-        "READ_MEDIA_VIDEO"
+        "READ_MEDIA_IMAGES"
       ]
       ]
     },
     },
     "web": {
     "web": {
@@ -91,4 +90,4 @@
       }
       }
     }
     }
   }
   }
-}
+}

+ 0 - 2
ios/LoanAssistant.xcodeproj/project.pbxproj

@@ -277,13 +277,11 @@
 			);
 			);
 			inputPaths = (
 			inputPaths = (
 				"${PODS_ROOT}/Target Support Files/Pods-LoanAssistant/Pods-LoanAssistant-frameworks.sh",
 				"${PODS_ROOT}/Target Support Files/Pods-LoanAssistant/Pods-LoanAssistant-frameworks.sh",
-				"${PODS_XCFRAMEWORKS_BUILD_DIR}/React-Core-prebuilt/React.framework/React",
 				"${PODS_XCFRAMEWORKS_BUILD_DIR}/ReactNativeDependencies/ReactNativeDependencies.framework/ReactNativeDependencies",
 				"${PODS_XCFRAMEWORKS_BUILD_DIR}/ReactNativeDependencies/ReactNativeDependencies.framework/ReactNativeDependencies",
 				"${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermesvm.framework/hermesvm",
 				"${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermesvm.framework/hermesvm",
 			);
 			);
 			name = "[CP] Embed Pods Frameworks";
 			name = "[CP] Embed Pods Frameworks";
 			outputPaths = (
 			outputPaths = (
-				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/React.framework",
 				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ReactNativeDependencies.framework",
 				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ReactNativeDependencies.framework",
 				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermesvm.framework",
 				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermesvm.framework",
 			);
 			);

+ 2 - 0
ios/LoanAssistant/Info.plist

@@ -49,6 +49,8 @@
     </dict>
     </dict>
     <key>NSCameraUsageDescription</key>
     <key>NSCameraUsageDescription</key>
     <string>需要访问相机权限以扫描二维码</string>
     <string>需要访问相机权限以扫描二维码</string>
+    <key>NSMicrophoneUsageDescription</key>
+    <string>Allow $(PRODUCT_NAME) to access your microphone</string>
     <key>NSPhotoLibraryUsageDescription</key>
     <key>NSPhotoLibraryUsageDescription</key>
     <string>需要访问照片权限以选择图片</string>
     <string>需要访问照片权限以选择图片</string>
     <key>NSUserActivityTypes</key>
     <key>NSUserActivityTypes</key>

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 6 - 128
ios/Podfile.lock


+ 3 - 0
package.json

@@ -23,16 +23,19 @@
     "@react-navigation/elements": "^2.9.10",
     "@react-navigation/elements": "^2.9.10",
     "@react-navigation/native": "^7.1.33",
     "@react-navigation/native": "^7.1.33",
     "axios": "^1.14.0",
     "axios": "^1.14.0",
+    "clsx": "^2.1.1",
     "expo": "~55.0.12",
     "expo": "~55.0.12",
     "expo-application": "~55.0.14",
     "expo-application": "~55.0.14",
     "expo-blur": "~55.0.14",
     "expo-blur": "~55.0.14",
     "expo-build-properties": "~55.0.13",
     "expo-build-properties": "~55.0.13",
     "expo-constants": "~55.0.12",
     "expo-constants": "~55.0.12",
     "expo-device": "~55.0.13",
     "expo-device": "~55.0.13",
+    "expo-document-picker": "~55.0.13",
     "expo-file-system": "~55.0.16",
     "expo-file-system": "~55.0.16",
     "expo-font": "~55.0.6",
     "expo-font": "~55.0.6",
     "expo-glass-effect": "~55.0.10",
     "expo-glass-effect": "~55.0.10",
     "expo-image": "~55.0.8",
     "expo-image": "~55.0.8",
+    "expo-image-picker": "~55.0.19",
     "expo-linking": "~55.0.11",
     "expo-linking": "~55.0.11",
     "expo-router": "~55.0.11",
     "expo-router": "~55.0.11",
     "expo-splash-screen": "~55.0.16",
     "expo-splash-screen": "~55.0.16",

+ 60 - 1
pnpm-lock.yaml

@@ -31,6 +31,9 @@ importers:
       axios:
       axios:
         specifier: ^1.14.0
         specifier: ^1.14.0
         version: 1.14.0
         version: 1.14.0
+      clsx:
+        specifier: ^2.1.1
+        version: 2.1.1
       expo:
       expo:
         specifier: ~55.0.12
         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-webview@13.16.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)(typescript@5.9.3)
         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-webview@13.16.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)(typescript@5.9.3)
@@ -49,6 +52,9 @@ importers:
       expo-device:
       expo-device:
         specifier: ~55.0.13
         specifier: ~55.0.13
         version: 55.0.13(expo@55.0.12)
         version: 55.0.13(expo@55.0.12)
+      expo-document-picker:
+        specifier: ~55.0.13
+        version: 55.0.13(expo@55.0.12)
       expo-file-system:
       expo-file-system:
         specifier: ~55.0.16
         specifier: ~55.0.16
         version: 55.0.16(expo@55.0.12)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))
         version: 55.0.16(expo@55.0.12)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))
@@ -61,6 +67,9 @@ importers:
       expo-image:
       expo-image:
         specifier: ~55.0.8
         specifier: ~55.0.8
         version: 55.0.8(expo@55.0.12)(react-native-web@0.21.2(react-dom@19.2.0(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)
         version: 55.0.8(expo@55.0.12)(react-native-web@0.21.2(react-dom@19.2.0(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)
+      expo-image-picker:
+        specifier: ~55.0.19
+        version: 55.0.19(expo@55.0.12)
       expo-linking:
       expo-linking:
         specifier: ~55.0.11
         specifier: ~55.0.11
         version: 55.0.11(expo@55.0.12)(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)
         version: 55.0.11(expo@55.0.12)(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)
@@ -1753,41 +1762,49 @@ packages:
     resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==}
     resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==}
     cpu: [arm64]
     cpu: [arm64]
     os: [linux]
     os: [linux]
+    libc: [glibc]
 
 
   '@unrs/resolver-binding-linux-arm64-musl@1.11.1':
   '@unrs/resolver-binding-linux-arm64-musl@1.11.1':
     resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==}
     resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==}
     cpu: [arm64]
     cpu: [arm64]
     os: [linux]
     os: [linux]
+    libc: [musl]
 
 
   '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1':
   '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1':
     resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==}
     resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==}
     cpu: [ppc64]
     cpu: [ppc64]
     os: [linux]
     os: [linux]
+    libc: [glibc]
 
 
   '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1':
   '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1':
     resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==}
     resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==}
     cpu: [riscv64]
     cpu: [riscv64]
     os: [linux]
     os: [linux]
+    libc: [glibc]
 
 
   '@unrs/resolver-binding-linux-riscv64-musl@1.11.1':
   '@unrs/resolver-binding-linux-riscv64-musl@1.11.1':
     resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==}
     resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==}
     cpu: [riscv64]
     cpu: [riscv64]
     os: [linux]
     os: [linux]
+    libc: [musl]
 
 
   '@unrs/resolver-binding-linux-s390x-gnu@1.11.1':
   '@unrs/resolver-binding-linux-s390x-gnu@1.11.1':
     resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==}
     resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==}
     cpu: [s390x]
     cpu: [s390x]
     os: [linux]
     os: [linux]
+    libc: [glibc]
 
 
   '@unrs/resolver-binding-linux-x64-gnu@1.11.1':
   '@unrs/resolver-binding-linux-x64-gnu@1.11.1':
     resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==}
     resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==}
     cpu: [x64]
     cpu: [x64]
     os: [linux]
     os: [linux]
+    libc: [glibc]
 
 
   '@unrs/resolver-binding-linux-x64-musl@1.11.1':
   '@unrs/resolver-binding-linux-x64-musl@1.11.1':
     resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==}
     resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==}
     cpu: [x64]
     cpu: [x64]
     os: [linux]
     os: [linux]
+    libc: [musl]
 
 
   '@unrs/resolver-binding-wasm32-wasi@1.11.1':
   '@unrs/resolver-binding-wasm32-wasi@1.11.1':
     resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==}
     resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==}
@@ -2224,6 +2241,10 @@ packages:
     resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==}
     resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==}
     engines: {node: '>=0.8'}
     engines: {node: '>=0.8'}
 
 
+  clsx@2.1.1:
+    resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
+    engines: {node: '>=6'}
+
   collection-visit@1.0.0:
   collection-visit@1.0.0:
     resolution: {integrity: sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw==}
     resolution: {integrity: sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw==}
     engines: {node: '>=0.10.0'}
     engines: {node: '>=0.10.0'}
@@ -2686,6 +2707,11 @@ packages:
     peerDependencies:
     peerDependencies:
       expo: '*'
       expo: '*'
 
 
+  expo-document-picker@55.0.13:
+    resolution: {integrity: sha512-IhswJElhdzs3fKDEKW8KXYRoFkWGEsXRMYAZT46Yo56zqqy8yQXrczo33RSwD2hFzNQBdLT97SJL9N311UyS3g==}
+    peerDependencies:
+      expo: '*'
+
   expo-eas-client@55.0.5:
   expo-eas-client@55.0.5:
     resolution: {integrity: sha512-wRagCeSbSnSGVXgP7V+qiGfXzZ9hTVKWvKIOP7lwrX3MIEenNmNlO4D3RVC3aNU2GhmO3ZCZIIEre80KZoUUHA==}
     resolution: {integrity: sha512-wRagCeSbSnSGVXgP7V+qiGfXzZ9hTVKWvKIOP7lwrX3MIEenNmNlO4D3RVC3aNU2GhmO3ZCZIIEre80KZoUUHA==}
 
 
@@ -2709,6 +2735,16 @@ packages:
       react: '*'
       react: '*'
       react-native: '*'
       react-native: '*'
 
 
+  expo-image-loader@55.0.0:
+    resolution: {integrity: sha512-NOjp56wDrfuA5aiNAybBIjqIn1IxKeGJ8CECWZncQ/GzjZfyTYAHTCyeApYkdKkMBLHINzI4BbTGSlbCa0fXXQ==}
+    peerDependencies:
+      expo: '*'
+
+  expo-image-picker@55.0.19:
+    resolution: {integrity: sha512-PqOOfRz7+hbB9IFN0LfNxpJJwuPlUG0Abr0qM3Wc61OJ7FFyuKJ50QJ/fFItzSuoXifET1YIFBiXx5nA8Gkinw==}
+    peerDependencies:
+      expo: '*'
+
   expo-image@55.0.8:
   expo-image@55.0.8:
     resolution: {integrity: sha512-fNdvdYVcGn3g1x6o5AXHKzk4xX8U6rg2W9vFdE1pQO80kWCNReh003ypqSrGy4dD+zA8FtZjrNF3oMDGnPpIGQ==}
     resolution: {integrity: sha512-fNdvdYVcGn3g1x6o5AXHKzk4xX8U6rg2W9vFdE1pQO80kWCNReh003ypqSrGy4dD+zA8FtZjrNF3oMDGnPpIGQ==}
     peerDependencies:
     peerDependencies:
@@ -3071,7 +3107,7 @@ packages:
 
 
   glob@7.2.3:
   glob@7.2.3:
     resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
     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
 
 
   globals@14.0.0:
   globals@14.0.0:
     resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
     resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
@@ -3590,48 +3626,56 @@ packages:
     engines: {node: '>= 12.0.0'}
     engines: {node: '>= 12.0.0'}
     cpu: [arm64]
     cpu: [arm64]
     os: [linux]
     os: [linux]
+    libc: [glibc]
 
 
   lightningcss-linux-arm64-gnu@1.32.0:
   lightningcss-linux-arm64-gnu@1.32.0:
     resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==}
     resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==}
     engines: {node: '>= 12.0.0'}
     engines: {node: '>= 12.0.0'}
     cpu: [arm64]
     cpu: [arm64]
     os: [linux]
     os: [linux]
+    libc: [glibc]
 
 
   lightningcss-linux-arm64-musl@1.27.0:
   lightningcss-linux-arm64-musl@1.27.0:
     resolution: {integrity: sha512-rCGBm2ax7kQ9pBSeITfCW9XSVF69VX+fm5DIpvDZQl4NnQoMQyRwhZQm9pd59m8leZ1IesRqWk2v/DntMo26lg==}
     resolution: {integrity: sha512-rCGBm2ax7kQ9pBSeITfCW9XSVF69VX+fm5DIpvDZQl4NnQoMQyRwhZQm9pd59m8leZ1IesRqWk2v/DntMo26lg==}
     engines: {node: '>= 12.0.0'}
     engines: {node: '>= 12.0.0'}
     cpu: [arm64]
     cpu: [arm64]
     os: [linux]
     os: [linux]
+    libc: [musl]
 
 
   lightningcss-linux-arm64-musl@1.32.0:
   lightningcss-linux-arm64-musl@1.32.0:
     resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
     resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
     engines: {node: '>= 12.0.0'}
     engines: {node: '>= 12.0.0'}
     cpu: [arm64]
     cpu: [arm64]
     os: [linux]
     os: [linux]
+    libc: [musl]
 
 
   lightningcss-linux-x64-gnu@1.27.0:
   lightningcss-linux-x64-gnu@1.27.0:
     resolution: {integrity: sha512-Dk/jovSI7qqhJDiUibvaikNKI2x6kWPN79AQiD/E/KeQWMjdGe9kw51RAgoWFDi0coP4jinaH14Nrt/J8z3U4A==}
     resolution: {integrity: sha512-Dk/jovSI7qqhJDiUibvaikNKI2x6kWPN79AQiD/E/KeQWMjdGe9kw51RAgoWFDi0coP4jinaH14Nrt/J8z3U4A==}
     engines: {node: '>= 12.0.0'}
     engines: {node: '>= 12.0.0'}
     cpu: [x64]
     cpu: [x64]
     os: [linux]
     os: [linux]
+    libc: [glibc]
 
 
   lightningcss-linux-x64-gnu@1.32.0:
   lightningcss-linux-x64-gnu@1.32.0:
     resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
     resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
     engines: {node: '>= 12.0.0'}
     engines: {node: '>= 12.0.0'}
     cpu: [x64]
     cpu: [x64]
     os: [linux]
     os: [linux]
+    libc: [glibc]
 
 
   lightningcss-linux-x64-musl@1.27.0:
   lightningcss-linux-x64-musl@1.27.0:
     resolution: {integrity: sha512-QKjTxXm8A9s6v9Tg3Fk0gscCQA1t/HMoF7Woy1u68wCk5kS4fR+q3vXa1p3++REW784cRAtkYKrPy6JKibrEZA==}
     resolution: {integrity: sha512-QKjTxXm8A9s6v9Tg3Fk0gscCQA1t/HMoF7Woy1u68wCk5kS4fR+q3vXa1p3++REW784cRAtkYKrPy6JKibrEZA==}
     engines: {node: '>= 12.0.0'}
     engines: {node: '>= 12.0.0'}
     cpu: [x64]
     cpu: [x64]
     os: [linux]
     os: [linux]
+    libc: [musl]
 
 
   lightningcss-linux-x64-musl@1.32.0:
   lightningcss-linux-x64-musl@1.32.0:
     resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
     resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
     engines: {node: '>= 12.0.0'}
     engines: {node: '>= 12.0.0'}
     cpu: [x64]
     cpu: [x64]
     os: [linux]
     os: [linux]
+    libc: [musl]
 
 
   lightningcss-win32-arm64-msvc@1.27.0:
   lightningcss-win32-arm64-msvc@1.27.0:
     resolution: {integrity: sha512-/wXegPS1hnhkeG4OXQKEMQeJd48RDC3qdh+OA8pCuOPCyvnm/yEayrJdJVqzBsqpy1aJklRCVxscpFur80o6iQ==}
     resolution: {integrity: sha512-/wXegPS1hnhkeG4OXQKEMQeJd48RDC3qdh+OA8pCuOPCyvnm/yEayrJdJVqzBsqpy1aJklRCVxscpFur80o6iQ==}
@@ -7918,6 +7962,8 @@ snapshots:
 
 
   clone@1.0.4: {}
   clone@1.0.4: {}
 
 
+  clsx@2.1.1: {}
+
   collection-visit@1.0.0:
   collection-visit@1.0.0:
     dependencies:
     dependencies:
       map-visit: 1.0.0
       map-visit: 1.0.0
@@ -8493,6 +8539,10 @@ snapshots:
       expo: 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-webview@13.16.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)(typescript@5.9.3)
       expo: 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-webview@13.16.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)(typescript@5.9.3)
       ua-parser-js: 0.7.41
       ua-parser-js: 0.7.41
 
 
+  expo-document-picker@55.0.13(expo@55.0.12):
+    dependencies:
+      expo: 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-webview@13.16.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)(typescript@5.9.3)
+
   expo-eas-client@55.0.5: {}
   expo-eas-client@55.0.5: {}
 
 
   expo-file-system@55.0.16(expo@55.0.12)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0)):
   expo-file-system@55.0.16(expo@55.0.12)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0)):
@@ -8513,6 +8563,15 @@ snapshots:
       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: 0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0)
 
 
+  expo-image-loader@55.0.0(expo@55.0.12):
+    dependencies:
+      expo: 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-webview@13.16.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)(typescript@5.9.3)
+
+  expo-image-picker@55.0.19(expo@55.0.12):
+    dependencies:
+      expo: 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-webview@13.16.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)(typescript@5.9.3)
+      expo-image-loader: 55.0.0(expo@55.0.12)
+
   expo-image@55.0.8(expo@55.0.12)(react-native-web@0.21.2(react-dom@19.2.0(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):
   expo-image@55.0.8(expo@55.0.12)(react-native-web@0.21.2(react-dom@19.2.0(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:
     dependencies:
       expo: 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-webview@13.16.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)(typescript@5.9.3)
       expo: 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-webview@13.16.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)(typescript@5.9.3)

+ 7 - 27
src/app/(tabs)/index.tsx

@@ -1,18 +1,15 @@
 import { SectionHeader } from '@/components/ui/section-header';
 import { SectionHeader } from '@/components/ui/section-header';
 import { StatusBadge } from '@/components/ui/status-badge';
 import { StatusBadge } from '@/components/ui/status-badge';
-import { Colors } from '@/constants/theme';
+import UIButton from '@/components/ui/UIButton';
 import api from '@/utils/api';
 import api from '@/utils/api';
 import { useSWC } from '@/utils/cache';
 import { useSWC } from '@/utils/cache';
-import { Icon } from '@ant-design/react-native';
+import { selectCredit } from '@/utils/tack';
 import { Ionicons } from '@expo/vector-icons';
 import { Ionicons } from '@expo/vector-icons';
-import { Link } from 'expo-router';
 
 
 import { Pressable, ScrollView, Text, View } from 'react-native';
 import { Pressable, ScrollView, Text, View } from 'react-native';
 import { SafeAreaView } from 'react-native-safe-area-context';
 import { SafeAreaView } from 'react-native-safe-area-context';
-type QuickAction = {
-  icon: keyof typeof Ionicons.glyphMap;
-  label: string;
-};
+
+
 
 
 type ActivityItem = {
 type ActivityItem = {
   id: string;
   id: string;
@@ -26,12 +23,6 @@ type ActivityItem = {
   avatarColor: string;
   avatarColor: string;
 };
 };
 
 
-const QUICK_ACTIONS: QuickAction[] = [
-  { icon: 'person-add-outline', label: '新增客户' },
-  { icon: 'cloud-upload-outline', label: '上传征信' },
-  { icon: 'calculator-outline', label: '额度测算' },
-  { icon: 'document-text-outline', label: '面谈记录' },
-];
 
 
 const RECENT_ACTIVITIES: ActivityItem[] = [
 const RECENT_ACTIVITIES: ActivityItem[] = [
   {
   {
@@ -98,7 +89,7 @@ function getGreeting() {
 }
 }
 
 
 export default function HomeScreen() {
 export default function HomeScreen() {
-  const { data: summary, loading: summaryLoding, error } = useSWC<{
+  const { data: summary } = useSWC<{
     pendding: number;
     pendding: number;
     matched: number;
     matched: number;
     completed: number;
     completed: number;
@@ -110,7 +101,6 @@ export default function HomeScreen() {
   }
   }
   );
   );
 
 
-
   return (
   return (
     <SafeAreaView className="flex-1 bg-surface" edges={['top']}>
     <SafeAreaView className="flex-1 bg-surface" edges={['top']}>
       <View className="h-14 flex-row items-center justify-between border-b border-outline-variant/20 bg-surface-container-lowest px-5">
       <View className="h-14 flex-row items-center justify-between border-b border-outline-variant/20 bg-surface-container-lowest px-5">
@@ -181,19 +171,9 @@ export default function HomeScreen() {
         <View className="mb-8">
         <View className="mb-8">
           <SectionHeader title="快捷入口" />
           <SectionHeader title="快捷入口" />
           <View className="flex-row items-start justify-around">
           <View className="flex-row items-start justify-around">
-              <Link href="/customer/add" asChild >
-                <Pressable className='flex-1 h-12 rounded-lg flex-row justify-center items-center bg-primary'>
-                  <Icon name='user-add' color='#fff' />
-                  <Text className='ml-2 text-lg font-medium text-white'>添加客户</Text>
-              </Pressable>
-            </Link>
+            <UIButton type='primary' href="/customer/add" icon="user-add" className='flex-1'>添加客户</UIButton>
             <View className='w-5' />
             <View className='w-5' />
-            <Link href="/customer/add" asChild>
-                <Pressable className='flex-1 h-12 rounded-lg flex-row justify-center items-center border-2 border-primary/50'>
-                  <Icon name='user-add' color={Colors.primary.DEFAULT} />
-                  <Text className='ml-2 text-lg font-medium text-primary'>添加客户</Text>
-              </Pressable>
-            </Link>
+            <UIButton onPress={selectCredit} icon="cloud-upload" className='flex-1'>征信分析</UIButton>
           </View>
           </View>
         </View>
         </View>
 
 

+ 5 - 7
src/app/_layout.tsx

@@ -14,7 +14,7 @@ import {
 import * as application from 'expo-application';
 import * as application from 'expo-application';
 import { useFonts } from 'expo-font';
 import { useFonts } from 'expo-font';
 import { Image } from 'expo-image';
 import { Image } from 'expo-image';
-import { Stack, useRouter } from 'expo-router';
+import { Stack } from 'expo-router';
 import * as SplashScreen from 'expo-splash-screen';
 import * as SplashScreen from 'expo-splash-screen';
 import * as updates from 'expo-updates';
 import * as updates from 'expo-updates';
 import { useEffect, useState } from 'react';
 import { useEffect, useState } from 'react';
@@ -53,7 +53,7 @@ export default function RootLayout() {
     async function checkApp() {
     async function checkApp() {
       try {
       try {
         const lastTime = parseInt(getGlobalStorage().getString('last_update_app') || '0');
         const lastTime = parseInt(getGlobalStorage().getString('last_update_app') || '0');
-        if (now - lastTime < 15*60000) {
+        if (now - lastTime < 15 * 60000) {
           SplashScreen.hide();
           SplashScreen.hide();
           return true;
           return true;
         }
         }
@@ -169,8 +169,6 @@ export default function RootLayout() {
   }, []);
   }, []);
 
 
 
 
-  const router = useRouter();
-
 
 
   return (
   return (
     <ThemeProvider value={DefaultTheme}>
     <ThemeProvider value={DefaultTheme}>
@@ -183,16 +181,16 @@ export default function RootLayout() {
               headerTransparent: true,
               headerTransparent: true,
               headerBlurEffect: 'light',
               headerBlurEffect: 'light',
               headerBackButtonDisplayMode: 'minimal',
               headerBackButtonDisplayMode: 'minimal',
-              // headerBackground: ()=><BlurView tint='light' style={{ flex: 1}} />,
+              headerBackground: Platform.OS === 'android' ? () => <View style={{ flex: 1, backgroundColor: 'rgba(255, 255, 255, 0.9)' }} /> : undefined,
               // headerLeft: ({ canGoBack }) => canGoBack && <Icon name="arrow-left" size={24} />,
               // headerLeft: ({ canGoBack }) => canGoBack && <Icon name="arrow-left" size={24} />,
               animation: 'ios_from_right'
               animation: 'ios_from_right'
             }}>
             }}>
-              <Stack.Screen name="(tabs)" options={{headerShown: false}} />
+              <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
               <Stack.Screen name="sign-in" options={{ animation: 'fade_from_bottom' }} />
               <Stack.Screen name="sign-in" options={{ animation: 'fade_from_bottom' }} />
             </Stack>
             </Stack>
           </AuthProvider>}
           </AuthProvider>}
       </Provider>
       </Provider>
-        {initing && <View className='absolute left-0 top-0 right-0 bottom-0 bg-[#f6f6f6] z-50'>
+      {initing && <View className='absolute left-0 top-0 right-0 bottom-0 bg-[#f6f6f6] z-50'>
         <Image contentFit='contain' style={{ flex: 1 }} source={require('@/assets/images/uploading.jpg')} />
         <Image contentFit='contain' style={{ flex: 1 }} source={require('@/assets/images/uploading.jpg')} />
       </View>}
       </View>}
     </ThemeProvider>
     </ThemeProvider>

+ 5 - 0
src/app/credit/select.tsx

@@ -0,0 +1,5 @@
+import { View } from "react-native";
+
+export default function SelectScreen() {
+    return <View />;
+}

+ 620 - 18
src/app/customer/add.tsx

@@ -1,29 +1,631 @@
-import { Button } from "@ant-design/react-native";
-import { KeyboardAvoidingView, Platform, ScrollView, View } from "react-native";
+import UIButton from "@/components/ui/UIButton";
+import { Colors } from "@/constants/theme";
+import { DatePicker, Input, Modal, Picker, Toast } from "@ant-design/react-native";
+import { Ionicons } from "@expo/vector-icons";
+import { usePreventRemove } from "@react-navigation/native";
+import { NavigationAction } from "@react-navigation/routers";
+import { Link, Stack, useNavigation } from "expo-router";
+import React, { useCallback, useRef, useState } from "react";
+import {
+  KeyboardAvoidingView,
+  Platform,
+  Pressable,
+  ScrollView,
+  StyleSheet,
+  Text,
+  View,
+} from "react-native";
+import { useSafeAreaInsets } from "react-native-safe-area-context";
+
+type CustomerForm = {
+  name: string;
+  mobile: string;
+  gender: string;
+  birthDate?: Date;
+  idCard: string;
+  maritalStatus: string;
+  hometown: string;
+  residence: string;
+  occupation: string;
+  others: string;
+};
+
+type FieldKey = "name" | "mobile" | "idCard";
+type FieldErrors = Partial<Record<FieldKey, string>>;
+type BirthDateSource = "unset" | "manual" | "id-card";
+
+type Option = {
+  label: string;
+  value: string;
+};
+
+const GENDER_OPTIONS: Option[] = [
+  { label: "男", value: "male" },
+  { label: "女", value: "female" },
+];
+
+const MARITAL_STATUS_OPTIONS: Option[] = [
+  { label: "未婚", value: "single" },
+  { label: "已婚", value: "married" },
+  { label: "离异", value: "divorced" },
+  { label: "丧偶", value: "widowed" },
+];
+
+const MOBILE_PATTERN = /^1\d{10}$/;
+const IDCARD_PATTERN = /^(?:\d{15}|\d{17}[\dX])$/;
+
+function sanitizePhone(value: string) {
+  return value.replace(/\D/g, "").slice(0, 11);
+}
+
+function sanitizeIdCard(value: string) {
+  return value.replace(/[^0-9xX]/g, "").toUpperCase().slice(0, 18);
+}
+
+function padZero(value: number) {
+  return String(value).padStart(2, "0");
+}
+
+function formatDate(date?: Date) {
+  if (!date) {
+    return "";
+  }
+
+  return `${date.getFullYear()}-${padZero(date.getMonth() + 1)}-${padZero(date.getDate())}`;
+}
+
+function isValidDatePart(year: number, month: number, day: number) {
+  if (year < 1900 || year > new Date().getFullYear()) {
+    return false;
+  }
+
+  const date = new Date(year, month - 1, day);
+
+  return (
+    date.getFullYear() === year &&
+    date.getMonth() === month - 1 &&
+    date.getDate() === day &&
+    date.getTime() <= Date.now()
+  );
+}
+
+function inferBirthDateFromIdCard(idCard: string) {
+  if (idCard.length < 14) {
+    return undefined;
+  }
+
+  const birthText = idCard.slice(6, 14);
+  if (!/^\d{8}$/.test(birthText)) {
+    return undefined;
+  }
+
+  const year = Number(birthText.slice(0, 4));
+  const month = Number(birthText.slice(4, 6));
+  const day = Number(birthText.slice(6, 8));
+
+  if (!isValidDatePart(year, month, day)) {
+    return undefined;
+  }
+
+  return new Date(year, month - 1, day);
+}
+
+function getOptionLabel(options: Option[], value: string) {
+  return options.find((item) => item.value === value)?.label ?? "";
+}
+
+function validateForm(form: CustomerForm): FieldErrors {
+  const nextErrors: FieldErrors = {};
+  const mobile = form.mobile.trim();
+  const idCard = form.idCard.trim();
+
+  if (!form.name.trim()) {
+    nextErrors.name = "请输入客户姓名";
+  }
+
+  if (!mobile && !idCard) {
+    nextErrors.mobile = "手机号和身份证号至少填写一项";
+    nextErrors.idCard = "手机号和身份证号至少填写一项";
+    return nextErrors;
+  }
+
+  if (mobile && !MOBILE_PATTERN.test(mobile)) {
+    nextErrors.mobile = "请输入 11 位手机号";
+  }
+
+  if (idCard && !IDCARD_PATTERN.test(idCard)) {
+    nextErrors.idCard = "请输入正确的身份证号";
+  }
+
+  return nextErrors;
+}
+
+function FieldLabel({
+  label,
+  required = false,
+}: {
+  label: string;
+  required?: boolean;
+}) {
+  return (
+    <View className="mb-2 flex-row items-center">
+      <Text className="text-sm font-semibold text-on-surface">{label}</Text>
+      {required ? <Text className="ml-1 text-sm font-semibold text-red-500">*</Text> : null}
+    </View>
+  );
+}
+
+function FieldMessage({
+  error,
+  helper,
+}: {
+  error?: string;
+  helper?: string;
+}) {
+  if (error) {
+    return <Text className="mt-2 text-xs leading-5 text-red-500">{error}</Text>;
+  }
+
+  if (helper) {
+    return <Text className="mt-2 text-xs leading-5 text-on-surface-variant">{helper}</Text>;
+  }
+
+  return null;
+}
+
+function PickerField({
+  label,
+  required = false,
+  value,
+  placeholder,
+  error,
+  helper,
+  disabled = false,
+  onPress,
+}: {
+  label: string;
+  required?: boolean;
+  value?: string;
+  placeholder: string;
+  error?: string;
+  helper?: string;
+  disabled?: boolean;
+  onPress: () => void;
+}) {
+  const hasValue = Boolean(value);
+
+  return (
+    <View className="mb-4">
+      <FieldLabel label={label} required={required} />
+      <Pressable
+        disabled={disabled}
+        onPress={onPress}
+        className={`flex-row items-center justify-between rounded-2xl border px-4 py-4 ${error
+          ? "border-red-300 bg-red-50"
+          : "border-outline-variant bg-surface-container-low"
+          }`}
+        style={({ pressed }) => ({
+          opacity: disabled ? 0.55 : pressed ? 0.9 : 1,
+        })}
+      >
+        <Text
+          className={`flex-1 text-base ${hasValue ? "text-on-surface" : "text-outline"}`}
+          numberOfLines={1}
+        >
+          {hasValue ? value : placeholder}
+        </Text>
+        <Ionicons
+          name="chevron-down"
+          size={18}
+          color={error ? "#ef4444" : Colors.color_text_paragraph}
+        />
+      </Pressable>
+      <FieldMessage error={error} helper={helper} />
+    </View>
+  );
+}
 
 
 export default function AddCustomerScreen() {
 export default function AddCustomerScreen() {
-    return <View className="flex-1">
+  const insets = useSafeAreaInsets();
+  const navigation = useNavigation();
+  const [form, setForm] = useState<CustomerForm>({
+    name: "",
+    mobile: "",
+    gender: "",
+    birthDate: undefined,
+    idCard: "",
+    maritalStatus: "",
+    hometown: "",
+    residence: "",
+    occupation: "",
+    others: "",
+  });
+  const [errors, setErrors] = useState<FieldErrors>({});
+  const [birthDateSource, setBirthDateSource] = useState<BirthDateSource>("unset");
+  const leaveConfirmVisibleRef = useRef(false);
+
+  const hasDraft = Boolean(
+    form.name.trim() ||
+    form.mobile.trim() ||
+    form.gender ||
+    form.birthDate ||
+    form.idCard.trim() ||
+    form.maritalStatus ||
+    form.hometown.trim() ||
+    form.residence.trim() ||
+    form.occupation.trim() ||
+    form.others.trim()
+  );
+
+  const openLeaveConfirm = useCallback(
+    (action: NavigationAction) => {
+      if (leaveConfirmVisibleRef.current) {
+        return;
+      }
+
+      leaveConfirmVisibleRef.current = true;
+      Modal.alert(
+        "放弃当前编辑",
+        "当前页面还有未保存的客户资料,确定要返回吗?",
+        [
+          {
+            text: "继续编辑",
+            style: "cancel",
+            onPress: () => {
+              leaveConfirmVisibleRef.current = false;
+            },
+          },
+          {
+            text: "确认返回",
+            style: "destructive",
+            onPress: () => {
+              leaveConfirmVisibleRef.current = false;
+              navigation.dispatch(action);
+            },
+          },
+        ],
+        () => {
+          leaveConfirmVisibleRef.current = false;
+          return true;
+        }
+      );
+    },
+    [navigation]
+  );
+
+  usePreventRemove(hasDraft, ({ data }) => {
+    openLeaveConfirm(data.action);
+  });
+
+  const clearErrors = (keys: FieldKey[]) => {
+    setErrors((prev) => {
+      const next = { ...prev };
+      keys.forEach((key) => {
+        delete next[key];
+      });
+      return next;
+    });
+  };
+
+  const handleNameChange = (value: string) => {
+    setForm((prev) => ({ ...prev, name: value }));
+    clearErrors(["name"]);
+  };
+
+  const handleMobileChange = (value: string) => {
+    setForm((prev) => ({ ...prev, mobile: sanitizePhone(value) }));
+    clearErrors(["mobile", "idCard"]);
+  };
+
+  const handleIdCardChange = (value: string) => {
+    const nextIdCard = sanitizeIdCard(value);
+    const inferredBirthDate = inferBirthDateFromIdCard(nextIdCard);
+    const canAutofillBirthDate = birthDateSource !== "manual" || !form.birthDate;
+
+    setForm((prev) => ({
+      ...prev,
+      idCard: nextIdCard,
+      birthDate:
+        inferredBirthDate && canAutofillBirthDate
+          ? inferredBirthDate
+          : !inferredBirthDate && birthDateSource === "id-card"
+            ? undefined
+            : prev.birthDate,
+    }));
+
+    if (inferredBirthDate && canAutofillBirthDate) {
+      setBirthDateSource("id-card");
+    } else if (!inferredBirthDate && birthDateSource === "id-card") {
+      setBirthDateSource("unset");
+    }
+
+    clearErrors(["mobile", "idCard"]);
+  };
+
+  const handleSubmit = () => {
+    const nextErrors = validateForm(form);
+    setErrors(nextErrors);
+
+    const firstError = Object.values(nextErrors)[0];
+    if (firstError) {
+      Toast.fail(firstError);
+      return;
+    }
+
+    Toast.success("表单校验通过,后续可直接接入保存接口");
+  };
+
+
+  const birthDateHelper =
+    birthDateSource === "id-card"
+      ? "已根据身份证自动识别,可继续手动调整"
+      : "填写身份证后可自动识别,也可以手动选择";
+
+  return (
+    <View className="flex-1 bg-surface">
+      <Stack.Screen options={{ title: "添加客户" }} />
       <KeyboardAvoidingView
       <KeyboardAvoidingView
         className="flex-1"
         className="flex-1"
-        behavior={Platform.OS === 'ios' ? 'padding' : undefined}
+        behavior={Platform.OS === "ios" ? "padding" : undefined}
       >
       >
         <ScrollView
         <ScrollView
           className="flex-1"
           className="flex-1"
-          contentContainerClassName="px-8"
-          
+          contentContainerClassName="px-5 pb-10"
+          contentContainerStyle={{
+            paddingTop: (insets.top ?? 0) + 60,
+            paddingBottom: (insets.bottom ?? 0) + 28,
+          }}
           keyboardShouldPersistTaps="handled"
           keyboardShouldPersistTaps="handled"
           showsVerticalScrollIndicator={false}
           showsVerticalScrollIndicator={false}
         >
         >
-          <View className="absolute -top-24 -right-24 h-96 w-96 rounded-full bg-primary-container/10" />
-          <View className="absolute top-32 -left-20 h-64 w-64 rounded-full bg-secondary-container/20" />
-          <View className="absolute -bottom-16 right-0 h-48 w-48 rounded-full bg-primary-fixed/50" />
-          <Button type="primary">ddd</Button>
-          <Button type="primary">ddd</Button>
-          <Button type="primary">ddd</Button>
-          <Button type="primary">ddd</Button>
-          <Button type="primary">ddd</Button>
-          <Button type="primary">ddd</Button>
-          </ScrollView>
-          </KeyboardAvoidingView>
+          <View className="mb-6">
+            <Text className="text-3xl font-extrabold tracking-tight text-on-surface">
+              新建客户
+            </Text>
+          </View>
+
+          <View className="mb-5 rounded-3xl border border-blue-100 bg-primary-fixed px-4 py-4">
+            <View className="flex-row items-start">
+              <View className="mr-3 mt-0.5 h-10 w-10 items-center justify-center rounded-2xl bg-primary-container">
+                <Ionicons name="information-circle-outline" size={20} color="#ffffff" />
+              </View>
+              <View className="flex-1">
+                <Text className="text-base font-bold text-on-surface">提示:</Text>
+                <Text className="mt-1 text-sm leading-6 text-on-surface-variant">
+                  您可以先<Link href="/credit/select" replace asChild><Text className="text-primary font-bold">上传分析征信</Text></Link>,然后从已分析征信中<Link href="/credit/select" asChild><Text className="text-primary font-bold">选择</Text></Link>一条信息以填充客户资料。
+                </Text>
+              </View>
+            </View>
+          </View>
+
+
+          <View className="mb-4">
+            <FieldLabel label="姓名" required />
+            <Input
+              value={form.name}
+              onChangeText={handleNameChange}
+              placeholder="请输入客户姓名"
+              allowClear
+              status={errors.name ? "error" : undefined}
+              style={[
+                styles.inputContainer,
+                errors.name ? styles.inputContainerError : undefined,
+              ]}
+              inputStyle={styles.inputText}
+            />
+            <FieldMessage error={errors.name} />
+          </View>
+
+          <Picker
+            data={GENDER_OPTIONS}
+            cols={1}
+            value={form.gender ? [form.gender] : []}
+            onOk={(values) => {
+              const selected = values[0];
+              setForm((prev) => ({
+                ...prev,
+                gender: typeof selected === "string" ? selected : String(selected ?? ""),
+              }));
+            }}
+            format={(labels) => labels[0] ?? ""}
+          >
+            {({ disabled, toggle }) => (
+              <PickerField
+                label="性别"
+                value={getOptionLabel(GENDER_OPTIONS, form.gender)}
+                placeholder="请选择性别"
+                disabled={disabled}
+                onPress={toggle}
+              />
+            )}
+          </Picker>
+
+          <DatePicker
+            precision="day"
+            value={form.birthDate}
+            minDate={new Date(1950, 0, 1)}
+            maxDate={new Date()}
+            format={formatDate}
+            onOk={(value) => {
+              setForm((prev) => ({ ...prev, birthDate: value }));
+              setBirthDateSource("manual");
+            }}
+          >
+            {({ disabled, toggle }) => (
+              <PickerField
+                label="出生日期"
+                value={formatDate(form.birthDate)}
+                placeholder="请选择出生日期"
+                helper={birthDateHelper}
+                disabled={disabled}
+                onPress={toggle}
+              />
+            )}
+          </DatePicker>
+
+          <View className="mb-4">
+            <FieldLabel label="手机号" />
+            <Input
+              value={form.mobile}
+              onChangeText={handleMobileChange}
+              placeholder="请输入 11 位手机号"
+              allowClear
+              keyboardType="phone-pad"
+              textContentType="telephoneNumber"
+              maxLength={11}
+              status={errors.mobile ? "error" : undefined}
+              style={[
+                styles.inputContainer,
+                errors.mobile ? styles.inputContainerError : undefined,
+              ]}
+              inputStyle={styles.inputText}
+            />
+            <FieldMessage
+              error={errors.mobile}
+              helper={errors.mobile ? undefined : "手机号和身份证号至少填写一项"}
+            />
+          </View>
+
+          <View className="mb-4">
+            <FieldLabel label="身份证号" />
+            <Input
+              value={form.idCard}
+              onChangeText={handleIdCardChange}
+              placeholder="请输入身份证号"
+              allowClear
+              autoCapitalize="characters"
+              maxLength={18}
+              status={errors.idCard ? "error" : undefined}
+              style={[
+                styles.inputContainer,
+                errors.idCard ? styles.inputContainerError : undefined,
+              ]}
+              inputStyle={styles.inputText}
+            />
+            <FieldMessage
+              error={errors.idCard}
+              helper={errors.idCard ? undefined : "填写 18 位身份证后会自动识别出生日期"}
+            />
+          </View>
+
+
+          <Picker
+            data={MARITAL_STATUS_OPTIONS}
+            cols={1}
+            value={form.maritalStatus ? [form.maritalStatus] : []}
+            onOk={(values) => {
+              const selected = values[0];
+              setForm((prev) => ({
+                ...prev,
+                maritalStatus:
+                  typeof selected === "string" ? selected : String(selected ?? ""),
+              }));
+            }}
+            format={(labels) => labels[0] ?? ""}
+          >
+            {({ disabled, toggle }) => (
+              <PickerField
+                label="婚姻状态"
+                value={getOptionLabel(MARITAL_STATUS_OPTIONS, form.maritalStatus)}
+                placeholder="请选择婚姻状态"
+                disabled={disabled}
+                onPress={toggle}
+              />
+            )}
+          </Picker>
+
+          <View className="mb-4">
+            <FieldLabel label="户籍所在地" />
+            <Input
+              value={form.hometown}
+              onChangeText={(value) => setForm((prev) => ({ ...prev, hometown: value }))}
+              placeholder="请输入户籍所在地"
+              allowClear
+              style={styles.inputContainer}
+              inputStyle={styles.inputText}
+            />
+          </View>
+
+          <View className="mb-4">
+            <FieldLabel label="现居住地" />
+            <Input
+              value={form.residence}
+              onChangeText={(value) => setForm((prev) => ({ ...prev, residence: value }))}
+              placeholder="请输入现居住地"
+              allowClear
+              style={styles.inputContainer}
+              inputStyle={styles.inputText}
+            />
+          </View>
+
+          <View className="mb-4">
+            <FieldLabel label="职业" />
+            <Input
+              value={form.occupation}
+              onChangeText={(value) => setForm((prev) => ({ ...prev, occupation: value }))}
+              placeholder="请输入职业"
+              allowClear
+              style={styles.inputContainer}
+              inputStyle={styles.inputText}
+            />
+          </View>
+
+          <View>
+            <FieldLabel label="其它" />
+            <Input.TextArea
+              value={form.others}
+              onChangeText={(value) => setForm((prev) => ({ ...prev, others: value }))}
+              placeholder="补充备注、渠道来源、客户标签等"
+              autoSize={{ minRows: 4, maxRows: 6 }}
+              maxLength={200}
+              showCount
+              allowClear
+              style={styles.textAreaContainer}
+              inputStyle={styles.textAreaText}
+            />
+          </View>
+
+          <UIButton type="primary" icon="save" onPress={handleSubmit}>
+            保存客户
+          </UIButton>
+        </ScrollView>
+      </KeyboardAvoidingView>
     </View>
     </View>
-}
+  );
+}
+
+const styles = StyleSheet.create({
+  inputContainer: {
+    minHeight: 54,
+    borderRadius: 16,
+    borderWidth: 1,
+    borderColor: Colors.border_color_base,
+    backgroundColor: Colors.fill_body,
+    paddingHorizontal: 14,
+  },
+  inputContainerError: {
+    borderColor: "#fca5a5",
+    backgroundColor: "#fef2f2",
+  },
+  inputText: {
+    color: Colors.color_text_base,
+    fontSize: 16,
+    paddingVertical: Platform.OS === "web" ? 12 : 10,
+  },
+  textAreaContainer: {
+    borderRadius: 16,
+    borderWidth: 1,
+    borderColor: Colors.border_color_base,
+    backgroundColor: Colors.fill_body,
+    paddingHorizontal: 14,
+    paddingTop: 12,
+    paddingBottom: 12,
+  },
+  textAreaText: {
+    color: Colors.color_text_base,
+    fontSize: 16,
+    lineHeight: 22,
+    minHeight: 96,
+    textAlignVertical: "top",
+  },
+});

+ 60 - 0
src/components/ui/UIButton.tsx

@@ -0,0 +1,60 @@
+import { Colors } from '@/constants/theme';
+import { Icon } from '@ant-design/react-native';
+import { type IconNames } from '@ant-design/react-native/lib/icon';
+import { clsx } from 'clsx';
+import { Href, Link } from "expo-router";
+import React, { useMemo } from "react";
+import { Pressable, Text, ViewStyle } from "react-native";
+
+interface UIButtonProps {
+    children?: string | React.ReactNode;
+    onPress?: () => void;
+    disabled?: boolean;
+    className?: string;
+    textClassName?: string;
+    href?: Href
+    type?: 'primary' | 'second' | 'link'
+    icon?: IconNames | React.ReactNode;
+    style?: ViewStyle;
+}
+
+function ButtonTextChild({ type, disabled, textClassName, children }: UIButtonProps) {
+
+    return <Text className={clsx(
+        textClassName,
+        'text-xl font-bold', {
+        'text-on-primary': !disabled && type === 'primary',
+        'text-primary': !disabled && type !== 'primary',
+        'text-secondary': !disabled && type === 'second',
+        'text-on-surface': !disabled && !type,
+        'text-on-surface-variant/65 font-normal': disabled,
+    })}>{children}</Text>
+}
+
+export default function UIButton({ onPress, href, type, icon, className, style, disabled, children, textClassName }: UIButtonProps) {
+
+    const inner = useMemo(() => <Pressable
+        className={clsx('h-14 rounded-3xl flex-row justify-center items-center border-2 opacity-100 active:opacity-75 active:scale-95',
+            className, {
+            'bg-primary border-primary': type === 'primary',
+            'bg-surface border-primary/25': !type,
+            'bg-surface border-on-surface/25': type === 'second',
+            'bg-transparent border-transparent': type === 'link',
+            'bg-primary-fixed-dim border-primary-fixed-dim': disabled && type === 'primary',
+            'bg-gray-300 border-gray-300': disabled && !type,
+            'bg-bg-gray-400 border-gray-200': disabled && type === 'second'
+        })}
+        disabled={disabled}
+        onPress={onPress}
+        style={style}>
+        {typeof icon === 'string' ? <Icon name={icon as IconNames} size={24} style={{ marginRight: 4, color: disabled ? Colors['on-surface']['variant'] : type === 'primary' ? '#fff' : type === 'second' ? Colors.secondary.DEFAULT : Colors.primary.DEFAULT }} /> : icon}
+        {typeof children == 'string' ? <ButtonTextChild type={type} disabled={disabled} textClassName={textClassName}>{children}</ButtonTextChild> : children}
+    </Pressable >, [children, className, disabled, icon, onPress, style, textClassName, type])
+    if (typeof href !== 'undefined') {
+        return <Link href={href} asChild>
+            {inner}
+        </Link>
+    }
+
+    return inner;
+}

+ 9 - 0
src/utils/os.tsx

@@ -0,0 +1,9 @@
+import { Linking } from "react-native";
+
+export const openSystemSettings = async () => {
+    if (await Linking.canOpenURL('app-settings:')) {
+        await Linking.openURL('app-settings:'); // iOS
+    } else {
+        await Linking.openSettings(); // Android
+    }
+};

+ 115 - 0
src/utils/tack.tsx

@@ -0,0 +1,115 @@
+import { ActionSheet, Modal } from "@ant-design/react-native";
+import { getDocumentAsync } from 'expo-document-picker';
+import { launchCameraAsync, launchImageLibraryAsync, requestCameraPermissionsAsync, requestMediaLibraryPermissionsAsync } from 'expo-image-picker';
+import { Platform } from "react-native";
+import { openSystemSettings } from "./os";
+const takePhoto = async () => {
+    const permission = await requestCameraPermissionsAsync();
+
+    if (!permission.granted) {
+        Modal.alert("请允许相册权限", "如果点击“确认“按钮后没有跳转,请自己前往系统设置开启相关权限", [
+            {
+                text: "确认",
+                onPress: openSystemSettings,
+            }
+        ]);
+        // 跳转到 ios/android 相关设置页面
+        return;
+    }
+    const result = await launchCameraAsync({
+        allowsEditing: false, // 是否允许裁剪
+        quality: 0.9,        // 照片质量
+    });
+
+    if (!result.canceled) {
+        // 拿到拍照后的图片 URI
+        alert(result.assets[0].uri);
+    }
+};
+
+const picImg = async () => {
+    const permission = await requestMediaLibraryPermissionsAsync();
+
+    if (!permission.granted) {
+        Modal.alert("请允许相册权限", "如果点击“确认“按钮后没有跳转,请自己前往系统设置开启相关权限", [
+            {
+                text: "确认",
+                onPress: openSystemSettings,
+            }
+        ]);
+        // 跳转到 ios/android 相关设置页面
+        return;
+    }
+
+    let result = await launchImageLibraryAsync({
+        mediaTypes: ['images'], // 只选图片
+        allowsEditing: false,
+        quality: .9, // 质量 0~1
+    });
+
+    if (!result.canceled) {
+        alert(result.assets[0].uri);
+    }
+};
+
+const pickDoc = async () => {
+    try {
+        let result = await getDocumentAsync({
+            type: [
+                // PDF
+                'application/pdf',
+
+                // Word 文档
+                'application/msword',
+                'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+
+                // Excel 表格
+                'application/vnd.ms-excel',
+                'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+
+                // WPS 文字 / WPS 表格
+                'application/wps-office.doc',
+                'application/wps-office.docx',
+                'application/wps-office.xls',
+                'application/wps-office.xlsx',
+                'application/wps-office.et',
+                'application/wps-office.wps',
+
+                // 纯文本
+                'text/plain',
+
+                // HTML
+                'text/html',
+
+                // 全部图片(jpg/png/gif/webp 等)
+                'image/*'
+            ],
+            copyToCacheDirectory: true, // 复制到应用缓存目录
+        });
+
+        if (!result.canceled) {
+            alert(result.assets[0]);
+        }
+    } catch (err) {
+        console.warn(err);
+    }
+};
+
+export const selectCredit = async () => {
+    ActionSheet.showActionSheetWithOptions({
+        title: '文件来源',
+        message: '以下列方式选择一个图片或文档立即提交分析',
+        cancelButtonIndex: Platform.OS === 'ios' ? 3 : 2,
+        options: Platform.OS === 'ios' ? ['拍照', '文件', '相册', '取消'] : ['拍照', '本地', '取消']
+    }, (idx) => {
+        if (idx === 0) {
+            takePhoto();
+        }
+        if (idx === 1) {
+            pickDoc();
+        }
+        if (idx === 2) {
+            Platform.OS === 'ios' && picImg();
+        }
+    });
+};

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio