SpreadViewer.vue 46 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433
  1. <template>
  2. <div v-show="showSpread"
  3. :class="[!isFullscreen ? 'lab-designer-container' : 'lab-designer-container-fullscreen']"
  4. v-loading="loading" >
  5. <div class="header-row">
  6. <div class="title">
  7. <slot name="title"></slot>
  8. </div>
  9. <div class="unit-footer-btns">
  10. <el-dropdown trigger="click">
  11. <el-button type="primary" plain style="margin-right: 12px;">
  12. 更多操作<el-icon class="el-icon--right"><arrow-down /></el-icon>
  13. </el-button>
  14. <template #dropdown>
  15. <el-dropdown-menu>
  16. <el-dropdown-item @click="handleGenerate(true)">
  17. <div class="dropdown-item-content">
  18. <el-button type="warning" plain style="margin-right: 12px;">生成续页</el-button>
  19. </div>
  20. </el-dropdown-item>
  21. <el-dropdown-item @click="openWingdingsDialog">
  22. <div class="dropdown-item-content">
  23. <el-button type="info" plain style="margin-right: 12px;">插入特殊符号</el-button>
  24. </div>
  25. </el-dropdown-item>
  26. <el-dropdown-item @click="handleCopyRow">
  27. <div class="dropdown-item-content">
  28. <el-button type="primary" plain style="margin-right: 12px;">复制表单</el-button>
  29. </div>
  30. </el-dropdown-item>
  31. </el-dropdown-menu>
  32. </template>
  33. </el-dropdown>
  34. <el-button type="success" @click="handleSave">保存</el-button>
  35. <BatchUploadFile @uploadImg="addPic" :colList="colListData" />
  36. <el-button type="primary" @click="openPdf">预览 PDF</el-button>
  37. <el-button type="danger" @click="close">关闭</el-button>
  38. </div>
  39. </div>
  40. <div v-loading="loading" ref="previewContainer" class="spread-container"></div>
  41. </div>
  42. <div v-show="showPdf" class="pdf-viewer-container" v-loading="pdfLoading" ref="pdfViewer">
  43. <div class="pdf-viewer-content">
  44. <VuePdfEmbed
  45. :key="pdfTimestamp"
  46. :height="pdfViewerHeight"
  47. :width="pdfContentWidth"
  48. :source="recordSource"
  49. text-layer
  50. :annotation-layer="false"
  51. @rendered="handlePdfRendered"
  52. />
  53. </div>
  54. </div>
  55. <!-- Wingdings 符号选择弹窗 -->
  56. <el-dialog
  57. v-model="wingdingsDialogVisible"
  58. title="选择特殊符号"
  59. width="600px"
  60. :close-on-click-modal="false"
  61. >
  62. <div class="wingdings-container">
  63. <div class="wingdings-grid">
  64. <div
  65. v-for="(item, index) in getWingdingsList"
  66. :key="index"
  67. class="wingdings-item"
  68. @click="selectWingdings(item)"
  69. >
  70. <span class="wingdings-symbol">{{ item.value }}</span>
  71. <span class="wingdings-label">{{ item.label }}</span>
  72. </div>
  73. </div>
  74. </div>
  75. <template #footer>
  76. <el-button @click="wingdingsDialogVisible = false">取消</el-button>
  77. </template>
  78. </el-dialog>
  79. </template>
  80. <script lang="ts" setup>
  81. import '@grapecity-software/spread-sheets-designer-resources-cn'
  82. import * as GC from '@grapecity-software/spread-sheets'
  83. // 导入PDF导出模块
  84. import '@grapecity-software/spread-sheets-pdf'
  85. import VuePdfEmbed from 'vue-pdf-embed'
  86. import 'vue-pdf-embed/dist/styles/annotationLayer.css'
  87. import 'vue-pdf-embed/dist/styles/textLayer.css'
  88. import * as ExcelIO from "@grapecity-software/spread-excelio"
  89. import SpreadDesigner from '@/components/SpreadDesigner/index.vue'
  90. import { Printer, Download, Refresh, DocumentAdd, Picture, ArrowDown } from '@element-plus/icons-vue'
  91. import { useTagsViewStore } from '@/store/modules/tagsView'
  92. import {
  93. getPDF
  94. } from '@/api/laboratory/standard/template';
  95. import {
  96. getPDFByInspectionLocal,
  97. getStandardTemplateInfo,
  98. } from '@/api/pressure2/standard/template'
  99. import {ref, onMounted,onUnmounted} from "vue";
  100. import {InitParams} from './SpreadInterface';
  101. import { buildFileUrl } from '@/utils'
  102. import axios from 'axios'
  103. import { DynamicTbApi } from '@/api/pressure2/dynamictb'
  104. import {DynamicTbColApi} from "@/api/pressure2/dynamictbcol";
  105. import { ExcelApi } from '@/api/pressure2/excel'
  106. import { DynamicTbValApi } from '@/api/pressure2/dynamictbval'
  107. import BatchUploadFile from "./BatchUploadFile.vue";
  108. import {editReport, handleCopy, collectSheetDataSource, collectShapesIntoDataSource, serializeDataSourceForStorage} from "@/utils/reportUtil";
  109. import Designer from '@grapecity-software/spread-sheets-designer-vue'
  110. import { ElLoading } from "element-plus"
  111. import {getPDF2, getPDFByInspection} from "@/api/pressure2/standard/template";
  112. import { useDictStore } from '@/store/modules/dict'
  113. defineOptions({ name: 'SpreadViewer' });
  114. const props = defineProps({
  115. initData: {
  116. type: Object as PropType<InitParams>,
  117. default: ()=>({}),
  118. required: true
  119. },
  120. isFullscreen:{
  121. type: Boolean,
  122. default: false,
  123. required: false
  124. }
  125. });
  126. const insId=ref<string>(null);
  127. const colListData=ref([]);
  128. const addPic = (picData) => {
  129. if (previewSpread) {
  130. if (props.initData.refName == '红外热成像检测') {
  131. picData.name.sheet = previewSpread.getActiveSheet().name()
  132. const sheet = previewSpread.getActiveSheet()
  133. // 实时获取当前工作表中的图片形状数量
  134. const existingShapes = sheet.shapes.all()
  135. const pictureCount = existingShapes.length
  136. let x, y
  137. if (pictureCount === 0) {
  138. // 第一张图片的坐标
  139. x = 55
  140. y = 430
  141. sheet.shapes.addPictureShape(JSON.stringify(picData.name), picData.base, x, y, 240, 320);
  142. return
  143. } else if(pictureCount === 1){
  144. // 第二张及后续图片的坐标
  145. x = 410
  146. y = 430
  147. sheet.shapes.addPictureShape(JSON.stringify(picData.name), picData.base, x, y, 240, 320);
  148. return
  149. }
  150. }
  151. picData.name.sheet = previewSpread.getActiveSheet().name()
  152. previewSpread.getActiveSheet().shapes.addPictureShape(JSON.stringify(picData.name), picData.base, picData.name.x, picData.name.y, picData.name.width, picData.name.height);
  153. }
  154. }
  155. /*
  156. watch(()=>[props.initData.templateId,props.initData.refId],([newTide,newRid],[oldTid,oldRid])=>{
  157. if(newTide && newRid){
  158. initPreview();
  159. }
  160. }); */
  161. //const route = useRoute()
  162. // spread相关
  163. const tagsViewStore = useTagsViewStore()
  164. const dictStore = useDictStore()
  165. const loading = ref(true)
  166. const showSpread=ref(true)
  167. const previewContainer = ref(null)
  168. let sheetData = {};
  169. let arrayFieldOffsetInfos = [];
  170. const designerKey = import.meta.env.VITE_SPREADJS_DESIGNER_KEY
  171. const nodeEnv = import.meta.env.VITE_NODE_ENV
  172. const licenseKey = import.meta.env.VITE_SPREADJS_LICENSE_KEY
  173. GC.Spread.Sheets.Designer.LicenseKey = designerKey
  174. //同时对SpreadJS与ExcelIO进行授权
  175. GC.Spread.Sheets.LicenseKey = licenseKey
  176. // pdf相关
  177. let previewSpread = null
  178. const pdfLoading = ref(false);
  179. const showPdf=ref(false);
  180. const recordSource = ref('')
  181. const pdfViewer = ref()
  182. const pdfContentWidth = ref(800)
  183. const pdfViewerHeight = ref(600)
  184. const pdfTimestamp = ref('')
  185. // Wingdings 弹窗相关
  186. const wingdingsDialogVisible = ref(false)
  187. const currentCell = ref(null)
  188. const getWingdingsList = computed(
  189. () => dictStore.getDictMap['Wingdings']
  190. )
  191. const handlePdfRendered = () => {
  192. pdfLoading.value = false
  193. }
  194. // 获取模版详情
  195. const fetchTemplateData = async () => {
  196. console.log(this)
  197. if(!props.initData.templateId){
  198. return;
  199. }
  200. const templateRes = await getStandardTemplateInfo({ id: props.initData.templateId })
  201. if (templateRes && templateRes.fileUrl) {
  202. const fileUrl = buildFileUrl(templateRes.fileUrl)
  203. const response = await axios.get(fileUrl, { responseType: 'blob' });
  204. return new Blob([response.data], { type: response.headers['content-type'] });
  205. }
  206. return;
  207. }
  208. const initPreview = async () => {
  209. const { refId, opType, templateId, enterPdfUrl, issueUrl } = props.initData;
  210. if (!refId) {
  211. return false;
  212. }
  213. // 有预生成的 PDF URL 时,跳过表格渲染直接预览 PDF
  214. const directPdfUrl = (enterPdfUrl || issueUrl || '').trim();
  215. if (opType === 1 && directPdfUrl) {
  216. loading.value = false;
  217. showSpread.value = false;
  218. showPdf.value = true;
  219. recordSource.value = buildFileUrl(directPdfUrl);
  220. pdfTimestamp.value = Date.now();
  221. console.log("使用url")
  222. setTimeout(() => {
  223. calculatePdfSize()
  224. }, 100)
  225. return;
  226. }
  227. loading.value = true;
  228. if (opType === 0) {
  229. showSpread.value = true;
  230. pdfLoading.value = false;
  231. }
  232. previewSpread?.destroy();
  233. previewSpread = new GC.Spread.Sheets.Workbook(previewContainer.value);
  234. const handleOpenError = (error: any) => {
  235. console.error('文件打开失败:', error);
  236. loading.value = false;
  237. };
  238. const handleCopyIfNeeded = () => {
  239. if (opType === 0) {
  240. handleCopy(previewSpread);
  241. }
  242. };
  243. const initCommonData = async () => {
  244. const res = await DynamicTbValApi.getDynamicTbInsAndValByRefId(props.initData);
  245. insId.value = res.dynamicTbInsRespVO.id;
  246. dynamicTbColRespVOList = res.dynamicTbColRespVOList;
  247. colListData.value = res.dynamicTbColRespVOList.filter(i => i.colValType === 4);
  248. return res;
  249. };
  250. if (nodeEnv === 'dev' || nodeEnv === 'hst' || nodeEnv === 'uat' || nodeEnv === 'prod') {
  251. try {
  252. // 1. 获取实例和字段数据(数据绑定留给前端处理,后端只做图片/高亮/续页处理)
  253. const res = await initCommonData();
  254. // 2. 调用后端接口获取处理好的 SJS
  255. const apiMethod = ExcelApi.excel;
  256. // const apiMethod = nodeEnv === 'dev' ? ExcelApi.process : ExcelApi.excel;
  257. const sjsBlob: any = await apiMethod({ templateId, refId });
  258. // 3. 打开 SJS 后在回调中绑定数据源
  259. previewSpread.open(sjsBlob, () => {
  260. console.log('后端处理完成,预览加载完成');
  261. const sheets = previewSpread.sheets;
  262. // 构建数据源并绑定(后端不 setDataSource,交由前端处理)
  263. // 后端已将 .jpg/.png 路径处理为背景图片,此处设 null 避免覆盖
  264. if (res.dynamicTbValRespVOList && res.dynamicTbValRespVOList.length > 0) {
  265. sheetData = {};
  266. res.dynamicTbValRespVOList.forEach(i => {
  267. const val = i.valValue;
  268. if (typeof val === 'string') {
  269. const trimmed = val.trim();
  270. // 后端已将图片路径值处理为背景图片,跳过避免文字覆盖
  271. if (trimmed.endsWith('.jpg') || trimmed.endsWith('.png')) {
  272. // sheetData[i.colCode] = null;
  273. } else if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
  274. if(i.colCode === 'Illustration'){
  275. sheetData[i.colCode] = null
  276. }else{
  277. try { sheetData[i.colCode] = JSON.parse(val); } catch { sheetData[i.colCode] = val; }
  278. }
  279. } else if (val == 'false' || val == 'true') {
  280. sheetData[i.colCode] = val == 'true' ? 'true' : null;
  281. } else {
  282. sheetData[i.colCode] = val;
  283. }
  284. } else {
  285. sheetData[i.colCode] = val;
  286. }
  287. });
  288. }
  289. let dataSource1 = new GC.Spread.Sheets.Bindings.CellBindingSource(sheetData);
  290. if (props.initData.dataSource) {
  291. dataSource1 = new GC.Spread.Sheets.Bindings.CellBindingSource(props.initData.dataSource);
  292. }
  293. sheets.forEach(sheet => sheet.setDataSource(dataSource1));
  294. // 表格行样式同步(数据绑定后前端处理)
  295. handleTableRowsAfterDataBind(sheets);
  296. // 续页处理
  297. hiddenPage().then(() => {
  298. if (opType === 1) {
  299. showSpread.value = false;
  300. pdfLoading.value = true;
  301. openPdf();
  302. } else {
  303. showPdf.value = false;
  304. loading.value = false;
  305. handleCopyIfNeeded();
  306. }
  307. });
  308. }, handleOpenError);
  309. } catch (error) {
  310. console.error('后端处理 Excel 失败:', error);
  311. loading.value = false;
  312. }
  313. return;
  314. }
  315. try {
  316. const blob = await fetchTemplateData();
  317. if (!blob) return;
  318. previewSpread.open(blob, async () => {
  319. console.log('预览加载完成');
  320. const sheets = previewSpread.sheets;
  321. const res = await DynamicTbValApi.getDynamicTbInsAndValByRefId(props.initData);
  322. insId.value = res.dynamicTbInsRespVO.id;
  323. if (res.dynamicTbValRespVOList && res.dynamicTbValRespVOList.length > 0) {
  324. sheetData = {};
  325. res.dynamicTbValRespVOList.forEach(i => {
  326. const val = i.valValue;
  327. if (typeof val === 'string') {
  328. const trimmed = val.trim();
  329. if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
  330. try {
  331. sheetData[i.colCode] = JSON.parse(val);
  332. } catch {
  333. sheetData[i.colCode] = val;
  334. }
  335. } else if (val == 'false' || val == 'true') {
  336. sheetData[i.colCode] = val == 'true' ? 'true' : null;
  337. } else {
  338. sheetData[i.colCode] = val;
  339. }
  340. } else {
  341. sheetData[i.colCode] = val;
  342. }
  343. });
  344. }
  345. let dataSource1 = new GC.Spread.Sheets.Bindings.CellBindingSource(sheetData);
  346. if (props.initData.dataSource) {
  347. dataSource1 = new GC.Spread.Sheets.Bindings.CellBindingSource(props.initData.dataSource);
  348. }
  349. sheets.forEach(sheet => sheet.setDataSource(dataSource1));
  350. dynamicTbColRespVOList = res.dynamicTbColRespVOList;
  351. const editCols = res.dynamicTbColRespVOList.filter(i => i.isEdit).map(i => i.colCode);
  352. const imgCols = res.dynamicTbColRespVOList.filter(i => i.colValType === 4).map(i => i.colCode);
  353. colListData.value = res.dynamicTbColRespVOList.filter(i => i.colValType === 4);
  354. for (const sheet of sheets) {
  355. await editReport(sheet, editCols, imgCols, opType);
  356. }
  357. handleTableRowsAfterDataBind(sheets);
  358. await hiddenPage();
  359. if (opType === 1) {
  360. showSpread.value = false;
  361. pdfLoading.value = true;
  362. await openPdf();
  363. } else {
  364. showPdf.value = false;
  365. loading.value = false;
  366. handleCopyIfNeeded();
  367. }
  368. }, handleOpenError);
  369. } catch (error) {
  370. console.error('预览加载失败:', error);
  371. }
  372. }
  373. let dynamicTbColRespVOList;
  374. const openPdf = async () => {
  375. const formData = new FormData()
  376. // 在异步操作开始前捕获 opType,防止切换项目时 props.initData 被覆盖导致竞态
  377. const currentOpType = props.initData.opType;
  378. const currentManualUrl = props.initData.manualUrl;
  379. const rules = []
  380. previewSpread.sheets.forEach((sheet) => {
  381. sheet.conditionalFormats.getRules().forEach(rule => {
  382. if (rule.condition() && rule.condition().formula() == 'TRUE') {
  383. console.log(rule)
  384. rules.push({
  385. sheet,
  386. rule
  387. })
  388. }
  389. })
  390. rules.forEach(rule => {
  391. sheet.conditionalFormats.removeRule(rule.rule)
  392. })
  393. });
  394. await previewSpread.save(async function (blob) {
  395. formData.append('file', blob)
  396. let response = null
  397. //prod环境葡萄城pdf接口
  398. if (nodeEnv == 'prod') {
  399. // 手动拼接pdf的url,适配检验方案
  400. if (currentManualUrl){
  401. formData.append('manualUrl', currentManualUrl)
  402. response = await getPDFByInspection(formData)
  403. }else{
  404. response = await getPDF2(formData)
  405. }
  406. } else {
  407. //本地测试环境葡萄城pdf接口
  408. if (currentManualUrl){
  409. // 手动拼接pdf的url,适配检验方案
  410. formData.append('manualUrl', currentManualUrl)
  411. response = await getPDFByInspectionLocal(formData)
  412. }else{
  413. response = await getPDF2(formData)
  414. }
  415. }
  416. if (response) {
  417. const flow = new Blob([response], { type: 'application/pdf' })
  418. recordSource.value = window.URL.createObjectURL(flow);
  419. if(currentOpType==0){
  420. loading.value = false;
  421. window.open(recordSource.value, '_blank');
  422. }
  423. if(currentOpType==1){
  424. //showSpread.value=false;
  425. pdfTimestamp.value = Date.now();
  426. showPdf.value=true;
  427. pdfLoading.value=true;
  428. setTimeout(() => {
  429. calculatePdfSize()
  430. },100)
  431. }
  432. }
  433. }, (e) => {
  434. }, {
  435. includeBindingSource: true
  436. })
  437. previewSpread.sheets.forEach((sheet) => {
  438. rules.forEach(rule => {
  439. if (rule.sheet === sheet){
  440. sheet.conditionalFormats.addRule(rule.rule)
  441. }
  442. })
  443. });
  444. }
  445. const handleSpreadPrint = () => {
  446. if (!previewSpread) return
  447. previewSpread.print()
  448. }
  449. /**
  450. * 打开 Wingdings 符号选择弹窗
  451. */
  452. const openWingdingsDialog = () => {
  453. if (!previewSpread) {
  454. ElMessage.warning('请先加载文档')
  455. return
  456. }
  457. // 获取当前选中的单元格
  458. const activeSheet = previewSpread.getActiveSheet()
  459. const selections = activeSheet.getSelections()
  460. if (!selections || selections.length === 0) {
  461. ElMessage.warning('请先选择一个单元格')
  462. return
  463. }
  464. // 保存当前单元格信息
  465. const selection = selections[0]
  466. currentCell.value = {
  467. sheet: activeSheet,
  468. row: selection.row,
  469. col: selection.col
  470. }
  471. wingdingsDialogVisible.value = true
  472. }
  473. /**
  474. * 选择 Wingdings 符号并插入到单元格
  475. */
  476. const selectWingdings = (item) => {
  477. if (!currentCell.value) {
  478. ElMessage.error('未找到目标单元格')
  479. return
  480. }
  481. const { sheet, row, col } = currentCell.value
  482. const cell = sheet.getCell(row, col)
  483. // if (!cell.allowEditInCell()){
  484. // wingdingsDialogVisible.value = false
  485. // return;
  486. // }
  487. // 获取当前单元格的值
  488. const currentValue = cell.value() || ''
  489. // 追加符号到单元格
  490. const newValue = currentValue + item.value
  491. cell.value(newValue)
  492. ElMessage.success(`已插入符号: ${item.label}`)
  493. wingdingsDialogVisible.value = false
  494. }
  495. const handleSave = () => {
  496. loading.value = true;
  497. let dataSource = {};
  498. const allSheetsData = []
  499. previewSpread.sheets.forEach((sheet) => {
  500. if (sheet.name() && sheet.name() !== '试用版警告') {
  501. const sheetData = collectSheetDataSource(sheet);
  502. const pageNo = getPageNoFromSheet(sheet);
  503. allSheetsData.push({ pageNo, data: sheetData, sheet });
  504. }
  505. });
  506. let copyRangePathSet = new Set();
  507. const hasContinuationPages = allSheetsData.some(({ pageNo }) => pageNo > 1);
  508. let pageNoBindingPath = null
  509. if (hasContinuationPages && generateData && generateData.copyRange) {
  510. const rangeInfo = getCopyRangeInfo();
  511. const templateSheet = previewSpread.getSheetFromName(generateData.sheetName);
  512. if (templateSheet && rangeInfo) {
  513. getCopyRangeBindingPaths(templateSheet, rangeInfo).forEach(p => copyRangePathSet.add(p));
  514. pageNoBindingPath = templateSheet.getBindingPath(rangeInfo.row, rangeInfo.col)
  515. }
  516. }
  517. for (const { pageNo, data, sheet } of allSheetsData) {
  518. for (const key in data) {
  519. if (pageNo > 1 && key === pageNoBindingPath) continue
  520. if (arrayFieldOffsetInfos.some(oi => key.startsWith(oi.prefix + '_'))) continue
  521. if (hasContinuationPages && copyRangePathSet.has(key)) {
  522. if (!dataSource[key]) {
  523. dataSource[key] = [];
  524. }
  525. if (Array.isArray(dataSource[key])) {
  526. while (dataSource[key].length < pageNo) {
  527. dataSource[key].push(null);
  528. }
  529. dataSource[key][pageNo - 1] = data[key];
  530. }
  531. } else if (hasContinuationPages && Array.isArray(sheetData?.[key]) && !arrayFieldOffsetInfos.some(oi => key === oi.prefix)) {
  532. if (!dataSource[key]) {
  533. dataSource[key] = [];
  534. }
  535. if (Array.isArray(data[key])) {
  536. dataSource[key] = dataSource[key].concat(data[key]);
  537. }
  538. } else {
  539. if (!dataSource.hasOwnProperty(key) && data[key] != null) {
  540. dataSource[key] = data[key];
  541. }
  542. }
  543. }
  544. collectShapesIntoDataSource(sheet, dataSource);
  545. }
  546. for (const oi of arrayFieldOffsetInfos) {
  547. const { prefix, fields } = oi
  548. const copyMinIndex = Math.min(...fields.map(f => f.index))
  549. const copyFieldNames = new Set(fields.map(f => f.path))
  550. const result = []
  551. const templateSheetData = allSheetsData.find(s => s.sheet.name() === generateData.sheetName)
  552. if (templateSheetData) {
  553. for (let i = 1; i < copyMinIndex; i++) {
  554. const fieldName = `${prefix}_${i}`
  555. if (!copyFieldNames.has(fieldName)) {
  556. const val = templateSheetData.data[fieldName]
  557. if (val != null) {
  558. result.push(Array.isArray(val) ? val[0] || null : val)
  559. } else {
  560. result.push(null)
  561. }
  562. }
  563. }
  564. }
  565. for (const { data, sheet } of allSheetsData) {
  566. if (sheet.name() !== generateData.sheetName && !sheet.name().startsWith(generateData.sheetName + ' (')) continue
  567. for (const f of fields) {
  568. result.push(data[f.path] != null ? data[f.path] : null)
  569. }
  570. }
  571. dataSource[`${prefix}_1`] = result
  572. }
  573. if (Object.keys(dataSource).length > 0) {
  574. const serializedData = serializeDataSourceForStorage(dataSource);
  575. console.log(serializedData);
  576. DynamicTbValApi.saveAllColValue(insId.value, serializedData).then(res => {
  577. if (res) {
  578. ElMessage.success('保存成功')
  579. emit('saveSuccess', {insId:insId.value,dataSource:serializedData,refId:props.initData.refId});
  580. } else{
  581. ElMessage.error('保存失败')
  582. emit('saveFail');
  583. }
  584. }).catch(e => {
  585. ElMessage.error('保存失败')
  586. emit('saveFail');
  587. }).finally(() => {
  588. loading.value = false;
  589. });
  590. } else {
  591. loading.value = false;
  592. }
  593. }
  594. const distributePageDataSources = () => {
  595. if (!generateData || !generateData.copyRange) return
  596. previewSpread.sheets.forEach((sheet) => {
  597. if (sheet.name() && sheet.name() !== '试用版警告' && sheet.name() !== generateData.sheetName) {
  598. const pageNo = getPageNoFromSheet(sheet)
  599. if (pageNo > 1 && sheetData && Object.keys(sheetData).length > 0) {
  600. const pageData = buildPageDataSource(sheetData, pageNo)
  601. const dataSource = new GC.Spread.Sheets.Bindings.CellBindingSource(pageData)
  602. sheet.setDataSource(dataSource)
  603. }
  604. }
  605. })
  606. }
  607. const handleCopyRow = () => {
  608. if (!previewSpread) return ElMessage.warning('请先加载文档')
  609. const activeSheet = previewSpread.getActiveSheet()
  610. const selections = activeSheet.getSelections()
  611. if (!selections || selections.length === 0) return ElMessage.warning('请先选择一个单元格')
  612. const { row, col } = selections[0]
  613. // 找到包含当前单元格的 table
  614. const tables = activeSheet.tables?.all() || []
  615. let targetTable = null
  616. for (const table of tables) {
  617. const range = table.range()
  618. if (row >= range.row && row < range.row + range.rowCount && col >= range.col && col < range.col + range.colCount) {
  619. targetTable = table
  620. break
  621. }
  622. }
  623. if (!targetTable) return ElMessage.warning('当前单元格不在表格内,请选中表格内的单元格')
  624. const range = targetTable.range()
  625. if (row < range.row + 1) return ElMessage.warning('请选中表格内的数据行')
  626. // 绑定源中追加空行,让 table 自动扩展一行
  627. const dataSource = activeSheet.getDataSource()
  628. const sourceData = dataSource?.getSource()
  629. const bindingPath = targetTable.bindingPath()
  630. if (!bindingPath || !sourceData?.[bindingPath] || !Array.isArray(sourceData[bindingPath])) {
  631. return ElMessage.warning('表格未绑定数据源')
  632. }
  633. // 在刷新数据源前,先收集第一行的合并规则(刷新时会清空)
  634. const { col: tableCol, colCount: tableColCount } = range
  635. const firstRowSpans = collectFirstRowSpans(activeSheet, range.row + 1, tableCol, tableColCount)
  636. // 记录新行位置(当前 rowCount 就是新增行的索引偏移)
  637. const newRowIndex = range.row + range.rowCount
  638. // 在绑定数组中追加空对象
  639. sourceData[bindingPath].push({})
  640. activeSheet.setDataSource(null)
  641. activeSheet.setDataSource(dataSource)
  642. // 复制样式/行高/合并到新行
  643. previewSpread.suspendPaint()
  644. try {
  645. const copyOps = GC.Spread.Sheets.CopyToOptions.style | GC.Spread.Sheets.CopyToOptions.formula
  646. activeSheet.copyTo(row, tableCol, newRowIndex, tableCol, 1, tableColCount, copyOps)
  647. activeSheet.setRowHeight(newRowIndex, activeSheet.getRowHeight(row))
  648. // 批量设置自动换行
  649. activeSheet.getRange(newRowIndex, tableCol, 1, tableColCount).wordWrap(true)
  650. // 新复制的行设为可编辑
  651. activeSheet.getRange(newRowIndex, tableCol, 1, tableColCount).allowEditInCell(true)
  652. activeSheet.getRange(newRowIndex, tableCol, 1, tableColCount).locked(false)
  653. if (firstRowSpans.length > 0) {
  654. applyMergeRules(activeSheet, newRowIndex, firstRowSpans)
  655. }
  656. } finally {
  657. previewSpread.resumePaint()
  658. }
  659. ElMessage.success('已复制行')
  660. }
  661. const close = () => {
  662. emit('close')
  663. }
  664. const getPageNoFromSheet = (sheet) => {
  665. const tag = sheet.tag()
  666. if (tag !== undefined && tag !== null && tag !== '' && !isNaN(parseInt(tag))) {
  667. return parseInt(tag)
  668. }
  669. const name = sheet.name()
  670. const match = name.match(/\((\d+)\)\s*$/)
  671. if (match) {
  672. return parseInt(match[1])
  673. }
  674. return 1
  675. }
  676. const getCopyRangeInfo = () => {
  677. if (!generateData || !generateData.copyRange) return null
  678. const col = parseInt(generateData.copyRange.topLeft.split(',')[0])
  679. const row = parseInt(generateData.copyRange.topLeft.split(',')[1])
  680. const colCount = parseInt(generateData.copyRange.topRight.split(',')[0]) - col
  681. const rowCount = parseInt(generateData.copyRange.bottomLeft.split(',')[1]) - row
  682. return { col, row, colCount, rowCount }
  683. }
  684. const getCopyRangeBindingPaths = (sheet, rangeInfo) => {
  685. if (!sheet || !rangeInfo) return []
  686. const { col, row, colCount, rowCount } = rangeInfo
  687. const paths = []
  688. for (let i = 0; i <= colCount; i++) {
  689. for (let j = 0; j <= rowCount; j++) {
  690. const bindingPath = sheet.getBindingPath(row + j, col + i)
  691. if (bindingPath) {
  692. paths.push(bindingPath)
  693. }
  694. }
  695. }
  696. return paths
  697. }
  698. const buildPageDataSource = (baseData, pageNo) => {
  699. if (!generateData || !generateData.copyRange) return baseData
  700. const rangeInfo = getCopyRangeInfo()
  701. if (!rangeInfo) return baseData
  702. const sheet = previewSpread.getSheetFromName(generateData.sheetName)
  703. if (!sheet) return baseData
  704. const pageData = { ...baseData }
  705. const bindingPaths = getCopyRangeBindingPaths(sheet, rangeInfo)
  706. const bindingPathSet = new Set(bindingPaths)
  707. for (const path of bindingPaths) {
  708. const val = baseData[path]
  709. if (Array.isArray(val)) {
  710. pageData[path] = val[pageNo - 1] !== undefined ? val[pageNo - 1] : null
  711. } else if (pageNo > 1) {
  712. pageData[path] = null
  713. }
  714. }
  715. const rowsPerPage = rangeInfo.rowCount + 1
  716. for (const key in baseData) {
  717. if (bindingPathSet.has(key)) continue
  718. const val = baseData[key]
  719. if (Array.isArray(val)) {
  720. const start = (pageNo - 1) * rowsPerPage
  721. pageData[key] = val.slice(start, start + rowsPerPage)
  722. }
  723. }
  724. for (const oi of arrayFieldOffsetInfos) {
  725. const { fullArray, fields } = oi
  726. const copyMinIndex = Math.min(...fields.map(f => f.index))
  727. const start = (copyMinIndex - 1) + (pageNo - 1) * oi.rowsPerPage
  728. let firstIdx = -1, lastIdx = -1
  729. let firstVal = undefined
  730. for (const f of fields) {
  731. const idx = start + (f.index - copyMinIndex)
  732. if (firstIdx === -1) { firstIdx = idx; firstVal = idx < fullArray.length ? fullArray[idx] : null }
  733. lastIdx = idx
  734. pageData[f.path] = idx < fullArray.length ? fullArray[idx] : null
  735. }
  736. console.log(`[buildPageDataSource] pageNo=${pageNo}, prefix=${oi.prefix}, start=${start}, idxRange=[${firstIdx},${lastIdx}], firstIdxVal=${JSON.stringify(firstVal)}, fullArrayLen=${fullArray.length}, copyMinIndex=${copyMinIndex}, rowsPerPage=${oi.rowsPerPage}`)
  737. }
  738. return pageData
  739. }
  740. const distributeArrayFieldToCells = () => {
  741. if (!generateData || !generateData.copyRange) {
  742. console.log('[distributeArrayFieldToCells] 跳过: generateData/copyRange 为空')
  743. return
  744. }
  745. if (!sheetData || !Object.keys(sheetData).length) {
  746. console.log('[distributeArrayFieldToCells] 跳过: sheetData 为空')
  747. return
  748. }
  749. const rangeInfo = getCopyRangeInfo()
  750. if (!rangeInfo) {
  751. console.log('[distributeArrayFieldToCells] 跳过: rangeInfo 为空')
  752. return
  753. }
  754. const baseSheet = previewSpread.getSheetFromName(generateData.sheetName)
  755. if (!baseSheet) {
  756. console.log('[distributeArrayFieldToCells] 跳过: 找不到基础 sheet')
  757. return
  758. }
  759. const pattern = /^(.+)_(\d+)$/
  760. const prefixGroups = {}
  761. for (let r = 0; r <= rangeInfo.rowCount; r++) {
  762. for (let c = 0; c <= rangeInfo.colCount; c++) {
  763. const bp = baseSheet.getBindingPath(rangeInfo.row + r, rangeInfo.col + c)
  764. if (bp) {
  765. const match = bp.match(pattern)
  766. if (match) {
  767. const pfx = match[1]
  768. if (!prefixGroups[pfx]) prefixGroups[pfx] = []
  769. prefixGroups[pfx].push({
  770. path: bp, prefix: pfx, index: parseInt(match[2]), rowInRange: r, col: c
  771. })
  772. }
  773. }
  774. }
  775. }
  776. console.log('[distributeArrayFieldToCells] copyRange 扫描到的前缀组:', Object.keys(prefixGroups))
  777. arrayFieldOffsetInfos = []
  778. const prefixSet = new Set()
  779. for (const pfx of Object.keys(prefixGroups)) {
  780. const fields = prefixGroups[pfx]
  781. let fullArray = null
  782. for (let i = 0; i <= 1000; i++) {
  783. const candidate = `${pfx}_${i}`
  784. const val = sheetData[candidate]
  785. if (Array.isArray(val) && val.length > 0) {
  786. fullArray = val
  787. break
  788. }
  789. }
  790. if (!fullArray) {
  791. console.log(`[distributeArrayFieldToCells] 前缀 "${pfx}" 未找到数组字段`)
  792. continue
  793. }
  794. for (let i = 0; i < fullArray.length; i++) {
  795. const key = `${pfx}_${i + 1}`
  796. sheetData[key] = fullArray[i]
  797. }
  798. arrayFieldOffsetInfos.push({ prefix: pfx, fullArray, rowsPerPage: rangeInfo.rowCount + 1, fields })
  799. prefixSet.add(pfx)
  800. console.log(`[distributeArrayFieldToCells] 前缀 "${pfx}" 成功: 数组长度=${fullArray.length}, rowsPerPage=${rangeInfo.rowCount}, copyRange 字段数=${fields.length}`)
  801. }
  802. if (arrayFieldOffsetInfos.length === 0) {
  803. console.log('[distributeArrayFieldToCells] 无有效数组,结束')
  804. return
  805. }
  806. previewSpread.suspendPaint()
  807. try {
  808. const copyStartRow = rangeInfo.row
  809. const copyRangeRowSet = new Set()
  810. for (let r = 0; r < rangeInfo.rowCount; r++) {
  811. copyRangeRowSet.add(copyStartRow + r)
  812. }
  813. for (const oi of arrayFieldOffsetInfos) {
  814. for (const f of oi.fields) {
  815. const idx = f.index - 1
  816. const cellRow = copyStartRow + f.rowInRange
  817. if (idx < oi.fullArray.length) {
  818. baseSheet.setValue(cellRow, rangeInfo.col + f.col, oi.fullArray[idx])
  819. }
  820. }
  821. }
  822. for (let r = 0; r < baseSheet.getRowCount(); r++) {
  823. if (copyRangeRowSet.has(r)) continue
  824. for (let c = 0; c < baseSheet.getColumnCount(); c++) {
  825. const bp = baseSheet.getBindingPath(r, c)
  826. if (bp) {
  827. const match = bp.match(pattern)
  828. if (match && prefixSet.has(match[1])) {
  829. const idx = parseInt(match[2]) - 1
  830. const oi = arrayFieldOffsetInfos.find(o => o.prefix === match[1])
  831. if (oi && idx < oi.fullArray.length) {
  832. baseSheet.setValue(r, c, oi.fullArray[idx])
  833. }
  834. }
  835. }
  836. }
  837. }
  838. const baseDS = baseSheet.getDataSource()
  839. if (baseDS && baseDS.rT) {
  840. for (const oi of arrayFieldOffsetInfos) {
  841. oi.fullArray.forEach((val, idx) => {
  842. baseDS.rT[`${oi.prefix}_${idx + 1}`] = val
  843. })
  844. }
  845. }
  846. } finally {
  847. previewSpread.resumePaint()
  848. }
  849. }
  850. const adjustBaseSheetArrays = () => {
  851. if (!generateData || !generateData.copyRange || !sheetData || !Object.keys(sheetData).length) return
  852. const rangeInfo = getCopyRangeInfo()
  853. if (!rangeInfo) return
  854. const baseSheet = previewSpread.getSheetFromName(generateData.sheetName)
  855. if (!baseSheet) return
  856. const bindingPaths = getCopyRangeBindingPaths(baseSheet, rangeInfo)
  857. const bindingPathSet = new Set(bindingPaths)
  858. const rowsPerPage = rangeInfo.rowCount + 1
  859. previewSpread.suspendPaint()
  860. try {
  861. for (let row = 0; row < baseSheet.getRowCount(); row++) {
  862. for (let col = 0; col < baseSheet.getColumnCount(); col++) {
  863. const bp = baseSheet.getBindingPath(row, col)
  864. if (bp && !bindingPathSet.has(bp) && Array.isArray(sheetData[bp])) {
  865. const val = baseSheet.getValue(row, col)
  866. if (Array.isArray(val)) {
  867. baseSheet.setValue(row, col, val.slice(0, rowsPerPage))
  868. }
  869. }
  870. }
  871. }
  872. } finally {
  873. previewSpread.resumePaint()
  874. }
  875. }
  876. const setPageNoToSheet = (sheet, pageNo, rangeInfo) => {
  877. if (!rangeInfo) return
  878. sheet.tag(String(pageNo))
  879. }
  880. let generateData
  881. let sheetNum = ref(2)
  882. /**
  883. * 生成续页
  884. * @param {boolean} isSetTimeout 是否设置定时器
  885. */
  886. const handleGenerate = async (isSetTimeout) => {
  887. if (!generateData || !generateData.sheetName){
  888. ElMessage.error('请配置模板续页名称')
  889. return
  890. }
  891. /**
  892. * 生成续页
  893. * @param isAdd 是否继续生成
  894. */
  895. const generate = async (isAdd) => {
  896. const sheet = previewSpread.getSheetFromName(generateData.sheetName)
  897. if (!sheet.visible()) {
  898. sheet.visible(true)
  899. }
  900. console.log('生成续页',generateData.sheetName + " (" + sheetNum.value + ")");
  901. previewSpread.commandManager().execute({
  902. cmd: "copySheet",
  903. sheetName: generateData.sheetName,
  904. targetIndex: 999,
  905. newName: generateData.sheetName + " (" + sheetNum.value + ")",
  906. includeBindingSource: true
  907. });
  908. // 范围内的字段叠加
  909. let col = parseInt(generateData.copyRange.topLeft.split(',')[0])
  910. let row = parseInt(generateData.copyRange.topLeft.split(',')[1])
  911. let colCount = parseInt(generateData.copyRange.topRight.split(',')[0]) - col
  912. let rowCount = parseInt(generateData.copyRange.bottomLeft.split(',')[1]) - row
  913. const sheet2 = previewSpread.getSheetFromName(generateData.sheetName + " (" + sheetNum.value + ")")
  914. const rangeInfo = getCopyRangeInfo()
  915. const pageNo = sheetNum.value
  916. sheetNum.value = sheetNum.value + 1
  917. // 先设页码(tag 持久化),后续 dataSource 会覆盖左上角单元格为正确数据
  918. setPageNoToSheet(sheet2, pageNo, rangeInfo)
  919. // 解绑 copySheet 带来的旧数据源,再绑定新数据源,防止旧数据覆盖新值
  920. if (sheet2) {
  921. sheet2.setDataSource(null)
  922. }
  923. if (sheet2 && sheetData && Object.keys(sheetData).length > 0) {
  924. const pageData = buildPageDataSource(sheetData, pageNo)
  925. const dataSource = new GC.Spread.Sheets.Bindings.CellBindingSource(pageData)
  926. sheet2.setDataSource(dataSource)
  927. // 验证:读取续页 copyRange 第一行和最后一行实际单元格值
  928. if (rangeInfo) {
  929. const firstRowVals = []
  930. const lastRowVals = []
  931. for (let c = 0; c <= rangeInfo.colCount; c++) {
  932. const bp = sheet2.getBindingPath(rangeInfo.row, rangeInfo.col + c)
  933. const firstVal = sheet2.getValue(rangeInfo.row, rangeInfo.col + c)
  934. const lastVal = sheet2.getValue(rangeInfo.row + rangeInfo.rowCount, rangeInfo.col + c)
  935. firstRowVals.push(`${bp || '?'}=${JSON.stringify(firstVal)}`)
  936. lastRowVals.push(`${bp || '?'}=${JSON.stringify(lastVal)}`)
  937. }
  938. console.log(`[generate] pageNo=${pageNo} 续页第一行:`, firstRowVals.join(' | '))
  939. console.log(`[generate] pageNo=${pageNo} 续页最后行:`, lastRowVals.join(' | '))
  940. }
  941. }
  942. if (isAdd){
  943. let col = parseInt(generateData.copyRange.topLeft.split(',')[0])
  944. let row = parseInt(generateData.copyRange.topLeft.split(',')[1])
  945. let colCount = parseInt(generateData.copyRange.topRight.split(',')[0]) - col
  946. let rowCount = parseInt(generateData.copyRange.bottomLeft.split(',')[1]) - row
  947. if (!sheet2) {
  948. return;
  949. }
  950. if (generateData.hidden) {
  951. let isEmpty = true;
  952. for (let i = 0; i <= colCount; i++) {
  953. if (i === 0) continue
  954. const range = sheet2.getCell(row, col + i)
  955. if (range.value()) {
  956. isEmpty = false;
  957. break;
  958. }
  959. }
  960. if (isEmpty) {
  961. sheet2.visible(false)
  962. }
  963. }
  964. let isNotEmpty = false;
  965. for (let i = 0; i <= colCount; i++) {
  966. const range = sheet2.getCell(row + rowCount, col + i)
  967. if (range.value()) {
  968. isNotEmpty = true;
  969. break;
  970. }
  971. }
  972. console.log(`[generate] pageNo=${pageNo} 单元格检测 isNotEmpty=${isNotEmpty} (检查最后行 row=${row + rowCount}, 列范围=[${col},${col + colCount}])`)
  973. if (!isNotEmpty && arrayFieldOffsetInfos.length > 0) {
  974. for (const oi of arrayFieldOffsetInfos) {
  975. const copyMinIndex = Math.min(...oi.fields.map(f => f.index))
  976. const usedSoFar = (copyMinIndex - 1) + pageNo * oi.rowsPerPage
  977. console.log(`[generate] 续页检查 pageNo=${pageNo}, prefix=${oi.prefix}, usedSoFar=${usedSoFar}, fullArrayLen=${oi.fullArray.length}, 是否需续页=${oi.fullArray.length > usedSoFar}`)
  978. if (oi.fullArray.length > usedSoFar) {
  979. isNotEmpty = true
  980. break
  981. }
  982. }
  983. }
  984. if (isNotEmpty) {
  985. await generate(true)
  986. }
  987. }
  988. }
  989. if (isSetTimeout) {
  990. const loading = ElLoading.service({text: '正在生成中'})
  991. setTimeout(async () => {
  992. try {
  993. await generate(false)
  994. } finally {
  995. loading.close()
  996. }
  997. }, 20)
  998. } else {
  999. const loading = ElLoading.service({text: '生成续页中,数据量较大'})
  1000. await generate(true)
  1001. loading.close()
  1002. }
  1003. }
  1004. /**
  1005. * 隐藏续页
  1006. */
  1007. const hiddenPage = async () => {
  1008. sheetNum.value = 2
  1009. const data = await DynamicTbApi.getDynamicTb(props.initData.templateId)
  1010. if (data && data.copyConfig) {
  1011. generateData = data.copyConfig
  1012. if (!generateData.isContinuePage){
  1013. return
  1014. }
  1015. } else {
  1016. return
  1017. }
  1018. // 计算坐标
  1019. let col = parseInt(generateData.copyRange.topLeft.split(',')[0])
  1020. let row = parseInt(generateData.copyRange.topLeft.split(',')[1])
  1021. let colCount = parseInt(generateData.copyRange.topRight.split(',')[0]) - col
  1022. let rowCount = parseInt(generateData.copyRange.bottomLeft.split(',')[1]) - row
  1023. distributeArrayFieldToCells()
  1024. const sheet = previewSpread.getSheetFromName(generateData.sheetName)
  1025. if (!sheet) {
  1026. return;
  1027. }
  1028. if (generateData.hidden) {
  1029. let isEmpty = true;
  1030. for (let i = 0; i <= colCount; i++) {
  1031. if (i === 0) continue
  1032. const range = sheet.getCell(row, col + i)
  1033. if (range.value()) {
  1034. isEmpty = false;
  1035. break;
  1036. }
  1037. }
  1038. if (isEmpty) {
  1039. sheet.visible(false)
  1040. }
  1041. }
  1042. let isNotEmpty = false;
  1043. for (let i = 0; i <= colCount; i++) {
  1044. const range = sheet.getCell(row + rowCount, col + i)
  1045. if (range.value()) {
  1046. isNotEmpty = true;
  1047. break;
  1048. }
  1049. }
  1050. console.log('[hiddenPage] 单元格检测 isNotEmpty:', isNotEmpty, '| arrayFieldOffsetInfos:', arrayFieldOffsetInfos.length)
  1051. if (arrayFieldOffsetInfos.length > 0) {
  1052. for (const oi of arrayFieldOffsetInfos) {
  1053. const copyMinIndex = Math.min(...oi.fields.map(f => f.index))
  1054. const usedOnPage1 = (copyMinIndex - 1) + oi.rowsPerPage
  1055. console.log(`[hiddenPage] 前缀=${oi.prefix}, 数组长度=${oi.fullArray.length}, copyMinIndex=${copyMinIndex}, rowsPerPage=${oi.rowsPerPage}, usedOnPage1=${usedOnPage1}`)
  1056. if (oi.fullArray.length > usedOnPage1) {
  1057. isNotEmpty = true
  1058. break
  1059. }
  1060. }
  1061. }
  1062. console.log('[hiddenPage] 最终 isNotEmpty:', isNotEmpty)
  1063. if (isNotEmpty) {
  1064. await handleGenerate(false)
  1065. adjustBaseSheetArrays()
  1066. } else {
  1067. distributePageDataSources()
  1068. }
  1069. }
  1070. /**
  1071. * 获取数据
  1072. */
  1073. const getData = () => {
  1074. let dataSource = {};
  1075. previewSpread.sheets.forEach((sheet) => {
  1076. const sheetData = collectSheetDataSource(sheet);
  1077. for (const key in sheetData) {
  1078. if (dataSource[key] && sheetData[key] == sheetData[key]) {
  1079. } else {
  1080. dataSource[key] = sheetData[key];
  1081. }
  1082. }
  1083. collectShapesIntoDataSource(sheet, dataSource);
  1084. });
  1085. return dataSource;
  1086. }
  1087. /**
  1088. * 数据绑定后同步表格样式——将第一行(模板行)的合并规则、样式、行高复制到所有自动扩展的数据行
  1089. * 参考 SpreadDesigner 的 handleSheetTableCopyToOptimized
  1090. */
  1091. const copyOptions = GC.Spread.Sheets.CopyToOptions.style | GC.Spread.Sheets.CopyToOptions.formula
  1092. const handleTableRowsAfterDataBind = (sheets) => {
  1093. previewSpread.suspendPaint()
  1094. try {
  1095. for (const sheet of sheets) {
  1096. const tables = sheet.tables?.all() || []
  1097. for (const table of tables) {
  1098. const range = table.range()
  1099. const { row, rowCount, col, colCount } = range
  1100. if (rowCount <= 1) continue
  1101. // 批量设置自动换行
  1102. sheet.getRange(row, col, rowCount, colCount).wordWrap(true)
  1103. // 表格内所有单元格设为可编辑
  1104. sheet.getRange(row, col, rowCount, colCount).allowEditInCell(true)
  1105. sheet.getRange(row, col, rowCount, colCount).locked(false)
  1106. // 收集第一行的合并规则
  1107. const firstRowSpans = collectFirstRowSpans(sheet, row, col, colCount)
  1108. // 复制样式+行高 并 应用合并
  1109. const firstRowHeight = sheet.getRowHeight(row)
  1110. for (let r = 1; r < rowCount; r++) {
  1111. const currentRow = row + r
  1112. // 复制第一行样式和公式到当前数据行
  1113. sheet.copyTo(row, col, currentRow, col, 1, colCount, copyOptions)
  1114. // 复制行高
  1115. sheet.setRowHeight(currentRow, firstRowHeight)
  1116. // 应用合并规则
  1117. if (firstRowSpans.length > 0) {
  1118. applyMergeRules(sheet, currentRow, firstRowSpans)
  1119. }
  1120. }
  1121. }
  1122. }
  1123. } finally {
  1124. previewSpread.resumePaint()
  1125. }
  1126. }
  1127. /** 收集第一行(模板行)的合并单元格规则 */
  1128. const collectFirstRowSpans = (sheet, row, col, colCount) => {
  1129. const firstRowSpans = []
  1130. for (let c = col; c < col + colCount; c++) {
  1131. const span = sheet.getSpan(row, c)
  1132. if (span && span.row === row) {
  1133. firstRowSpans.push({ startCol: span.col, colCount: span.colCount })
  1134. c += span.colCount - 1
  1135. }
  1136. }
  1137. // 如果没有预置合并单元格,根据第一行内容自动识别(相邻同值列合并)
  1138. if (firstRowSpans.length === 0) {
  1139. const firstRowValues = []
  1140. for (let c = col; c < col + colCount; c++) {
  1141. firstRowValues.push(sheet.getValue(row, c))
  1142. }
  1143. let startMergeCol = 0
  1144. for (let c = 1; c <= colCount; c++) {
  1145. if (c === colCount || firstRowValues[c] !== firstRowValues[c - 1]) {
  1146. if (c - startMergeCol > 1) {
  1147. const startCol = col + startMergeCol
  1148. const mergeColCount = c - startMergeCol
  1149. firstRowSpans.push({ startCol, colCount: mergeColCount })
  1150. sheet.addSpan(row, startCol, 1, mergeColCount)
  1151. }
  1152. startMergeCol = c
  1153. }
  1154. }
  1155. }
  1156. return firstRowSpans
  1157. }
  1158. /** 将合并规则应用到指定行(直接按模板列的 span 结构合并,不做内容相等校验) */
  1159. const applyMergeRules = (sheet, currentRow, firstRowSpans) => {
  1160. firstRowSpans.forEach((mergeInfo) => {
  1161. try {
  1162. sheet.addSpan(currentRow, mergeInfo.startCol, 1, mergeInfo.colCount)
  1163. } catch (error) {
  1164. // 忽略合并失败
  1165. }
  1166. })
  1167. }
  1168. defineExpose({reloadView:initPreview, handleSave, getWorkbook: () => previewSpread,getData});
  1169. const emit = defineEmits(['saveSuccess','docReady','docCreate','saveFail','close'])
  1170. // 动态计算PDF查看器尺寸
  1171. const calculatePdfSize = () => {
  1172. pdfContentWidth.value = pdfViewer.value.clientWidth - 45
  1173. pdfViewerHeight.value = pdfViewer.value.clientHeight
  1174. console.log('props.pdfUrl', pdfContentWidth.value, pdfViewerHeight.value)
  1175. }
  1176. onMounted(() => {
  1177. window.addEventListener('resize', calculatePdfSize)
  1178. console.log("SpreadViewer,onMounted:"+JSON.stringify(props.initData));
  1179. //initPreview();
  1180. })
  1181. onUnmounted(()=>{
  1182. window.removeEventListener('resize', calculatePdfSize)
  1183. previewSpread?.destroy();
  1184. })
  1185. </script>
  1186. <style lang="scss" scoped>
  1187. :deep(.default-toolbar) {
  1188. padding-top: 0;
  1189. }
  1190. .lab-designer-container {
  1191. position: relative;
  1192. display: flex;
  1193. flex-direction: column;
  1194. align-items: stretch;
  1195. /*height: calc(100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 140px);*/
  1196. height: calc(100vh - var(--top-tool-height) - 5px );
  1197. .spread-designer-container {
  1198. width: calc(100% - 440px);
  1199. height: 100%;
  1200. padding: 0;
  1201. }
  1202. }
  1203. .lab-designer-container-fullscreen {
  1204. position: relative;
  1205. display: flex;
  1206. flex-direction: column;
  1207. align-items: stretch;
  1208. /*height: calc(100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 140px);*/
  1209. height: 100%;
  1210. .spread-designer-container {
  1211. width: calc(100% - 440px);
  1212. height: 100%;
  1213. padding: 0;
  1214. }
  1215. }
  1216. .header-row {
  1217. display: flex;
  1218. justify-content: space-between;
  1219. align-items: center;
  1220. padding: 0px 20px 16px 20px;
  1221. background: #fff;
  1222. border-bottom: 1px solid #e8e8e8;
  1223. }
  1224. .header-row .title {
  1225. font-size: 16px;
  1226. font-weight: 600;
  1227. color: #303133;
  1228. }
  1229. .unit-footer-btns {
  1230. align-items: center;
  1231. }
  1232. .dropdown-item-content {
  1233. display: flex;
  1234. align-items: center;
  1235. gap: 8px;
  1236. justify-content: center;
  1237. width: 100%;
  1238. }
  1239. .spread-container {
  1240. flex: 1;
  1241. background: #f5f7fa;
  1242. overflow: auto;
  1243. padding: 0;
  1244. }
  1245. .pdf-viewer-container {
  1246. width: 100%;
  1247. height: calc(100% - 40px);
  1248. //background-color: #8E8E9D;
  1249. padding: 5px;
  1250. box-sizing: border-box;
  1251. overflow-y: auto;
  1252. }
  1253. .pdf-viewer-content {
  1254. display: flex;
  1255. justify-content: center;
  1256. align-items: center;
  1257. }
  1258. // Wingdings 符号选择样式
  1259. .wingdings-container {
  1260. max-height: 500px;
  1261. overflow-y: auto;
  1262. }
  1263. .wingdings-grid {
  1264. display: grid;
  1265. grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
  1266. gap: 12px;
  1267. padding: 10px;
  1268. }
  1269. .wingdings-item {
  1270. display: flex;
  1271. flex-direction: column;
  1272. align-items: center;
  1273. justify-content: center;
  1274. padding: 12px 8px;
  1275. border: 1px solid #dcdfe6;
  1276. border-radius: 4px;
  1277. cursor: pointer;
  1278. transition: all 0.3s;
  1279. &:hover {
  1280. border-color: #409eff;
  1281. background-color: #ecf5ff;
  1282. transform: translateY(-2px);
  1283. box-shadow: 0 2px 8px rgba(64, 158, 255, 0.2);
  1284. }
  1285. }
  1286. .wingdings-symbol {
  1287. font-size: 24px;
  1288. margin-bottom: 4px;
  1289. color: #303133;
  1290. }
  1291. .wingdings-label {
  1292. font-size: 12px;
  1293. color: #909399;
  1294. text-align: center;
  1295. word-break: break-all;
  1296. }
  1297. </style>