Explorar o código

feat[utils]: exportExcel

chenlei hai 1 mes
pai
achega
b5942ae3c5
Modificáronse 38 ficheiros con 1221 adicións e 20 borrados
  1. 9 0
      packages/backend-cli/template/CHANGELOG.md
  2. 1 1
      packages/backend-cli/template/package.json
  3. 12 0
      packages/docs/.umirc.ts
  4. 8 0
      packages/docs/CHANGELOG.md
  5. 8 0
      packages/docs/docs/log/DOCS_CHANGELOG.md
  6. 7 0
      packages/docs/docs/log/KRPANO_CHANGELOG.md
  7. 8 0
      packages/docs/docs/log/PC-COMPONENTS_CHANGELOG.md
  8. 7 0
      packages/docs/docs/log/SERVICE_CHANGELOG.md
  9. 8 0
      packages/docs/docs/log/UTILS_CHANGELOG.md
  10. 49 0
      packages/docs/docs/utils/export-excel/demo.tsx
  11. 81 0
      packages/docs/docs/utils/export-excel/demo2.tsx
  12. 72 0
      packages/docs/docs/utils/export-excel/demo3.tsx
  13. 92 0
      packages/docs/docs/utils/export-excel/index.md
  14. 3 1
      packages/docs/docs/utils/image.md
  15. 14 0
      packages/docs/docs/utils/number.md
  16. 25 4
      packages/docs/docs/utils/string.md
  17. 9 0
      packages/docs/docs/utils/valid-form.md
  18. 1 1
      packages/docs/package.json
  19. 7 0
      packages/krpano/CHANGELOG.md
  20. 2 2
      packages/krpano/package.json
  21. 8 0
      packages/pc-components/CHANGELOG.md
  22. 1 1
      packages/pc-components/package.json
  23. 1 1
      packages/pc-components/tsconfig.build.tsbuildinfo
  24. 7 0
      packages/service/CHANGELOG.md
  25. 1 1
      packages/service/package.json
  26. 8 0
      packages/utils/CHANGELOG.md
  27. 2 1
      packages/utils/package.json
  28. 27 1
      packages/utils/src/base64.ts
  29. 178 0
      packages/utils/src/exportExcel/index.ts
  30. 42 0
      packages/utils/src/exportExcel/types.ts
  31. 48 0
      packages/utils/src/exportExcel/utils.ts
  32. 3 0
      packages/utils/src/index.ts
  33. 53 0
      packages/utils/src/number.ts
  34. 15 0
      packages/utils/src/string.ts
  35. 40 0
      packages/utils/src/validForm.ts
  36. 1 1
      packages/utils/tsconfig.build.commonjs.tsbuildinfo
  37. 1 1
      packages/utils/tsconfig.build.tsbuildinfo
  38. 362 4
      pnpm-lock.yaml

+ 9 - 0
packages/backend-cli/template/CHANGELOG.md

@@ -1,5 +1,14 @@
 # @dage/backend-template
 
+## 1.0.1
+
+### Patch Changes
+
+- Updated dependencies
+  - @dage/utils@1.3.0
+  - @dage/pc-components@1.3.12
+  - @dage/service@1.0.6
+
 ## 1.0.22
 
 ### Patch Changes

+ 1 - 1
packages/backend-cli/template/package.json

