SpreadViewer.vue 45 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428
  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. if (firstRowSpans.length > 0) {
  649. applyMergeRules(activeSheet, newRowIndex, firstRowSpans)
  650. }
  651. } finally {
  652. previewSpread.resumePaint()
  653. }
  654. ElMessage.success('已复制行')
  655. }
  656. const close = () => {
  657. emit('close')
  658. }
  659. const getPageNoFromSheet = (sheet) => {
  660. const tag = sheet.tag()
  661. if (tag !== undefined && tag !== null && tag !== '' && !isNaN(parseInt(tag))) {
  662. return parseInt(tag)
  663. }
  664. const name = sheet.name()
  665. const match = name.match(/\((\d+)\)\s*$/)
  666. if (match) {
  667. return parseInt(match[1])
  668. }
  669. return 1
  670. }
  671. const getCopyRangeInfo = () => {
  672. if (!generateData || !generateData.copyRange) return null
  673. const col = parseInt(generateData.copyRange.topLeft.split(',')[0])
  674. const row = parseInt(generateData.copyRange.topLeft.split(',')[1])
  675. const colCount = parseInt(generateData.copyRange.topRight.split(',')[0]) - col
  676. const rowCount = parseInt(generateData.copyRange.bottomLeft.split(',')[1]) - row
  677. return { col, row, colCount, rowCount }
  678. }
  679. const getCopyRangeBindingPaths = (sheet, rangeInfo) => {
  680. if (!sheet || !rangeInfo) return []
  681. const { col, row, colCount, rowCount } = rangeInfo
  682. const paths = []
  683. for (let i = 0; i <= colCount; i++) {
  684. for (let j = 0; j <= rowCount; j++) {
  685. const bindingPath = sheet.getBindingPath(row + j, col + i)
  686. if (bindingPath) {
  687. paths.push(bindingPath)
  688. }
  689. }
  690. }
  691. return paths
  692. }
  693. const buildPageDataSource = (baseData, pageNo) => {
  694. if (!generateData || !generateData.copyRange) return baseData
  695. const rangeInfo = getCopyRangeInfo()
  696. if (!rangeInfo) return baseData
  697. const sheet = previewSpread.getSheetFromName(generateData.sheetName)
  698. if (!sheet) return baseData
  699. const pageData = { ...baseData }
  700. const bindingPaths = getCopyRangeBindingPaths(sheet, rangeInfo)
  701. const bindingPathSet = new Set(bindingPaths)
  702. for (const path of bindingPaths) {
  703. const val = baseData[path]
  704. if (Array.isArray(val)) {
  705. pageData[path] = val[pageNo - 1] !== undefined ? val[pageNo - 1] : null
  706. } else if (pageNo > 1) {
  707. pageData[path] = null
  708. }
  709. }
  710. const rowsPerPage = rangeInfo.rowCount + 1
  711. for (const key in baseData) {
  712. if (bindingPathSet.has(key)) continue
  713. const val = baseData[key]
  714. if (Array.isArray(val)) {
  715. const start = (pageNo - 1) * rowsPerPage
  716. pageData[key] = val.slice(start, start + rowsPerPage)
  717. }
  718. }
  719. for (const oi of arrayFieldOffsetInfos) {
  720. const { fullArray, fields } = oi
  721. const copyMinIndex = Math.min(...fields.map(f => f.index))
  722. const start = (copyMinIndex - 1) + (pageNo - 1) * oi.rowsPerPage
  723. let firstIdx = -1, lastIdx = -1
  724. let firstVal = undefined
  725. for (const f of fields) {
  726. const idx = start + (f.index - copyMinIndex)
  727. if (firstIdx === -1) { firstIdx = idx; firstVal = idx < fullArray.length ? fullArray[idx] : null }
  728. lastIdx = idx
  729. pageData[f.path] = idx < fullArray.length ? fullArray[idx] : null
  730. }
  731. 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}`)
  732. }
  733. return pageData
  734. }
  735. const distributeArrayFieldToCells = () => {
  736. if (!generateData || !generateData.copyRange) {
  737. console.log('[distributeArrayFieldToCells] 跳过: generateData/copyRange 为空')
  738. return
  739. }
  740. if (!sheetData || !Object.keys(sheetData).length) {
  741. console.log('[distributeArrayFieldToCells] 跳过: sheetData 为空')
  742. return
  743. }
  744. const rangeInfo = getCopyRangeInfo()
  745. if (!rangeInfo) {
  746. console.log('[distributeArrayFieldToCells] 跳过: rangeInfo 为空')
  747. return
  748. }
  749. const baseSheet = previewSpread.getSheetFromName(generateData.sheetName)
  750. if (!baseSheet) {
  751. console.log('[distributeArrayFieldToCells] 跳过: 找不到基础 sheet')
  752. return
  753. }
  754. const pattern = /^(.+)_(\d+)$/
  755. const prefixGroups = {}
  756. for (let r = 0; r <= rangeInfo.rowCount; r++) {
  757. for (let c = 0; c <= rangeInfo.colCount; c++) {
  758. const bp = baseSheet.getBindingPath(rangeInfo.row + r, rangeInfo.col + c)
  759. if (bp) {
  760. const match = bp.match(pattern)
  761. if (match) {
  762. const pfx = match[1]
  763. if (!prefixGroups[pfx]) prefixGroups[pfx] = []
  764. prefixGroups[pfx].push({
  765. path: bp, prefix: pfx, index: parseInt(match[2]), rowInRange: r, col: c
  766. })
  767. }
  768. }
  769. }
  770. }
  771. console.log('[distributeArrayFieldToCells] copyRange 扫描到的前缀组:', Object.keys(prefixGroups))
  772. arrayFieldOffsetInfos = []
  773. const prefixSet = new Set()
  774. for (const pfx of Object.keys(prefixGroups)) {
  775. const fields = prefixGroups[pfx]
  776. let fullArray = null
  777. for (let i = 0; i <= 1000; i++) {
  778. const candidate = `${pfx}_${i}`
  779. const val = sheetData[candidate]
  780. if (Array.isArray(val) && val.length > 0) {
  781. fullArray = val
  782. break
  783. }
  784. }
  785. if (!fullArray) {
  786. console.log(`[distributeArrayFieldToCells] 前缀 "${pfx}" 未找到数组字段`)
  787. continue
  788. }
  789. for (let i = 0; i < fullArray.length; i++) {
  790. const key = `${pfx}_${i + 1}`
  791. sheetData[key] = fullArray[i]
  792. }
  793. arrayFieldOffsetInfos.push({ prefix: pfx, fullArray, rowsPerPage: rangeInfo.rowCount + 1, fields })
  794. prefixSet.add(pfx)
  795. console.log(`[distributeArrayFieldToCells] 前缀 "${pfx}" 成功: 数组长度=${fullArray.length}, rowsPerPage=${rangeInfo.rowCount}, copyRange 字段数=${fields.length}`)
  796. }
  797. if (arrayFieldOffsetInfos.length === 0) {
  798. console.log('[distributeArrayFieldToCells] 无有效数组,结束')
  799. return
  800. }
  801. previewSpread.suspendPaint()
  802. try {
  803. const copyStartRow = rangeInfo.row
  804. const copyRangeRowSet = new Set()
  805. for (let r = 0; r < rangeInfo.rowCount; r++) {
  806. copyRangeRowSet.add(copyStartRow + r)
  807. }
  808. for (const oi of arrayFieldOffsetInfos) {
  809. for (const f of oi.fields) {
  810. const idx = f.index - 1
  811. const cellRow = copyStartRow + f.rowInRange
  812. if (idx < oi.fullArray.length) {
  813. baseSheet.setValue(cellRow, rangeInfo.col + f.col, oi.fullArray[idx])
  814. }
  815. }
  816. }
  817. for (let r = 0; r < baseSheet.getRowCount(); r++) {
  818. if (copyRangeRowSet.has(r)) continue
  819. for (let c = 0; c < baseSheet.getColumnCount(); c++) {
  820. const bp = baseSheet.getBindingPath(r, c)
  821. if (bp) {
  822. const match = bp.match(pattern)
  823. if (match && prefixSet.has(match[1])) {
  824. const idx = parseInt(match[2]) - 1
  825. const oi = arrayFieldOffsetInfos.find(o => o.prefix === match[1])
  826. if (oi && idx < oi.fullArray.length) {
  827. baseSheet.setValue(r, c, oi.fullArray[idx])
  828. }
  829. }
  830. }
  831. }
  832. }
  833. const baseDS = baseSheet.getDataSource()
  834. if (baseDS && baseDS.rT) {
  835. for (const oi of arrayFieldOffsetInfos) {
  836. oi.fullArray.forEach((val, idx) => {
  837. baseDS.rT[`${oi.prefix}_${idx + 1}`] = val
  838. })
  839. }
  840. }
  841. } finally {
  842. previewSpread.resumePaint()
  843. }
  844. }
  845. const adjustBaseSheetArrays = () => {
  846. if (!generateData || !generateData.copyRange || !sheetData || !Object.keys(sheetData).length) return
  847. const rangeInfo = getCopyRangeInfo()
  848. if (!rangeInfo) return
  849. const baseSheet = previewSpread.getSheetFromName(generateData.sheetName)
  850. if (!baseSheet) return
  851. const bindingPaths = getCopyRangeBindingPaths(baseSheet, rangeInfo)
  852. const bindingPathSet = new Set(bindingPaths)
  853. const rowsPerPage = rangeInfo.rowCount + 1
  854. previewSpread.suspendPaint()
  855. try {
  856. for (let row = 0; row < baseSheet.getRowCount(); row++) {
  857. for (let col = 0; col < baseSheet.getColumnCount(); col++) {
  858. const bp = baseSheet.getBindingPath(row, col)
  859. if (bp && !bindingPathSet.has(bp) && Array.isArray(sheetData[bp])) {
  860. const val = baseSheet.getValue(row, col)
  861. if (Array.isArray(val)) {
  862. baseSheet.setValue(row, col, val.slice(0, rowsPerPage))
  863. }
  864. }
  865. }
  866. }
  867. } finally {
  868. previewSpread.resumePaint()
  869. }
  870. }
  871. const setPageNoToSheet = (sheet, pageNo, rangeInfo) => {
  872. if (!rangeInfo) return
  873. sheet.tag(String(pageNo))
  874. }
  875. let generateData
  876. let sheetNum = ref(2)
  877. /**
  878. * 生成续页
  879. * @param {boolean} isSetTimeout 是否设置定时器
  880. */
  881. const handleGenerate = async (isSetTimeout) => {
  882. if (!generateData || !generateData.sheetName){
  883. ElMessage.error('请配置模板续页名称')
  884. return
  885. }
  886. /**
  887. * 生成续页
  888. * @param isAdd 是否继续生成
  889. */
  890. const generate = async (isAdd) => {
  891. const sheet = previewSpread.getSheetFromName(generateData.sheetName)
  892. if (!sheet.visible()) {
  893. sheet.visible(true)
  894. }
  895. console.log('生成续页',generateData.sheetName + " (" + sheetNum.value + ")");
  896. previewSpread.commandManager().execute({
  897. cmd: "copySheet",
  898. sheetName: generateData.sheetName,
  899. targetIndex: 999,
  900. newName: generateData.sheetName + " (" + sheetNum.value + ")",
  901. includeBindingSource: true
  902. });
  903. // 范围内的字段叠加
  904. let col = parseInt(generateData.copyRange.topLeft.split(',')[0])
  905. let row = parseInt(generateData.copyRange.topLeft.split(',')[1])
  906. let colCount = parseInt(generateData.copyRange.topRight.split(',')[0]) - col
  907. let rowCount = parseInt(generateData.copyRange.bottomLeft.split(',')[1]) - row
  908. const sheet2 = previewSpread.getSheetFromName(generateData.sheetName + " (" + sheetNum.value + ")")
  909. const rangeInfo = getCopyRangeInfo()
  910. const pageNo = sheetNum.value
  911. sheetNum.value = sheetNum.value + 1
  912. // 先设页码(tag 持久化),后续 dataSource 会覆盖左上角单元格为正确数据
  913. setPageNoToSheet(sheet2, pageNo, rangeInfo)
  914. // 解绑 copySheet 带来的旧数据源,再绑定新数据源,防止旧数据覆盖新值
  915. if (sheet2) {
  916. sheet2.setDataSource(null)
  917. }
  918. if (sheet2 && sheetData && Object.keys(sheetData).length > 0) {
  919. const pageData = buildPageDataSource(sheetData, pageNo)
  920. const dataSource = new GC.Spread.Sheets.Bindings.CellBindingSource(pageData)
  921. sheet2.setDataSource(dataSource)
  922. // 验证:读取续页 copyRange 第一行和最后一行实际单元格值
  923. if (rangeInfo) {
  924. const firstRowVals = []
  925. const lastRowVals = []
  926. for (let c = 0; c <= rangeInfo.colCount; c++) {
  927. const bp = sheet2.getBindingPath(rangeInfo.row, rangeInfo.col + c)
  928. const firstVal = sheet2.getValue(rangeInfo.row, rangeInfo.col + c)
  929. const lastVal = sheet2.getValue(rangeInfo.row + rangeInfo.rowCount, rangeInfo.col + c)
  930. firstRowVals.push(`${bp || '?'}=${JSON.stringify(firstVal)}`)
  931. lastRowVals.push(`${bp || '?'}=${JSON.stringify(lastVal)}`)
  932. }
  933. console.log(`[generate] pageNo=${pageNo} 续页第一行:`, firstRowVals.join(' | '))
  934. console.log(`[generate] pageNo=${pageNo} 续页最后行:`, lastRowVals.join(' | '))
  935. }
  936. }
  937. if (isAdd){
  938. let col = parseInt(generateData.copyRange.topLeft.split(',')[0])
  939. let row = parseInt(generateData.copyRange.topLeft.split(',')[1])
  940. let colCount = parseInt(generateData.copyRange.topRight.split(',')[0]) - col
  941. let rowCount = parseInt(generateData.copyRange.bottomLeft.split(',')[1]) - row
  942. if (!sheet2) {
  943. return;
  944. }
  945. if (generateData.hidden) {
  946. let isEmpty = true;
  947. for (let i = 0; i <= colCount; i++) {
  948. if (i === 0) continue
  949. const range = sheet2.getCell(row, col + i)
  950. if (range.value()) {
  951. isEmpty = false;
  952. break;
  953. }
  954. }
  955. if (isEmpty) {
  956. sheet2.visible(false)
  957. }
  958. }
  959. let isNotEmpty = false;
  960. for (let i = 0; i <= colCount; i++) {
  961. const range = sheet2.getCell(row + rowCount, col + i)
  962. if (range.value()) {
  963. isNotEmpty = true;
  964. break;
  965. }
  966. }
  967. console.log(`[generate] pageNo=${pageNo} 单元格检测 isNotEmpty=${isNotEmpty} (检查最后行 row=${row + rowCount}, 列范围=[${col},${col + colCount}])`)
  968. if (!isNotEmpty && arrayFieldOffsetInfos.length > 0) {
  969. for (const oi of arrayFieldOffsetInfos) {
  970. const copyMinIndex = Math.min(...oi.fields.map(f => f.index))
  971. const usedSoFar = (copyMinIndex - 1) + pageNo * oi.rowsPerPage
  972. console.log(`[generate] 续页检查 pageNo=${pageNo}, prefix=${oi.prefix}, usedSoFar=${usedSoFar}, fullArrayLen=${oi.fullArray.length}, 是否需续页=${oi.fullArray.length > usedSoFar}`)
  973. if (oi.fullArray.length > usedSoFar) {
  974. isNotEmpty = true
  975. break
  976. }
  977. }
  978. }
  979. if (isNotEmpty) {
  980. await generate(true)
  981. }
  982. }
  983. }
  984. if (isSetTimeout) {
  985. const loading = ElLoading.service({text: '正在生成中'})
  986. setTimeout(async () => {
  987. try {
  988. await generate(false)
  989. } finally {
  990. loading.close()
  991. }
  992. }, 20)
  993. } else {
  994. const loading = ElLoading.service({text: '生成续页中,数据量较大'})
  995. await generate(true)
  996. loading.close()
  997. }
  998. }
  999. /**
  1000. * 隐藏续页
  1001. */
  1002. const hiddenPage = async () => {
  1003. sheetNum.value = 2
  1004. const data = await DynamicTbApi.getDynamicTb(props.initData.templateId)
  1005. if (data && data.copyConfig) {
  1006. generateData = data.copyConfig
  1007. if (!generateData.isContinuePage){
  1008. return
  1009. }
  1010. } else {
  1011. return
  1012. }
  1013. // 计算坐标
  1014. let col = parseInt(generateData.copyRange.topLeft.split(',')[0])
  1015. let row = parseInt(generateData.copyRange.topLeft.split(',')[1])
  1016. let colCount = parseInt(generateData.copyRange.topRight.split(',')[0]) - col
  1017. let rowCount = parseInt(generateData.copyRange.bottomLeft.split(',')[1]) - row
  1018. distributeArrayFieldToCells()
  1019. const sheet = previewSpread.getSheetFromName(generateData.sheetName)
  1020. if (!sheet) {
  1021. return;
  1022. }
  1023. if (generateData.hidden) {
  1024. let isEmpty = true;
  1025. for (let i = 0; i <= colCount; i++) {
  1026. if (i === 0) continue
  1027. const range = sheet.getCell(row, col + i)
  1028. if (range.value()) {
  1029. isEmpty = false;
  1030. break;
  1031. }
  1032. }
  1033. if (isEmpty) {
  1034. sheet.visible(false)
  1035. }
  1036. }
  1037. let isNotEmpty = false;
  1038. for (let i = 0; i <= colCount; i++) {
  1039. const range = sheet.getCell(row + rowCount, col + i)
  1040. if (range.value()) {
  1041. isNotEmpty = true;
  1042. break;
  1043. }
  1044. }
  1045. console.log('[hiddenPage] 单元格检测 isNotEmpty:', isNotEmpty, '| arrayFieldOffsetInfos:', arrayFieldOffsetInfos.length)
  1046. if (arrayFieldOffsetInfos.length > 0) {
  1047. for (const oi of arrayFieldOffsetInfos) {
  1048. const copyMinIndex = Math.min(...oi.fields.map(f => f.index))
  1049. const usedOnPage1 = (copyMinIndex - 1) + oi.rowsPerPage
  1050. console.log(`[hiddenPage] 前缀=${oi.prefix}, 数组长度=${oi.fullArray.length}, copyMinIndex=${copyMinIndex}, rowsPerPage=${oi.rowsPerPage}, usedOnPage1=${usedOnPage1}`)
  1051. if (oi.fullArray.length > usedOnPage1) {
  1052. isNotEmpty = true
  1053. break
  1054. }
  1055. }
  1056. }
  1057. console.log('[hiddenPage] 最终 isNotEmpty:', isNotEmpty)
  1058. if (isNotEmpty) {
  1059. await handleGenerate(false)
  1060. adjustBaseSheetArrays()
  1061. } else {
  1062. distributePageDataSources()
  1063. }
  1064. }
  1065. /**
  1066. * 获取数据
  1067. */
  1068. const getData = () => {
  1069. let dataSource = {};
  1070. previewSpread.sheets.forEach((sheet) => {
  1071. const sheetData = collectSheetDataSource(sheet);
  1072. for (const key in sheetData) {
  1073. if (dataSource[key] && sheetData[key] == sheetData[key]) {
  1074. } else {
  1075. dataSource[key] = sheetData[key];
  1076. }
  1077. }
  1078. collectShapesIntoDataSource(sheet, dataSource);
  1079. });
  1080. return dataSource;
  1081. }
  1082. /**
  1083. * 数据绑定后同步表格样式——将第一行(模板行)的合并规则、样式、行高复制到所有自动扩展的数据行
  1084. * 参考 SpreadDesigner 的 handleSheetTableCopyToOptimized
  1085. */
  1086. const copyOptions = GC.Spread.Sheets.CopyToOptions.style | GC.Spread.Sheets.CopyToOptions.formula
  1087. const handleTableRowsAfterDataBind = (sheets) => {
  1088. previewSpread.suspendPaint()
  1089. try {
  1090. for (const sheet of sheets) {
  1091. const tables = sheet.tables?.all() || []
  1092. for (const table of tables) {
  1093. const range = table.range()
  1094. const { row, rowCount, col, colCount } = range
  1095. if (rowCount <= 1) continue
  1096. // 批量设置自动换行
  1097. sheet.getRange(row, col, rowCount, colCount).wordWrap(true)
  1098. // 表格内所有单元格设为可编辑
  1099. sheet.getRange(row, col, rowCount, colCount).allowEditInCell(true)
  1100. sheet.getRange(row, col, rowCount, colCount).locked(false)
  1101. // 收集第一行的合并规则
  1102. const firstRowSpans = collectFirstRowSpans(sheet, row, col, colCount)
  1103. // 复制样式+行高 并 应用合并
  1104. const firstRowHeight = sheet.getRowHeight(row)
  1105. for (let r = 1; r < rowCount; r++) {
  1106. const currentRow = row + r
  1107. // 复制第一行样式和公式到当前数据行
  1108. sheet.copyTo(row, col, currentRow, col, 1, colCount, copyOptions)
  1109. // 复制行高
  1110. sheet.setRowHeight(currentRow, firstRowHeight)
  1111. // 应用合并规则
  1112. if (firstRowSpans.length > 0) {
  1113. applyMergeRules(sheet, currentRow, firstRowSpans)
  1114. }
  1115. }
  1116. }
  1117. }
  1118. } finally {
  1119. previewSpread.resumePaint()
  1120. }
  1121. }
  1122. /** 收集第一行(模板行)的合并单元格规则 */
  1123. const collectFirstRowSpans = (sheet, row, col, colCount) => {
  1124. const firstRowSpans = []
  1125. for (let c = col; c < col + colCount; c++) {
  1126. const span = sheet.getSpan(row, c)
  1127. if (span && span.row === row) {
  1128. firstRowSpans.push({ startCol: span.col, colCount: span.colCount })
  1129. c += span.colCount - 1
  1130. }
  1131. }
  1132. // 如果没有预置合并单元格,根据第一行内容自动识别(相邻同值列合并)
  1133. if (firstRowSpans.length === 0) {
  1134. const firstRowValues = []
  1135. for (let c = col; c < col + colCount; c++) {
  1136. firstRowValues.push(sheet.getValue(row, c))
  1137. }
  1138. let startMergeCol = 0
  1139. for (let c = 1; c <= colCount; c++) {
  1140. if (c === colCount || firstRowValues[c] !== firstRowValues[c - 1]) {
  1141. if (c - startMergeCol > 1) {
  1142. const startCol = col + startMergeCol
  1143. const mergeColCount = c - startMergeCol
  1144. firstRowSpans.push({ startCol, colCount: mergeColCount })
  1145. sheet.addSpan(row, startCol, 1, mergeColCount)
  1146. }
  1147. startMergeCol = c
  1148. }
  1149. }
  1150. }
  1151. return firstRowSpans
  1152. }
  1153. /** 将合并规则应用到指定行(直接按模板列的 span 结构合并,不做内容相等校验) */
  1154. const applyMergeRules = (sheet, currentRow, firstRowSpans) => {
  1155. firstRowSpans.forEach((mergeInfo) => {
  1156. try {
  1157. sheet.addSpan(currentRow, mergeInfo.startCol, 1, mergeInfo.colCount)
  1158. } catch (error) {
  1159. // 忽略合并失败
  1160. }
  1161. })
  1162. }
  1163. defineExpose({reloadView:initPreview, handleSave, getWorkbook: () => previewSpread,getData});
  1164. const emit = defineEmits(['saveSuccess','docReady','docCreate','saveFail','close'])
  1165. // 动态计算PDF查看器尺寸
  1166. const calculatePdfSize = () => {
  1167. pdfContentWidth.value = pdfViewer.value.clientWidth - 45
  1168. pdfViewerHeight.value = pdfViewer.value.clientHeight
  1169. console.log('props.pdfUrl', pdfContentWidth.value, pdfViewerHeight.value)
  1170. }
  1171. onMounted(() => {
  1172. window.addEventListener('resize', calculatePdfSize)
  1173. console.log("SpreadViewer,onMounted:"+JSON.stringify(props.initData));
  1174. //initPreview();
  1175. })
  1176. onUnmounted(()=>{
  1177. window.removeEventListener('resize', calculatePdfSize)
  1178. previewSpread?.destroy();
  1179. })
  1180. </script>
  1181. <style lang="scss" scoped>
  1182. :deep(.default-toolbar) {
  1183. padding-top: 0;
  1184. }
  1185. .lab-designer-container {
  1186. position: relative;
  1187. display: flex;
  1188. flex-direction: column;
  1189. align-items: stretch;
  1190. /*height: calc(100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 140px);*/
  1191. height: calc(100vh - var(--top-tool-height) - 5px );
  1192. .spread-designer-container {
  1193. width: calc(100% - 440px);
  1194. height: 100%;
  1195. padding: 0;
  1196. }
  1197. }
  1198. .lab-designer-container-fullscreen {
  1199. position: relative;
  1200. display: flex;
  1201. flex-direction: column;
  1202. align-items: stretch;
  1203. /*height: calc(100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 140px);*/
  1204. height: 100%;
  1205. .spread-designer-container {
  1206. width: calc(100% - 440px);
  1207. height: 100%;
  1208. padding: 0;
  1209. }
  1210. }
  1211. .header-row {
  1212. display: flex;
  1213. justify-content: space-between;
  1214. align-items: center;
  1215. padding: 0px 20px 16px 20px;
  1216. background: #fff;
  1217. border-bottom: 1px solid #e8e8e8;
  1218. }
  1219. .header-row .title {
  1220. font-size: 16px;
  1221. font-weight: 600;
  1222. color: #303133;
  1223. }
  1224. .unit-footer-btns {
  1225. align-items: center;
  1226. }
  1227. .dropdown-item-content {
  1228. display: flex;
  1229. align-items: center;
  1230. gap: 8px;
  1231. justify-content: center;
  1232. width: 100%;
  1233. }
  1234. .spread-container {
  1235. flex: 1;
  1236. background: #f5f7fa;
  1237. overflow: auto;
  1238. padding: 0;
  1239. }
  1240. .pdf-viewer-container {
  1241. width: 100%;
  1242. height: calc(100% - 40px);
  1243. //background-color: #8E8E9D;
  1244. padding: 5px;
  1245. box-sizing: border-box;
  1246. overflow-y: auto;
  1247. }
  1248. .pdf-viewer-content {
  1249. display: flex;
  1250. justify-content: center;
  1251. align-items: center;
  1252. }
  1253. // Wingdings 符号选择样式
  1254. .wingdings-container {
  1255. max-height: 500px;
  1256. overflow-y: auto;
  1257. }
  1258. .wingdings-grid {
  1259. display: grid;
  1260. grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
  1261. gap: 12px;
  1262. padding: 10px;
  1263. }
  1264. .wingdings-item {
  1265. display: flex;
  1266. flex-direction: column;
  1267. align-items: center;
  1268. justify-content: center;
  1269. padding: 12px 8px;
  1270. border: 1px solid #dcdfe6;
  1271. border-radius: 4px;
  1272. cursor: pointer;
  1273. transition: all 0.3s;
  1274. &:hover {
  1275. border-color: #409eff;
  1276. background-color: #ecf5ff;
  1277. transform: translateY(-2px);
  1278. box-shadow: 0 2px 8px rgba(64, 158, 255, 0.2);
  1279. }
  1280. }
  1281. .wingdings-symbol {
  1282. font-size: 24px;
  1283. margin-bottom: 4px;
  1284. color: #303133;
  1285. }
  1286. .wingdings-label {
  1287. font-size: 12px;
  1288. color: #909399;
  1289. text-align: center;
  1290. word-break: break-all;
  1291. }
  1292. </style>