yblunan@gmail.com 1 kuukausi sitten
vanhempi
sitoutus
b8e72da449
6 muutettua tiedostoa jossa 236 lisäystä ja 127 poistoa
  1. 5 2
      src/app/(tabs)/index.tsx
  2. 192 0
      src/app/credit/upload.tsx
  3. 3 2
      src/components/ui/UIButton.tsx
  4. 35 8
      src/utils/api.ts
  5. 0 115
      src/utils/tack.tsx
  6. 1 0
      theme.js

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

@@ -3,11 +3,12 @@ import { StatusBadge } from '@/components/ui/status-badge';
 import UIButton from '@/components/ui/UIButton';
 import api from '@/utils/api';
 import { useSWC } from '@/utils/cache';
-import { selectCredit } from '@/utils/tack';
 import { Ionicons } from '@expo/vector-icons';
 
+import { useState } from 'react';
 import { Pressable, ScrollView, Text, View } from 'react-native';
 import { SafeAreaView } from 'react-native-safe-area-context';
+import UploadScreen from '../credit/upload';
 
 
 
@@ -101,6 +102,7 @@ export default function HomeScreen() {
   }
   );
 
+  const [selectCredit, setSelectCredit] = useState(false);
   return (
     <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">
@@ -173,7 +175,7 @@ export default function HomeScreen() {
           <View className="flex-row items-start justify-around">
             <UIButton type='primary' href="/customer/add" icon="user-add" className='flex-1'>添加客户</UIButton>
             <View className='w-5' />
-            <UIButton onPress={selectCredit} icon="cloud-upload" className='flex-1'>征信分析</UIButton>
+            <UIButton onPress={() => setSelectCredit(true)} icon="cloud-upload" className='flex-1'>征信分析</UIButton>
           </View>
         </View>
 
@@ -256,6 +258,7 @@ export default function HomeScreen() {
           AI 助理
         </Text>
       </View>
+      <UploadScreen visible={selectCredit} onClose={() => setSelectCredit(false)} />
     </SafeAreaView>
   );
 }

+ 192 - 0
src/app/credit/upload.tsx

@@ -0,0 +1,192 @@
+
+import UIButton from "@/components/ui/UIButton";
+import { Colors } from "@/constants/theme";
+import { ActivityIndicator, Icon, Modal, Toast } from "@ant-design/react-native";
+import { Ionicons } from '@expo/vector-icons';
+import { Platform, Pressable, Text, View } from "react-native";
+
+import api from "@/utils/api";
+import { openSystemSettings } from "@/utils/os";
+import { DocumentPickerAsset, getDocumentAsync } from 'expo-document-picker';
+import { ImagePickerAsset, launchCameraAsync, launchImageLibraryAsync, requestCameraPermissionsAsync, requestMediaLibraryPermissionsAsync } from 'expo-image-picker';
+import { Link } from "expo-router";
+import { useCallback, useState } from "react";
+export default function UploadScreen({ visible, onClose }: { visible: boolean, onClose: () => void }) {
+
+
+    const [state, setState] = useState(0);
+    const upload = useCallback(async (assets: ImagePickerAsset[] | DocumentPickerAsset[]) => {
+        setState(1);
+        const list = assets.map((item) => api.uploadFile('common/upload', item.uri));
+        try {
+            const results = await Promise.all(list);
+            await api.post('/credit/a', {
+                fid: results,
+            });
+            setState(2);
+            // eslint-disable-next-line @typescript-eslint/no-unused-vars
+        } catch (err) {
+            setState(0);
+            Toast.fail('有文件上传失败,请重试');
+        }
+    }, []);
+
+    const takePhoto = useCallback(async () => {
+        const permission = await requestCameraPermissionsAsync();
+
+        if (!permission.granted) {
+            Modal.alert("请允许相册权限", "如果点击“确认“按钮后没有跳转,请自己前往系统设置开启相关权限", [
+                {
+                    text: "确认",
+                    onPress: openSystemSettings,
+                }
+            ]);
+            // 跳转到 ios/android 相关设置页面
+            return;
+        }
+        const l = Toast.loading('正在打开相机...');
+        try {
+            const result = await launchCameraAsync({
+                allowsEditing: false, // 是否允许裁剪
+                quality: 0.9,        // 照片质量
+            });
+
+            if (!result.canceled) {
+                upload(result.assets);
+            }
+        } catch (err) {
+            console.warn(err);
+            Toast.fail('打开相机失败,请重试');
+        }
+        finally {
+            Toast.remove(l);
+        }
+    }, [upload]);
+
+    const picImg = useCallback(async () => {
+        const permission = await requestMediaLibraryPermissionsAsync();
+
+        if (!permission.granted) {
+            Modal.alert("请允许相册权限", "如果点击“确认“按钮后没有跳转,请自己前往系统设置开启相关权限", [
+                {
+                    text: "确认",
+                    onPress: openSystemSettings,
+                }
+            ]);
+            // 跳转到 ios/android 相关设置页面
+            return;
+        }
+        const l = Toast.loading('正在打开相册...');
+        try {
+            let result = await launchImageLibraryAsync({
+                mediaTypes: ['images'], // 只选图片
+                allowsEditing: false,
+                quality: .9, // 质量 0~1
+            });
+
+            if (!result.canceled) {
+                upload(result.assets);
+            }
+        } catch (err) {
+            console.warn(err);
+            Toast.fail('打开相册失败,请重试');
+        } finally {
+
+            Toast.remove(l);
+        }
+    }, [upload]);
+
+    const pickDoc = useCallback(async () => {
+        const l = Toast.loading('正在打开相册...');
+
+        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) {
+                upload(result.assets);
+            }
+        } catch (err) {
+            console.warn(err);
+            Toast.fail('打开文件失败,请重试');
+        }
+        finally {
+            Toast.remove(l);
+        }
+    }, [upload]);
+
+    return (
+        <Modal visible={visible} transparent={true} animationType="slide">
+            <View className="flex-row items-center">
+                <Ionicons name="cloud-upload" size={48} color={Colors.tint} />
+                <Text className="ml-4 text-3xl font-bold">征信分析</Text>
+            </View>
+            {state === 0 && (
+                <View className="w-72 h-auto px-6">
+
+                    <Text className="mt-2 text-2xl">请选择征信上传方式</Text>
+                    <UIButton title="从本机文件" icon="folder" onPress={pickDoc} className="mt-8" />
+                    {Platform.OS === 'ios' && <UIButton title="从本地相册" icon="picture" onPress={picImg} className="mt-4" />}
+
+                    <UIButton title="拍照上传" icon="camera" onPress={takePhoto} className="mt-4" />
+                </View>)}
+            {
+                state === 1 && (
+                    <View className="w-72 h-auto p-8">
+                        <ActivityIndicator size="large" color={Colors.tint} text="正在上传,请稍候..." />
+
+                    </View>
+                )
+            }
+            {
+                state === 2 && (
+                    <>
+                        <View className="w-72 h-auto p-8 flex-row">
+                            <Ionicons name="time-outline" size={48} color={Colors.tint} />
+                            <Text className="mt-4 ml-4 text-2xl">AI 正在分析</Text>
+                        </View>
+                        <Text className="mt-4 ml-4 text-2xl">
+                            <Link asChild href='/(tabs)/analytics' onPress={onClose}>
+                                <Text className="text-primary">分析页</Text>
+                            </Link>
+                            查看状态和结果</Text>
+                    </>
+                )
+            }
+            <Pressable hitSlop={8} className="absolute top-0 right-4" onPress={() => { onClose(); }}>
+                <Icon name="close" size={32} />
+            </Pressable>
+        </Modal >
+    );
+}
+

