reportUtil.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369
  1. import { buildFileUrl } from "@/utils";
  2. import { urlToBase64 } from "@/utils/filt";
  3. import GC from "@grapecity-software/spread-sheets";
  4. /**
  5. * 将值安全转为数组(兼容 JSON 字符串和已解析的数组)
  6. */
  7. function ensureArray(val: any): any[] {
  8. if (Array.isArray(val)) return val
  9. if (typeof val === 'string' && val.trim()) {
  10. try {
  11. const parsed = JSON.parse(val)
  12. return Array.isArray(parsed) ? parsed : []
  13. } catch (e) {
  14. return []
  15. }
  16. }
  17. return []
  18. }
  19. /**
  20. * 修改报表配置
  21. * @param sheet 报表页
  22. * @param res 可编辑字段
  23. * @param imgCol 图片字段
  24. * @param isPdf 是否pdf
  25. */
  26. export const editReport = async (sheet, res?, imgCol?, isPdf: boolean = false) => {
  27. sheet.suspendPaint()
  28. try {
  29. for (let i = 0; i < sheet.getRowCount(); i++) {
  30. for (let j = 0; j < sheet.getColumnCount(); j++) {
  31. const cell = sheet.getCell(i, j);
  32. if (sheet.getBindingPath(i, j) && cell.value()) {
  33. // 值的后缀为jpg、png
  34. const cellValue = cell.value();
  35. if (typeof cellValue === 'string' && (cellValue.endsWith('.jpg') || cellValue.endsWith('.png'))) {
  36. if (!cellValue.includes(',')) {
  37. const fileUrl = buildFileUrl(cell.value())
  38. const base64 = await urlToBase64(fileUrl)
  39. let x = 0, y = 0;
  40. for (let col = 0; col < j; col++) {
  41. x += sheet.getColumnWidth(col);
  42. }
  43. for (let row = 0; row < i; row++) {
  44. y += sheet.getRowHeight(row);
  45. }
  46. // 拿到合并单元格的宽度和高度
  47. let colCount = 1, rowCount = 1
  48. if (sheet.getSpans(new GC.Spread.Sheets.Range(i, j, 1, 1)).length > 0) {
  49. colCount = sheet.getSpans(new GC.Spread.Sheets.Range(i, j, 1, 1))[0].colCount
  50. rowCount = sheet.getSpans(new GC.Spread.Sheets.Range(i, j, 1, 1))[0].rowCount
  51. }
  52. let columnWidth = 0
  53. for (let k = 0; k < colCount; k++) {
  54. columnWidth += sheet.getColumnWidth(k + j);
  55. }
  56. let rowHeight = 0
  57. for (let k = 0; k < rowCount; k++) {
  58. rowHeight += sheet.getRowHeight(k + i);
  59. }
  60. // 边框宽度
  61. const a = cell.borderLeft()?.style | 1
  62. const b = cell.borderTop()?.style | 1
  63. const c = cell.borderRight()?.style | 1
  64. const d = cell.borderBottom()?.style | 1
  65. console.log(cellValue)
  66. const addPictureShape = sheet.shapes.addPictureShape(cellValue, base64, x + a, y + b, columnWidth - c, rowHeight - d);
  67. addPictureShape.allowMove(false)
  68. addPictureShape.allowResize(false)
  69. addPictureShape.allowRotate(false)
  70. addPictureShape.isLocked(true)
  71. } else {
  72. // 多个图片
  73. const imgArr: string[] = []
  74. for (const item of cellValue.split(',')) {
  75. const fileUrl = buildFileUrl(item)
  76. const base64 = await urlToBase64(fileUrl)
  77. imgArr.push(base64)
  78. }
  79. // 图片位置
  80. let x = 0, y = 0;
  81. for (let col = 0; col < j; col++) {
  82. x += sheet.getColumnWidth(col);
  83. }
  84. for (let row = 0; row < i; row++) {
  85. y += sheet.getRowHeight(row);
  86. }
  87. // 拿到合并单元格的宽度和高度
  88. let colCount = 1, rowCount = 1
  89. if (sheet.getSpans(new GC.Spread.Sheets.Range(i, j, 1, 1)).length > 0) {
  90. colCount = sheet.getSpans(new GC.Spread.Sheets.Range(i, j, 1, 1))[0].colCount
  91. rowCount = sheet.getSpans(new GC.Spread.Sheets.Range(i, j, 1, 1))[0].rowCount
  92. }
  93. let columnWidth = 0
  94. for (let k = 0; k < colCount; k++) {
  95. columnWidth += sheet.getColumnWidth(k + j);
  96. }
  97. let rowHeight = 0
  98. for (let k = 0; k < rowCount; k++) {
  99. rowHeight += sheet.getRowHeight(k + i);
  100. }
  101. // 边框宽度
  102. const a = cell.borderLeft()?.style | 1
  103. const b = cell.borderTop()?.style | 1
  104. const c = cell.borderRight()?.style | 1
  105. const d = cell.borderBottom()?.style | 1
  106. const imgW = (columnWidth - c - a) / imgArr.length
  107. let imgX = x + a
  108. for (const base64 of imgArr) {
  109. const addPictureShape = sheet.shapes.addPictureShape(cellValue, base64, imgX, y + b, imgW, rowHeight - d)
  110. addPictureShape.allowMove(false)
  111. addPictureShape.allowResize(false)
  112. addPictureShape.allowRotate(false)
  113. addPictureShape.isLocked(true)
  114. imgX += imgW
  115. }
  116. }
  117. }
  118. if (imgCol && imgCol.includes(sheet.getBindingPath(i, j))) {
  119. if (cellValue && cellValue.startsWith('[') && cellValue.endsWith(']')) {
  120. cell.value(null)
  121. const imgArr = JSON.parse(cellValue)
  122. for (const item of imgArr) {
  123. const fileUrl = buildFileUrl(item.url)
  124. const base64 = await urlToBase64(fileUrl)
  125. sheet.shapes.addPictureShape(JSON.stringify(item), base64, item.x, item.y, item.width, item.height)
  126. }
  127. }
  128. }
  129. }
  130. if (sheet.getRange(i, j).cellType() && sheet.getRange(i, j).cellType().typeName === '5') {
  131. if (cell.value() != 'true') {
  132. cell.value(null)
  133. } else {
  134. cell.value(true)
  135. }
  136. }
  137. }
  138. }
  139. // 填充浮动图片
  140. const text = sheet.getDataSource().rT.Illustration;
  141. if (text) {
  142. const illustration = ensureArray(text)
  143. const remainingItems: any[] = []
  144. for (const item of illustration) {
  145. if (item.sheet === sheet.name()) {
  146. const fileUrl = buildFileUrl(item.url)
  147. const base64 = await urlToBase64(fileUrl)
  148. sheet.shapes.addPictureShape(JSON.stringify(item), base64, item.x, item.y, item.width, item.height)
  149. } else {
  150. remainingItems.push(item)
  151. }
  152. }
  153. sheet.getDataSource().rT.Illustration = JSON.stringify(remainingItems)
  154. }
  155. if (isPdf) {
  156. return
  157. }
  158. const spreadNS = GC.Spread.Sheets;
  159. const cfs = sheet.conditionalFormats
  160. const style = new spreadNS.Style();
  161. style.backColor = "#FFF895";
  162. const arr: any[] = []
  163. const cellRange = sheet.getRange(0, 0, sheet.getRowCount(), sheet.getColumnCount());
  164. cellRange.allowEditInCell(false);
  165. for (let i = 0; i < sheet.getRowCount(); i++) {
  166. for (let j = 0; j < sheet.getColumnCount(); j++) {
  167. if (sheet.getBindingPath(i, j) && res && res.includes(sheet.getBindingPath(i, j))) {
  168. setTimeout(() => {
  169. sheet.getCell(i, j).allowEditInCell(true)
  170. }, 1)
  171. // 一个个设置背景颜色太费时间了
  172. // setTimeout(() => {
  173. // sheet.getRange(i, j, 1, 1).backColor("#FFF895")
  174. // }, 1)
  175. arr.push(new spreadNS.Range(i, j, 1, 1))
  176. }
  177. }
  178. }
  179. // 优化性能版
  180. // 设置可编辑背景
  181. cfs.addSpecificTextRule(
  182. spreadNS.ConditionalFormatting.ComparisonOperators.greaterThanOrEqualsTo,
  183. null,
  184. style,
  185. arr
  186. );
  187. // 设置必填
  188. // cfs.addCellValueRule(
  189. // spreadNS.ConditionalFormatting.ComparisonOperators.equalsTo,
  190. // null,null,
  191. // style,
  192. // arr
  193. // );
  194. } finally {
  195. sheet.resumePaint()
  196. sheet.repaint()
  197. }
  198. }
  199. /**
  200. * 添加复制事件
  201. * @param spread 工作簿
  202. */
  203. export const handleCopy = (spread) => {
  204. let bindingPathRec = new Map();
  205. spread.bind(GC.Spread.Sheets.Events.ClipboardPasting, function (_e, info) {
  206. const cellRange = info.cellRange;
  207. bindingPathRec = new Map()
  208. // 边界检查
  209. if (
  210. !info.sheet ||
  211. !cellRange ||
  212. cellRange.rowCount <= 0 ||
  213. cellRange.colCount <= 0
  214. ) {
  215. console.error("Invalid fill range or sheet reference.");
  216. return;
  217. }
  218. try {
  219. // 处理 pasteData.text,去除末尾的制表符
  220. let processedText = info.pasteData.text;
  221. if (typeof processedText === 'string') {
  222. processedText = processedText.replace(/\t+$/, '');
  223. }
  224. for (let i = 0; i < cellRange.rowCount; i++) {
  225. for (let j = 0; j < cellRange.colCount; j++) {
  226. const path = info.sheet.getBindingPath(cellRange.row + i, cellRange.col + j);
  227. const compositeKey = `${cellRange.row + i},${cellRange.col + j}`;
  228. bindingPathRec.set(compositeKey, {
  229. row: cellRange.row + i,
  230. col: cellRange.col + j,
  231. path: path,
  232. text: processedText
  233. });
  234. }
  235. }
  236. } catch (getErr) {
  237. console.error("Error getting binding path:", getErr);
  238. }
  239. });
  240. spread.bind(GC.Spread.Sheets.Events.ClipboardPasted, function (_e, info) {
  241. bindingPathRec.forEach(value => {
  242. info.sheet.setBindingPath(value.row, value.col, value.path)
  243. info.sheet.setValue(value.row, value.col, value.text)
  244. })
  245. })
  246. let bindingPathRecMap = new Map();
  247. spread.bind(GC.Spread.Sheets.Events.DragFillBlock, function (_e, info) {
  248. console.log(info)
  249. const fillRange = info.fillRange;
  250. bindingPathRecMap = new Map()
  251. // 边界检查
  252. if (
  253. !info.sheet ||
  254. !fillRange ||
  255. fillRange.rowCount <= 0 ||
  256. fillRange.colCount <= 0
  257. ) {
  258. console.error("Invalid fill range or sheet reference.");
  259. return;
  260. }
  261. try {
  262. for (let row = fillRange.row; row < fillRange.row + fillRange.rowCount; row++) {
  263. for (let col = fillRange.col; col < fillRange.col + fillRange.colCount; col++) {
  264. // 获取当前单元格的bindingPath
  265. const path = info.sheet.getBindingPath(row, col);
  266. // Matthew:注意,没有path时也不能跳过,因为绑定路径为undefined也是一种绑定路径
  267. // 如果path存在,则记录;否则,跳过该单元格
  268. // 使用复合键存储信息
  269. const compositeKey = `${row},${col}`;
  270. bindingPathRecMap.set(compositeKey, {
  271. row: row,
  272. col: col,
  273. path: path,
  274. });
  275. }
  276. }
  277. } catch (getErr) {
  278. console.error("Error getting binding path:", getErr);
  279. }
  280. });
  281. spread.bind(GC.Spread.Sheets.Events.DragFillBlockCompleted, function (_e, info) {
  282. bindingPathRecMap.forEach(value => {
  283. info.sheet.setBindingPath(value.row, value.col, value.path)
  284. })
  285. })
  286. }
  287. /**
  288. * 从 sheet 数据源提取所有字段值(兼容 table 绑定字段)
  289. */
  290. export const collectSheetDataSource = (sheet): Record<string, any> => {
  291. console.log(sheet.getDataSource())
  292. if(!sheet.getDataSource()) return {}
  293. const rT = sheet.getDataSource().rT
  294. const result: Record<string, any> = { ...rT }
  295. const tables = sheet.tables?.all() || []
  296. for (const table of tables) {
  297. const tableBindingPath = table.bindingPath()
  298. if (tableBindingPath) {
  299. const tableData = rT[tableBindingPath]
  300. if (Array.isArray(tableData)) {
  301. result[tableBindingPath] = tableData
  302. }
  303. }
  304. }
  305. return result
  306. }
  307. /**
  308. * 将数据源值序列化为后端存储格式(对象/数组 → JSON 字符串)
  309. */
  310. export const normalizeValueForStorage = (val: any): any => {
  311. if (val === null || val === undefined) return val
  312. if (typeof val === 'object') return JSON.stringify(val)
  313. return val
  314. }
  315. /**
  316. * 递归遍历整个 dataSource,将所有值序列化
  317. */
  318. export const serializeDataSourceForStorage = (dataSource: Record<string, any>): Record<string, any> => {
  319. const result: Record<string, any> = {}
  320. for (const key in dataSource) {
  321. result[key] = normalizeValueForStorage(dataSource[key])
  322. }
  323. return result
  324. }
  325. /**
  326. * 收集 sheet 中的形状(浮动图片)数据,合并到 dataSource 中
  327. */
  328. export const collectShapesIntoDataSource = (sheet, dataSource: Record<string, any>) => {
  329. sheet.shapes.all().forEach(shape => {
  330. if (shape.name() && shape.name().startsWith('{') && shape.name().endsWith('}')) {
  331. try {
  332. const data = JSON.parse(shape.name())
  333. data.x = shape.x()
  334. data.y = shape.y()
  335. data.width = shape.width()
  336. data.height = shape.height()
  337. const existing = dataSource[data.path]
  338. if (existing !== undefined && existing !== null) {
  339. const arr = ensureArray(existing)
  340. arr.push(data)
  341. dataSource[data.path] = arr
  342. } else {
  343. dataSource[data.path] = [data]
  344. }
  345. } catch (e) {
  346. // 忽略解析失败的形状
  347. }
  348. }
  349. })
  350. }