@@ -1,6 +1,6 @@
 {
   "name": "template",
-  "version": "1.0.0",
+  "version": "1.0.1",
   "private": true,
   "dependencies": {
     "@ant-design/icons": "^5.6.1",

+ 12 - 0
packages/docs/.umirc.ts

@@ -96,6 +96,10 @@ export default defineConfig({
         path: "/utils/string",
       },
       {
+        title: "number 方法",
+        path: "/utils/number",
+      },
+      {
         title: "图片方法",
         path: "/utils/image",
       },
@@ -104,6 +108,14 @@ export default defineConfig({
         path: "/utils/date",
       },
       {
+        title: "导出Excel",
+        path: "/utils/export-excel",
+      },
+      {
+        title: "表单验证",
+        path: "/utils/valid-form",
+      },
+      {
         title: "EventBus 事件",
         path: "/utils/eventbus",
       },

+ 8 - 0
packages/docs/CHANGELOG.md

@@ -1,5 +1,13 @@
 # @dage/docs
 
+## 1.1.0
+
+### Minor Changes
+
+- 新增导出 Excel 方法
+  新增 getImageUrlsFromHtml 方法
+  新增 numberToChinese 方法
+
 ## 1.0.4
 
 ### Patch Changes

+ 8 - 0
packages/docs/docs/log/DOCS_CHANGELOG.md

@@ -1,5 +1,13 @@
 # @dage/docs
 
+## 1.1.0
+
+### Minor Changes
+
+- 新增导出 Excel 方法
+  新增 getImageUrlsFromHtml 方法
+  新增 numberToChinese 方法
+
 ## 1.0.4
 
 ### Patch Changes

+ 7 - 0
packages/docs/docs/log/KRPANO_CHANGELOG.md

@@ -1,5 +1,12 @@
 # @dage/krpano
 
+## 4.0.0
+
+### Patch Changes
+
+- Updated dependencies
+  - @dage/utils@1.3.0
+
 ## 3.0.0
 
 ### Patch Changes

+ 8 - 0
packages/docs/docs/log/PC-COMPONENTS_CHANGELOG.md

@@ -1,5 +1,13 @@
 # @dage/pc-components
 
+## 1.3.12
+
+### Patch Changes
+
+- Updated dependencies
+  - @dage/utils@1.3.0
+  - @dage/service@1.0.6
+
 ## 1.3.11
 
 ### Patch Changes

+ 7 - 0
packages/docs/docs/log/SERVICE_CHANGELOG.md

@@ -1,5 +1,12 @@
 # @dage/service
 
+## 1.0.6
+
+### Patch Changes
+
+- Updated dependencies
+  - @dage/utils@1.3.0
+
 ## 1.0.5
 
 ### Patch Changes

+ 8 - 0
packages/docs/docs/log/UTILS_CHANGELOG.md

@@ -1,5 +1,13 @@
 # @dage/utils
 
+## 1.3.0
+
+### Minor Changes
+
+- 新增导出 Excel 方法
+  新增 getImageUrlsFromHtml 方法
+  新增 numberToChinese 方法
+
 ## 1.1.1
 
 ### Patch Changes

+ 49 - 0
packages/docs/docs/utils/export-excel/demo.tsx

@@ -0,0 +1,49 @@
+import { FC } from "react";
+import { Button, Table } from "antd";
+import { exportExcel } from "@dage/utils";
+
+const SHEET_HEADER = ["header1", "header2", "header3"];
+const DATA = [
+  {
+    a: 1,
+    b: 2,
+    c: 3,
+  },
+  {
+    a: 4,
+    b: 5,
+    c: 6,
+  },
+];
+
+export const ExportExcelDemo: FC = () => {
+  const handleClick = () => {
+    exportExcel({
+      fileName: "demo",
+      options: {
+        sheetHeader: SHEET_HEADER,
+        sheetFilter: ["a", "b", "c"],
+        columnWidths: [5, 10, 15],
+      },
+      data: DATA,
+    });
+  };
+
+  return (
+    <>
+      <Button onClick={handleClick}>导出Excel</Button>
+
+      <Table
+        bordered
+        rowKey="a"
+        pagination={false}
+        columns={SHEET_HEADER.map((title, i) => ({
+          title,
+          dataIndex: Object.keys(DATA[0])[i],
+        }))}
+        dataSource={DATA}
+        style={{ marginTop: 20 }}
+      />
+    </>
+  );
+};

+ 81 - 0
packages/docs/docs/utils/export-excel/demo2.tsx

@@ -0,0 +1,81 @@
+import { FC } from "react";
+import { Button, Table } from "antd";
+import { exportExcel } from "@dage/utils";
+
+const SHEET_HEADER = [
+  "header1",
+  ["header2", "child1", "child2", "child3"],
+  "header3",
+];
+const SHEET_FILTER = ["a", "b1", "b2", "b3", "c"];
+const DATA = [
+  {
+    a: 1,
+    b1: 2,
+    b2: 3,
+    b3: 4,
+    c: 5,
+  },
+  {
+    a: 11,
+    b1: 22,
+    b2: 33,
+    b3: 44,
+    c: 55,
+  },
+];
+
+export const ExportExcelDemo: FC = () => {
+  const handleClick = () => {
+    exportExcel({
+      fileName: "demo",
+      options: {
+        sheetHeader: SHEET_HEADER,
+        sheetFilter: SHEET_FILTER,
+        columnWidths: [5, 10, 10, 10, 15],
+      },
+      data: DATA,
+    });
+  };
+
+  return (
+    <>
+      <Button onClick={handleClick}>导出合并表头Excel</Button>
+
+      <Table
+        rowKey="a"
+        bordered
+        pagination={false}
+        columns={[
+          {
+            title: "header1",
+            dataIndex: "a",
+          },
+          {
+            title: "header2",
+            children: [
+              {
+                title: "child1",
+                dataIndex: "b1",
+              },
+              {
+                title: "child2",
+                dataIndex: "b2",
+              },
+              {
+                title: "child3",
+                dataIndex: "b3",
+              },
+            ],
+          },
+          {
+            title: "header3",
+            dataIndex: "c",
+          },
+        ]}
+        dataSource={DATA}
+        style={{ marginTop: 20 }}
+      />
+    </>
+  );
+};

+ 72 - 0
packages/docs/docs/utils/export-excel/demo3.tsx

@@ -0,0 +1,72 @@
+import { FC, useState } from "react";
+import { Button, Table, Image } from "antd";
+import { SheetFilterItem, exportExcel } from "@dage/utils";
+
+const SHEET_HEADER = ["header1", "header2", "header3"];
+const SHEET_FILTER: SheetFilterItem[] = ["a", { key: "b", type: "img" }, "c"];
+const DATA = [
+  {
+    a: 1,
+    b: "https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png",
+    c: 3,
+  },
+  {
+    a: 4,
+    b: "https://fuss10.elemecdn.com/3/63/4e7f3a15429bfda99bce42a18cdd1jpeg.jpeg?imageMogr2/thumbnail/360x360/format/webp/quality/100",
+    c: 6,
+  },
+];
+
+export const ExportExcelDemo: FC = () => {
+  const [loading, setLoading] = useState(false);
+
+  const handleClick = () => {
+    exportExcel({
+      fileName: "demo",
+      options: {
+        sheetHeader: SHEET_HEADER,
+        sheetFilter: SHEET_FILTER,
+        columnWidths: [5, 10, 15],
+      },
+      data: DATA,
+      onBefore() {
+        setLoading(true);
+      },
+      onAfter() {
+        setLoading(false);
+      },
+    });
+  };
+
+  return (
+    <>
+      <Button loading={loading} onClick={handleClick}>
+        导出带图片Excel
+      </Button>
+
+      <Table
+        bordered
+        rowKey="a"
+        pagination={false}
+        columns={[
+          {
+            title: "header1",
+            dataIndex: "a",
+          },
+          {
+            title: "header2",
+            render(val) {
+              return <Image width={50} height={50} src={val.b} />;
+            },
+          },
+          {
+            title: "header3",
+            dataIndex: "c",
+          },
+        ]}
+        dataSource={DATA}
+        style={{ marginTop: 20 }}
+      />
+    </>
+  );
+};

+ 92 - 0
packages/docs/docs/utils/export-excel/index.md

@@ -0,0 +1,92 @@
+## 基本使用
+
+```tsx
+import React from "react";
+import { ExportExcelDemo } from "./demo";
+
+const Demo = () => {
+  return <ExportExcelDemo />;
+};
+
+export default Demo;
+```
+
+## 表头合并
+
+`sheetHeader` 传入数组表示需要合并表头,第一位为父级表头,后面都为子级表头。(目前仅支持二维数组表头合并)
+
+`sheetFilter`和`columnWidths`需要将`sheetHeader`当成一维数组(排除父级表头),按序填入,具体看示例。
+
+```tsx
+import React from "react";
+import { ExportExcelDemo } from "./demo2";
+
+const Demo = () => {
+  return <ExportExcelDemo />;
+};
+
+export default Demo;
+```
+
+## 图片导出
+
+`SheetFilterItem`中的`type`为`img`类型时,则会将值判断为图片链接,转成 base64 输出图片。
+
+如果图片不满足要求可以将 [showImageId](#sheetfilteritem) 设置为`true`,在钩子函数中自行处理。
+
+```tsx
+import React from "react";
+import { ExportExcelDemo } from "./demo3";
+
+const Demo = () => {
+  return <ExportExcelDemo />;
+};
+
+export default Demo;
+```
+
+## Parameter
+
+更多操作见[文档](https://github.com/exceljs/exceljs/blob/HEAD/README_zh.md)
+
+| Parameter      | Description          | Type                                                                 | Default      |
+| -------------- | -------------------- | -------------------------------------------------------------------- | ------------ |
+| fileName       | 文件名               | `string`                                                             | `(required)` |
+| data           | 数据                 | `Record<string, any>[]`                                              | `(required)` |
+| options        | 配置                 | [OptionsType](#options)                                              | `(required)` |
+| showBorder     | 是否显示边线         | `booean`                                                             | `true`       |
+| onBefore       | 表格绘制前钩子       | `(worksheet: ExcelJS.Worksheet, workbook: ExcelJS.Workbook) => void` | `--`         |
+| onHandler      | 已绘制表头和数据钩子 | `(worksheet: ExcelJS.Worksheet, workbook: ExcelJS.Workbook) => void` | `--`         |
+| onAfterHandler | 绘制 buffer 前钩子   | `(worksheet: ExcelJS.Worksheet, workbook: ExcelJS.Workbook) => void` | `--`         |
+| onAfte         | buffer 绘制完钩子    | `() => void`                                                         | `--`         |
+
+<a id="options"></a>
+
+### OptionsType
+
+| Name             | Description | Type                                              | Default      |
+| ---------------- | ----------- | ------------------------------------------------- | ------------ |
+| sheetHeader      | 表头        | `SheetHeaderItem[]`                               | `(required)` |
+| sheetFilter      | 键名        | [SheetFilterItem](#sheetfilteritem)[] \| string[] | `(required)` |
+| columnWidths     | 单元格宽度  | `number[]`                                        | `--`         |
+| defaultRowHeight | 默认行高    | `number`                                          | `30`         |
+
+<a id="sheetfilteritem"></a>
+
+### SheetFilterItem
+
+| Name        | Description                         | Type         | Default      |
+| ----------- | ----------------------------------- | ------------ | ------------ |
+| key         | 键名                                | `string`     | `(required)` |
+| type        | 类型                                | `txt \| img` | `(required)` |
+| showImageId | 只展示工作簿中的图片 id,不展示图片 | `boolean`    | `--`         |
+
+## 方法
+
+### getExcelImgBase64Ext
+
+获取图片 base64 的后缀类型
+
+### getExcelColumnLetter
+
+获取索引对应的字母

+ 3 - 1
packages/docs/docs/utils/image.md

@@ -1,6 +1,8 @@
 ## 方法
 
-### 图片压缩 compressImages
+### compressImages
+
+图片压缩
 
 支持最大宽度或者等比缩放
 

+ 14 - 0
packages/docs/docs/utils/number.md

@@ -0,0 +1,14 @@
+## 方法
+
+### numberToChinese
+
+数字转中文
+
+```typescript
+numberToChinese(1);
+// => 壹
+```
+
+| Name | Description | Type      | Default      |
+| ---- | ----------- | --------- | ------------ |
+| num  | `--`        | `number ` | `(required)` |

+ 25 - 4
packages/docs/docs/utils/string.md

@@ -1,6 +1,8 @@
 ## 方法
 
-### addTrailingSlash 字符串`末尾`添加`/`
+### addTrailingSlash
+
+字符串`末尾`添加`/`
 
 ```typescript
 addTrailingSlash("http://localhost:8000");
@@ -11,7 +13,9 @@ addTrailingSlash("http://localhost:8000");
 | ---- | ----------- | --------- | ------------ |
 | path | `--`        | `string ` | `(required)` |
 
-### removeHeadingString 删除特定`开头`的`字符串`
+### removeHeadingString
+
+删除特定`开头`的`字符串`
 
 ```typescript
 removeHeadingString("/api/example", "/api");
@@ -23,7 +27,9 @@ removeHeadingString("/api/example", "/api");
 | path    | `--`        | `string ` | `(required)` |
 | heading | `--`        | `string ` | `(required)` |
 
-### removeHeadingSlash 删除字符串`开头`的`/`
+### removeHeadingSlash
+
+删除字符串`开头`的`/`
 
 ```typescript
 removeHeadingSlash("/api/example");
@@ -34,7 +40,9 @@ removeHeadingSlash("/api/example");
 | ---- | ----------- | --------- | ------------ |
 | path | `--`        | `string ` | `(required)` |
 
-### 去除字符串中的 HTML 标签
+### stripHtmlTags
+
+去除字符串中的 HTML 标签
 
 ```typescript
 stripHtmlTags(
@@ -48,3 +56,16 @@ stripHtmlTags(
 | --------- | ----------- | --------- | ------------ |
 | input     | `文本`      | `string ` | `(required)` |
 | maxLength | `最大长度`  | `string ` | `(required)` |
+
+### getImageUrlsFromHtml
+
+获取 HTML 中的图片链接
+
+```typescript
+getImageUrlsFromHtml('<img src="image1.jpg" alt="图片1"/>');
+// => ["image1.jpg"]
+```
+
+| Name  | Description | Type      | Default      |
+| ----- | ----------- | --------- | ------------ |
+| input | `文本`      | `string ` | `(required)` |

+ 9 - 0
packages/docs/docs/utils/valid-form.md

@@ -0,0 +1,9 @@
+## 方法
+
+### validateIdCard
+
+验证身份证
+
+### validatePhone
+
+验证手机号

+ 1 - 1
packages/docs/package.json

@@ -1,7 +1,7 @@
 {
   "private": true,
   "name": "@dage/docs",
-  "version": "1.0.4",
+  "version": "1.1.0",
   "scripts": {
     "start": "dumi dev",
     "docs:build": "dumi build",

+ 7 - 0
packages/krpano/CHANGELOG.md

@@ -1,5 +1,12 @@
 # @dage/krpano
 
+## 4.0.0
+
+### Patch Changes
+
+- Updated dependencies
+  - @dage/utils@1.3.0
+
 ## 3.0.0
 
 ### Patch Changes

+ 2 - 2
packages/krpano/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@dage/krpano",
-  "version": "3.0.0",
+  "version": "4.0.0",
   "description": "krpano sdk",
   "module": "build/index.js",
   "main": "build/index.js",
@@ -18,7 +18,7 @@
   "peerDependencies": {
     "react": ">=18",
     "react-dom": ">=18",
-    "@dage/utils": ">=1.1.0"
+    "@dage/utils": ">=1.3.0"
   },
   "devDependencies": {
     "@babel/core": "^7.22.10",

+ 8 - 0
packages/pc-components/CHANGELOG.md

@@ -1,5 +1,13 @@
 # @dage/pc-components
 
+## 1.3.12
+
+### Patch Changes
+
+- Updated dependencies
+  - @dage/utils@1.3.0
+  - @dage/service@1.0.6
+
 ## 1.3.11
 
 ### Patch Changes

+ 1 - 1
packages/pc-components/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@dage/pc-components",
-  "version": "1.3.11",
+  "version": "1.3.12",
   "description": "PC 端组件库",
   "module": "dist/index.js",
   "main": "dist/index.js",

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 1 - 1
packages/pc-components/tsconfig.build.tsbuildinfo


+ 7 - 0
packages/service/CHANGELOG.md

@@ -1,5 +1,12 @@
 # @dage/service
 
+## 1.0.6
+
+### Patch Changes
+
+- Updated dependencies
+  - @dage/utils@1.3.0
+
 ## 1.0.5
 
 ### Patch Changes

+ 1 - 1
packages/service/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@dage/service",
-  "version": "1.0.5",
+  "version": "1.0.6",
   "description": "接口请求工具",
   "module": "dist/index.js",
   "main": "dist/index.js",

+ 8 - 0
packages/utils/CHANGELOG.md

@@ -1,5 +1,13 @@
 # @dage/utils
 
+## 1.3.0
+
+### Minor Changes
+
+- 新增导出 Excel 方法
+  新增 getImageUrlsFromHtml 方法
+  新增 numberToChinese 方法
+
 ## 1.1.1
 
 ### Patch Changes

+ 2 - 1
packages/utils/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@dage/utils",
-  "version": "1.1.1",
+  "version": "1.3.0",
   "description": "工具类",
   "sideEffects": false,
   "module": "dist/index.js",
@@ -33,6 +33,7 @@
   "dependencies": {
     "@dage/events": "workspace:^",
     "dayjs": "^1.11.9",
+    "exceljs": "^4.4.0",
     "js-base64": "^3.7.5",
     "query-string": "^8.1.0"
   }

+ 27 - 1
packages/utils/src/base64.ts

@@ -1 +1,27 @@
-export * from 'js-base64';
+export * from "js-base64";
+
+export const getImgBase64Sync = (imgUrl: string) => {
+  return new Promise<string>(function (resolve, reject) {
+    // 一定要设置为let,不然图片不显示
+    let image = new Image();
+    image.crossOrigin = "anonymous";
+    image.src = imgUrl;
+    image.onload = function () {
+      let canvas = document.createElement("canvas");
+      canvas.width = image.width;
+      canvas.height = image.height;
+      let context = canvas.getContext("2d");
+      context?.drawImage(image, 0, 0, image.width, image.height);
+      //图片后缀名
+      let ext = image.src
+        .substring(image.src.lastIndexOf(".") + 1)
+        .toLowerCase();
+      let quality = 0.8;
+      let dataurl = canvas.toDataURL("image/" + ext, quality);
+      resolve(dataurl);
+    };
+    image.onerror = function (err) {
+      reject(err);
+    };
+  });
+};

+ 178 - 0
packages/utils/src/exportExcel/index.ts

@@ -0,0 +1,178 @@
+import ExcelJS from "exceljs";
+import { ExportExcelParams } from "./types";
+import { getExcelColumnLetter, getExcelImgBase64Ext } from "./utils";
+import { cloneDeep, isObject } from "lodash";
+import { getImgBase64Sync } from "../base64";
+
+/**
+ * 导出 excel
+ */
+export const exportExcel = async (params: ExportExcelParams) => {
+  const {
+    fileName,
+    showBorder = true,
+    options,
+    data,
+    onBefore,
+    onHandler,
+    onAfterHandler,
+    onAfter,
+  } = params;
+  const workbook = new ExcelJS.Workbook();
+  const worksheet = workbook.addWorksheet("Sheet1", {
+    properties: { defaultRowHeight: options.defaultRowHeight || 30 },
+    views: [{ showGridLines: false }],
+    pageSetup: {
+      horizontalCentered: true,
+      verticalCentered: true,
+      paperSize: 9,
+    },
+  });
+
+  onBefore?.(worksheet, workbook);
+
+  // 表头需要占据的行数
+  const headOccupyLine = Math.max(
+    ...options.sheetHeader.map((head: string | string[]) =>
+      Array.isArray(head) ? 2 : 1
+    )
+  );
+  let headTemp: string[] = [];
+  let mergeHeadNum = 0;
+  let fatherHeadTemp: {
+    title: string;
+    position: string[];
+  }[] = [];
+
+  options?.sheetHeader.forEach((head: string | string[], index: number) => {
+    if (Array.isArray(head)) {
+      fatherHeadTemp.push({
+        title: head.shift() || "",
+        position: [
+          `${getExcelColumnLetter(index + mergeHeadNum)}1`,
+          `${getExcelColumnLetter(index + head.length - 1 + mergeHeadNum)}1`,
+        ],
+      });
+      headTemp.push(...head);
+      mergeHeadNum += head.length - 1;
+    } else {
+      headTemp.push(head);
+    }
+  });
+  worksheet.addRow(headTemp);
+  if (fatherHeadTemp.length) {
+    // 存在需要合并的父级表头
+    worksheet.insertRow(1, []);
+    fatherHeadTemp.forEach((head) => {
+      const cell = worksheet.getCell(head.position[0]);
+      cell.value = head.title;
+      worksheet.mergeCells(`${head.position[0]}:${head.position[1]}`);
+    });
+  }
+  if (headOccupyLine > 1) {
+    // 合并表头列
+    headTemp.forEach((head, index) => {
+      const pos = `${getExcelColumnLetter(index)}1:${getExcelColumnLetter(
+        index
+      )}${headOccupyLine}`;
+      const cell = worksheet.getCell(`${getExcelColumnLetter(index)}1`);
+      if (!cell.isMerged) {
+        worksheet.mergeCells(pos);
+        cell.value = head;
+      }
+    });
+  }
+
+  const startRowCount = worksheet.rowCount;
+  const imgOpts: (string | number)[][] = [];
+  for (const [index, item] of cloneDeep(data).entries()) {
+    const _temp: string[] = [];
+    for (const [index2, filter] of options.sheetFilter.entries()) {
+      const key = isObject(filter) ? filter.key : filter;
+      const isImg = isObject(filter) && filter.type === "img";
+      // 绘制图片
+      if (isImg && Boolean(item[key])) {
+        const base64 = await getImgBase64Sync(item[key]);
+        const extension = getExcelImgBase64Ext(base64);
+        if (extension) {
+          const imageId = workbook.addImage({
+            base64,
+            extension,
+          });
+          const lineIndex = index + headOccupyLine + startRowCount;
+
+          item[key] = imageId;
+          !filter.showImageId &&
+            imgOpts.push([
+              imageId,
+              `${getExcelColumnLetter(
+                index2
+              )}${lineIndex}:${getExcelColumnLetter(index2)}${lineIndex}`,
+            ]);
+        }
+      }
+      _temp.push(item[key]);
+    }
+    worksheet.addRow(_temp);
+  }
+  imgOpts.forEach((opts) => {
+    worksheet.addImage(opts[0] as number, opts[1] as string);
+  });
+
+  // 设置宽度
+  options.columnWidths?.forEach((width: number, index: number) => {
+    worksheet.getColumn(index + 1).width = width;
+  });
+
+  onHandler?.(worksheet, workbook);
+
+  if (showBorder) {
+    const borderStyle: ExcelJS.Border = {
+      style: "thin",
+      color: { argb: "FF000000" },
+    };
+
+    for (let row = 1; row <= worksheet.actualRowCount; row++) {
+      for (let col = 1; col <= worksheet.actualColumnCount; col++) {
+        const cell = worksheet.getCell(row, col);
+        cell.border = {
+          top: borderStyle,
+          left: borderStyle,
+          bottom: borderStyle,
+          right: borderStyle,
+        };
+      }
+    }
+
+    worksheet.eachRow((row) => {
+      row.eachCell((cell) => {
+        cell.alignment = {
+          vertical: "middle",
+          horizontal: "center",
+          wrapText: true,
+        };
+      });
+    });
+  }
+
+  onAfterHandler?.(worksheet, workbook);
+
+  const buffer = await workbook.xlsx.writeBuffer();
+  const blob = new Blob([buffer], {
+    type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+  });
+
+  const url = URL.createObjectURL(blob);
+  const a = document.createElement("a");
+  a.href = url;
+  a.download = fileName + ".xlsx";
+  a.click();
+
+  onAfter?.();
+
+  // 释放内存
+  setTimeout(() => URL.revokeObjectURL(url), 100);
+};
+
+export * from "./utils";
+export * from "./types";

+ 42 - 0
packages/utils/src/exportExcel/types.ts

@@ -0,0 +1,42 @@
+import ExcelJS from "exceljs";
+
+export type SheetHeaderItem = string | string[];
+
+export type SheetFilterItem =
+  | {
+      key: string;
+      type: "txt" | "img";
+      /** 只展示工作簿中的图片id,不展示图片 */
+      showImageId?: boolean;
+    }
+  | string;
+
+export interface ExportExcelParams {
+  fileName: string;
+  data: Record<string, any>[];
+  options: {
+    sheetHeader: SheetHeaderItem[];
+    sheetFilter: SheetFilterItem[];
+    columnWidths?: number[];
+    /**
+     * 默认行高
+     * @default 30
+     */
+    defaultRowHeight?: number;
+  };
+  /**
+   * 是否显示边线
+   * @default true
+   */
+  showBorder?: boolean;
+  onBefore?: (worksheet: ExcelJS.Worksheet, workbook: ExcelJS.Workbook) => void;
+  onHandler?: (
+    worksheet: ExcelJS.Worksheet,
+    workbook: ExcelJS.Workbook
+  ) => void;
+  onAfterHandler?: (
+    worksheet: ExcelJS.Worksheet,
+    workbook: ExcelJS.Workbook
+  ) => void;
+  onAfter?: () => void;
+}

+ 48 - 0
packages/utils/src/exportExcel/utils.ts

@@ -0,0 +1,48 @@
+/**
+ * 获取图片base64的后缀类型
+ */
+export const getExcelImgBase64Ext = (
+  str: string
+): "jpeg" | "png" | "gif" | null => {
+  const base64Regex = /^data:image\/([a-zA-Z+]*);base64,([^\s]*)$/;
+
+  if (!base64Regex.test(str)) {
+    return null;
+  }
+
+  // 提取MIME类型
+  const matches = str.match(base64Regex);
+  if (!matches || matches.length < 2) {
+    return null;
+  }
+
+  const mimeType = matches[1].toLowerCase();
+
+  // 只允许jpeg/jpg/png/gif三种类型
+  switch (mimeType) {
+    case "jpeg":
+    case "jpg":
+      return "jpeg";
+    case "png":
+      return "png";
+    case "gif":
+      return "gif";
+    default:
+      return null;
+  }
+};
+
+/**
+ * 获取索引对应的字母
+ */
+export const getExcelColumnLetter = (index: number) => {
+  let result = "";
+  let remaining = index;
+
+  do {
+    result = String.fromCharCode(65 + (remaining % 26)) + result;
+    remaining = Math.floor(remaining / 26) - 1;
+  } while (remaining >= 0);
+
+  return result;
+};

+ 3 - 0
packages/utils/src/index.ts

@@ -2,6 +2,9 @@ export * from "./eventBus";
 export * from "./base64";
 export * from "./date";
 export * from "./string";
+export * from "./number";
 export * from "./query-string";
 export * from "./noop";
 export * from "./image";
+export * from "./exportExcel";
+export * from "./validForm";

+ 53 - 0
packages/utils/src/number.ts

@@ -0,0 +1,53 @@
+/**
+ * 数字转中文
+ */
+export const numberToChinese = (num: number) => {
+  if (isNaN(num)) {
+    throw new Error("输入必须是一个有效的数字");
+  }
+  if (num < 0 || num > 9999) {
+    throw new Error("输入数字超出范围 (0-9999)");
+  }
+  if (num === 0) {
+    return "零";
+  }
+
+  const chineseNumbers = [
+    "零",
+    "壹",
+    "贰",
+    "叁",
+    "肆",
+    "伍",
+    "陆",
+    "柒",
+    "捌",
+    "玖",
+  ];
+  const chineseUnits = ["", "拾", "佰", "仟"];
+
+  let result = "";
+  const numStr = num.toString();
+  const length = numStr.length;
+
+  for (let i = 0; i < length; i++) {
+    const digit = parseInt(numStr[i]);
+    const unit = length - i - 1;
+
+    if (digit !== 0) {
+      result += chineseNumbers[digit] + chineseUnits[unit];
+    } else {
+      // 处理连续的零,只保留一个
+      if (i < length - 1 && numStr[i + 1] !== "0") {
+        result += chineseNumbers[digit];
+      }
+    }
+  }
+
+  // 处理10-19的情况,去掉开头的"壹"
+  if (num >= 10 && num < 20) {
+    result = result.replace("壹拾", "拾");
+  }
+
+  return result;
+};

+ 15 - 0
packages/utils/src/string.ts

@@ -31,3 +31,18 @@ export function stripHtmlTags(input: string, maxLength: number): string {
 
   return strippedText;
 }
+
+/**
+ * 获取 HTML 中的图片链接
+ */
+export const getImageUrlsFromHtml = (input: string) => {
+  const imgRegex = /<img[^>]+src="([^">]+)"/g;
+  const urls: string[] = [];
+  let match;
+
+  while ((match = imgRegex.exec(input)) !== null) {
+    urls.push(match[1]);
+  }
+
+  return urls;
+};

+ 40 - 0
packages/utils/src/validForm.ts

@@ -0,0 +1,40 @@
+/**
+ * 验证身份证
+ */
+export const validateIdCard = (value: string) => {
+  if (!value) return false;
+
+  const reg = /(^\d{15}$)|(^\d{17}(\d|X|x)$)/;
+  if (!reg.test(value)) {
+    return false;
+  }
+
+  // 校验位验证(18位身份证)
+  if (value.length === 18) {
+    const weight = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]; // 加权因子
+    const validate = ["1", "0", "X", "9", "8", "7", "6", "5", "4", "3", "2"]; // 校验码
+
+    let sum = 0;
+    for (let i = 0; i < 17; i++) {
+      sum += parseInt(value.charAt(i)) * weight[i];
+    }
+
+    const mod = sum % 11;
+    if (value.charAt(17).toUpperCase() !== validate[mod]) {
+      return false;
+    }
+  }
+
+  return true;
+};
+
+/**
+ * 验证手机号
+ */
+export const validatePhone = (value: string) => {
+  const reg = /^1[3-9]\d{9}$/;
+  if (value && !reg.test(value)) {
+    return false;
+  }
+  return true;
+};

+ 1 - 1
packages/utils/tsconfig.build.commonjs.tsbuildinfo

@@ -1 +1 @@
-{"root":["./src/base64.ts","./src/date.ts","./src/eventbus.ts","./src/image.ts","./src/index.ts","./src/noop.ts","./src/query-string.ts","./src/string.ts"],"version":"5.8.2"}
+{"root":["./src/base64.ts","./src/date.ts","./src/eventbus.ts","./src/image.ts","./src/index.ts","./src/noop.ts","./src/number.ts","./src/query-string.ts","./src/string.ts","./src/validform.ts","./src/exportexcel/index.ts","./src/exportexcel/types.ts","./src/exportexcel/utils.ts"],"version":"5.8.2"}

+ 1 - 1
packages/utils/tsconfig.build.tsbuildinfo

@@ -1 +1 @@
-{"root":["./src/base64.ts","./src/date.ts","./src/eventbus.ts","./src/image.ts","./src/index.ts","./src/noop.ts","./src/query-string.ts","./src/string.ts"],"version":"5.8.2"}
+{"root":["./src/base64.ts","./src/date.ts","./src/eventbus.ts","./src/image.ts","./src/index.ts","./src/noop.ts","./src/number.ts","./src/query-string.ts","./src/string.ts","./src/validform.ts","./src/exportexcel/index.ts","./src/exportexcel/types.ts","./src/exportexcel/utils.ts"],"version":"5.8.2"}

+ 362 - 4
pnpm-lock.yaml

@@ -318,7 +318,7 @@ importers:
         version: 7.26.0(@babel/core@7.26.10)
       '@testing-library/react-hooks':
         specifier: ^8.0.1
-        version: 8.0.1(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+        version: 8.0.1(@types/react@16.14.62)(react-dom@16.14.0(react@19.0.0))(react@19.0.0)
       babel-jest:
         specifier: ^29.6.2
         version: 29.7.0(@babel/core@7.26.10)
@@ -457,6 +457,9 @@ importers:
       dayjs:
         specifier: ^1.11.9
         version: 1.11.13
+      exceljs:
+        specifier: ^4.4.0
+        version: 4.4.0
       js-base64:
         specifier: ^3.7.5
         version: 3.7.7
@@ -1483,6 +1486,12 @@ packages:
     resolution: {integrity: sha512-XXrH9Uarn0stsyldqDYq8r++mROmWRI1xKMXa640Bb//SY1+ECYX6VzT6Lcx5frD0V30XieqJ0oX9I2Xj5aoMA==}
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
 
+  '@fast-csv/format@4.3.5':
+    resolution: {integrity: sha512-8iRn6QF3I8Ak78lNAa+Gdl5MJJBM5vRHivFtMRUWINdevNo00K7OXxS2PshawLKTejVwieIlPmK5YlLu6w4u8A==}
+
+  '@fast-csv/parse@4.3.6':
+    resolution: {integrity: sha512-uRsLYksqpbDmWaSmzvJcuApSEe38+6NQZBUsuAyMZKqHxH0g1wcJgsKUvN3WC8tewaqFjBMMGrkHmC+T7k8LvA==}
+
   '@humanwhocodes/config-array@0.5.0':
     resolution: {integrity: sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==}
     engines: {node: '>=10.10.0'}
@@ -2780,6 +2789,18 @@ packages:
   arch@2.2.0:
     resolution: {integrity: sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==}
 
+  archiver-utils@2.1.0:
+    resolution: {integrity: sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==}
+    engines: {node: '>= 6'}
+
+  archiver-utils@3.0.4:
+    resolution: {integrity: sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==}
+    engines: {node: '>= 10'}
+
+  archiver@5.3.2:
+    resolution: {integrity: sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==}
+    engines: {node: '>= 10'}
+
   arg@5.0.2:
     resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
 
@@ -3097,6 +3118,10 @@ packages:
     resolution: {integrity: sha512-+e/UqUzwmzJamNF50tBV6tZPTORow7gQ96iFow+8b562OdMpEK0BcJEq2OSPEDmAbSMBQ7PKZ87ubFkgxpYWgw==}
     engines: {node: '>= 8.0.0'}
 
+  big-integer@1.6.52:
+    resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==}
+    engines: {node: '>=0.6'}
+
   big.js@5.2.2:
     resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==}
 
@@ -3108,6 +3133,9 @@ packages:
     resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==}
     engines: {node: '>=8'}
 
+  binary@0.3.0:
+    resolution: {integrity: sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==}
+
   binaryextensions@2.3.0:
     resolution: {integrity: sha512-nAihlQsYGyc5Bwq6+EsubvANYGExeJKHDO3RjnvwU042fawQTQfM3Kxn7IHUXQOz4bzfwsGYYHGSvXyW4zOGLg==}
     engines: {node: '>=0.8'}
@@ -3118,6 +3146,12 @@ packages:
   bl@1.2.3:
     resolution: {integrity: sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==}
 
+  bl@4.1.0:
+    resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
+
+  bluebird@3.4.7:
+    resolution: {integrity: sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==}
+
   bluebird@3.7.2:
     resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==}
 
@@ -3199,12 +3233,19 @@ packages:
   buffer-alloc@1.2.0:
     resolution: {integrity: sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==}
 
+  buffer-crc32@0.2.13:
+    resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==}
+
   buffer-fill@1.0.0:
     resolution: {integrity: sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==}
 
   buffer-from@1.1.2:
     resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
 
+  buffer-indexof-polyfill@1.0.2:
+    resolution: {integrity: sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==}
+    engines: {node: '>=0.10'}
+
   buffer-indexof@1.1.1:
     resolution: {integrity: sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g==}
 
@@ -3214,6 +3255,13 @@ packages:
   buffer@4.9.2:
     resolution: {integrity: sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==}
 
+  buffer@5.7.1:
+    resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
+
+  buffers@0.1.1:
+    resolution: {integrity: sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==}
+    engines: {node: '>=0.2.0'}
+
   builtin-modules@3.3.0:
     resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==}
     engines: {node: '>=6'}
@@ -3320,6 +3368,9 @@ packages:
   ccount@1.1.0:
     resolution: {integrity: sha512-vlNK021QdI7PNeiUh/lKkC/mNHHfV0m/Ad5JoI0TYtlBnJAslM/JIkm/tGC88bkLIwO6OQ5uV6ztS6kVAtCDlg==}
 
+  chainsaw@0.1.0:
+    resolution: {integrity: sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==}
+
   chalk@2.4.2:
     resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==}
     engines: {node: '>=4'}
@@ -3544,6 +3595,10 @@ packages:
   component-emitter@1.3.1:
     resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==}
 
+  compress-commons@4.1.2:
+    resolution: {integrity: sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==}
+    engines: {node: '>= 10'}
+
   compressible@2.0.18:
     resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==}
     engines: {node: '>= 0.6'}
@@ -3662,6 +3717,15 @@ packages:
     resolution: {integrity: sha512-jHTjZhsbg9xWgsP2vuNW2jnnzBX+p4T+vNI9Lbjzs1n4KhOfa22bQppiFYLsWQKd8TzmL5aSP/Me3yfsCwXbDA==}
     hasBin: true
 
+  crc-32@1.2.2:
+    resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==}
+    engines: {node: '>=0.8'}
+    hasBin: true
+
+  crc32-stream@4.0.3:
+    resolution: {integrity: sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==}
+    engines: {node: '>= 10'}
+
   create-ecdh@4.0.4:
     resolution: {integrity: sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==}
 
@@ -4188,6 +4252,9 @@ packages:
     resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
     engines: {node: '>= 0.4'}
 
+  duplexer2@0.1.4:
+    resolution: {integrity: sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==}
+
   duplexer3@0.1.5:
     resolution: {integrity: sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==}
 
@@ -4645,6 +4712,10 @@ packages:
   evp_bytestokey@1.0.3:
     resolution: {integrity: sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==}
 
+  exceljs@4.4.0:
+    resolution: {integrity: sha512-XctvKaEMaj1Ii9oDOqbW/6e1gXknSY4g/aLCDicOXqBE4M0nRWkUu0PTp++UPNzoFY12BNHMfs/VadKIS6llvg==}
+    engines: {node: '>=8.3.0'}
+
   exec-sh@0.3.6:
     resolution: {integrity: sha512-nQn+hI3yp+oD0huYhKwvYI32+JFeq+XkNcD1GAo3Y/MjxsfVGmrrzrnzjWiNY6f+pUCP440fThsFh5gZrRAU/w==}
 
@@ -4733,6 +4804,10 @@ packages:
     resolution: {integrity: sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==}
     engines: {node: '>=0.10.0'}
 
+  fast-csv@4.3.6:
+    resolution: {integrity: sha512-2RNSpuwwsJGP0frGsOmTb9oUF+VkFSM4SyLTDgwf2ciHWTarN0lQTC+F2f/t5J9QjW+c65VFIAAu85GsvMIusw==}
+    engines: {node: '>=10.0.0'}
+
   fast-deep-equal@3.1.3:
     resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
 
@@ -4990,6 +5065,11 @@ packages:
     engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
     os: [darwin]
 
+  fstream@1.0.12:
+    resolution: {integrity: sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==}
+    engines: {node: '>=0.6'}
+    deprecated: This package is no longer supported.
+
   function-bind@1.1.1:
     resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==}
 
@@ -5122,6 +5202,7 @@ packages:
 
   glob@7.2.3:
     resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
+    deprecated: Glob versions prior to v9 are no longer supported
 
   global-dirs@0.1.1:
     resolution: {integrity: sha512-NknMLn7F2J7aflwFOlGdNIuCDpN3VGoSoB+aap3KABFWbHVn1TCgFC+np23J8W2BiZbjfEw3BFBycSMv1AFblg==}
@@ -5519,6 +5600,9 @@ packages:
     engines: {node: '>=6.9.0'}
     hasBin: true
 
+  immediate@3.0.6:
+    resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
+
   immer@10.1.1:
     resolution: {integrity: sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==}
 
@@ -5574,6 +5658,7 @@ packages:
 
   inflight@1.0.6:
     resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
+    deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
 
   inherits@2.0.3:
     resolution: {integrity: sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==}
@@ -6531,6 +6616,9 @@ packages:
     resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==}
     engines: {node: '>=4.0'}
 
