exportWordUtils.ts 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. import { getBase64Sync } from './exportWord'
  2. import { baseURL } from './http'
  3. type WallItem = {
  4. url: string
  5. width: number
  6. height: number
  7. img: string
  8. }
  9. export const numberToChinese = (num: number) => {
  10. if (isNaN(num)) {
  11. throw new Error('输入必须是一个有效的数字')
  12. }
  13. if (num < 0 || num > 9999) {
  14. throw new Error('输入数字超出范围 (0-9999)')
  15. }
  16. if (num === 0) {
  17. return '零'
  18. }
  19. const chineseNumbers = ['零', '壹', '贰', '叁', '肆', '伍', '陆', '柒', '捌', '玖']
  20. const chineseUnits = ['', '拾', '佰', '仟']
  21. let result = ''
  22. const numStr = num.toString()
  23. const length = numStr.length
  24. for (let i = 0; i < length; i++) {
  25. const digit = parseInt(numStr[i])
  26. const unit = length - i - 1
  27. if (digit !== 0) {
  28. result += chineseNumbers[digit] + chineseUnits[unit]
  29. } else {
  30. // 处理连续的零,只保留一个
  31. if (i < length - 1 && numStr[i + 1] !== '0') {
  32. result += chineseNumbers[digit]
  33. }
  34. }
  35. }
  36. // 处理10-19的情况,去掉开头的"壹"
  37. if (num >= 10 && num < 20) {
  38. result = result.replace('壹拾', '拾')
  39. }
  40. return result
  41. }
  42. export const getImageDimensions = (url: string): Promise<{ width: number; height: number }> => {
  43. return new Promise(resolve => {
  44. const img = new Image()
  45. img.onload = () => {
  46. resolve({ width: img.width, height: img.height })
  47. }
  48. img.onerror = () => {
  49. resolve({ width: 100, height: 100 })
  50. }
  51. img.src = url
  52. })
  53. }
  54. /**
  55. * 将一维数组根据图片宽高比转成三维数组
  56. */
  57. export const arrangeImages = async (images: { thumb: string }[]) => {
  58. const MAX_WALL_WIDTH = 520
  59. const MAX_WALL_HEIGHT = 750
  60. const MAX_ROWS_PER_WALL = 3
  61. const walls: Array<Array<Array<WallItem>>> = []
  62. let currentWall: Array<Array<WallItem>> = []
  63. let currentRow: Array<WallItem> = []
  64. let currentRowHeight = 0
  65. let currentX = 0
  66. const imagesWithDimensions = await Promise.all(
  67. images.map(async img => {
  68. const url = baseURL + img.thumb
  69. const dimensions = await getImageDimensions(url)
  70. return {
  71. url,
  72. originalWidth: dimensions.width,
  73. originalHeight: dimensions.height
  74. }
  75. })
  76. )
  77. for (const img of imagesWithDimensions) {
  78. const maxRowHeight = MAX_WALL_HEIGHT / MAX_ROWS_PER_WALL
  79. const aspectRatio = img.originalWidth / img.originalHeight
  80. let scaledWidth, scaledHeight
  81. scaledHeight = maxRowHeight
  82. scaledWidth = maxRowHeight * aspectRatio
  83. if (scaledWidth > MAX_WALL_WIDTH) {
  84. scaledWidth = MAX_WALL_WIDTH
  85. scaledHeight = MAX_WALL_WIDTH / aspectRatio
  86. }
  87. // TOFIX: 暂时无法解决图片并列渲染问题
  88. if (currentX + scaledWidth <= MAX_WALL_WIDTH && false) {
  89. currentRow.push({
  90. url: img.url,
  91. width: scaledWidth,
  92. height: scaledHeight,
  93. img: await getBase64Sync(img.url)
  94. })
  95. currentX += scaledWidth
  96. currentRowHeight = Math.max(currentRowHeight, scaledHeight)
  97. } else {
  98. if (currentRow.length > 0) {
  99. // eslint-disable-next-line no-loop-func
  100. currentRow.forEach(item => {
  101. item.height = currentRowHeight
  102. })
  103. currentWall.push(currentRow)
  104. }
  105. if (currentWall.length >= MAX_ROWS_PER_WALL) {
  106. walls.push(currentWall)
  107. currentWall = []
  108. }
  109. currentRow = [
  110. {
  111. url: img.url,
  112. width: scaledWidth,
  113. height: scaledHeight,
  114. img: await getBase64Sync(img.url)
  115. }
  116. ]
  117. currentX = scaledWidth
  118. currentRowHeight = scaledHeight
  119. }
  120. }
  121. if (currentRow.length > 0) {
  122. currentWall.push(currentRow)
  123. }
  124. if (currentWall.length > 0) {
  125. walls.push(currentWall)
  126. }
  127. return walls
  128. }
  129. export const getEffectiveLength = (str: string) => {
  130. let length = 0
  131. for (const char of str) {
  132. // 全角字符(包括中文、全角符号等)的 Unicode 范围判断
  133. if (char.match(/[\u4e00-\u9fa5\u3000-\u303f\uff00-\uffef]/)) {
  134. length += 2 // 全角算2字符
  135. } else {
  136. length += 1 // 半角算1字符
  137. }
  138. }
  139. return length
  140. }
  141. export const calculateRowCharLines = (
  142. row: Record<string, string>,
  143. perLine: Record<string, number>
  144. ) => {
  145. let maxCharLines = 0
  146. for (const [field, text] of Object.entries(row)) {
  147. if (!perLine[field]) continue
  148. const fieldLength = getEffectiveLength(text)
  149. const fieldCharLines = Math.ceil(fieldLength / perLine[field])
  150. maxCharLines = Math.max(maxCharLines, fieldCharLines)
  151. }
  152. return maxCharLines || 1
  153. }
  154. export const removeHtmlTags = (html: string) => {
  155. return html
  156. .replace(/<[^>]*>/g, '') // 移除HTML标签
  157. .replace(/\s+/g, ' ') // 合并多个空格
  158. .replace(/&nbsp;/g, ' ') // 替换HTML空格实体
  159. .replace(/&amp;/g, '&') // 替换HTML & 实体
  160. .replace(/&lt;/g, '<') // 替换HTML < 实体
  161. .replace(/&gt;/g, '>') // 替换HTML > 实体
  162. .replace(/&quot;/g, '"') // 替换HTML " 实体
  163. .replace(/&apos;/g, "'") // 替换HTML ' 实体
  164. .trim() // 去除首尾空格
  165. }
  166. export const getExcelColumnLetter = (index: number) => {
  167. let result = ''
  168. let remaining = index
  169. do {
  170. result = String.fromCharCode(65 + (remaining % 26)) + result
  171. remaining = Math.floor(remaining / 26) - 1
  172. } while (remaining >= 0)
  173. return result
  174. }
  175. export interface ITEMPLATE {
  176. fileName: string
  177. templateName?: string
  178. options?: Record<string, any>
  179. perLine?: Record<string, number>
  180. row?: Record<string, number>
  181. }
  182. /**
  183. * 计算表格数据的分页情况
  184. */
  185. export const calcTablePages = (rows: any[], config: Required<ITEMPLATE>) => {
  186. if (rows.length === 0) return 0
  187. let totalPages = 0
  188. let currentPageRows: any[] = []
  189. let currentPageCharLines = 0
  190. let isFirstPage = true
  191. let isLastPage = false
  192. for (let i = 0; i < rows.length; i++) {
  193. const row = rows[i]
  194. const rowCharLines = calculateRowCharLines(row, config.perLine)
  195. currentPageCharLines += Math.max(rowCharLines, config.row.minRowCharLine)
  196. currentPageRows.push(row)
  197. // 判断是否超出首页
  198. if (isFirstPage && currentPageCharLines > config.row.maxFirstCharLine) {
  199. isFirstPage = false
  200. totalPages++
  201. currentPageCharLines -= config.row.maxFirstCharLine
  202. // 如果剩下行数还超出尾页最大行数,也先预处理
  203. if (currentPageCharLines > config.row.maxLastCharLine) {
  204. totalPages++
  205. currentPageCharLines -= config.row.maxLastCharLine
  206. }
  207. }
  208. // 确定当前页的限制
  209. const maxRows = isFirstPage
  210. ? config.row.maxRowFirstPage
  211. : isLastPage
  212. ? config.row.maxRowLastPage
  213. : config.row.maxRowPage
  214. const maxCharLines = isFirstPage
  215. ? config.row.maxFirstCharLine
  216. : isLastPage
  217. ? config.row.maxLastCharLine
  218. : config.row.maxCharLine
  219. // 检查是否换页
  220. const isPageFull = currentPageRows.length >= maxRows || currentPageCharLines >= maxCharLines
  221. if (isPageFull || i === rows.length - 1) {
  222. totalPages +=
  223. Math.floor(currentPageCharLines / maxCharLines) +
  224. (currentPageCharLines % maxCharLines > 0 ? 1 : 0)
  225. currentPageRows = []
  226. currentPageCharLines = 0
  227. isFirstPage = false
  228. isLastPage = i === rows.length - 1
  229. }
  230. }
  231. return totalPages
  232. }