import { cloneDeep } from 'lodash' import { exportWordDocx, getBase64Sync } from './exportWord' import { baseURL } from './http' import { resJiLianFu } from './history' export enum EXPORT_WORD_ENUM { /** 借用藏品点交凭证 */ BORROW = 1, /** 藏品馆内提退凭单 */ HALL_PUT_BACK = 2, /** 分库藏品提退出入库记录单 */ SUB_PUT_BACK = 3, /** 分库藏品入库记录单 */ SUB_PUT_IN = 4, /** 入馆凭证 */ VOUCHER = 5, /** 藏品图片及相关数字化信息使用申请单 */ FORM_FOR_DIGITAL = 6, /** 藏品卡片 */ COLLECTION_CARD = 7 } type WallItem = { url: string width: number height: number img: string } const WORD_FILE_NAME_MAP = { [EXPORT_WORD_ENUM.BORROW]: { fileName: '义乌市博物馆借用藏品点交凭证', templateName: '1.docx' }, [EXPORT_WORD_ENUM.HALL_PUT_BACK]: { fileName: '义乌市博物馆藏品馆内提退凭单', templateName: '2.docx', // 每个字段单行最大字符数 perLine: { num: 9, name: 24, pcsUnit: 4, rtf: 6 }, row: { // 首页最大行数 maxRowFirstPage: 1, // 尾页最大行数 maxRowLastPage: 1, // 每页最大行数 maxRowPage: 1, // 首页单行最大字符行数 maxFirstCharLine: 20, // 尾页单行最大字符行数 maxLastCharLine: 18, // 每页最大字符行数 maxCharLine: 26, // 表格每行最小字符行数 minRowCharLine: 2 } }, [EXPORT_WORD_ENUM.SUB_PUT_BACK]: { fileName: '义乌市博物馆分库藏品提退出入库记录单', templateName: '3.docx' }, [EXPORT_WORD_ENUM.SUB_PUT_IN]: { fileName: '义乌市博物馆分库藏品入库记录单', templateName: '4.docx' }, [EXPORT_WORD_ENUM.VOUCHER]: { fileName: '义乌市博物馆入馆凭证', templateName: '5.docx' }, [EXPORT_WORD_ENUM.FORM_FOR_DIGITAL]: { fileName: '义乌市博物馆藏品图片及相关数字化信息使用申请单', templateName: '6.docx' }, [EXPORT_WORD_ENUM.COLLECTION_CARD]: { fileName: '义乌市博物馆藏品卡片', templateName: '7.docx' } } 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 } const getImageDimensions = (url: string): Promise<{ width: number; height: number }> => { return new Promise(resolve => { const img = new Image() img.onload = () => { resolve({ width: img.width, height: img.height }) } img.onerror = () => { resolve({ width: 100, height: 100 }) } img.src = url }) } /** * 将一维数组根据图片宽高比转成三维数组 */ const arrangeImages = async (images: { thumb: string }[]) => { const MAX_WALL_WIDTH = 520 const MAX_WALL_HEIGHT = 750 const MAX_ROWS_PER_WALL = 3 const walls: Array>> = [] let currentWall: Array> = [] let currentRow: Array = [] let currentRowHeight = 0 let currentX = 0 const imagesWithDimensions = await Promise.all( images.map(async img => { const url = baseURL + img.thumb const dimensions = await getImageDimensions(url) return { url, originalWidth: dimensions.width, originalHeight: dimensions.height } }) ) for (const img of imagesWithDimensions) { const maxRowHeight = MAX_WALL_HEIGHT / MAX_ROWS_PER_WALL const aspectRatio = img.originalWidth / img.originalHeight let scaledWidth, scaledHeight scaledHeight = maxRowHeight scaledWidth = maxRowHeight * aspectRatio if (scaledWidth > MAX_WALL_WIDTH) { scaledWidth = MAX_WALL_WIDTH scaledHeight = MAX_WALL_WIDTH / aspectRatio } // TOFIX: 暂时无法解决图片并列渲染问题 if (currentX + scaledWidth <= MAX_WALL_WIDTH && false) { currentRow.push({ url: img.url, width: scaledWidth, height: scaledHeight, img: await getBase64Sync(img.url) }) currentX += scaledWidth currentRowHeight = Math.max(currentRowHeight, scaledHeight) } else { if (currentRow.length > 0) { // eslint-disable-next-line no-loop-func currentRow.forEach(item => { item.height = currentRowHeight }) currentWall.push(currentRow) } if (currentWall.length >= MAX_ROWS_PER_WALL) { walls.push(currentWall) currentWall = [] } currentRow = [ { url: img.url, width: scaledWidth, height: scaledHeight, img: await getBase64Sync(img.url) } ] currentX = scaledWidth currentRowHeight = scaledHeight } } if (currentRow.length > 0) { currentWall.push(currentRow) } if (currentWall.length > 0) { walls.push(currentWall) } return walls } const getEffectiveLength = (str: string) => { let length = 0 for (const char of str) { // 全角字符(包括中文、全角符号等)的 Unicode 范围判断 if (char.match(/[\u4e00-\u9fa5\u3000-\u303f\uff00-\uffef]/)) { length += 2 // 全角算2字符 } else { length += 1 // 半角算1字符 } } return length } const calculateRowCharLines = (row: Record, perLine: Record) => { let maxCharLines = 0 for (const [field, text] of Object.entries(row)) { if (!perLine[field]) continue const fieldLength = getEffectiveLength(text) const fieldCharLines = Math.ceil(fieldLength / perLine[field]) maxCharLines = Math.max(maxCharLines, fieldCharLines) } return maxCharLines || 1 } /** * 计算表格数据的分页情况 */ const calcTablePages = (rows: any[], config: (typeof WORD_FILE_NAME_MAP)[2]) => { 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 += rowCharLines 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) => { let temp = cloneDeep(data) let page = 1 const date = new Date() const item = WORD_FILE_NAME_MAP[type] temp.sizeUnit && (temp.sizeUnit = resJiLianFu(temp.sizeUnit)) temp.qualityUnit && (temp.qualityUnit = resJiLianFu(temp.qualityUnit)) temp.dictAge && (temp.dictAge = resJiLianFu(temp.dictAge)) if (Array.isArray(temp.goods) && temp.goods.length) { for (let i = 0; i < temp.goods.length; i++) { const good = temp.goods[i] good.index = i + 1 good.rtf && (good.rtf = JSON.parse(good.rtf).txtArr[0].txt) good.thumb && (good.thumb = await getBase64Sync(baseURL + good.thumb)) good.dictAge && (good.dictAge = resJiLianFu(good.dictAge)) good.pcsUnit && (good.pcsUnit = resJiLianFu(good.pcsUnit)) good.dictTexture3 && (good.dictTexture = resJiLianFu(good.dictTexture3)) good.dictTorn && (good.dictTorn = resJiLianFu(good.dictTorn)) good.source && (good.source = resJiLianFu(good.source)) } } else { temp.goods = [] } // @ts-ignore if (item.perLine) { // @ts-ignore page = calcTablePages(temp.goods, item) } switch (type) { case EXPORT_WORD_ENUM.BORROW: temp = { ...temp, group: numberToChinese(temp.goods.length), num: numberToChinese( temp.goods.reduce((sum: number, item: any) => sum + (item.pcs || 0), 0) ), page: numberToChinese(page) } break case EXPORT_WORD_ENUM.HALL_PUT_BACK: temp = { ...temp, year: date.getFullYear(), group: numberToChinese(temp.goods.length), page: numberToChinese(page) } break case EXPORT_WORD_ENUM.VOUCHER: temp = { ...temp, year: date.getFullYear(), month: date.getMonth() + 1, day: date.getDay(), group: numberToChinese(temp.goods.length), page: numberToChinese(page) } break case EXPORT_WORD_ENUM.FORM_FOR_DIGITAL: temp = { ...temp, year: date.getFullYear(), month: date.getMonth() + 1, day: date.getDay() } break case EXPORT_WORD_ENUM.COLLECTION_CARD: temp.imagePages = await arrangeImages(temp.imagePages) break } exportWordDocx(`/templates/${item.templateName}`, temp, item.fileName) }