npm package: @sl-material/sl-import
Description: 导入组件 - 支持批量导入文件，提供多种模板配置
Latest version: 1.1.1

# @sl-material/sl-import

批量导入弹窗组件，支持多种导入模板配置、文件分片上传、品牌选择、图片比例选择等功能。

## 特性

- **多种模板类型** - 内置 5 种预设模板，支持自定义配置
- **文件分片上传** - 支持大文件分片上传，自动合并，支持延迟上传
- **多文件上传** - 支持批量文件上传，横向滚动列表展示
- **上传时机控制** - 支持立即上传和延迟上传两种模式
- **灵活文件校验** - 可选的文件数量和大小校验，不传则不校验
- **品牌选择** - 支持静态数据或异步加载品牌列表
- **图片比例选择** - 支持配置图片裁剪比例选项
- **日期范围选择** - 支持月份和日期范围选择
- **统一 Loading 样式** - 文件卡片和全局遮罩使用相同的渐变圆环动画
- **纯原生实现** - 不依赖 Vue/React 等框架，可在任何环境使用
- **TypeScript 支持** - 完整的类型定义
- **标准化国际化** - 支持 zh_CN、en_US、ja_JP、zh_HK、zh_TW、id 等语言

## 安装

```bash
npm install @sl-material/sl-import
# 或
yarn add @sl-material/sl-import
# 或
pnpm add @sl-material/sl-import
```

## 兼容性

- **浏览器支持**：Chrome 80+, Firefox 72+, Safari 13.1+, Edge 80+
- **Node.js 支持**：14.0+
- **TypeScript 支持**：4.0+
- **ES 版本**：编译为 ES2020，兼容现代浏览器和 Node.js 环境

## 使用方式

### ES 模块

```typescript
import { ImportDialog } from "@sl-material/sl-import";
```

### CommonJS

```typescript
const { ImportDialog } = require("@sl-material/sl-import");
```

> **注意**：CSS 样式已自动内联到 JS 文件中，无需手动导入 CSS 文件。

## 使用示例

### 基础导入（立即上传）

```typescript
import { ImportDialog, TemplateTypeEnum } from "@sl-material/sl-import";

ImportDialog.open({
  title: "导入数据",
  templateType: TemplateTypeEnum.BASE,
  templateUrl: "/template/data_template.xlsx",
  tips: ["提示信息 1", "提示信息 2"],
  uploadConfig: {
    autoUpload: true, // 选择文件后立即上传
    customUpload: async (file, onProgress) => {
      // 自定义上传逻辑
      const formData = new FormData();
      formData.append("file", file);

      const response = await fetch("/api/upload", {
        method: "POST",
        body: formData,
      });

      return response.json();
    },
  },
});
```

### 使用文件操作功能

```typescript
import { ImportDialog, TemplateTypeEnum } from "@sl-material/sl-import";

ImportDialog.open({
  title: "导入数据",
  templateType: TemplateTypeEnum.BASE,
  uploadConfig: {
    autoUpload: false,
    customUpload: async (file, context) => {
      // 如果文件数量超过限制，删除第一个文件
      console.info("file", file, "context", context);
      return {
        file,
      };
    },
  },
  onConfirm: async (data) => {
    // 此时文件已上传完成，data.files 包含上传结果
    console.log("上传结果:", data);
    return { success: true };
  },
});
```

### 多文件上传

```typescript
ImportDialog.open({
  locale: "zh_CN" as I18nLocaleEnum,
  type: ExportTypeEnum.excel,
  templateType: TemplateTypeEnum.BASE,
  templateUrl: "/template/base_template.xlsx",
  tips: ["提示信息 1", "提示信息 2"],
  onConfirm: async (data) => {
    return submitImportMock(data);
    // 可以在这里进行额外的后处理
  },
  uploadConfig: {
    multiple: true,
    confirmLoading: false,
    maxFileSize: 1000,
    // maxFiles: 2,
    maxFileSizeUnit: "KB",
    // 文件预处理函数：选择文件后立即调用
    customUpload: async (file: File, context) => {
      if (file.name.includes(",")) {
        context?.removeFile(
          context?.fileList.findIndex((f) => f.name === file.name),
        );
      }
      // 返回值会存储在 fileItem.response 中
      return { file };
    },
  },
});
```

### 文件校验配置