+  jszip@3.10.1:
+    resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==}
+
   katex@0.12.0:
     resolution: {integrity: sha512-y+8btoc/CK70XqcHqjxiGWBOeIL8upbS0peTPXTvgrh21n1RiWWcIpSWM+4uXq+IAgNh9YYQWdc7LVDPDAEEAg==}
     hasBin: true
@@ -6575,6 +6663,10 @@ packages:
     resolution: {integrity: sha512-RE2g0b5VGZsOCFOCgP7omTRYFqydmZkBwl5oNnQ1lDYC57uyO9KqNnNVxT7COSHTxrRCWVcAVOcbjk+tvh/rgQ==}
     engines: {node: '>=0.10.0'}
 
+  lazystream@1.0.1:
+    resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==}
+    engines: {node: '>= 0.6.3'}
+
   lcid@3.1.1:
     resolution: {integrity: sha512-M6T051+5QCGLBQb8id3hdvIW8+zeFV2FyBGFS9IEK5H9Wt4MueD4bW1eWikpHgZp+5xR3l5c8pZUkQsIA0BFZg==}
     engines: {node: '>=8'}
@@ -6587,6 +6679,9 @@ packages:
     resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
     engines: {node: '>= 0.8.0'}
 
+  lie@3.3.0:
+    resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==}
+
   lilconfig@2.1.0:
     resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==}
     engines: {node: '>=10'}
