lv 1 miesiąc temu
rodzic
commit
0c836bf1f2
54 zmienionych plików z 556 dodań i 405 usunięć
  1. 7 1
      .vscode/extensions.json
  2. 18 6
      .vscode/settings.json
  3. BIN
      android/app/src/main/res/mipmap-hdpi/ic_launcher.webp
  4. BIN
      android/app/src/main/res/mipmap-hdpi/ic_launcher_background.webp
  5. BIN
      android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp
  6. BIN
      android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
  7. BIN
      android/app/src/main/res/mipmap-mdpi/ic_launcher.webp
  8. BIN
      android/app/src/main/res/mipmap-mdpi/ic_launcher_background.webp
  9. BIN
      android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp
  10. BIN
      android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
  11. BIN
      android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
  12. BIN
      android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.webp
  13. BIN
      android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp
  14. BIN
      android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
  15. BIN
      android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
  16. BIN
      android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.webp
  17. BIN
      android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp
  18. BIN
      android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
  19. BIN
      android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
  20. BIN
      android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.webp
  21. BIN
      android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp
  22. BIN
      android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
  23. BIN
      assets/images/android-icon-background.png
  24. BIN
      assets/images/android-icon-foreground.png
  25. BIN
      assets/images/expo-badge-white.png
  26. BIN
      assets/images/expo-badge.png
  27. BIN
      assets/images/expo-logo.png
  28. BIN
      assets/images/logo-glow.png
  29. BIN
      assets/images/react-logo.png
  30. BIN
      assets/images/react-logo@2x.png
  31. BIN
      assets/images/react-logo@3x.png
  32. BIN
      assets/images/tutorial-web.png
  33. 35 3
      eslint.config.js
  34. 4 4
      ios/LoanAssistant.xcodeproj/project.pbxproj
  35. 3 3
      ios/Podfile.lock
  36. 3 4
      src/app/(tabs)/_layout.tsx
  37. 139 114
      src/app/(tabs)/analytics.tsx
  38. 40 12
      src/app/(tabs)/customer.tsx
  39. 5 6
      src/app/(tabs)/index.tsx
  40. 4 4
      src/app/(tabs)/profile.tsx
  41. 207 182
      src/app/(tabs)/reports.tsx
  42. 1 1
      src/app/+html.tsx
  43. 7 7
      src/app/_layout.tsx
  44. 3 0
      src/app/credit/detail.tsx
  45. 4 4
      src/app/customer/add.tsx
  46. 21 15
      src/app/sign-in.tsx
  47. 27 18
      src/app/sign-up.tsx
  48. 3 3
      src/components/captcha-box.tsx
  49. 7 5
      src/components/ui/ui-button.tsx
  50. 8 6
      src/components/upload.tsx
  51. 1 2
      src/constants/theme.ts
  52. 5 3
      src/utils/api.ts
  53. 2 1
      src/utils/storage.ts
  54. 2 1
      src/utils/storage.web.ts

+ 7 - 1
.vscode/extensions.json

@@ -1 +1,7 @@
-{ "recommendations": ["expo.vscode-expo-tools"] }
+{
+  "recommendations": [
+    "expo.vscode-expo-tools",
+    "dbaeumer.vscode-eslint",
+    "bradlc.vscode-tailwindcss"
+  ]
+}

+ 18 - 6
.vscode/settings.json

@@ -1,23 +1,35 @@
 {
+  // 保存时:先 TS organizeImports(合并/清理未用),再 ESLint fix(import/order + type 拆分等)
   "editor.codeActionsOnSave": {
-    "source.fixAll": "explicit",
     "source.organizeImports": "explicit",
-    "source.sortMembers": "explicit"
+    "source.fixAll.eslint": "explicit"
   },
+  // 启用 ESLint flat config,并对常见 JS/TS 文件启用
+  "eslint.useFlatConfig": true,
+  "eslint.validate": [
+    "javascript",
+    "javascriptreact",
+    "typescript",
+    "typescriptreact"
+  ],
+  "eslint.run": "onType",
+  // 自动补全 import 时优先生成 `import type`
+  "typescript.preferences.preferTypeOnlyAutoImports": true,
+  "typescript.tsdk": "node_modules/typescript/lib",
+  "typescript.enablePromptUseWorkspaceTsdk": true,
+  // 自动完成触发
   "editor.quickSuggestions": {
     "other": "on",
     "comments": "off",
     "strings": "on"
   },
-  // 2. 确保在输入特定字符(如引号、等号)时立即触发提示
   "editor.suggestOnTriggerCharacters": true,
-  // 3. 如果你在写属性名后进入了 Snippet 模式(光标在引号内),
-  // 下面这一项设置为 false 可以防止 Snippet 模式阻塞建议列表
   "editor.suggest.snippetsPreventQuickSuggestions": false,
+  "editor.formatOnSave": false,
   "tailwindCSS.experimental.classRegex": [
     [
       "clsx\\(([^)]*)\\)",
       "(?:'|\"|`)([^']*)(?:'|\"|`)"
     ]
   ]
-}
+}

BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher.webp


BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_background.webp


BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp


BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp


BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher.webp


BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_background.webp


BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp


BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp


BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp


BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.webp


BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp


BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp


BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp


BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.webp


BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp


BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp


BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp


BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.webp


BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp


BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp


BIN
assets/images/android-icon-background.png


BIN
assets/images/android-icon-foreground.png


BIN
assets/images/expo-badge-white.png


BIN
assets/images/expo-badge.png


BIN
assets/images/expo-logo.png


BIN
assets/images/logo-glow.png


BIN
assets/images/react-logo.png


BIN
assets/images/react-logo@2x.png


BIN
assets/images/react-logo@3x.png


BIN
assets/images/tutorial-web.png


+ 35 - 3
eslint.config.js

@@ -1,10 +1,42 @@
 // https://docs.expo.dev/guides/using-eslint/
 const { defineConfig } = require('eslint/config');
-const expoConfig = require("eslint-config-expo/flat");
+const expoConfig = require('eslint-config-expo/flat');
 
 module.exports = defineConfig([
   expoConfig,
   {
-    ignores: ["dist/*"],
-  }
+    files: ['**/*.{ts,tsx,js,jsx}'],
+    rules: {
+      // type-only 导入与值导入分开,autofix 自动加 `type` 关键字
+      '@typescript-eslint/consistent-type-imports': [
+        'error',
+        { prefer: 'type-imports', fixStyle: 'separate-type-imports' },
+      ],
+      // 当一条 import 同时包含 type 和 value,强制拆成两条 `import type` / `import`
+      'import/consistent-type-specifier-style': ['error', 'prefer-top-level'],
+      // import 自动排序与分组
+      'import/order': [
+        'error',
+        {
+          groups: [
+            ['builtin', 'external'],
+            'internal',
+            ['parent', 'sibling', 'index'],
+            'object',
+            'type',
+          ],
+          pathGroups: [
+            { pattern: '@/**', group: 'internal', position: 'before' },
+          ],
+          pathGroupsExcludedImportTypes: ['builtin', 'type'],
+          'newlines-between': 'never',
+          alphabetize: { order: 'asc', caseInsensitive: true },
+        },
+      ],
+      'import/no-duplicates': ['error', { 'prefer-inline': false }],
+    },
+  },
+  {
+    ignores: ['dist/*'],
+  },
 ]);

+ 4 - 4
ios/LoanAssistant.xcodeproj/project.pbxproj

@@ -372,11 +372,11 @@
 				);
 				OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
 				PRODUCT_BUNDLE_IDENTIFIER = com.cdloan.assistant;
-				PRODUCT_NAME = LoanAssistant;
+				PRODUCT_NAME = "LoanAssistant";
 				SWIFT_OBJC_BRIDGING_HEADER = "LoanAssistant/LoanAssistant-Bridging-Header.h";
 				SWIFT_OPTIMIZATION_LEVEL = "-Onone";
 				SWIFT_VERSION = 5.0;