```typescript
ImportDialog.open({
  title: "导入数据",
  templateType: TemplateTypeEnum.BASE,
  uploadConfig: {
    multiple: true,
    // 不配置 maxFiles 和 maxFileSize，则不进行任何校验
    customUpload: async (file, context, onProgress) => {
      return { file };
    },
  },
  onConfirm: async (data) => {
    console.log("上传结果:", data);
    return { success: true };
  },
});
```

```typescript
ImportDialog.open({
  title: "导入数据",
  templateType: TemplateTypeEnum.BASE,
  uploadConfig: {
    multiple: true,
    maxFiles: 10, // 限制最多 10 个文件
    maxFileSize: 50, // 限制单个文件最大 50MB
    maxFileSizeUnit: "MB",
    customUpload: async (file, context, onProgress) => {
      return { file };
    },
  },
  onConfirm: async (data) => {
    console.log("上传结果:", data);
    return { success: true };
  },
});
```

### 禁用确定按钮 Loading

```typescript
ImportDialog.open({
  title: "导入数据",
  templateType: TemplateTypeEnum.BASE,
  uploadConfig: {
    autoUpload: false,
    confirmLoading: false, // 不显示确定按钮 loading
    customUpload: async (file, onProgress) => {
      // 上传逻辑
    },
  },
  onConfirm: async (data) => {
    console.log("上传结果:", data.files);
    return { success: true };
  },
});
```

### 分片上传（立即上传） 注意分片上传目前只支持单个文件上传

```typescript
ImportDialog.open({
  title: "大文件上传",
  templateType: TemplateTypeEnum.BASE,
  uploadConfig: {
    autoUpload: true, // 选择文件后立即开始上传
    maxFileSize: 500, // 可选：限制文件大小为 500MB
    maxFileSizeUnit: "MB",
    chunkedUpload: {
      initUpload: async (fileName, fileSize) => {
        const response = await fetch("/api/upload/init", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ fileName, fileSize }),
        });
        return response.json();
      },
      getResumableConfig: (initResult) => ({
        target: "/api/upload/chunk",
        headers: {
          Authorization: `Bearer ${token}`,
        },
        query: { uploadSessionId: initResult.uploadSessionId },
      }),
      mergeChunks: async (uploadSessionId) => {
        const response = await fetch(`/api/upload/merge/${uploadSessionId}`, {
          method: "POST",
        });
        return response.json();
      },
    },
  },
});
```

### 分片上传 点击确定上传

````typescript
ImportDialog.open({
      type: ExportTypeEnum.excel,
      title: "分片上传（延迟）",
      templateType: TemplateTypeEnum.BASE,
      tabs: [{ label: "分片延迟上传", key: "chunked-delayed" }],
      tips: [
        "1. 选择文件后不会立即上传",
        "2. 点击确定按钮时才开始分片上传",
        "3. autoUpload: false（默认值）",
      ],
      uploadTitle: "选择大文件",
      uploadHint: "选择文件后点击确定开始分片上传",
      acceptTypes: ".xlsx,.xls,.csv,.zip",
      uploadConfig: {
        maxFileSize: 500,
        maxFileSizeUnit: "MB",
        autoUpload: false, // 选择文件后不立即上传，等待点击确定
        chunkedUpload: {
          initUpload: async (fileName: string, fileSize: number) => {
            const response = await initImportTask({ fileName, fileSize });
            console.info("initResult>>>", response);
            return {
              uploadSessionId: response?.uploadSessionId,
              partSize: response?.partSize,
              totalParts: response?.totalParts,
            };
          },
          getResumableConfig: (initResult) => ({
            target: `/newProxy/basic-data-io/api/file/upload/part?uploadSessionId=${initResult.uploadSessionId}`,
            headers: {
              "TCSL-BP-TOKEN":
                window.sessionStorage.getItem("token") || slyToken,
            },
            // query: { uploadSessionId: initResult.uploadSessionId },
          }),
          mergeChunks: async (uploadSessionId: string) => {
            const response = await uploadFileMerge(uploadSessionId);

            return response;
          },
        },
      },
      onConfirm: async (data) => {
        console.info("openChunkedUploadDelayed>>>", data);
        return { success: true, message: "导入成功" };
      },
    });
    ```

### 带品牌选择的导入

```typescript
ImportDialog.open({
  title: "导入商品",
  templateType: TemplateTypeEnum.TEMPLATE1,
  brandData: async () => {
    const res = await fetch("/api/brands");
    return res.json();
  },
  uploadConfig: {
    autoUpload: false, // 延迟上传
  },
  onConfirm: async (data) => {
    console.log("品牌 ID:", data.formData.brandId);
    console.log("图片比例:", data.formData.imageRatio);
    console.log("上传文件:", data.files);

    // 处理业务逻辑
    await processImport(data.files, {
      brandId: data.formData.brandId,
      imageRatio: data.formData.imageRatio,
    });

    return { success: true };
  },
});
````

