SpreadViewer.vue 45 KB

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