-				TARGETED_DEVICE_FAMILY = 1;
+				TARGETED_DEVICE_FAMILY = "1";
 				VERSIONING_SYSTEM = "apple-generic";
 			};
 			name = Debug;
@@ -403,10 +403,10 @@
 				);
 				OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
 				PRODUCT_BUNDLE_IDENTIFIER = com.cdloan.assistant;
-				PRODUCT_NAME = LoanAssistant;
+				PRODUCT_NAME = "LoanAssistant";
 				SWIFT_OBJC_BRIDGING_HEADER = "LoanAssistant/LoanAssistant-Bridging-Header.h";
 				SWIFT_VERSION = 5.0;
-				TARGETED_DEVICE_FAMILY = 1;
+				TARGETED_DEVICE_FAMILY = "1";
 				VERSIONING_SYSTEM = "apple-generic";
 			};
 			name = Release;

+ 3 - 3
ios/Podfile.lock

@@ -2681,7 +2681,7 @@ SPEC CHECKSUMS:
   EXUpdates: 1457ed285cd7c5e9d4c388ace518af55fa4b21ec
   EXUpdatesInterface: 39d05400a9226c438565dbaaa8447b5f0672ba45
   FBLazyVector: 061f518bbd81677ed8a8317e2ae60b8779495808
-  hermes-engine: f4c4bf9a6979a9d777e0e4696e56be72362744d6
+  hermes-engine: e4ecc5957f671df3fa4828a33b435bd6955c25a5
   libavif: 84bbb62fb232c3018d6f1bab79beea87e35de7b7
   libdav1d: 23581a4d8ec811ff171ed5e2e05cd27bad64c39f
   libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
@@ -2697,7 +2697,7 @@ SPEC CHECKSUMS:
   React: 3e14066ac707b3e369d09e2e923d8bee7f8c33ff
   React-callinvoker: bd7959a24564feaf5e4c8c11789e64884da13482
   React-Core: f060f4e14e9301685ce63ea65fbe903bf3397e45
-  React-Core-prebuilt: 0bd7bd67a89706f97a8d6150c409951862aecfc2
+  React-Core-prebuilt: e958d0ca3f8828f37f35c4cff2594b6e080fd991
   React-CoreModules: 89a1a544297be88ca4cdff317423f9e0d0c192a4
   React-cxxreact: 81b1bfa305b1b759cc0fe18327644816216e5993
   React-debug: 234fddb309822e13b9b442ef1bdca8d2b8c3cbf2
@@ -2760,7 +2760,7 @@ SPEC CHECKSUMS:
   ReactAppDependencyProvider: 2b19d66e5ddfe8dc7afb6338a4626156cbf2bab1
   ReactCodegen: 43f0948182edee9407c7b977fb059455dfeb9361
   ReactCommon: c6e81cc1ae185fa84863f3ea1d58caac4be741d7
-  ReactNativeDependencies: bcf3a9cabdc4c83653b9318342a4025f5a2effc1
+  ReactNativeDependencies: e3081feec6a8266d996ca8ce3d5707360258baf8
   RNGestureHandler: 0ea8153746a92b3744d4eaadade647debedf646e
   RNReanimated: 86e5991396f1aa514db90d6c79d4c3e37a37bb10
   RNScreens: 02e62f995ceb94ea465864b3bf4d4767b87f0362

+ 3 - 4
src/app/(tabs)/_layout.tsx

@@ -1,13 +1,12 @@
-import { Colors } from '@/constants/theme';
-import { useAuth } from '@/utils/auth';
 import { BlurView } from 'expo-blur';
 import { Image } from 'expo-image';
 import { router, Tabs } from 'expo-router';
 import React, { useEffect } from 'react';
 import { Platform, View } from 'react-native';
-
-const HOME_ICON = require('@/assets/images/tabIcons/home.png');
+import { Colors } from '@/constants/theme';
+import { useAuth } from '@/utils/auth';
 const EXPLORE_ICON = require('@/assets/images/tabIcons/explore.png');
+const HOME_ICON = require('@/assets/images/tabIcons/home.png');
 const ICON_STYLE = { width: 24, height: 24 };
 
 function tinted(source: number) {

+ 139 - 114
src/app/(tabs)/analytics.tsx

@@ -1,14 +1,16 @@
-import { SectionHeader } from '@/components/ui/section-header';
-import api, { ListResponse } from '@/utils/api';
-import { useSWC } from '@/utils/cache';
 import { ActivityIndicator } from '@ant-design/react-native';
 import { Ionicons } from '@expo/vector-icons';
-import { useFocusEffect } from 'expo-router';
+import { BlurView } from 'expo-blur';
+import { Stack, useFocusEffect } from 'expo-router';
 import React, { useCallback } from 'react';
-import { Pressable, ScrollView, Text, View } from 'react-native';
-import { SafeAreaView } from 'react-native-safe-area-context';
+import { Pressable, Text, View } from 'react-native';
+import Animated, { Extrapolation, interpolate, useAnimatedScrollHandler, useAnimatedStyle, useSharedValue } from 'react-native-reanimated';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+import { SectionHeader } from '@/components/ui/section-header';
+import api from '@/utils/api';
+import { useSWC } from '@/utils/cache';
 import { UploadComponent } from '../../components/upload';
-
+import type { ListResponse } from '@/utils/api';
 
 
 type AnalysisRecord = {
@@ -35,122 +37,145 @@ export default function AnalyticsScreen() {
       load();
     }, [])
   );