### 自定义日期范围

```typescript
ImportDialog.open({
  title: "导入报表",
  templateType: TemplateTypeEnum.TEMPLATE5,
  showDateRange: true,
  dateRangeShortcuts: [
    {
      label: "最近7天",
      value: [new Date(Date.now() - 6 * 24 * 60 * 60 * 1000), new Date()],
    },
    {
      label: "最近30天",
      value: [new Date(Date.now() - 29 * 24 * 60 * 60 * 1000), new Date()],
    },
  ],
  disabledDate: (date) => {
    // 禁用未来日期
    return date > new Date();
  },
});
```

## API 文档

### ImportDialog 类

#### 静态方法

| 方法                  | 参数               | 返回值                      | 说明                   |
| --------------------- | ------------------ | --------------------------- | ---------------------- |
| `open(options)`       | `OpenModalOptions` | `Promise<ImportSubmitData>` | 打开弹窗，返回 Promise |
| `close()`             | -                  | `void`                      | 关闭弹窗               |
| `setLoading(loading)` | `boolean`          | `void`                      | 设置加载状态           |
| `isReady()`           | -                  | `boolean`                   | 检查服务是否就绪       |
| `setLocale(locale)`   | `I18nLocale`       | `void`                      | 设置国际化语言         |
| `getLocale()`         | -                  | `I18nLocale`                | 获取当前语言           |

#### 实例方法

| 方法                         | 参数               | 返回值 | 说明                 |
| ---------------------------- | ------------------ | ------ | -------------------- |
| `openModal(options)`         | `OpenModalOptions` | `void` | 打开导入弹窗         |
| `closeModal()`               | -                  | `void` | 关闭弹窗             |
| `setConfirmLoading(loading)` | `boolean`          | `void` | 设置确认按钮加载状态 |
| `hide()`                     | -                  | `void` | 隐藏弹窗             |
| `destroy()`                  | -                  | `void` | 销毁实例             |

### OpenModalOptions 配置

| 参数                 | 类型                                              | 默认值       | 说明                   |
| -------------------- | ------------------------------------------------- | ------------ | ---------------------- |
| `type`               | `ExportTypeEnum`                                  | `excel`      | 导出文件类型           |
| `title`              | `string`                                          | `'批量导入'` | 弹窗标题               |
| `width`              | `string`                                          | `'480px'`    | 弹窗宽度               |
| `templateType`       | `TemplateTypeEnum`                                | `BASE`       | 模板类型               |
| `tabs`               | `TabConfig[]`                                     | `[]`         | 自定义标签页配置       |
| `defaultActiveTab`   | `number`                                          | `0`          | 默认激活的标签页       |
| `brandData`          | `BrandOption[] \| (() => Promise<BrandOption[]>)` | -            | 品牌数据或异步获取函数 |
| `brandPlaceholder`   | `string`                                          | `'默认品牌'` | 品牌选择占位符         |
| `imageRatios`        | `ImageRatioOption[]`                              | -            | 图片比例选项           |
| `extendImageRatios`  | `ImageRatioOption[]`                              | -            | 扩展图片比例选项       |
| `customTips`         | `string[]`                                        | -            | 自定义提示信息         |
| `templateUrl`        | `string`                                          | -            | 模板下载链接           |
| `uploadTitle`        | `string`                                          | -            | 上传区域标题           |
| `uploadLinkText`     | `string`                                          | -            | 上传链接文字           |
| `uploadHint`         | `string`                                          | -            | 上传提示信息           |
| `acceptTypes`        | `string`                                          | -            | 接受的文件类型         |
| `showBrand`          | `boolean`                                         | -            | 是否显示品牌选择       |
| `showImageRatio`     | `boolean`                                         | -            | 是否显示图片比例选择   |
| `showMonth`          | `boolean`                                         | -            | 是否显示月份选择       |
| `showDateRange`      | `boolean`                                         | -            | 是否显示日期范围选择   |
| `dateRangeShortcuts` | `DateRangeShortcut[]`                             | -            | 日期快捷选项           |
| `disabledDate`       | `(date: Date) => boolean`                         | -            | 禁用日期函数           |
| `locale`             | `I18nLocale`                                      | -            | 国际化语言             |