@@ -6594,6 +6689,9 @@ packages:
   lines-and-columns@1.2.4:
     resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
 
+  listenercount@1.0.1:
+    resolution: {integrity: sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==}
+
   loader-runner@4.3.0:
     resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==}
     engines: {node: '>=6.11.5'}
@@ -6631,6 +6729,18 @@ packages:
   lodash.debounce@4.0.8:
     resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
 
+  lodash.defaults@4.2.0:
+    resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==}
+
+  lodash.difference@4.5.0:
+    resolution: {integrity: sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==}
+
+  lodash.escaperegexp@4.1.2:
+    resolution: {integrity: sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==}
+
+  lodash.flatten@4.4.0:
+    resolution: {integrity: sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==}
+
   lodash.foreach@4.5.0:
     resolution: {integrity: sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ==}
 
@@ -6638,10 +6748,28 @@ packages:
     resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==}
     deprecated: This package is deprecated. Use the optional chaining (?.) operator instead.
 
+  lodash.groupby@4.6.0:
+    resolution: {integrity: sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==}
+
+  lodash.isboolean@3.0.3:
+    resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==}
+
   lodash.isequal@4.5.0:
     resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==}
     deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead.
 
+  lodash.isfunction@3.0.9:
+    resolution: {integrity: sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==}
+
+  lodash.isnil@4.0.0:
+    resolution: {integrity: sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==}
+
+  lodash.isplainobject@4.0.6:
+    resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
+
+  lodash.isundefined@3.0.1:
+    resolution: {integrity: sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA==}
+
   lodash.memoize@4.1.2:
     resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==}
 
