Bladeren bron

Merge branch 'master' of http://192.168.0.115:3000/shaogen1995/YW_Goods into master

shaogen1995 2 maanden geleden
bovenliggende
commit
a90a179f91

BIN
public/templates/11.docx


BIN
public/templates/12.docx


BIN
public/templates/13.docx


BIN
public/templates/14.docx


BIN
public/templates/15.docx


BIN
public/templates/16.docx


+ 1 - 1
src/components/MyTable/index.tsx

@@ -213,7 +213,7 @@ const MyTable = forwardRef<MyTableMethods, MyTableProps>(
           datePicker: (item: any) => {
             return (
               <Form.Item noStyle name={`${item.id}-${v[2]}`}>
-                <DatePicker readOnly={readOnly} />
+                <DatePicker disabled={readOnly} />
               </Form.Item>
             )
           },

+ 4 - 0
src/pages/A3_ledger/C1ledger/index.tsx

@@ -290,6 +290,10 @@ function C1ledger() {
             批量导出
           </Button>
           &emsp;
+          <Button type='primary' onClick={deriveFu} hidden={antiqueSearch}>
+            导出藏品总账
+          </Button>
+          &emsp;
           <Button danger={advanced} onClick={() => advancedFu(!advanced)}>
             {advanced ? '收起' : ''}高级搜索
           </Button>

+ 1 - 1
src/pages/A_workbench/A3flow/index.tsx

@@ -132,7 +132,7 @@ function A3flow() {
   const handleExport = async () => {
     const res = await A3_APIList(
       {
-        ...formDataRef.current,
+        ...filterEmptyStrings(formDataRef.current),
         pageNum: 1,
         pageSize: 99999
       },

+ 2 - 1
src/pages/A_workbench/A4voucher/constants.ts

@@ -7,7 +7,8 @@ export const DEFAULT_A4VOUCHER_PARAMS: IA4voucherParams = {
   pageSize: 10,
   goodNum: '',
   fileName: '',
-  moduleName: ''
+  moduleName: '',
+  date: []
 }
 
 export const A4VOUCHER_PARAM_ROWS: A4voucherSearchType[] = [

+ 5 - 3
src/pages/A_workbench/A4voucher/index.tsx

@@ -16,6 +16,7 @@ import { A4_APIDownload, A4_APIList } from '@/store/action/A4voucher'
 import { filterEmptyStrings } from '@/utils/objects'
 import { fileImgArr } from '@/store/action/layout'
 import { BUSINESS_DETAIL_PATH_MAP } from '../A3flow/data'
+import dayjs from 'dayjs'
 
 const { RangePicker } = DatePicker
 
@@ -65,7 +66,8 @@ function A4voucher() {
               <RangePicker
                 format='YYYY-MM-DD'
                 allowClear={true}
-                onChange={(e, dateStrings) => setFormData({ ...formData, [item.key]: dateStrings })}
+                value={formData[item.key] as undefined}
+                onChange={e => setFormData({ ...formData, [item.key]: e })}
               />
             ) : null}
           </div>
@@ -109,9 +111,9 @@ function A4voucher() {
     const { date, ...rest } = formDataRef.current
     if (Array.isArray(date) && date.length) {
       // @ts-ignore
-      rest.startTime = date[0]
+      rest.startTime = dayjs(date[0]).format('YYYY-MM-DD')
       // @ts-ignore
-      rest.endTime = date[1]
+      rest.endTime = dayjs(date[1]).format('YYYY-MM-DD')
     }
     dispatch(A4_APIList(filterEmptyStrings(rest)))
   }, [dispatch])

+ 16 - 7
src/pages/D_storeManage/D3staff/D3edit/index.tsx

@@ -26,6 +26,7 @@ import { OutsiderModal } from '../components/OutsiderModal'
 import { D3STAFF_OUTSIDER_TABLE_COLUMNS } from '../constants'
 import { D2_APIgetList } from '@/store/action/D2storSet'
 import dayjs from 'dayjs'
+import { EXPORT_WORD_ENUM } from '@/utils/exportTemplates'
 
 export const pageTitTxtObj = {
   1: '新增',
@@ -48,6 +49,7 @@ function C21edit() {
   const { list: storageIdArr } = useSelector((state: RootState) => state.D2storSet.tableInfo)
   const pageDisabled = ['3', '4'].includes(key)
   const [checkedOversider, setCheckedOversider] = useState<null | D3StaffOversiderType>(null)
+  const [curTab, setCurTab] = useState(SON_TYPE_NAME.OUT)
 
   const dispatch = useDispatch()
 
@@ -61,7 +63,6 @@ function C21edit() {
     if (res.code === 0) {
       setTopInfo({
         ...res.data,
-        sonTypeName: SON_TYPE_NAME.OUT,
         date: dayjs().format('YYYY-MM-DD')
       })
     }
@@ -72,9 +73,10 @@ function C21edit() {
     async (id2?: number) => {
       const res = await D3_APIgetInfo(id2 || id)
       if (res.code === 0) {
-        const { memberIds, ...rest } = res.data
-        setTopInfo(rest)
+        const { memberIds, sonTypeName } = res.data
+        setTopInfo(res.data)
         setOversiderList(JSON.parse(memberIds || '') || [])
+        setCurTab(sonTypeName)
       }
     },
     [id]
@@ -148,7 +150,8 @@ function C21edit() {
         const obj = {
           ...topInfo,
           rtf: JSON.stringify(rtf1.val || ''),
-          memberIds: JSON.stringify(oversiderList)
+          memberIds: JSON.stringify(oversiderList),
+          sonTypeName: curTab
         }
         // console.log(123, obj)
 
@@ -171,7 +174,7 @@ function C21edit() {
         }
       }
     },
-    [auditSta, checkDataFu, getInfoFu, oversiderList, topInfo]
+    [auditSta, checkDataFu, curTab, getInfoFu, oversiderList, topInfo]
   )
 
   // 查看的按钮创建-提交-撤回
@@ -246,7 +249,11 @@ function C21edit() {
           </Button>
         ) : null}
 
-        {EXbtnFu(topInfo)}
+        {EXbtnFu(topInfo, [
+          curTab === SON_TYPE_NAME.IN
+            ? EXPORT_WORD_ENUM.INSIDER_FORM
+            : EXPORT_WORD_ENUM.OUTSIDER_FORM
+        ])}
 
         {btnFlagFu2(topInfo)['删除'] ? (
           <MyPopconfirm
@@ -263,7 +270,7 @@ function C21edit() {
         <Button onClick={() => history.push('/staff')}>返回</Button>
       </>
     )
-  }, [delFu, lookBtnFu, lookJumpFu, topInfo])
+  }, [curTab, delFu, lookBtnFu, lookJumpFu, topInfo])
 
   // 申请记录
   const [auditsShow, setAuditsShow] = useState(false)
@@ -387,6 +394,7 @@ function C21edit() {
 
           <Tabs
             type='card'
+            activeKey={curTab}
             items={[
               {
                 key: SON_TYPE_NAME.OUT,
@@ -476,6 +484,7 @@ function C21edit() {
                 )
               }
             ]}
+            onChange={e => setCurTab(e as SON_TYPE_NAME)}
           />
         </div>
 

+ 3 - 1
src/pages/D_storeManage/D7check/types.ts

@@ -1,6 +1,8 @@
 import { FourTableType } from '@/pages/B_enterTibet/B1collect/type'
 
-export interface ID7CheckItem extends FourTableType {}
+export interface ID7CheckItem extends FourTableType {
+  sonNum?: string
+}
 
 export type D7CheckSearchType = {
   name: string

+ 2 - 1
src/pages/E_goodsStorage/E1accident/E1edit/index.tsx

@@ -28,6 +28,7 @@ import { EXbtnFu } from '@/utils/EXBtn'
 import ZflowTable from '@/components/ZflowTable'
 import ZupFileTable from '@/components/ZupFileTable'
 import { API_goodsInfo } from '@/store/action/C1ledger'
+import { EXPORT_WORD_ENUM } from '@/utils/exportTemplates'
 function E1edit() {
   const { key, id } = useParams<any>()
   // key:1 新增 2编辑 3审批 4查看
@@ -321,7 +322,7 @@ function E1edit() {
           </Button>
         ) : null}
 
-        {EXbtnFu(topInfo)}
+        {EXbtnFu(topInfo, [EXPORT_WORD_ENUM.ACCIDENT_HANDLING_CERTIFICATE])}
 
         {btnFlagFu2(topInfo)['删除'] ? (
           <MyPopconfirm

+ 2 - 1
src/pages/E_goodsStorage/E3actuality/E3edit/index.tsx

@@ -32,6 +32,7 @@ import ZRichTexts from '@/components/ZRichTexts'
 import { useDispatch, useSelector } from 'react-redux'
 import { RootState } from '@/store'
 import { D7_APIList } from '@/store/action/D7check'
+import { EXPORT_WORD_ENUM } from '@/utils/exportTemplates'
 function E3edit() {
   // 相关盘点单下拉数据
   // 从仓库拿数据
@@ -318,7 +319,7 @@ function E3edit() {
           </Button>
         ) : null}
 
-        {EXbtnFu(topInfo)}
+        {EXbtnFu(topInfo, [EXPORT_WORD_ENUM.COLLECTION_CURRENT_STATUS])}
 
         {btnFlagFu2(topInfo)['删除'] ? (
           <MyPopconfirm

+ 2 - 1
src/pages/E_goodsStorage/E4repair/E4edit/index.tsx

@@ -32,6 +32,7 @@ import Y1cathet from '@/pages/Y_goodsDetails/Y1cathet'
 import ZGaddNow from '@/components/ZGaddNow'
 import ZupFileTable from '@/components/ZupFileTable'
 import { API_goodsInfo } from '@/store/action/C1ledger'
+import { EXPORT_WORD_ENUM } from '@/utils/exportTemplates'
 
 const tableArrTemp = [
   { name: '损坏情况', key: 'txt1' },
@@ -415,7 +416,7 @@ function E4edit() {
           </Button>
         ) : null}
 
-        {EXbtnFu(topInfo)}
+        {EXbtnFu(topInfo, [EXPORT_WORD_ENUM.RELIC_REPAIR_LIST])}
 
         {btnFlagFu2(topInfo)['删除'] ? (
           <MyPopconfirm

+ 3 - 1
src/pages/F_exhibition/F1exhibition/F1edit/index.tsx

@@ -27,6 +27,7 @@ import { pageTitTxtObj } from '@/pages/D_storeManage/D4impStor/D4edit'
 import { C1GoodType } from '@/pages/A3_ledger/C1ledger/type'
 import { areAllCheckersFilled } from '@/utils/objects'
 import dayjs from 'dayjs'
+import { EXPORT_WORD_ENUM } from '@/utils/exportTemplates'
 
 function F1edit() {
   const { key, id } = useParams<any>()
@@ -220,7 +221,7 @@ function F1edit() {
           </Button>
         ) : null}
 
-        {EXbtnFu(topInfo)}
+        {EXbtnFu(topInfo, [EXPORT_WORD_ENUM.REGISTER_LEDGER])}
 
         {btnFlagFu2(topInfo)['删除'] ? (
           <MyPopconfirm
@@ -505,6 +506,7 @@ function F1edit() {
             columnsTemp={F1_GOOD_COLUMNS}
             pagingInfo={false}
             lastBtn={tableLastBtn}
+            readOnly={pageDisabled}
           />
         </div>
       </div>

+ 244 - 74
src/utils/exportTemplates.ts

@@ -5,12 +5,15 @@ import { baseURL } from './http'
 import { resJiLianFu } from './history'
 import {
   arrangeImages,
-  calculateRowCharLines,
+  calcTablePages,
+  getExcelColumnLetter,
+  ITEMPLATE,
   numberToChinese,
   removeHtmlTags
 } from './exportWordUtils'
 import { myTableTransferSize } from '@/components/MyTable'
 import dayjs from 'dayjs'
+import { D7CEHCK_COLLECTION_RESULT_OPTIONS } from '@/pages/D_storeManage/D7check/constants'
 
 export enum EXPORT_WORD_ENUM {
   /** 借用藏品点交凭证 */
@@ -27,7 +30,6 @@ export enum EXPORT_WORD_ENUM {
   FORM_FOR_DIGITAL = 6,
   /** 藏品档案 */
   COLLECTION_ARCHIVES = 7,
-
   /** 拟征集藏品清单 */
   COLLECTION_LIST = 8,
   /** 文物鉴定书 */
@@ -41,18 +43,64 @@ export enum EXPORT_WORD_ENUM {
   /** 藏品移库 */
   COLLECTION_RELOCATION = 13,
   /** 藏品盘点记录单 */
-  COLLECTION_INVENTORY = 14
-}
-
-export interface ITEMPLATE {
-  fileName: string
-  templateName?: string
-  options?: Record<string, any>
-  perLine?: Record<string, number>
-  row?: Record<string, number>
+  COLLECTION_INVENTORY = 14,
+  /** 馆内人员进库申请单 */
+  INSIDER_FORM = 15,
+  /** 文物库房外来人员出入库审批表 */
+  OUTSIDER_FORM = 16,
+  /** 文物修复单 */
+  RELIC_REPAIR_LIST = 17,
+  /** 事故处理凭证 */
+  ACCIDENT_HANDLING_CERTIFICATE = 18,
+  /** 藏品现状登记 */
+  COLLECTION_CURRENT_STATUS = 19,
+  /** 馆内展览借用藏品登记台账 */
+  REGISTER_LEDGER = 20
 }
 
 export const EXPORT_TEMPLATE_MAP: Record<EXPORT_WORD_ENUM, ITEMPLATE> = {
+  [EXPORT_WORD_ENUM.COLLECTION_CURRENT_STATUS]: {
+    fileName: '藏品现状登记',
+    templateName: '16.docx'
+  },
+  [EXPORT_WORD_ENUM.ACCIDENT_HANDLING_CERTIFICATE]: {
+    fileName: '藏品事故处理凭证',
+    templateName: '15.docx'
+  },
+  [EXPORT_WORD_ENUM.RELIC_REPAIR_LIST]: {
+    fileName: '义乌市博物馆文物修复单',
+    templateName: '14.docx',
+    // 每个字段单行最大字符数
+    perLine: {
+      name: 9,
+      dictLevel: 24,
+      txt2: 4
+    },
+    row: {
+      // 首页最大行数
+      maxRowFirstPage: 3,
+      // 尾页最大行数
+      maxRowLastPage: 6,
+      // 每页最大行数
+      maxRowPage: 8,
+      // 首页单行最大字符行数
+      maxFirstCharLine: 20,
+      // 尾页单行最大字符行数
+      maxLastCharLine: 18,
+      // 每页最大字符行数
+      maxCharLine: 26,
+      // 表格每行最小字符行数
+      minRowCharLine: 3
+    }
+  },
+  [EXPORT_WORD_ENUM.OUTSIDER_FORM]: {
+    fileName: '文物库房外来人员出入库审批表',
+    templateName: '13.docx'
+  },
+  [EXPORT_WORD_ENUM.INSIDER_FORM]: {
+    fileName: '馆内人员进库申请单',
+    templateName: '12.docx'
+  },
   [EXPORT_WORD_ENUM.COLLECTION_INVENTORY]: {
     fileName: '义乌市博物馆分库藏品盘点记录单',
     templateName: '11.docx'
@@ -93,6 +141,49 @@ export const EXPORT_TEMPLATE_MAP: Record<EXPORT_WORD_ENUM, ITEMPLATE> = {
       columnWidths: [10, 10, 10, 15, 10, 15, 15, 12, 15, 15, 15]
     }
   },
+  [EXPORT_WORD_ENUM.REGISTER_LEDGER]: {
+    fileName: '义乌市博物馆馆内展览借用藏品登记台账',
+    options: {
+      sheetHeader: [
+        '序号',
+        '年度',
+        '借用部门',
+        '展览名称',
+        '借用文物 实际数量(合计)',
+        ['借用藏品基本情况', '藏品总登记号', '藏品名称'],
+        '出借时间',
+        ['出借经手人', '甲方', '乙方'],
+        '藏品提用审批单号',
+        '馆内提退凭证号',
+        '预计归还日期',
+        '归还时间',
+        ['归还经手人', '甲方', '乙方'],
+        '记录人',
+        '备注'
+      ],
+      sheetFilter: [
+        'index',
+        'year',
+        'sonUnit',
+        'typeName',
+        'pcs',
+        'num',
+        'name',
+        'lendDate',
+        'lenderA',
+        'lenderb',
+        'sonNum',
+        'sonNum2',
+        'expectedReturnDate',
+        'returnDate',
+        'handlerA',
+        'handlerB',
+        'recorder',
+        'remark'
+      ],
+      columnWidths: [5, 5, 12, 15, 12, 10, 15, 12, 10, 10, 15, 15, 15, 15, 10, 10, 10, 15]
+    }
+  },
   [EXPORT_WORD_ENUM.CERTIFICATE]: {
     fileName: '义乌市博物馆馆藏文物鉴定书',
     options: {
@@ -221,68 +312,6 @@ export const EXPORT_TEMPLATE_MAP: Record<EXPORT_WORD_ENUM, ITEMPLATE> = {
 }
 
 /**
- * 计算表格数据的分页情况
- */
-const calcTablePages = (rows: any[], config: Required<ITEMPLATE>) => {
-  if (rows.length === 0) return 0
-
-  let totalPages = 0
-  let currentPageRows: any[] = []
-  let currentPageCharLines = 0
-  let isFirstPage = true
-  let isLastPage = false
-
-  for (let i = 0; i < rows.length; i++) {
-    const row = rows[i]
-    const rowCharLines = calculateRowCharLines(row, config.perLine)
-
-    currentPageCharLines += Math.max(rowCharLines, config.row.minRowCharLine)
-    currentPageRows.push(row)
-
-    // 判断是否超出首页
-    if (isFirstPage && currentPageCharLines > config.row.maxFirstCharLine) {
-      isFirstPage = false
-      totalPages++
-      currentPageCharLines -= config.row.maxFirstCharLine
-
-      // 如果剩下行数还超出尾页最大行数,也先预处理
-      if (currentPageCharLines > config.row.maxLastCharLine) {
-        totalPages++
-        currentPageCharLines -= config.row.maxLastCharLine
-      }
-    }
-
-    // 确定当前页的限制
-    const maxRows = isFirstPage
-      ? config.row.maxRowFirstPage
-      : isLastPage
-      ? config.row.maxRowLastPage
-      : config.row.maxRowPage
-
-    const maxCharLines = isFirstPage
-      ? config.row.maxFirstCharLine
-      : isLastPage
-      ? config.row.maxLastCharLine
-      : config.row.maxCharLine
-
-    // 检查是否换页
-    const isPageFull = currentPageRows.length >= maxRows || currentPageCharLines >= maxCharLines
-
-    if (isPageFull || i === rows.length - 1) {
-      totalPages +=
-        Math.floor(currentPageCharLines / maxCharLines) +
-        (currentPageCharLines % maxCharLines > 0 ? 1 : 0)
-      currentPageRows = []
-      currentPageCharLines = 0
-      isFirstPage = false
-      isLastPage = i === rows.length - 1
-    }
-  }
-
-  return totalPages
-}
-
-/**
  * 根据业务类型导出数据
  */
 export const exportWordHandler = async (type: EXPORT_WORD_ENUM, data: Record<any, any>) => {
@@ -364,6 +393,22 @@ export const exportWordHandler = async (type: EXPORT_WORD_ENUM, data: Record<any
         day: date.format('DD')
       }
       break
+    case EXPORT_WORD_ENUM.COLLECTION_INVENTORY:
+      temp = {
+        ...temp,
+        year: date.format('YYYY'),
+        month: date.format('MM'),
+        day: date.format('DD')
+      }
+      temp.goods.forEach((i: any) => {
+        i.checker = i.cusForm[`${i.id}-checker`]
+        i.remark = i.cusForm[`${i.id}-remark`]
+        i.cusForm[`${i.id}-statusCheck`] &&
+          (i.statusCheck = D7CEHCK_COLLECTION_RESULT_OPTIONS.find(
+            ii => ii.value === i.cusForm[`${i.id}-statusCheck`]
+          )?.label)
+      })
+      break
     case EXPORT_WORD_ENUM.VOUCHER:
     case EXPORT_WORD_ENUM.STORAGE_VOUCHER:
       temp = {
@@ -402,6 +447,15 @@ export const exportWordHandler = async (type: EXPORT_WORD_ENUM, data: Record<any
         date: dayjs(temp.date).format('YYYY年MM月DD日')
       }
       break
+    case EXPORT_WORD_ENUM.ACCIDENT_HANDLING_CERTIFICATE:
+      temp = {
+        ...temp,
+        year: date.format('YYYY'),
+        rtf: removeHtmlTags(JSON.parse(temp.rtf).txtArr[0].txt),
+        authInfoRtf: removeHtmlTags(JSON.parse(temp.authInfoRtf).txtArr[0].txt),
+        authResultRtf: removeHtmlTags(JSON.parse(temp.authResultRtf).txtArr[0].txt)
+      }
+      break
     case EXPORT_WORD_ENUM.COLLECTION_CARD:
       temp = {
         ...temp,
@@ -425,6 +479,22 @@ export const exportWordHandler = async (type: EXPORT_WORD_ENUM, data: Record<any
     case EXPORT_WORD_ENUM.COLLECTION_ARCHIVES:
       temp.imagePages = await arrangeImages(temp.imagePages)
       break
+    case EXPORT_WORD_ENUM.OUTSIDER_FORM:
+      const memberss = JSON.parse(temp.memberIds)
+      temp = {
+        ...temp,
+        time: dayjs(temp.date).format('HH:mm:ss'),
+        date: dayjs(temp.date).format('YYYY-MM-DD'),
+        members: memberss.map((i: any) => i.name),
+        companys: memberss.map((i: any) => i.remark || '空'),
+        idCards: memberss.map((i: any) => i.papers || '空'),
+        phones: memberss.map((i: any) => i.phone || '空')
+      }
+      break
+    case EXPORT_WORD_ENUM.RELIC_REPAIR_LIST:
+    case EXPORT_WORD_ENUM.COLLECTION_CURRENT_STATUS:
+      temp.rtf = removeHtmlTags(JSON.parse(temp.rtf).txtArr[0].txt)
+      break
     case EXPORT_WORD_ENUM.COLLECTION_LIST:
       temp.goods.push({
         index: '合计',
@@ -674,10 +744,74 @@ export const exportWordHandler = async (type: EXPORT_WORD_ENUM, data: Record<any
         }
       }
       break
+    case EXPORT_WORD_ENUM.REGISTER_LEDGER:
+      const totalPcs = temp.goods.reduce((sum: number, i: any) => sum + (i._pcs || 0), 0)
+      temp.goods.forEach((i: any) => {
+        i.year = date.format('YYYY') + '年度'
+        i.sonUnit = temp.sonUnit
+        i.typeName = temp.typeName
+        i.lendDate = dayjs(i.cusForm[`${i.id}-lendDate`]).format('YYYY年MM月DD日')
+        i.lenderA = i.cusForm[`${i.id}-lenderA`]
+        i.lenderb = i.cusForm[`${i.id}-lenderb`]
+        i.sonNum = temp.sonNum
+        i.sonNum2 = temp.sonNum2
+        i.handlerA = i.cusForm[`${i.id}-handlerA`]
+        i.handlerB = i.cusForm[`${i.id}-handlerB`]
+        i.recorder = i.cusForm[`${i.id}-recorder`]
+        i.remark = i.cusForm[`${i.id}-remark`]
+        i.pcs = `${i.pcs},计${totalPcs}件`
+        const expectedReturnDate = i.cusForm[`${i.id}-expectedReturnDate`]
+        expectedReturnDate &&
+          (i.expectedReturnDate = dayjs(expectedReturnDate).format('YYYY年MM月DD日'))
+        const returnDate = i.cusForm[`${i.id}-returnDate`]
+        returnDate && (i.returnDate = dayjs(returnDate).format('YYYY年MM月DD日'))
+      })
+
+      excelHandler = worksheet => {
+        const borderStyle: ExcelJS.Border = {
+          style: 'thin',
+          color: { argb: 'FF000000' }
+        }
+
+        for (let row = 1; row <= temp.goods.length + 2; row++) {
+          for (let col = 1; col <= 18; 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
+            }
+          })
+        })
+      }
+      break
   }
 
   if (!item.templateName) {
     // 没有templateName则输出excel
+    let mergeHeadNum = 0
+    let headTemp: string[] = []
+    let fatherHeadTemp: {
+      title: string
+      position: string[]
+    }[] = []
+    // 表头需要占据的行数
+    const headRow = Math.max(
+      ...item.options?.sheetHeader.map((head: string | string[]) =>
+        Array.isArray(head) ? head.length - 1 : 1
+      )
+    )
     const workbook = new ExcelJS.Workbook()
     const worksheet = workbook.addWorksheet('Sheet1', {
       properties: { defaultRowHeight: 30 },
@@ -689,7 +823,43 @@ export const exportWordHandler = async (type: EXPORT_WORD_ENUM, data: Record<any
       }
     })
 
-    worksheet.addRow(item.options?.sheetHeader)
+    item.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++
+      } 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 (headRow > 1) {
+      // 合并表头列
+      headTemp.forEach((head, index) => {
+        const pos = `${getExcelColumnLetter(index)}1:${getExcelColumnLetter(index)}${headRow}`
+        const cell = worksheet.getCell(`${getExcelColumnLetter(index)}1`)
+        if (!cell.isMerged) {
+          worksheet.mergeCells(pos)
+          cell.value = head
+        }
+      })
+    }
+
     temp.goods.forEach((good: Record<string, any>, goodIndex: number) => {
       const _temp: string[] = []
       item.options?.sheetFilter.forEach((key: string, index: number) => {

+ 70 - 0
src/utils/exportWordUtils.ts

@@ -201,3 +201,73 @@ export const getExcelColumnLetter = (index: number) => {
 
   return result
 }
+
+export interface ITEMPLATE {
+  fileName: string
+  templateName?: string
+  options?: Record<string, any>
+  perLine?: Record<string, number>
+  row?: Record<string, number>
+}
+
+/**
+ * 计算表格数据的分页情况
+ */
+export const calcTablePages = (rows: any[], config: Required<ITEMPLATE>) => {
+  if (rows.length === 0) return 0
+
+  let totalPages = 0
+  let currentPageRows: any[] = []
+  let currentPageCharLines = 0
+  let isFirstPage = true
+  let isLastPage = false
+
+  for (let i = 0; i < rows.length; i++) {
+    const row = rows[i]
+    const rowCharLines = calculateRowCharLines(row, config.perLine)
+
+    currentPageCharLines += Math.max(rowCharLines, config.row.minRowCharLine)
+    currentPageRows.push(row)
+
+    // 判断是否超出首页
+    if (isFirstPage && currentPageCharLines > config.row.maxFirstCharLine) {
+      isFirstPage = false
+      totalPages++
+      currentPageCharLines -= config.row.maxFirstCharLine
+
+      // 如果剩下行数还超出尾页最大行数,也先预处理
+      if (currentPageCharLines > config.row.maxLastCharLine) {
+        totalPages++
+        currentPageCharLines -= config.row.maxLastCharLine
+      }
+    }
+
+    // 确定当前页的限制
+    const maxRows = isFirstPage
+      ? config.row.maxRowFirstPage
+      : isLastPage
+      ? config.row.maxRowLastPage
+      : config.row.maxRowPage
+
+    const maxCharLines = isFirstPage
+      ? config.row.maxFirstCharLine
+      : isLastPage
+      ? config.row.maxLastCharLine
+      : config.row.maxCharLine
+
+    // 检查是否换页
+    const isPageFull = currentPageRows.length >= maxRows || currentPageCharLines >= maxCharLines
+
+    if (isPageFull || i === rows.length - 1) {
+      totalPages +=
+        Math.floor(currentPageCharLines / maxCharLines) +
+        (currentPageCharLines % maxCharLines > 0 ? 1 : 0)
+      currentPageRows = []
+      currentPageCharLines = 0
+      isFirstPage = false
+      isLastPage = i === rows.length - 1
+    }
+  }
+
+  return totalPages
+}