#### 上传配置 (UploadConfig)

所有上传相关配置统一放在 `uploadConfig` 对象中：

| 参数              | 类型                                            | 默认值  | 说明                         |
| ----------------- | ----------------------------------------------- | ------- | ---------------------------- |
| `autoUpload`      | `boolean`                                       | `false` | 是否立即上传文件             |
| `multiple`        | `boolean`                                       | `false` | 是否支持多文件上传           |
| `maxFiles`        | `number`                                        | -       | 最大文件数量，不传则不校验   |
| `maxFileSize`     | `number`                                        | -       | 单文件最大大小，不传则不校验 |
| `maxFileSizeUnit` | `'KB' \| 'MB' \| 'GB'`                          | `'MB'`  | 文件大小单位                 |
| `customUpload`    | `(file, context?, onProgress?) => Promise<any>` | -       | 自定义上传函数               |
| `chunkedUpload`   | `ChunkUploadStrategy`                           | -       | 分片上传策略配置             |
| `confirmLoading`  | `boolean`                                       | `true`  | 确定按钮 loading 控制        |
| `onFileChange`    | `(files: FileUploadItem[]) => void`             | -       | 文件列表变化回调             |
| `onProgress`      | `(file, progress) => void`                      | -       | 上传进度回调                 |
| `onSuccess`       | `(file, response) => void`                      | -       | 上传成功回调                 |
| `onError`         | `(file, error) => void`                         | -       | 上传失败回调                 |

**文件校验配置说明**：

- `maxFiles`：限制最大文件数量，不传递则不进行数量校验
- `maxFileSize` 和 `maxFileSizeUnit` 组合使用来限制文件大小，不传递则不进行大小校验
- 例如：`maxFileSize: 10, maxFileSizeUnit: 'KB'` 表示最大 10KB
- 例如：`maxFileSize: 500, maxFileSizeUnit: 'MB'` 表示最大 500MB
- 校验参数完全可选，可根据需要灵活配置

##### 分片上传策略 (ChunkUploadStrategy)

```typescript
interface ChunkUploadStrategy {
  initUpload: (params: InitUploadParams) => Promise<InitUploadResult>;
  getResumableOptions: (initResult: InitUploadResult) => ResumableOptions;
  mergeChunks: (uploadSessionId: string) => Promise<any>;
}
```

##### 文件操作上下文

`customUpload` 函数的第三个参数，提供基本的文件操作能力：

```typescript
interface Context {
  /** 当前文件列表 */
  fileList: FileUploadItem[];
  /** 删除指定索引的文件 */
  removeFile: (index: number) => void;
}
```

#### 回调函数

| 参数               | 类型                              | 说明                                |
| ------------------ | --------------------------------- | ----------------------------------- |
| `onBeforeConfirm`  | `(data) => void \| Promise<void>` | 确认前回调                          |
| `onConfirm`        | `(data) => void`                  | 确认回调，返回 `{success: boolean}` |
| `onBeforeCancel`   | `() => void \| Promise<void>`     | 取消前回调                          |
| `onCancel`         | `() => void`                      | 取消回调                            |
| `onTabChange`      | `(data) => void`                  | 标签切换回调                        |
| `onFileChange`     | `(files) => void`                 | 文件变化回调                        |
| `onUploadProgress` | `(file, progress) => void`        | 上传进度回调                        |
| `onUploadSuccess`  | `(file, response) => void`        | 上传成功回调                        |
| `onUploadError`    | `(file, error) => void`           | 上传失败回调                        |

### 模板类型 (TemplateTypeEnum)

| 类型        | 说明                     |
| ----------- | ------------------------ |
| `BASE`      | 基础模板，仅文件上传     |
| `TEMPLATE1` | 品牌 + 图片比例选择      |
| `TEMPLATE2` | 覆盖/增量导入 + 图片上传 |
| `TEMPLATE3` | 月份选择 + 营业目标导入  |
| `TEMPLATE4` | 品牌选择 + 菜谱方案导入  |
| `TEMPLATE5` | 合批修改价格 + 日期范围  |

