123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390 |
- 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<Array<Array<WallItem>>> = []
- let currentWall: Array<Array<WallItem>> = []
- let currentRow: Array<WallItem> = []
- 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<string, string>, perLine: Record<string, number>) => {
- 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<any, any>) => {
- 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)
- }
|