@@ -6663,6 +6791,9 @@ packages:
   lodash.truncate@4.4.2:
     resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==}
 
+  lodash.union@4.6.0:
+    resolution: {integrity: sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==}
+
   lodash.uniq@4.5.0:
     resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==}
 
@@ -8777,6 +8908,9 @@ packages:
     resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
     engines: {node: '>= 6'}
 
+  readdir-glob@1.1.3:
+    resolution: {integrity: sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==}
+
   readdirp@2.2.1:
     resolution: {integrity: sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==}
     engines: {node: '>=0.10'}
@@ -9820,6 +9954,10 @@ packages:
     resolution: {integrity: sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==}
     engines: {node: '>= 0.8.0'}
 
+  tar-stream@2.2.0:
+    resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==}
+    engines: {node: '>=6'}
+
   temp-dir@2.0.0:
     resolution: {integrity: sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==}
     engines: {node: '>=8'}
@@ -9972,6 +10110,9 @@ packages:
     resolution: {integrity: sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==}
     engines: {node: '>=8'}
 
+  traverse@0.3.9:
+    resolution: {integrity: sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==}
+
   trim-newlines@3.0.1:
     resolution: {integrity: sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==}
     engines: {node: '>=8'}
@@ -10211,6 +10352,9 @@ packages:
     resolution: {integrity: sha512-N0XH6lqDtFH84JxptQoZYmloF4nzrQqqrAymNj+/gW60AO2AZgOcf4O/nUXJcYfyQkqvMo9lSupBZmmgvuVXlw==}
     engines: {node: '>=4'}
 