+ 3 - 2
src/components/ui/UIButton.tsx

@@ -8,6 +8,7 @@ import { Pressable, Text, ViewStyle } from "react-native";
 
 interface UIButtonProps {
     children?: string | React.ReactNode;
+    title?: string | React.ReactNode;
     onPress?: () => void;
     disabled?: boolean;
     className?: string;
@@ -31,8 +32,8 @@ function ButtonTextChild({ type, disabled, textClassName, children }: UIButtonPr
     })}>{children}</Text>
 }
 
-export default function UIButton({ onPress, href, type, icon, className, style, disabled, children, textClassName }: UIButtonProps) {
-
+export default function UIButton({ onPress, href, type, icon, className, style, disabled, children, title, textClassName }: UIButtonProps) {
+    children = children || title;
     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, {

+ 35 - 8
src/utils/api.ts

@@ -1,8 +1,10 @@
-// import { getAccessToken } from '@/apis/auth';
 import axios, { AxiosRequestConfig } from 'axios';
 import Constants from 'expo-constants';
 import { Platform } from 'react-native';
 import { type AccessToken } from './auth';
+
+import * as fs from 'expo-file-system/legacy';
+
 const { api: apiConfig, jsVersion } = require('@/config.json') as AppConfig;
 let accessToken: AccessToken | undefined | null = null;
 
@@ -58,8 +60,8 @@ export class HttpError extends Error {
 
 
 export class ApiError extends HttpError {
-     name: string = 'ApiError';
-     
+    name: string = 'ApiError';
+
 }
 
 
@@ -149,12 +151,12 @@ if (__DEV__) {
     );
 }
 
-type Options = Omit<AxiosRequestConfig, 'headers'> & {headers?: Record<string, any>};
+type Options = Omit<AxiosRequestConfig, 'headers'> & { headers?: Record<string, any> };
 async function request<T>(url: string, method: 'get' | 'post' | 'put' | 'delete', data?: any, config?: Options): Promise<T> {
     const headers: Record<string, string> = {
         ...config?.headers
     };
-    
+
     let token = getAccessToken();
     if (token?.token) {
         headers['Authorization'] = `Bearer ${token.token}`;
@@ -209,9 +211,33 @@ const rawRequest = async <T>(req: AxiosRequestConfig) => {
     return res;
 }
 
+async function uploadFile<T>(url: string, fileUri: string, config?: Options): Promise<T> {
+    const headers: Record<string, any> = {
+        'Content-Type': 'multipart/form-data',
+        ...config?.headers,
+    };
+    const token = getAccessToken();
+    if (token?.token) {
+        headers['Authorization'] = `Bearer ${token.token}`;
+    }
+    const result = await fs.uploadAsync(`${apiConfig.url}${url}`, fileUri, {
+        fieldName: 'file',
+        httpMethod: 'POST',
+        headers
+    });
+    if (result.status !== 200) {
+        throw new HttpError(result.body, result.status);
+    }
+    const res = JSON.parse(result.body) as ApiResponse<T>;
+    if (`${res?.code}` !== '1') {
+        throw new ApiError(res?.message || 'upload error', res?.code || 0, res?.data);
+    }
+    return res.data as T;
+}
+
 
 async function get<T>(api: string, params?: Record<string, any>): Promise<T> {
-    return await request<T>(api, 'get', undefined, {params});
+    return await request<T>(api, 'get', undefined, { params });
 }
 
 async function post<T>(api: string, data?: any, config?: Options): Promise<T> {
@@ -224,8 +250,9 @@ async function put<T>(api: string, data?: any, config?: Options): Promise<T> {
 
 async function deleted<T>(api: string, config?: Options): Promise<T> {
     return await request<T>(api, 'delete', undefined, config);
-} const api = {
-    post, get, put, deleted, request, rawRequest
+}
+const api = {
+    post, get, put, deleted, request, rawRequest, uploadFile
 }
 
 

+ 0 - 115
src/utils/tack.tsx

@@ -1,115 +0,0 @@
-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();
-        }
-    });
-};

+ 1 - 0
theme.js

@@ -1,5 +1,6 @@
 export default {
   colors: {
+    tint: "#2563eb",
     primary: {
       DEFAULT: "#2563eb",
       container: "#2563eb",