exportWordUtils.ts 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  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. }