+  unzipper@0.10.14:
+    resolution: {integrity: sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==}
+
   upath@1.2.0:
     resolution: {integrity: sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==}
     engines: {node: '>=4'}
@@ -10656,6 +10800,10 @@ packages:
     resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
     engines: {node: '>=10'}
 
+  zip-stream@4.1.1:
+    resolution: {integrity: sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==}
+    engines: {node: '>= 10'}
+
   zwitch@1.0.5:
     resolution: {integrity: sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==}
 
@@ -12098,6 +12246,25 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
+  '@fast-csv/format@4.3.5':
+    dependencies:
+      '@types/node': 14.18.63
+      lodash.escaperegexp: 4.1.2
+      lodash.isboolean: 3.0.3
+      lodash.isequal: 4.5.0
+      lodash.isfunction: 3.0.9
+      lodash.isnil: 4.0.0
+
+  '@fast-csv/parse@4.3.6':
+    dependencies:
+      '@types/node': 14.18.63
+      lodash.escaperegexp: 4.1.2
+      lodash.groupby: 4.6.0
+      lodash.isfunction: 3.0.9
+      lodash.isnil: 4.0.0
+      lodash.isundefined: 3.0.1
+      lodash.uniq: 4.5.0
+
   '@humanwhocodes/config-array@0.5.0':
     dependencies:
       '@humanwhocodes/object-schema': 1.2.1
