---
trigger: manual
---

# 文件上传功能实现指南

## 概述

文件上传是系统中常见的功能，用于上传图片、文档等文件。本指南将详细介绍如何在系统中实现文件上传功能。

## 技术栈

系统使用的是基于 Taro 的 React 框架，上传组件来自 `@nutui/nutui-react-taro` 库，文件上传通过 Taro 的 uploadFile API 实现。

## 基本用法

### 1. 导入相关组件和工具

```tsx
import { Uploader, UploaderFileItem } from "@nutui/nutui-react-taro";
import { uploadFile } from "@/utils";
```

### 2. 基本属性

Uploader 组件常用属性包括：

- `value`: 当前已上传的文件列表（UploaderFileItem[]）
- `onChange`: 文件列表变化时的回调函数
- `upload`: 自定义上传函数
- `sourceType`: 选择图片的来源（["album", "camera"]）
- `maxCount`: 最大上传数量
- `multiple`: 是否支持多选
- `uploadIcon`: 自定义上传图标
- `uploadLabel`: 自定义上传标签文本

### 3. 基本结构

```tsx
<Uploader
  value={fileList}
  onChange={handleFileChange}
  sourceType={["album", "camera"]}
  maxCount={1}
  upload={uploadFile}
  multiple
/>
```

## 实现步骤

### 1. 状态管理

定义文件列表状态：

```tsx
const [fileList, setFileList] = useState<UploaderFileItem[]>([]);
```

### 2. 实现上传工具函数

系统中使用统一的上传工具函数 [utils/uploader.ts](file:///D:/xinfaleisheng/ERPTurbo_Client/packages/app-client/src/utils/uploader.ts)：

```tsx
import Taro from "@tarojs/taro";
import { Toast } from "@nutui/nutui-react-taro";

export const uploadFile = async (file: File | string) => {
  console.log("file", file);
  const res = await Taro.uploadFile({
    url: process.env.TARO_API_DOMAIN + `/auth/upload`,
    // @ts-ignore
    filePath: file.tempFilePath || file,
    name: "file",
    header: {
      saToken: Taro.getStorageSync("saToken"),
      "Content-Type": "multipart/form-data",
    },
  });

  if (res.errMsg == "uploadFile:ok") {
    const data = JSON.parse(res.data);

    if (data.errCode == "401") {
      Taro.removeStorageSync("user");
      Toast.show("toast", {
        icon: "warn",
        title: "",
        content: "超时请重试",
      });
      return Promise.reject();
    } else {
      return {
        url: data?.data,
      };
    }
  } else {
    Toast.show("toast", {
      icon: "fail",
      title: "",
      content: "上传失败",
    });
    return Promise.reject();
  }
};
```

### 3. 处理文件变化

实现 onChange 回调函数处理文件变化：

```tsx
const handleFileChange = (files: UploaderFileItem[]) => {
  setFileList(files);
  
  // 处理上传后的逻辑
  if (files.length > 0 && files[0].status === 'success') {
    // 文件上传成功，可以保存URL到业务数据中
    const fileUrls = files.map(file => file.url).filter(url => url) as string[];
    // 保存到业务状态中
    setBusinessData(fileUrls);
  }
};
```

### 4. 初始化已上传文件

如果有已上传的文件需要显示，需要将其转换为 UploaderFileItem 格式：

```tsx
useEffect(() => {
  if (existingFileUrls && existingFileUrls.length > 0) {
    const fileList = existingFileUrls.map((url, index) => ({
      url: url,
      name: `file-${index}`,
      status: 'success'
    }));
    setFileList(fileList);
  }
}, [existingFileUrls]);
```

## 常见使用场景

### 1. 单图片上传

用于头像上传等只需要一张图片的场景：

```tsx
<Uploader
  value={avatarList}
  onChange={handleAvatarChange}
  sourceType={["album", "camera"]}
  maxCount={1}
  upload={uploadFile}
/>
```

### 2. 多图片上传

用于需要上传多张图片的场景，如合同、发票等：

```tsx
<Uploader
  value={contractImgList}
  onChange={handleContractImgChange}
  sourceType={["album", "camera"]}
  uploadIcon={<Icon name={"camera"} size={36} />}
  uploadLabel={
    <View className={"flex flex-col items-center"}>
      <View className="text-sm">拍照上传合同</View>
    </View>
  }
  maxCount={5}
  upload={uploadFile}
  multiple
/>
```

### 3. 不同类型文件上传

系统中常见的文件上传场景包括：

1. 空磅照片上传
2. 总磅照片上传
3. 发票照片上传
4. 合同照片上传
5. 头像上传

## 最佳实践

### 1. 状态管理

- 使用 useState 管理文件列表状态
- 将上传成功的文件URL保存到业务数据中
- 在组件卸载时清理相关状态

### 2. 用户体验

- 提供清晰的上传指引说明
- 显示上传进度和状态
- 支持图片预览功能
- 合理设置最大上传数量

### 3. 错误处理

- 在上传工具函数中统一处理上传错误
- 提供友好的错误提示信息
- 处理网络异常情况

### 4. 性能优化

- 对大文件上传提供进度提示
- 支持断点续传（如果后端支持）
- 合理设置图片压缩参数

## 注意事项

1. 确保在组件卸载时清理相关状态和副作用
2. 注意文件大小和格式限制
3. 处理上传失败的重试机制
4. 考虑移动端网络环境的适配
5. 遵循系统统一的上传接口规范

## 示例代码

```tsx
import { View } from "@tarojs/components";
import { Uploader, UploaderFileItem } from "@nutui/nutui-react-taro";
import { useState, useEffect } from "react";
import { uploadFile } from "@/utils";

export default function UploadExample() {
  // 头像上传状态
  const [avatarList, setAvatarList] = useState<UploaderFileItem[]>([]);
  
  // 合同照片上传状态
  const [contractImgList, setContractImgList] = useState<UploaderFileItem[]>([]);
  
  // 初始化已上传的文件
  useEffect(() => {
    // 假设从服务器获取到已上传的文件URL
    const existingAvatarUrl = "https://example.com/avatar.jpg";
    if (existingAvatarUrl) {
      setAvatarList([{
        url: existingAvatarUrl,
        name: 'avatar',
        status: 'success'
      }]);
    }
  }, []);
  
  // 处理头像上传变化
  const handleAvatarChange = (files: UploaderFileItem[]) => {
    setAvatarList(files);
    
    // 如果上传成功，保存URL到业务数据
    if (files.length > 0 && files[0].status === 'success' && files[0].url) {
      // 保存头像URL到用户信息中
      saveAvatarUrl(files[0].url);
    }
  };
  
  // 处理合同照片上传变化
  const handleContractImgChange = (files: UploaderFileItem[]) => {
    setContractImgList(files);
    
    // 保存所有成功上传的文件URL
    const urls = files
      .filter(file => file.status === 'success' && file.url)
      .map(file => file.url!) as string[];
    
    // 保存到业务数据中
    saveContractImgUrls(urls);
  };
  
  return (
    <View className="p-4">
      <View className="mb-6">
        <View className="mb-2 text-sm font-medium">头像上传</View>
        <Uploader
          value={avatarList}
          onChange={handleAvatarChange}
          sourceType={["album", "camera"]}
          maxCount={1}
          upload={uploadFile}
        />
      </View>
      
      <View>
        <View className="mb-2 text-sm font-medium">合同照片上传</View>
        <Uploader
          value={contractImgList}
          onChange={handleContractImgChange}
          sourceType={["album", "camera"]}
          maxCount={5}
          upload={uploadFile}
          multiple
        />
      </View>
    </View>
  );
}
```