+  const insets = useSafeAreaInsets();
+
+  const scrollOffsetY = useSharedValue(0);
+  const scrollHandler = useAnimatedScrollHandler((e) => {
+    scrollOffsetY.value = e.contentOffset.y;
+  });
+  const headerStyle = useAnimatedStyle(() => ({
+    opacity: interpolate(scrollOffsetY.value, [0, 24 + insets.top], [0, 1], Extrapolation.CLAMP),
+  }));
 
   return (
-    <SafeAreaView className="flex-1 bg-surface" edges={['top']}>
-      <ScrollView
-        className="flex-1"
-        contentContainerClassName="px-5 pt-3 pb-24"
-        showsVerticalScrollIndicator={false}
-      >
-        <Text className="mb-2 text-3xl font-extrabold tracking-tight text-on-surface">
-          征信分析
+    <Animated.ScrollView
+      className="flex-1"
+      contentInset={{ top: insets.top, bottom: insets.bottom }}
+      automaticallyAdjustContentInsets
+      contentContainerClassName="px-5 pt-3 pb-24"
+      onScroll={scrollHandler}
+      scrollEventThrottle={16}
+    >
+      <Stack.Screen options={{
+        headerShown: true,
+        headerTransparent: true,
+        header: () => (
+          <Animated.View style={headerStyle} className="border-b border-outline-variant/60 android:bg-surface-container-lowest/90">
+            <BlurView tint="light" className="bg-background">
+              <Text style={{ marginTop: insets.top }} className="pl-10 h-[44px] text-3xl font-extrabold tracking-tight text-on-surface">
+                征信分析
+              </Text>
+            </BlurView>
+          </Animated.View>
+        ),
+      }} />
+      <Text className="pl-5 h-[44px] text-3xl font-extrabold tracking-tight text-on-surface">
+        征信分析
+      </Text>
+
+      <View className="mb-3 rounded-2xl border border-outline-variant bg-surface-container-lowest p-4">
+        <Text className="mb-3 text-xs font-bold uppercase tracking-widest text-outline">
+          选择客户
         </Text>
-
-        <View className="mb-3 rounded-2xl border border-outline-variant bg-surface-container-lowest p-4">
-          <Text className="mb-3 text-xs font-bold uppercase tracking-widest text-outline">
-            选择客户
+        <Pressable
+          className="flex-row items-center rounded-2xl bg-surface-container-low px-4 py-3.5"
+          style={({ pressed }) => ({
+            opacity: pressed ? 0.9 : 1,
+          })}
+        >
+          <Ionicons name="person-outline" size={18} color="#737686" />
+          <Text className="ml-3 flex-1 text-base font-medium text-on-surface">
+            张德发(138****8888)
           </Text>
-          <Pressable
-            className="flex-row items-center rounded-2xl bg-surface-container-low px-4 py-3.5"
-            style={({ pressed }) => ({
-              opacity: pressed ? 0.9 : 1,
-            })}
-          >
-            <Ionicons name="person-outline" size={18} color="#737686" />
-            <Text className="ml-3 flex-1 text-base font-medium text-on-surface">
-              张德发(138****8888)
-            </Text>
-            <Ionicons name="chevron-down" size={18} color="#c3c6d7" />
-          </Pressable>
-        </View>
+          <Ionicons name="chevron-down" size={18} color="#c3c6d7" />
+        </Pressable>
+      </View>
 
-        <UploadComponent onComplete={() => { refresh() }} />
+      <UploadComponent onComplete={() => { refresh() }} />
 
 
 
-        <SectionHeader title="解析记录" actionText="查看全部" />
-        <View className="gap-3">
-          {loading === true && <ActivityIndicator />}
-          {list?.list?.map((record) => (
-            <Pressable
-              key={record.id}
-              className="flex-row items-center rounded-2xl border border-outline-variant bg-surface-container-lowest px-4 py-3.5"
-              style={({ pressed }) => ({
-                opacity: pressed ? 0.93 : 1,
-              })}
+      <SectionHeader title="解析记录" actionText="查看全部" />
+      <View className="gap-3">
+        {loading === true && <ActivityIndicator />}
+        {list?.list?.map((record) => (
+          <Pressable
+            key={record.id}
+            className="flex-row items-center rounded-2xl border border-outline-variant bg-surface-container-lowest px-4 py-3.5"
+            style={({ pressed }) => ({
+              opacity: pressed ? 0.93 : 1,
+            })}
+          >
+            <View
+              className={`mr-3 h-11 w-11 items-center justify-center rounded-full ${record.status === 'completed'
+                ? 'bg-green-50'
+                : record.status === 'pending'
+                  ? 'bg-blue-50'
+                  : 'bg-error-container'
+                }`}
             >
-              <View
-                className={`mr-3 h-11 w-11 items-center justify-center rounded-full ${record.status === 'completed'
-                  ? 'bg-green-50'
-                  : record.status === 'pending'
-                    ? 'bg-blue-50'
-                    : 'bg-error-container'
-                  }`}
-              >
-                <Ionicons
-                  name={
-                    record.status === 'completed'
-                      ? 'checkmark-circle'
-                      : record.status === 'pending'
-                        ? 'hourglass-outline'
-                        : 'alert-circle'
-                  }
-                  size={20}
-                  color={
-                    record.status === 'completed'
-                      ? '#16a34a'
-                      : record.status === 'pending'
-                        ? '#2563eb'
-                        : '#ba1a1a'
-                  }
-                />
+              <Ionicons
+                name={
+                  record.status === 'completed'
+                    ? 'checkmark-circle'
+                    : record.status === 'pending'
+                      ? 'hourglass-outline'
+                      : 'alert-circle'
+                }
+                size={20}
+                color={
+                  record.status === 'completed'
+                    ? '#16a34a'
+                    : record.status === 'pending'
+                      ? '#2563eb'
+                      : '#ba1a1a'
+                }
+              />
+            </View>
+
+            <View className="flex-1">
+              <View className="mb-1 flex-row items-center justify-between gap-3">
+                <Text className="text-base font-bold text-on-surface">
+                  {record.customer_name || record.name || '客户'}
+                </Text>
+                <Text className="text-xs text-on-surface-variant">{record.createtime}</Text>
               </View>
-
-              <View className="flex-1">
-                <View className="mb-1 flex-row items-center justify-between gap-3">
-                  <Text className="text-base font-bold text-on-surface">
-                    {record.customer_name || record.name || '客户'}
-                  </Text>
-                  <Text className="text-xs text-on-surface-variant">{record.createtime}</Text>
-                </View>
-                {(record.status === 'pending' || record.status == 'failed') && (
-                  <View>
-                    <View className="mb-2 flex-row items-center justify-between">
-                      <Text className="text-sm text-on-surface-variant">...</Text>
-                      <Text className="text-sm font-bold text-primary">
-                        {record.score}
-                      </Text>
-                    </View>
-
-                  </View>)}
-
-                {record.status === 'completed' && (
-                  <View>
-                    <View className="mb-2 flex-row items-center justify-between">
-                      <Text className="text-sm text-on-surface-variant">...</Text>
-                      <Text className="text-sm font-bold text-primary">
-                        {record.score}
-                      </Text>
-                    </View>
-                    <View className="h-1.5 rounded-full bg-surface-container">
-                      <View
-                        className="h-full rounded-full bg-primary-container"
-                        style={{ width: '10%' }}
-                      />
-                    </View>
-                  </View>)}
-                {record.status === 'canceled' && (
-                  <Text className="text-sm text-error">解析失败,请联系管理员</Text>
-                )}
-
-              </View>
-
-              <Ionicons name="chevron-forward" size={18} color="#c3c6d7" />
-            </Pressable>
-          ))}
-        </View>
-      </ScrollView>
-    </SafeAreaView>
+              {(record.status === 'pending' || record.status == 'failed') && (
+                <View>
+                  <View className="mb-2 flex-row items-center justify-between">
+                    <Text className="text-sm text-on-surface-variant">...</Text>
+                    <Text className="text-sm font-bold text-primary">
+                      {record.score}
+                    </Text>
+                  </View>
+
+                </View>)}
+
+              {record.status === 'completed' && (
+                <View>
+                  <View className="mb-2 flex-row items-center justify-between">
+                    <Text className="text-sm text-on-surface-variant">...</Text>
+                    <Text className="text-sm font-bold text-primary">
+                      {record.score}
+                    </Text>
+                  </View>
+                  <View className="h-1.5 rounded-full bg-surface-container">
+                    <View
+                      className="h-full rounded-full bg-primary-container"
+                      style={{ width: '10%' }}
+                    />
+                  </View>
+                </View>)}
+              {record.status === 'canceled' && (
+                <Text className="text-sm text-error">解析失败,请联系管理员</Text>
+              )}
+
+            </View>
+
+            <Ionicons name="chevron-forward" size={18} color="#c3c6d7" />
+          </Pressable>
+        ))}
+      </View>
+    </Animated.ScrollView>
   );
 }

+ 40 - 12
src/app/(tabs)/customer.tsx

@@ -1,12 +1,15 @@
-import { StatusBadge } from '@/components/ui/status-badge';
-import api, { ListResponse } from '@/utils/api';
-import { getApiCache } from '@/utils/storage';
 import { ActivityIndicator, Toast } from '@ant-design/react-native';
 import { Ionicons } from '@expo/vector-icons';
-import { useFocusEffect } from 'expo-router';
+import { BlurView } from 'expo-blur';
+import { Stack, useFocusEffect } from 'expo-router';
 import React, { useCallback, useRef, useState } from 'react';