@@ -12989,14 +13156,14 @@ snapshots:
       lodash: 4.17.21
       redent: 3.0.0
 
-  '@testing-library/react-hooks@8.0.1(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
+  '@testing-library/react-hooks@8.0.1(@types/react@16.14.62)(react-dom@16.14.0(react@19.0.0))(react@19.0.0)':
     dependencies:
       '@babel/runtime': 7.26.10
       react: 19.0.0
       react-error-boundary: 3.1.4(react@19.0.0)
     optionalDependencies:
-      '@types/react': 19.0.11
-      react-dom: 19.0.0(react@19.0.0)
+      '@types/react': 16.14.62
+      react-dom: 16.14.0(react@19.0.0)
 
   '@testing-library/react@16.2.0(@testing-library/dom@10.4.0)(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
     dependencies:
@@ -14182,6 +14349,42 @@ snapshots:
 
   arch@2.2.0: {}
 
+  archiver-utils@2.1.0:
+    dependencies:
+      glob: 7.2.3
+      graceful-fs: 4.2.11
+      lazystream: 1.0.1
+      lodash.defaults: 4.2.0
+      lodash.difference: 4.5.0
+      lodash.flatten: 4.4.0
+      lodash.isplainobject: 4.0.6
+      lodash.union: 4.6.0
+      normalize-path: 3.0.0
+      readable-stream: 2.3.8
+
+  archiver-utils@3.0.4:
+    dependencies:
+      glob: 7.2.3
+      graceful-fs: 4.2.11
+      lazystream: 1.0.1
+      lodash.defaults: 4.2.0
+      lodash.difference: 4.5.0
+      lodash.flatten: 4.4.0
+      lodash.isplainobject: 4.0.6
+      lodash.union: 4.6.0
+      normalize-path: 3.0.0
+      readable-stream: 3.6.2
+
+  archiver@5.3.2:
+    dependencies:
+      archiver-utils: 2.1.0
+      async: 3.2.4
+      buffer-crc32: 0.2.13
+      readable-stream: 3.6.2
+      readdir-glob: 1.1.3
+      tar-stream: 2.2.0
+      zip-stream: 4.1.1
+
   arg@5.0.2: {}
 
   argparse@1.0.10:
@@ -14659,12 +14862,19 @@ snapshots:
       hoopy: 0.1.4
       tryer: 1.0.1
 
+  big-integer@1.6.52: {}
+
   big.js@5.2.2: {}
 
   binary-extensions@1.13.1: {}
 
   binary-extensions@2.2.0: {}
 
+  binary@0.3.0:
+    dependencies:
+      buffers: 0.1.1
+      chainsaw: 0.1.0
+
   binaryextensions@2.3.0: {}
 
   bindings@1.5.0:
@@ -14677,6 +14887,14 @@ snapshots:
       readable-stream: 2.3.8
       safe-buffer: 5.2.1
 
+  bl@4.1.0:
+    dependencies:
+      buffer: 5.7.1
+      inherits: 2.0.4
+      readable-stream: 3.6.2
+
+  bluebird@3.4.7: {}
+
   bluebird@3.7.2: {}
 
   bn.js@4.12.1: {}
@@ -14822,10 +15040,14 @@ snapshots:
       buffer-alloc-unsafe: 1.1.0
       buffer-fill: 1.0.0
 
+  buffer-crc32@0.2.13: {}
+
   buffer-fill@1.0.0: {}
 
   buffer-from@1.1.2: {}
 
+  buffer-indexof-polyfill@1.0.2: {}
+
   buffer-indexof@1.1.1: {}
 
   buffer-xor@1.0.3: {}
@@ -14836,6 +15058,13 @@ snapshots:
       ieee754: 1.2.1
       isarray: 1.0.0
 
