ERPTurbo_Client/.lingma/rules/upload-implementation.md
shenyifei d37626d5bf refactor(utils): 重构工具函数导入路径
- 统一从 utils 目录导入工具函数,而非具体的子文件
- 更新了 uploadFile, buildUrl, formatCurrency 等函数的导入路径
- 修改 CustomTabBar 组件接收 userRoleVO 对象而非 role 字符串
- 调整金额格式化相关工具函数的引用方式
- 更新文档中项目结构和费用管理相关说明
- 优化用户角色权限相关的数据传递逻辑
2025-11-21 19:59:49 +08:00

7.2 KiB
Raw Blame History

trigger
manual

文件上传功能实现指南

概述

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

技术栈

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

基本用法

1. 导入相关组件和工具

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. 基本结构

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

实现步骤

1. 状态管理

定义文件列表状态:

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

2. 实现上传工具函数

系统中使用统一的上传工具函数 utils/uploader.ts

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 回调函数处理文件变化:

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 格式:

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

常见使用场景

1. 单图片上传

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

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

2. 多图片上传

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

<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. 遵循系统统一的上传接口规范

示例代码

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>
  );
}