-import { FlatList, Pressable, Text, TextInput, View } from 'react-native';
-import { SafeAreaView } from 'react-native-safe-area-context';
+import { Pressable, Text, TextInput, View } from 'react-native';
+import Animated, { Extrapolation, interpolate, useAnimatedScrollHandler, useAnimatedStyle, useSharedValue } from 'react-native-reanimated';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+import { StatusBadge } from '@/components/ui/status-badge';
+import api from '@/utils/api';
+import { getApiCache } from '@/utils/storage';
+import type { ListResponse } from '@/utils/api';
 
 type CustomerLoanStatus = 'matched' | 'unmatch' | 'pending' | 'completed' | undefined;
 const CustomerLoanStatusText: Record<NonNullable<CustomerLoanStatus>, string> = {
@@ -96,6 +99,15 @@ export default function CustomerScreens() {
   const hasMoreRef = useRef(true);
   const loadingRef = useRef(false);
   const [loading, setLoading] = useState(true);
+  const insets = useSafeAreaInsets();
+
+  const scrollOffsetY = useSharedValue(0);
+  const scrollHandler = useAnimatedScrollHandler((e) => {
+    scrollOffsetY.value = e.contentOffset.y;
+  });
+  const headerStyle = useAnimatedStyle(() => ({
+    opacity: interpolate(scrollOffsetY.value, [0, 24 + insets.top], [0, 1], Extrapolation.CLAMP),
+  }));
 
   const load = useCallback(async (start: number, loanStatus?: CustomerLoanStatus) => {
     if (loadingRef.current) return;
@@ -154,7 +166,7 @@ export default function CustomerScreens() {
 
   const ListHeader = (
     <>
-      <Text className="mb-2 text-3xl font-extrabold tracking-tight text-on-surface">客户</Text>
+      <Text className="h-[44px] text-3xl font-extrabold tracking-tight text-on-surface">客户</Text>
       <Text className="mb-5 text-base leading-7 text-on-surface-variant">
         统一跟进客户资料、征信进度和产品匹配状态
       </Text>
@@ -179,7 +191,7 @@ export default function CustomerScreens() {
         {Object.entries(CustomerLoanStatusText).map(([key, item]) => (
           <Pressable
             key={key}
-            onPress={() => {}}
+            onPress={() => { }}
             className="rounded-full bg-surface-container-lowest px-4 py-2"
             style={({ pressed }) => ({ opacity: pressed ? 0.84 : 1 })}
           >
@@ -214,9 +226,11 @@ export default function CustomerScreens() {
     ) : null;
 
   return (
-    <SafeAreaView className="flex-1 bg-surface" edges={['top']}>
-      <FlatList
+    <>
+      <Animated.FlatList
         className="flex-1"
+        contentInset={{ top: insets.top, bottom: insets.bottom }}
+        automaticallyAdjustContentInsets
         contentContainerClassName="px-5 pt-3 pb-24 gap-3"
         data={list}
         keyExtractor={keyExtractor}
@@ -228,8 +242,22 @@ export default function CustomerScreens() {
         ListFooterComponent={ListFooter}
         onEndReached={loadMore}
         onEndReachedThreshold={0.5}
+        onScroll={scrollHandler}
+        scrollEventThrottle={16}
       />
-
+      <Stack.Screen options={{
+        headerShown: true,
+        headerTransparent: true,
+        header: () => (
+          <Animated.View style={headerStyle} className="border-b border-outline-variant/60 android:bg-surface-container-lowest/90">
+            <BlurView tint="light" className="bg-background">
+              <Text style={{ marginTop: insets.top }} className="pl-10 h-[44px] text-3xl font-extrabold tracking-tight text-on-surface">
+                客户
+              </Text>
+            </BlurView>
+          </Animated.View>
+        ),
+      }} />
       <View className="absolute bottom-7 right-5">
         <Pressable
           className="h-14 w-14 items-center justify-center rounded-full bg-primary-container"
@@ -241,6 +269,6 @@ export default function CustomerScreens() {
           <Ionicons name="person-add" size={22} color="#ffffff" />
         </Pressable>
       </View>
-    </SafeAreaView>
+    </>
   );
 }

+ 5 - 6
src/app/(tabs)/index.tsx

@@ -1,14 +1,13 @@
-import { SectionHeader } from '@/components/ui/section-header';
-import { StatusBadge } from '@/components/ui/status-badge';
-import UIButton from '@/components/ui/ui-button';
-import api from '@/utils/api';
-import { useSWC } from '@/utils/cache';
 import { Ionicons } from '@expo/vector-icons';
-
 import { useFocusEffect } from 'expo-router';
 import { useCallback, useState } from 'react';
 import { Pressable, ScrollView, Text, View } from 'react-native';
 import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
+import { SectionHeader } from '@/components/ui/section-header';
+import { StatusBadge } from '@/components/ui/status-badge';
+import UIButton from '@/components/ui/ui-button';
+import api from '@/utils/api';
+import { useSWC } from '@/utils/cache';
 import { UploadScreen } from '../../components/upload';
 
 

+ 4 - 4
src/app/(tabs)/profile.tsx

@@ -1,12 +1,12 @@
-import { MenuRow } from '@/components/ui/menu-row';
-import api from '@/utils/api';
-import { signOut, useAuthContext } from '@/utils/auth';
-import { useSWC } from '@/utils/cache';
 import { ActivityIndicator, Modal, Toast } from '@ant-design/react-native';
 import { Ionicons } from '@expo/vector-icons';
 import { Image } from 'expo-image';
 import { Pressable, ScrollView, Text, View } from 'react-native';
 import { SafeAreaView } from 'react-native-safe-area-context';
+import { MenuRow } from '@/components/ui/menu-row';
+import api from '@/utils/api';
+import { signOut, useAuthContext } from '@/utils/auth';
+import { useSWC } from '@/utils/cache';
 export default function ProfileScreen() {
   const { setToken } = useAuthContext();
 

+ 207 - 182
src/app/(tabs)/reports.tsx

@@ -1,8 +1,11 @@
-import { StatusBadge } from '@/components/ui/status-badge';
 import { Ionicons } from '@expo/vector-icons';
+import { BlurView } from 'expo-blur';
+import { Stack } from 'expo-router';
 import React, { useState } from 'react';
-import { Pressable, ScrollView, Text, View } from 'react-native';
-import { SafeAreaView } from 'react-native-safe-area-context';
+import { Pressable, Text, View } from 'react-native';
+import Animated, { Extrapolation, interpolate, useAnimatedScrollHandler, useAnimatedStyle, useSharedValue } from 'react-native-reanimated';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+import { StatusBadge } from '@/components/ui/status-badge';
 
 type ReportTab = '全部' | '征信报告' | '匹配结果';
 
@@ -91,6 +94,15 @@ function getStatusVariant(status: Report['status']) {
 
 export default function ReportsScreen() {
   const [activeTab, setActiveTab] = useState<ReportTab>('全部');
+  const insets = useSafeAreaInsets();
+
+  const scrollOffsetY = useSharedValue(0);
+  const scrollHandler = useAnimatedScrollHandler((e) => {
+    scrollOffsetY.value = e.contentOffset.y;
+  });
+  const headerStyle = useAnimatedStyle(() => ({
+    opacity: interpolate(scrollOffsetY.value, [0, 24 + insets.top], [0, 1], Extrapolation.CLAMP),
+  }));
 
   const filteredReports = REPORTS.filter(
     (item) => activeTab === '全部' || item.type === activeTab
@@ -99,205 +111,218 @@ export default function ReportsScreen() {
   const completedCount = REPORTS.filter((item) => item.status === '已完成').length;
 
   return (
-    <SafeAreaView className="flex-1 bg-surface" edges={['top']}>
-      <ScrollView
-        className="flex-1"
-        contentContainerClassName="px-5 pt-3 pb-24"
-        showsVerticalScrollIndicator={false}
-      >
-        <Text className="mb-2 text-3xl font-extrabold tracking-tight text-on-surface">
-          报表
-        </Text>
-        <Text className="mb-5 text-base leading-7 text-on-surface-variant">
-          汇总查看征信分析结果、匹配建议和当前处理进度
-        </Text>
 
-        <View className="mb-5 flex-row gap-2.5">
-          <View className="flex-1 items-center rounded-2xl border border-outline-variant bg-surface-container-lowest px-3 py-3.5">
-            <Text className="text-2xl font-extrabold text-primary">{REPORTS.length}</Text>
-            <Text className="mt-2 text-xs font-bold uppercase tracking-widest text-outline">
-              总报告数
-            </Text>
-          </View>
-          <View className="flex-1 items-center rounded-2xl border border-outline-variant bg-surface-container-lowest px-3 py-3.5">
-            <Text className="text-2xl font-extrabold text-primary">{completedCount}</Text>
-            <Text className="mt-2 text-xs font-bold uppercase tracking-widest text-outline">
-              已完成
-            </Text>
-          </View>
-          <View className="flex-1 items-center rounded-2xl border border-outline-variant bg-surface-container-lowest px-3 py-3.5">
-            <Text className="text-2xl font-extrabold text-primary">92%</Text>
-            <Text className="mt-2 text-xs font-bold uppercase tracking-widest text-outline">
-              最高匹配
-            </Text>
-          </View>
+    <Animated.ScrollView
+      className="flex-1"
+      contentInset={{ top: insets.top, bottom: insets.bottom }}
+      automaticallyAdjustContentInsets
+      contentContainerClassName="px-5 pt-3 pb-24"
+      showsVerticalScrollIndicator={false}
+      onScroll={scrollHandler}
+      scrollEventThrottle={16}
+    >
+      <Stack.Screen options={{
+        headerShown: true,
+        headerTransparent: true,
+        header: () => (
+          <Animated.View style={headerStyle} className="border-b border-outline-variant/60 android:bg-surface-container-lowest/90">
+            <BlurView tint="light" className="bg-background">
+              <Text style={{ marginTop: insets.top }} className="pl-10 h-[44px] text-3xl font-extrabold tracking-tight text-on-surface">
+                报表
+              </Text>
+            </BlurView>
+          </Animated.View>
+        ),
+      }} />
+      <Text className="h-[44px] text-3xl font-extrabold tracking-tight text-on-surface">
+        报表
+      </Text>
+      <Text className="mb-5 text-base leading-7 text-on-surface-variant">
+        汇总查看征信分析结果、匹配建议和当前处理进度
+      </Text>
+
+      <View className="mb-5 flex-row gap-2.5">
+        <View className="flex-1 items-center rounded-2xl border border-outline-variant bg-surface-container-lowest px-3 py-3.5">
+          <Text className="text-2xl font-extrabold text-primary">{REPORTS.length}</Text>
+          <Text className="mt-2 text-xs font-bold uppercase tracking-widest text-outline">
+            总报告数
+          </Text>
+        </View>
+        <View className="flex-1 items-center rounded-2xl border border-outline-variant bg-surface-container-lowest px-3 py-3.5">
+          <Text className="text-2xl font-extrabold text-primary">{completedCount}</Text>
+          <Text className="mt-2 text-xs font-bold uppercase tracking-widest text-outline">
+            已完成
+          </Text>
         </View>
+        <View className="flex-1 items-center rounded-2xl border border-outline-variant bg-surface-container-lowest px-3 py-3.5">
+          <Text className="text-2xl font-extrabold text-primary">92%</Text>
+          <Text className="mt-2 text-xs font-bold uppercase tracking-widest text-outline">
+            最高匹配
+          </Text>
+        </View>
+      </View>
 
-        <View className="mb-5 rounded-2xl bg-surface-container-low p-1">
-          <View className="flex-row">
-            {TABS.map((tab) => {
-              const active = activeTab === tab;
-              return (
-                <Pressable
-                  key={tab}
-                  onPress={() => setActiveTab(tab)}
-                  className={`flex-1 rounded-xl py-2.5 ${
-                    active ? 'bg-surface-container-lowest' : ''
+      <View className="mb-5 rounded-2xl bg-surface-container-low p-1">
+        <View className="flex-row">
+          {TABS.map((tab) => {
+            const active = activeTab === tab;
+            return (
+              <Pressable
+                key={tab}
+                onPress={() => setActiveTab(tab)}
+                className={`flex-1 rounded-xl py-2.5 ${active ? 'bg-surface-container-lowest' : ''
                   }`}
-                  style={({ pressed }) => ({
-                    opacity: pressed ? 0.88 : 1,
-                  })}
-                >
-                  <Text
-                    className={`text-center text-sm font-bold ${
-                      active ? 'text-primary' : 'text-on-surface-variant'
+                style={({ pressed }) => ({
+                  opacity: pressed ? 0.88 : 1,
+                })}
+              >
+                <Text
+                  className={`text-center text-sm font-bold ${active ? 'text-primary' : 'text-on-surface-variant'
                     }`}
-                  >
-                    {tab}
-                  </Text>
-                </Pressable>
-              );
-            })}
-          </View>
+                >
+                  {tab}
+                </Text>
+              </Pressable>
+            );
+          })}
         </View>
+      </View>
 
-        <View className="gap-3">
-          {filteredReports.map((report) => (
-            <Pressable
-              key={report.id}
-              className="rounded-2xl border border-outline-variant bg-surface-container-lowest px-4 py-4"
-              style={({ pressed }) => ({
-                opacity: pressed ? 0.93 : 1,
-              })}
-            >
-              <View className="mb-3 flex-row items-start justify-between gap-3">
-                <View className="flex-1 flex-row items-center gap-3">
-                  <View
-                    className={`h-11 w-11 items-center justify-center rounded-2xl ${
-                      report.type === '征信报告' ? 'bg-blue-50' : 'bg-green-50'
+      <View className="gap-3">
+        {filteredReports.map((report) => (
+          <Pressable
+            key={report.id}
+            className="rounded-2xl border border-outline-variant bg-surface-container-lowest px-4 py-4"
+            style={({ pressed }) => ({
+              opacity: pressed ? 0.93 : 1,
+            })}
+          >
+            <View className="mb-3 flex-row items-start justify-between gap-3">
+              <View className="flex-1 flex-row items-center gap-3">
+                <View
+                  className={`h-11 w-11 items-center justify-center rounded-2xl ${report.type === '征信报告' ? 'bg-blue-50' : 'bg-green-50'
                     }`}
-                  >
-                    <Ionicons
-                      name={
-                        report.type === '征信报告'
-                          ? 'document-text-outline'
-                          : 'git-compare-outline'
-                      }
-                      size={20}
-                      color={report.type === '征信报告' ? '#2563eb' : '#16a34a'}
-                    />
-                  </View>
-                  <View className="flex-1">
-                    <Text className="text-lg font-bold text-on-surface">
-                      {report.customerName}
-                    </Text>
-                    <Text className="mt-1 text-sm text-on-surface-variant">
-                      {report.type}
-                    </Text>
-                  </View>
-                </View>
-                <View className="items-end">
-                  <Text className="mb-1.5 text-xs text-on-surface-variant">{report.time}</Text>
-                  <StatusBadge
-                    text={report.status}
-                    variant={getStatusVariant(report.status)}
+                >
+                  <Ionicons
+                    name={
+                      report.type === '征信报告'
+                        ? 'document-text-outline'
+                        : 'git-compare-outline'
+                    }
+                    size={20}
+                    color={report.type === '征信报告' ? '#2563eb' : '#16a34a'}
                   />
                 </View>
-              </View>
-
-              {report.type === '征信报告' && report.status === '已完成' ? (
-                <View>
-                  <View className="mb-3 flex-row items-center gap-3">
-                    <View className="h-14 w-14 items-center justify-center rounded-full border-4 border-primary-fixed bg-surface-container-lowest">
-                      <Text className="text-lg font-extrabold text-primary">
-                        {report.score}
-                      </Text>
-                    </View>
-                    <View className="flex-1">
-                      <Text className="mb-1.5 text-sm text-on-surface-variant">关键标签</Text>
-                      <View className="flex-row flex-wrap gap-2">
-                        {report.tags?.map((tag) => (
-                          <StatusBadge
-                            key={`${report.id}-${tag.text}`}
-                            text={tag.text}
-                            variant={tag.variant}
-                          />
-                        ))}
-                      </View>
-                    </View>
-                  </View>
-                  <Text className="text-sm leading-6 text-on-surface-variant">
-                    建议可申请额度:20万-35万,优先推荐更看重流水稳定性的产品。
+                <View className="flex-1">
+                  <Text className="text-lg font-bold text-on-surface">
+                    {report.customerName}
+                  </Text>
+                  <Text className="mt-1 text-sm text-on-surface-variant">
+                    {report.type}
                   </Text>
                 </View>
-              ) : null}
+              </View>
+              <View className="items-end">
+                <Text className="mb-1.5 text-xs text-on-surface-variant">{report.time}</Text>
+                <StatusBadge
+                  text={report.status}
+                  variant={getStatusVariant(report.status)}
+                />
+              </View>
+            </View>
 
-              {report.type === '匹配结果' && report.status === '已完成' ? (
-                <View className="flex-row gap-2.5">
-                  <View className="flex-1 rounded-xl bg-surface-container-low px-4 py-3.5">
-                    <Text className="text-2xl font-bold text-on-surface">
-                      {report.matchCount}
-                    </Text>
-                    <Text className="mt-1 text-xs text-on-surface-variant">匹配产品数</Text>
-                  </View>
-                  <View className="flex-1 rounded-xl bg-surface-container-low px-4 py-3.5">
-                    <Text className="text-2xl font-bold text-primary">
-                      {report.matchRate}
+            {report.type === '征信报告' && report.status === '已完成' ? (
+              <View>
+                <View className="mb-3 flex-row items-center gap-3">
+                  <View className="h-14 w-14 items-center justify-center rounded-full border-4 border-primary-fixed bg-surface-container-lowest">
+                    <Text className="text-lg font-extrabold text-primary">
+                      {report.score}
                     </Text>
-                    <Text className="mt-1 text-xs text-on-surface-variant">最高匹配度</Text>
-                  </View>
-                </View>
-              ) : null}
-
-              {report.status === '解析中' ? (
-                <View className="mt-1">
-                  <View className="mb-2 flex-row items-center justify-between">
-                    <Text className="text-sm text-on-surface-variant">正在生成报告...</Text>
-                    <Text className="text-sm font-bold text-primary">72%</Text>
                   </View>
-                  <View className="h-1.5 rounded-full bg-surface-container">
-                    <View
-                      className="h-full rounded-full bg-primary-container"
-                      style={{ width: '72%' }}
-                    />
+                  <View className="flex-1">
+                    <Text className="mb-1.5 text-sm text-on-surface-variant">关键标签</Text>
+                    <View className="flex-row flex-wrap gap-2">
+                      {report.tags?.map((tag) => (
+                        <StatusBadge
+                          key={`${report.id}-${tag.text}`}
+                          text={tag.text}
+                          variant={tag.variant}
+                        />
+                      ))}
+                    </View>
                   </View>
                 </View>
-              ) : null}
-
-              {report.status === '待匹配' ? (
                 <Text className="text-sm leading-6 text-on-surface-variant">
-                  当前客户信息已同步,待进入智能匹配流程生成推荐结果
+                  建议可申请额度:20万-35万,优先推荐更看重流水稳定性的产品。
                 </Text>
-              ) : null}
+              </View>
+            ) : null}
 
-              {report.status === '已完成' ? (
-                <View className="mt-3 flex-row gap-2.5">
-                  <Pressable
-                    className="flex-1 items-center rounded-xl bg-primary-container py-2.5"
-                    style={({ pressed }) => ({
-                      opacity: pressed ? 0.88 : 1,
-                    })}
-                  >
-                    <Text className="text-sm font-bold text-on-primary">
-                      {report.type === '征信报告' ? '查看详情' : '查看匹配'}
-                    </Text>
-                  </Pressable>
-                  <Pressable
-                    className="flex-1 items-center rounded-xl bg-surface-container-high py-2.5"
-                    style={({ pressed }) => ({
-                      opacity: pressed ? 0.88 : 1,
-                    })}
-                  >
-                    <Text className="text-sm font-semibold text-on-surface">
-                      {report.type === '征信报告' ? '智能匹配' : '重新匹配'}
-                    </Text>
-                  </Pressable>
+            {report.type === '匹配结果' && report.status === '已完成' ? (
+              <View className="flex-row gap-2.5">
+                <View className="flex-1 rounded-xl bg-surface-container-low px-4 py-3.5">
+                  <Text className="text-2xl font-bold text-on-surface">
+                    {report.matchCount}
+                  </Text>
+                  <Text className="mt-1 text-xs text-on-surface-variant">匹配产品数</Text>
                 </View>
-              ) : null}
-            </Pressable>
-          ))}
-        </View>
-      </ScrollView>
-    </SafeAreaView>
+                <View className="flex-1 rounded-xl bg-surface-container-low px-4 py-3.5">
+                  <Text className="text-2xl font-bold text-primary">
+                    {report.matchRate}
+                  </Text>
+                  <Text className="mt-1 text-xs text-on-surface-variant">最高匹配度</Text>
+                </View>
+              </View>
+            ) : null}
+
+            {report.status === '解析中' ? (
+              <View className="mt-1">
+                <View className="mb-2 flex-row items-center justify-between">
+                  <Text className="text-sm text-on-surface-variant">正在生成报告...</Text>
+                  <Text className="text-sm font-bold text-primary">72%</Text>
+                </View>
+                <View className="h-1.5 rounded-full bg-surface-container">
+                  <View
+                    className="h-full rounded-full bg-primary-container"
+                    style={{ width: '72%' }}
+                  />
+                </View>
+              </View>
+            ) : null}
+
+            {report.status === '待匹配' ? (
+              <Text className="text-sm leading-6 text-on-surface-variant">
+                当前客户信息已同步,待进入智能匹配流程生成推荐结果。
+              </Text>
+            ) : null}
+
+            {report.status === '已完成' ? (
+              <View className="mt-3 flex-row gap-2.5">
+                <Pressable
+                  className="flex-1 items-center rounded-xl bg-primary-container py-2.5"
+                  style={({ pressed }) => ({
+                    opacity: pressed ? 0.88 : 1,
+                  })}
+                >
+                  <Text className="text-sm font-bold text-on-primary">
+                    {report.type === '征信报告' ? '查看详情' : '查看匹配'}
+                  </Text>
+                </Pressable>
+                <Pressable
+                  className="flex-1 items-center rounded-xl bg-surface-container-high py-2.5"
+                  style={({ pressed }) => ({
+                    opacity: pressed ? 0.88 : 1,
+                  })}
+                >
+                  <Text className="text-sm font-semibold text-on-surface">
+                    {report.type === '征信报告' ? '智能匹配' : '重新匹配'}
+                  </Text>
+                </Pressable>
+              </View>
+            ) : null}
+          </Pressable>
+        ))}
+      </View>
+    </Animated.ScrollView>
   );
 }

+ 1 - 1
src/app/+html.tsx

@@ -1,5 +1,5 @@
 import { ScrollViewStyleReset } from 'expo-router/html';
-import { type PropsWithChildren } from 'react';
+import type {PropsWithChildren} from 'react';
 
 // This file is web-only and used to configure the root HTML for every
 // web page during static rendering.

+ 7 - 7
src/app/_layout.tsx

@@ -1,14 +1,7 @@
-import { AnimatedSplashOverlay } from '@/components/animated-icon';
-import { antdTheme } from '@/constants/antd-theme';
-import { Colors } from '@/constants/theme';
 import '@/global.css';
-import api from '@/utils/api';
-import { AuthProvider } from '@/utils/auth';
-import { getGlobalStorage } from '@/utils/storage';
 import { Modal, Provider, Toast } from '@ant-design/react-native';
 import {
   DefaultTheme as ReactNavigationDefaultTheme,
-  Theme,
   ThemeProvider,
 } from '@react-navigation/native';
 import * as application from 'expo-application';
@@ -19,6 +12,13 @@ import * as SplashScreen from 'expo-splash-screen';
 import * as updates from 'expo-updates';
 import { useEffect, useState } from 'react';
 import { Linking, Platform, View } from 'react-native';
+import { AnimatedSplashOverlay } from '@/components/animated-icon';
+import { antdTheme } from '@/constants/antd-theme';
+import { Colors } from '@/constants/theme';
+import api from '@/utils/api';
+import { AuthProvider } from '@/utils/auth';
+import { getGlobalStorage } from '@/utils/storage';
+import type { Theme } from '@react-navigation/native';
 
 
 SplashScreen.preventAutoHideAsync();

+ 3 - 0
src/app/credit/detail.tsx

@@ -0,0 +1,3 @@
+export default function CreditDetail() {
+
+}

+ 4 - 4
src/app/customer/add.tsx

@@ -1,10 +1,6 @@
-import UIButton from "@/components/ui/ui-button";
-import { Colors } from "@/constants/theme";
-import api from "@/utils/api";
 import { DatePicker, Input, Modal, Radio, Toast } from "@ant-design/react-native";
 import { Ionicons } from "@expo/vector-icons";
 import { usePreventRemove, useRoute } 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 {
@@ -18,7 +14,11 @@ import {
   View,
 } from "react-native";
 import { useSafeAreaInsets } from "react-native-safe-area-context";
+import UIButton from "@/components/ui/ui-button";
+import { Colors } from "@/constants/theme";
+import api from "@/utils/api";
 import { UploadScreen } from "../../components/upload";
+import type { NavigationAction } from "@react-navigation/routers";
 
 type CustomerForm = {
   name: string;

+ 21 - 15
src/app/sign-in.tsx

@@ -1,4 +1,3 @@
-import { signIn, smsSignIn, useAuth } from '@/utils/auth';
 import { Button, Toast } from '@ant-design/react-native';
 import { Ionicons } from '@expo/vector-icons';
 import { Link, router, useLocalSearchParams } from 'expo-router';
@@ -8,18 +7,19 @@ import {
   KeyboardAvoidingView,
   Platform,
   Pressable,
-  ScrollView,
   StyleSheet,
   Text,
   TextInput,
-  View,
+  View
 } from 'react-native';
+import Animated, { scrollTo, useAnimatedRef, useDerivedValue, useSharedValue } from 'react-native-reanimated';
 import { useSafeAreaInsets } from 'react-native-safe-area-context';
-
-import CaptchaBox, { CaptchaRes } from '@/components/captcha-box';
+import CaptchaBox from '@/components/captcha-box';
 import { site } from '@/config.json';
 import { useInterval } from '@/hooks/hooks';
 import api, { ApiError } from '@/utils/api';
+import { signIn, smsSignIn, useAuth } from '@/utils/auth';
+import type { CaptchaRes } from '@/components/captcha-box';
 
 function FieldLabel({ children }: { children: React.ReactNode }) {
   return (
@@ -40,12 +40,17 @@ export default function SignInScreen() {
 
   const { redirectTo } = useLocalSearchParams<{ redirectTo?: string }>();
   const { setToken } = useAuth();
-  const scrollView = useRef<ScrollView>(null);
+  const scrollViewRef = useAnimatedRef();
+  const scrollY = useSharedValue(0);
+
+  useDerivedValue(() => {
+    scrollTo(scrollViewRef, 0, scrollY.value, true);
+  });
 
   const [captchaVisible, setCaptchaVisible] = useState<boolean>(false);
   const [smsTtl, setSmsTtl] = useState(0);
 
-  
+
   const waitCaptcha = useRef<(res: CaptchaRes) => void | null>(null);
   const needsCaptchaRef = useRef(false);
   const handleCaptcha = (res: CaptchaRes) => {
@@ -113,6 +118,7 @@ export default function SignInScreen() {
 
   const handleLogin = async () => {
     setFlushAgree(false);
+    scrollY.value = 0;
     if (mobile.trim().length !== 11) {
       Toast.fail('请输入正确的手机号');
       return;
@@ -130,9 +136,9 @@ export default function SignInScreen() {
     }
 
     if (!agreed) {
-      Toast.fail('请先阅读并同意协议');
+      scrollY.value = 99999;
       setFlushAgree(true);
-      // scrollView.current?.scrollToEnd({ animated: true });
+      Toast.fail('请先阅读并同意协议');
       return;
     }
 
@@ -164,8 +170,8 @@ export default function SignInScreen() {
         className="flex-1"
         behavior={Platform.OS === 'ios' ? 'padding' : undefined}
       >
-        <ScrollView
-          ref={scrollView}
+        <Animated.ScrollView
+          ref={scrollViewRef}
           className="flex-1"
           contentContainerClassName="px-8"
           keyboardShouldPersistTaps="handled"
@@ -176,7 +182,7 @@ export default function SignInScreen() {
           <View className="absolute -bottom-16 right-0 h-48 w-48 rounded-full bg-primary-fixed/50" />
 
           <View className="flex-1 justify-between"
-            style={{ paddingTop: (insets.top ?? 10) + 52, paddingBottom: (insets.bottom ?? 8) + 24}}>
+            style={{ paddingTop: (insets.top ?? 10) + 52, paddingBottom: (insets.bottom ?? 8) + 24 }}>
             <View>
               <View className="mb-6">
                 <View className="mb-3 h-16 w-16 items-center justify-center rounded-2xl bg-primary-container shadow-lg">
@@ -267,7 +273,7 @@ export default function SignInScreen() {
                         disabled={loading || smsTtl > 0}
                         hitSlop={8}
                         onPress={handleSendCode}
-  
+
                       >
                         <Text className={`text-lg w-22 text-center font-bold leading-6 ${loading || smsTtl > 0 ? 'text-on-secondary-fixed/50' : 'text-primary'}`}>
                           {smsTtl > 0 ? `${smsTtl}s` : '发送验证码'}
@@ -361,10 +367,10 @@ export default function SignInScreen() {
               </View>
             </View>
           </View>
-        </ScrollView>
+        </Animated.ScrollView>
       </KeyboardAvoidingView>
       <CaptchaBox visible={captchaVisible} onClose={handleCaptcha} />
-      {loading && <View className="absolute left-0 right-0 top-0 bottom-0 z-1 bg-white/5"  />}
+      {loading && <View className="absolute left-0 right-0 top-0 bottom-0 z-1 bg-white/5" />}
     </View>
   );
 }

+ 27 - 18
src/app/sign-up.tsx

@@ -1,8 +1,3 @@
-import CaptchaBox, { CaptchaRes } from '@/components/captcha-box';
-import { site } from '@/config.json';
-import { useInterval } from '@/hooks/hooks';
-import api, { ApiError } from '@/utils/api';
-import { signUp, useAuth } from '@/utils/auth';
 import { Button, Toast } from '@ant-design/react-native';
 import { Ionicons } from '@expo/vector-icons';
 import { Link, router, useLocalSearchParams } from 'expo-router';
@@ -12,13 +7,19 @@ import {
   KeyboardAvoidingView,
   Platform,
   Pressable,
-  ScrollView,
   StyleSheet,
   Text,
   TextInput,
-  View,
+  View
 } from 'react-native';
+import Animated, { scrollTo, useAnimatedRef, useDerivedValue, useSharedValue } from 'react-native-reanimated';
 import { useSafeAreaInsets } from 'react-native-safe-area-context';
+import CaptchaBox from '@/components/captcha-box';
+import { site } from '@/config.json';
+import { useInterval } from '@/hooks/hooks';
+import api, { ApiError } from '@/utils/api';
+import { signUp, useAuth } from '@/utils/auth';
+import type { CaptchaRes } from '@/components/captcha-box';
 
 function FieldLabel({ children }: { children: React.ReactNode }) {
   return (
@@ -39,13 +40,20 @@ export default function SignUpScreen() {
   const [agreed, setAgreed] = useState(false);
   const [agreementHighlight, setFlushAgree] = useState(false);
   const [loading, setLoading] = useState(false);
-  const scrollView = useRef<ScrollView>(null);
+
+  const scrollViewRef = useAnimatedRef();
+  const scrollY = useSharedValue(0);
+
+  useDerivedValue(() => {
+    scrollTo(scrollViewRef, 0, scrollY.value, true);
+  });
+
   const [captchaVisible, setCaptchaVisible] = useState<boolean>(false);
   const [smsTtl, setSmsTtl] = useState(0);
   const { setToken } = useAuth();
 
   const { signIn, } = useLocalSearchParams<{ signIn: string; redirectTo?: string }>();
-  
+
   const waitCaptcha = useRef<(res: CaptchaRes) => void | null>(null);
   const needsCaptchaRef = useRef(false);
   const handleCaptcha = (res: CaptchaRes) => {
@@ -110,9 +118,9 @@ export default function SignUpScreen() {
   }, smsTtl > 0 ? 1000 : null);
 
   const handleRegister = async () => {
-    
-    setFlushAgree(false);
 
+    setFlushAgree(false);
+    scrollY.value = 0;
     if (mobile.trim().length !== 11) {
       Toast.fail('请输入正确的手机号');
       return;
@@ -145,9 +153,10 @@ export default function SignUpScreen() {
 
 
     if (!agreed) {
+      scrollY.value = 99999;
+      setFlushAgree(true);
+
       Toast.fail('请先阅读并同意协议');
-      requestAnimationFrame(()=>setFlushAgree(true));
-      scrollView.current?.scrollToEnd({ animated: true });
       return;
     }
 
@@ -182,11 +191,11 @@ export default function SignUpScreen() {
         className="flex-1"
         behavior={Platform.OS === 'ios' ? 'padding' : undefined}
       >
-        <ScrollView
-          ref={scrollView}
+        <Animated.ScrollView
+          ref={scrollViewRef}
           className="flex-1"
           contentContainerClassName="px-8"
-          
+
           keyboardShouldPersistTaps="handled"
           showsVerticalScrollIndicator={false}
         >
@@ -195,7 +204,7 @@ export default function SignUpScreen() {
           <View className="absolute -bottom-16 right-0 h-48 w-48 rounded-full bg-primary-fixed/50" />
 
           <View className="flex-1 justify-between"
-              style={{ paddingTop: (insets.top ?? 10) + 2, paddingBottom: (insets.bottom ?? 8) + 24}}>
+            style={{ paddingTop: (insets.top ?? 10) + 2, paddingBottom: (insets.bottom ?? 8) + 24 }}>
             <View>
               <View className="mb-12">
                 <View className="mb-6 h-16 w-16 items-center justify-center rounded-2xl bg-primary-container shadow-lg">
@@ -382,7 +391,7 @@ export default function SignUpScreen() {
               </Text>
             </View>
           </View>
-        </ScrollView>
+        </Animated.ScrollView>
       </KeyboardAvoidingView>
       <CaptchaBox visible={captchaVisible} onClose={handleCaptcha} />
     </View>

+ 3 - 3
src/components/captcha-box.tsx

@@ -1,10 +1,10 @@
-import { api as apiCfg } from '@/config.json';
-import { Colors } from '@/constants/theme';
-import api from '@/utils/api';
 import { Icon, Modal } from '@ant-design/react-native';
 import { Image } from 'expo-image';
 import { useCallback, useEffect, useRef, useState } from 'react';
 import { Pressable, TextInput, View } from 'react-native';
+import { api as apiCfg } from '@/config.json';
+import { Colors } from '@/constants/theme';
+import api from '@/utils/api';
 
 function blobToDataUrl(blob: Blob) {
     return new Promise<string>((resolve, reject) => {

+ 7 - 5
src/components/ui/ui-button.tsx

@@ -1,11 +1,13 @@
-import { Colors } from '@/constants/theme';
-import { isPromise } from '@/utils/tsutils';
 import { ActivityIndicator, 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 { Link } from 'expo-router';
 import React, { useEffect, useState } from 'react';
-import { Pressable, Text, View, ViewStyle } from 'react-native';
+import { Pressable, Text, View } from 'react-native';
+import { Colors } from '@/constants/theme';
+import { isPromise } from '@/utils/tsutils';
+import type {IconNames} from '@ant-design/react-native/lib/icon';
+import type { Href} from 'expo-router';
+import type { ViewStyle } from 'react-native';
 
 interface UIButtonProps {
     children?: string | React.ReactNode;

+ 8 - 6
src/components/upload.tsx

@@ -1,14 +1,9 @@
-import UIButton from '@/components/ui/ui-button';
-import { Colors } from '@/constants/theme';
-import api from '@/utils/api';
-import { openSystemSettings } from '@/utils/os';
 import { ActionSheet, ActivityIndicator, Icon, Modal, Toast } from '@ant-design/react-native';
 import { Ionicons } from '@expo/vector-icons';
 import clsx from 'clsx';
-import { DocumentPickerAsset, getDocumentAsync } from 'expo-document-picker';
+import { getDocumentAsync } from 'expo-document-picker';
 import { File } from 'expo-file-system';
 import {
-    ImagePickerAsset,
     launchCameraAsync,
     launchImageLibraryAsync,
     requestCameraPermissionsAsync,
@@ -17,6 +12,13 @@ import {
 import { Link } from 'expo-router';
 import { useCallback, useEffect, useState } from 'react';
 import { Platform, Pressable, Text, View } from 'react-native';
+import UIButton from '@/components/ui/ui-button';
+import { Colors } from '@/constants/theme';
+import api from '@/utils/api';
+import { openSystemSettings } from '@/utils/os';
+import type { DocumentPickerAsset} from 'expo-document-picker';
+import type {
+    ImagePickerAsset} from 'expo-image-picker';
 
 type Asset = ImagePickerAsset | DocumentPickerAsset;
 type UploadHandler = (assets: Asset[]) => void;

+ 1 - 2
src/constants/theme.ts

@@ -3,10 +3,9 @@
  * There are many other ways to style your app. For example, [Nativewind](https://www.nativewind.dev/), [Tamagui](https://tamagui.dev/), [unistyles](https://reactnativeunistyles.vercel.app), etc.
  */
 
-import theme from '@/../theme';
 import '@/global.css';
-
 import { Platform } from 'react-native';
+import theme from '@/../theme';
 import { antdTheme } from './antd-theme';
 export const Colors = {
 

+ 5 - 3
src/utils/api.ts

@@ -1,8 +1,10 @@
-import axios, { AxiosRequestConfig } from 'axios';
+import axios from 'axios';
+import { fetch } from 'expo/fetch';
 import Constants from 'expo-constants';
-import { fetch, FetchRequestInit } from 'expo/fetch';
 import { Platform } from 'react-native';
-import { type AccessToken } from './auth';
+import type {AccessToken} from './auth';
+import type { AxiosRequestConfig } from 'axios';
+import type { FetchRequestInit } from 'expo/fetch';
 
 const { api: apiConfig, jsVersion } = require('@/config.json') as AppConfig;
 let accessToken: AccessToken | undefined | null = null;

+ 2 - 1
src/utils/storage.ts

@@ -1,5 +1,6 @@
 import * as fs from 'expo-file-system/legacy';
-import { createMMKV, MMKV } from 'react-native-mmkv';
+import { createMMKV } from 'react-native-mmkv';
+import type { MMKV } from 'react-native-mmkv';
 
 const TTL_KEY = '$__$t_';
 const LAST_CLEAR_KEY = '$__last_clear_time$_';

+ 2 - 1
src/utils/storage.web.ts

@@ -1,4 +1,5 @@
-import { createMMKV, MMKV } from 'react-native-mmkv';
+import { createMMKV } from 'react-native-mmkv';
+import type { MMKV } from 'react-native-mmkv';
 
 const TTL_KEY = '$__$t_';