+  buffer@5.7.1:
+    dependencies:
+      base64-js: 1.5.1
+      ieee754: 1.2.1
+
+  buffers@0.1.1: {}
+
   builtin-modules@3.3.0: {}
 
   builtin-status-codes@3.0.0: {}
@@ -14964,6 +15193,10 @@ snapshots:
 
   ccount@1.1.0: {}
 
+  chainsaw@0.1.0:
+    dependencies:
+      traverse: 0.3.9
+
   chalk@2.4.2:
     dependencies:
       ansi-styles: 3.2.1
@@ -15216,6 +15449,13 @@ snapshots:
 
   component-emitter@1.3.1: {}
 
+  compress-commons@4.1.2:
+    dependencies:
+      buffer-crc32: 0.2.13
+      crc32-stream: 4.0.3
+      normalize-path: 3.0.0
+      readable-stream: 3.6.2
+
   compressible@2.0.18:
     dependencies:
       mime-db: 1.52.0
@@ -15350,6 +15590,13 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
+  crc-32@1.2.2: {}
+
+  crc32-stream@4.0.3:
+    dependencies:
+      crc-32: 1.2.2
+      readable-stream: 3.6.2
+
   create-ecdh@4.0.4:
     dependencies:
       bn.js: 4.12.1
@@ -15923,6 +16170,10 @@ snapshots:
       es-errors: 1.3.0
       gopd: 1.2.0
 
+  duplexer2@0.1.4:
+    dependencies:
+      readable-stream: 2.3.8
+
   duplexer3@0.1.5: {}
 
   duplexer@0.1.2: {}
@@ -16660,6 +16911,18 @@ snapshots:
       md5.js: 1.3.5
       safe-buffer: 5.2.1
 
+  exceljs@4.4.0:
+    dependencies:
+      archiver: 5.3.2
+      dayjs: 1.11.13
+      fast-csv: 4.3.6
+      jszip: 3.10.1
+      readable-stream: 3.6.2
+      saxes: 5.0.1
+      tmp: 0.2.1
+      unzipper: 0.10.14
+      uuid: 8.3.2
+
   exec-sh@0.3.6: {}
 
   execa@0.7.0:
@@ -16836,6 +17099,11 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
+  fast-csv@4.3.6:
+    dependencies:
+      '@fast-csv/format': 4.3.5
+      '@fast-csv/parse': 4.3.6
+
   fast-deep-equal@3.1.3: {}
 
   fast-glob@3.3.3:
@@ -17107,6 +17375,13 @@ snapshots:
   fsevents@2.3.3:
     optional: true
 
+  fstream@1.0.12:
+    dependencies:
+      graceful-fs: 4.2.11
+      inherits: 2.0.4
+      mkdirp: 0.5.6
+      rimraf: 2.7.1
+
   function-bind@1.1.1: {}
 
   function-bind@1.1.2: {}
@@ -17751,6 +18026,8 @@ snapshots:
     dependencies:
       queue: 6.0.1
 
+  immediate@3.0.6: {}
+
   immer@10.1.1: {}
 
   immer@9.0.21: {}
@@ -19384,6 +19661,13 @@ snapshots:
       object.assign: 4.1.4
       object.values: 1.1.6
 
+  jszip@3.10.1:
+    dependencies:
+      lie: 3.3.0
+      pako: 1.0.11
+      readable-stream: 2.3.8
+      setimmediate: 1.0.5
+
   katex@0.12.0:
     dependencies:
       commander: 2.20.3
@@ -19418,6 +19702,10 @@ snapshots:
 
   lazy-cache@1.0.4: {}
 
+  lazystream@1.0.1:
+    dependencies:
+      readable-stream: 2.3.8
+
   lcid@3.1.1:
     dependencies:
       invert-kv: 3.0.1
@@ -19429,10 +19717,16 @@ snapshots:
       prelude-ls: 1.2.1
       type-check: 0.4.0
 
+  lie@3.3.0:
+    dependencies:
+      immediate: 3.0.6
+
   lilconfig@2.1.0: {}
 
   lines-and-columns@1.2.4: {}
 
+  listenercount@1.0.1: {}
+
   loader-runner@4.3.0: {}
 
   loader-utils@1.4.2:
@@ -19468,12 +19762,32 @@ snapshots:
 
   lodash.debounce@4.0.8: {}
 
+  lodash.defaults@4.2.0: {}
+
+  lodash.difference@4.5.0: {}
+
+  lodash.escaperegexp@4.1.2: {}
+
+  lodash.flatten@4.4.0: {}
+
   lodash.foreach@4.5.0: {}
 
   lodash.get@4.4.2: {}
 
+  lodash.groupby@4.6.0: {}
+
+  lodash.isboolean@3.0.3: {}
+
   lodash.isequal@4.5.0: {}
 
+  lodash.isfunction@3.0.9: {}
+
+  lodash.isnil@4.0.0: {}
+
+  lodash.isplainobject@4.0.6: {}
+
+  lodash.isundefined@3.0.1: {}
+
   lodash.memoize@4.1.2: {}
 
   lodash.merge@4.6.2: {}
@@ -19488,6 +19802,8 @@ snapshots:
 
   lodash.truncate@4.4.2: {}
 
+  lodash.union@4.6.0: {}
+
   lodash.uniq@4.5.0: {}
 
   lodash.zip@4.2.0: {}
@@ -21917,6 +22233,15 @@ snapshots:
       react: 16.14.0
       scheduler: 0.19.1
 
+  react-dom@16.14.0(react@19.0.0):
+    dependencies:
+      loose-envify: 1.4.0
+      object-assign: 4.1.1
+      prop-types: 15.8.1
+      react: 19.0.0
+      scheduler: 0.19.1
+    optional: true
+
   react-dom@18.3.1(react@18.3.1):
     dependencies:
       loose-envify: 1.4.0
@@ -22094,6 +22419,10 @@ snapshots:
       string_decoder: 1.3.0
       util-deprecate: 1.0.2
 
+  readdir-glob@1.1.3:
+    dependencies:
+      minimatch: 5.1.6
+
   readdirp@2.2.1:
     dependencies:
       graceful-fs: 4.2.11
@@ -23360,6 +23689,14 @@ snapshots:
       to-buffer: 1.1.1
       xtend: 4.0.2
 
+  tar-stream@2.2.0:
+    dependencies:
+      bl: 4.1.0
+      end-of-stream: 1.4.4
+      fs-constants: 1.0.0
+      inherits: 2.0.4
+      readable-stream: 3.6.2
+
   temp-dir@2.0.0: {}
 
   tempy@0.6.0:
@@ -23500,6 +23837,8 @@ snapshots:
     dependencies:
       punycode: 2.3.1
 
+  traverse@0.3.9: {}
+
   trim-newlines@3.0.1: {}
 
   trim-repeated@1.0.0:
@@ -23765,6 +24104,19 @@ snapshots:
 
   unzip-response@2.0.1: {}
 
+  unzipper@0.10.14:
+    dependencies:
+      big-integer: 1.6.52
+      binary: 0.3.0
+      bluebird: 3.4.7
+      buffer-indexof-polyfill: 1.0.2
+      duplexer2: 0.1.4
+      fstream: 1.0.12
+      graceful-fs: 4.2.11
+      listenercount: 1.0.1
+      readable-stream: 2.3.8
+      setimmediate: 1.0.5
+
   upath@1.2.0: {}
 
   update-browserslist-db@1.1.3(browserslist@4.24.4):
@@ -24369,4 +24721,10 @@ snapshots:
 
   yocto-queue@0.1.0: {}
 
+  zip-stream@4.1.1:
+    dependencies:
+      archiver-utils: 3.0.4
+      compress-commons: 4.1.2
+      readable-stream: 3.6.2
+
   zwitch@1.0.5: {}