### 国际化 (I18nLocaleEnum)

| 语言             | 枚举值  |
| ---------------- | ------- |
| 简体中文         | `zh_CN` |
| 繁體中文（香港） | `zh_HK` |
| 繁體中文（臺灣） | `zh_TW` |
| 英语（美国）     | `en_US` |
| 日本语           | `ja_JP` |
| 印尼语           | `id`    |

**使用方式**：

```typescript
import { ImportDialog, I18nLocaleEnum } from "@sl-material/sl-import";

ImportDialog.open({
  locale: I18nLocaleEnum.ZH_CN, // 简体中文
  // locale: I18nLocaleEnum.ZH_HK, // 繁體中文（香港）
  // locale: I18nLocaleEnum.ZH_TW, // 繁體中文（臺灣）
  // locale: I18nLocaleEnum.EN_US, // 英语（美国）
  // locale: I18nLocaleEnum.JA_JP, // 日本语
  // locale: I18nLocaleEnum.ID,    // 印尼语
  // ... 其他配置
});
```

### 类型定义

```typescript
interface TabConfig {
  label: string;
  key: string;
}

interface BrandOption {
  label: string;
  value: string | number;
}

interface ImageRatioOption {
  label: string;
  value: string;
}

interface ImportFormData {
  brandId: string | number | undefined;
  imageRatio: string;
  month: number;
  dateRange: [Date | null, Date | null];
}

interface ImportSubmitData {
  files: File[];
  formData: ImportFormData;
  activeTab: TabConfig | undefined;
  activeTabIndex: number;
}

interface FileUploadItem {
  id: string;
  file: File;
  name: string;
  size: number;
  progress: number;
  status: "pending" | "uploading" | "success" | "error";
  errorMessage?: string;
  uploadSessionId?: string;
  response?: any;
}
```

## 最新特性

### 🎯 灵活的文件校验

文件校验参数完全可选，可根据需要灵活配置：

```typescript
// 不进行任何校验
uploadConfig: {
  multiple: true,
  customUpload: myUploadFunction
}

// 完整校验配置
uploadConfig: {
  multiple: true,
  maxFiles: 10,        // 限制最多 10 个文件
  maxFileSize: 50,     // 限制单个文件最大 50MB
  maxFileSizeUnit: "MB",
  customUpload: myUploadFunction
}
```

### 🌍 标准化国际化

支持 6 种语言，使用标准的语言代码格式：

- `zh_CN` - 简体中文
- `zh_HK` - 繁體中文（香港）
- `zh_TW` - 繁體中文（臺灣）
- `en_US` - 英语（美国）
- `ja_JP` - 日本语
- `id` - 印尼语

### ⚡ 统一 Loading 动画

文件卡片和全局遮罩使用相同的渐变圆环动画，视觉效果统一且流畅。

### 🔄 分片上传延迟模式

支持分片上传的延迟模式，文件选择后不会立即上传，用户可以：

1. 选择多个文件
2. 预览文件列表
3. 手动触发上传或删除文件
4. 点击确认按钮开始上传

## UMD 使用方式

```html
<script src="https://unpkg.com/@sl-material/sl-import/dist/sl-import.umd.umd.js"></script>
<script>
  const { ImportDialog } = window.SlImport;

  ImportDialog.open({
    title: "批量导入",
    templateType: "BASE",
  });
</script>
```

> **注意**：UMD 版本同样已内联 CSS，无需额外引入样式文件。

## 更新日志

### v1.1.0-beta4

#### 🚀 新增特性

- **灵活文件校验**：`maxFiles` 和 `maxFileSize` 参数改为可选，不传则不校验
- **标准化国际化**：统一语言代码格式为 `zh_CN`、`en_US`、`ja_JP` 等
- **新增语言支持**：添加 `zh_HK`（繁體中文-香港）支持
- **统一 Loading 样式**：文件卡片和全局遮罩使用相同的渐变圆环动画
- **分片上传延迟模式**：支持文件选择后延迟上传，用户可手动控制上传时机

#### 🐛 修复问题

- 修复 loading 动画跳动问题
- 修复分片上传延迟模式下的配置问题
- 优化文件校验逻辑的时机和顺序

#### 💎 优化改进

- 所有硬编码中文提示改为国际化调用
- 优化文件大小单位配置，支持 KB、MB、GB
- 改进错误提示信息的准确性和友好性

## License

MIT