exportWordUtils.ts 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  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. if (!Array.isArray(images)) return []
  59. const MAX_WALL_WIDTH = 520
  60. const MAX_WALL_HEIGHT = 750
  61. const MAX_ROWS_PER_WALL = 3
  62. const walls: Array<Array<Array<WallItem>>> = []
  63. let currentWall: Array<Array<WallItem>> = []
  64. let currentRow: Array<WallItem> = []
  65. let currentRowHeight = 0
  66. let currentX = 0
  67. const imagesWithDimensions = await Promise.all(
  68. images.map(async img => {
  69. const url = baseURL + img.thumb
  70. const dimensions = await getImageDimensions(url)
  71. return {
  72. url,
  73. originalWidth: dimensions.width,
  74. originalHeight: dimensions.height
  75. }
  76. })
  77. )
  78. for (const img of imagesWithDimensions) {
  79. const maxRowHeight = MAX_WALL_HEIGHT / MAX_ROWS_PER_WALL
  80. const aspectRatio = img.originalWidth / img.originalHeight
  81. let scaledWidth, scaledHeight
  82. scaledHeight = maxRowHeight
  83. scaledWidth = maxRowHeight * aspectRatio
  84. if (scaledWidth > MAX_WALL_WIDTH) {
  85. scaledWidth = MAX_WALL_WIDTH
  86. scaledHeight = MAX_WALL_WIDTH / aspectRatio
  87. }
  88. // TOFIX: 暂时无法解决图片并列渲染问题
  89. if (currentX + scaledWidth <= MAX_WALL_WIDTH && false) {
  90. currentRow.push({
  91. url: img.url,
  92. width: scaledWidth,
  93. height: scaledHeight,
  94. img: await getBase64Sync(img.url)
  95. })
  96. currentX += scaledWidth
  97. currentRowHeight = Math.max(currentRowHeight, scaledHeight)
  98. } else {
  99. if (currentRow.length > 0) {
  100. // eslint-disable-next-line no-loop-func
  101. currentRow.forEach(item => {
  102. item.height = currentRowHeight
  103. })
  104. currentWall.push(currentRow)
  105. }
  106. if (currentWall.length >= MAX_ROWS_PER_WALL) {
  107. walls.push(currentWall)
  108. currentWall = []
  109. }
  110. currentRow = [
  111. {
  112. url: img.url,
  113. width: scaledWidth,
  114. height: scaledHeight,
  115. img: await getBase64Sync(img.url)
  116. }
  117. ]
  118. currentX = scaledWidth
  119. currentRowHeight = scaledHeight
  120. }
  121. }
  122. if (currentRow.length > 0) {
  123. currentWall.push(currentRow)
  124. }
  125. if (currentWall.length > 0) {
  126. walls.push(currentWall)
  127. }
  128. return walls
  129. }
  130. export const getEffectiveLength = (str: string) => {
  131. let length = 0
  132. for (const char of str) {
  133. // 全角字符(包括中文、全角符号等)的 Unicode 范围判断
  134. if (char.match(/[\u4e00-\u9fa5\u3000-\u303f\uff00-\uffef]/)) {
  135. length += 2 // 全角算2字符
  136. } else {
  137. length += 1 // 半角算1字符
  138. }
  139. }
  140. return length
  141. }
  142. export const calculateRowCharLines = (
  143. row: Record<string, string>,
  144. perLine: Record<string, number>
  145. ) => {
  146. let maxCharLines = 0
  147. for (const [field, text] of Object.entries(row)) {
  148. if (!perLine[field]) continue
  149. const fieldLength = getEffectiveLength(text)
  150. const fieldCharLines = Math.ceil(fieldLength / perLine[field])
  151. maxCharLines = Math.max(maxCharLines, fieldCharLines)
  152. }
  153. return maxCharLines || 1
  154. }
  155. export const removeHtmlTags = (html: string) => {
  156. return html
  157. .replace(/<[^>]*>/g, '') // 移除HTML标签
  158. .replace(/\s+/g, ' ') // 合并多个空格
  159. .replace(/&nbsp;/g, ' ') // 替换HTML空格实体
  160. .replace(/&amp;/g, '&') // 替换HTML & 实体
  161. .replace(/&lt;/g, '<') // 替换HTML < 实体
  162. .replace(/&gt;/g, '>') // 替换HTML > 实体
  163. .replace(/&quot;/g, '"') // 替换HTML " 实体
  164. .replace(/&apos;/g, "'") // 替换HTML ' 实体
  165. .trim() // 去除首尾空格
  166. }
  167. export const getExcelColumnLetter = (index: number) => {
  168. let result = ''
  169. let remaining = index
  170. do {
  171. result = String.fromCharCode(65 + (remaining % 26)) + result
  172. remaining = Math.floor(remaining / 26) - 1
  173. } while (remaining >= 0)
  174. return result
  175. }
  176. export interface ITEMPLATE {
  177. fileName: string
  178. templateName?: string
  179. options?: Record<string, any>
  180. perLine?: Record<string, number>
  181. row?: Record<string, number>
  182. }
  183. /**
  184. * 计算表格数据的分页情况
  185. */
  186. export const calcTablePages = (rows: any[], config: Required<ITEMPLATE>) => {
  187. if (rows.length === 0) return 0
  188. let totalPages = 0
  189. let currentPageRows: any[] = []
  190. let currentPageCharLines = 0
  191. let isFirstPage = true
  192. let isLastPage = false
  193. for (let i = 0; i < rows.length; i++) {
  194. const row = rows[i]
  195. const rowCharLines = calculateRowCharLines(row, config.perLine)
  196. currentPageCharLines += Math.max(rowCharLines, config.row.minRowCharLine)
  197. currentPageRows.push(row)
  198. // 判断是否超出首页
  199. if (isFirstPage && currentPageCharLines > config.row.maxFirstCharLine) {
  200. isFirstPage = false
  201. totalPages++
  202. currentPageCharLines -= config.row.maxFirstCharLine
  203. // 如果剩下行数还超出尾页最大行数,也先预处理
  204. if (currentPageCharLines > config.row.maxLastCharLine) {
  205. totalPages++
  206. currentPageCharLines -= config.row.maxLastCharLine
  207. }
  208. }
  209. // 确定当前页的限制
  210. const maxRows = isFirstPage
  211. ? config.row.maxRowFirstPage
  212. : isLastPage
  213. ? config.row.maxRowLastPage
  214. : config.row.maxRowPage
  215. const maxCharLines = isFirstPage
  216. ? config.row.maxFirstCharLine
  217. : isLastPage
  218. ? config.row.maxLastCharLine
  219. : config.row.maxCharLine
  220. // 检查是否换页
  221. const isPageFull = currentPageRows.length >= maxRows || currentPageCharLines >= maxCharLines
  222. if (isPageFull || i === rows.length - 1) {
  223. totalPages +=
  224. Math.floor(currentPageCharLines / maxCharLines) +
  225. (currentPageCharLines % maxCharLines > 0 ? 1 : 0)
  226. currentPageRows = []
  227. currentPageCharLines = 0
  228. isFirstPage = false
  229. isLastPage = i === rows.length - 1
  230. }
  231. }
  232. return totalPages
  233. }