| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035 |
- <template>
- <div v-show="showSpread"
- :class="[!isFullscreen ? 'lab-designer-container' : 'lab-designer-container-fullscreen']"
- v-loading="loading" >
- <div class="header-row">
- <div class="title">
- <slot name="title"></slot>
- </div>
- <div class="unit-footer-btns">
- <el-dropdown trigger="click">
- <el-button type="primary" plain style="margin-right: 12px;">
- 更多操作<el-icon class="el-icon--right"><arrow-down /></el-icon>
- </el-button>
- <template #dropdown>
- <el-dropdown-menu>
- <el-dropdown-item @click="handleGenerate(true)">
- <div class="dropdown-item-content">
- <el-button type="warning" plain style="margin-right: 12px;">生成续页</el-button>
- </div>
- </el-dropdown-item>
- <el-dropdown-item>
- <div class="dropdown-item-content">
- <BatchUploadFile @uploadImg="addPic" :colList="colListData" />
- </div>
- </el-dropdown-item>
- <el-dropdown-item @click="openWingdingsDialog">
- <div class="dropdown-item-content">
- <el-button type="info" plain style="margin-right: 12px;">插入特殊符号</el-button>
- </div>
- </el-dropdown-item>
- <el-dropdown-item @click="handleCopyRow">
- <div class="dropdown-item-content">
- <el-button type="primary" plain style="margin-right: 12px;">复制表单</el-button>
- </div>
- </el-dropdown-item>
- </el-dropdown-menu>
- </template>
- </el-dropdown>
- <el-button type="success" @click="handleSave">保存</el-button>
- <el-button type="primary" @click="openPdf">预览 PDF</el-button>
- <el-button type="danger" @click="close">关闭</el-button>
- </div>
- </div>
- <div v-loading="loading" ref="previewContainer" class="spread-container"></div>
- </div>
- <div v-show="showPdf" class="pdf-viewer-container" v-loading="pdfLoading" ref="pdfViewer">
- <div class="pdf-viewer-content">
- <VuePdfEmbed
- :key="pdfTimestamp"
- :height="pdfViewerHeight"
- :width="pdfContentWidth"
- :source="recordSource"
- text-layer
- :annotation-layer="false"
- @rendered="handlePdfRendered"
- />
- </div>
- </div>
-
- <!-- Wingdings 符号选择弹窗 -->
- <el-dialog
- v-model="wingdingsDialogVisible"
- title="选择特殊符号"
- width="600px"
- :close-on-click-modal="false"
- >
- <div class="wingdings-container">
- <div class="wingdings-grid">
- <div
- v-for="(item, index) in getWingdingsList"
- :key="index"
- class="wingdings-item"
- @click="selectWingdings(item)"
- >
- <span class="wingdings-symbol">{{ item.value }}</span>
- <span class="wingdings-label">{{ item.label }}</span>
- </div>
- </div>
- </div>
- <template #footer>
- <el-button @click="wingdingsDialogVisible = false">取消</el-button>
- </template>
- </el-dialog>
- </template>
- <script lang="ts" setup>
- import '@grapecity-software/spread-sheets-designer-resources-cn'
- import * as GC from '@grapecity-software/spread-sheets'
- // 导入PDF导出模块
- import '@grapecity-software/spread-sheets-pdf'
- import VuePdfEmbed from 'vue-pdf-embed'
- import 'vue-pdf-embed/dist/styles/annotationLayer.css'
- import 'vue-pdf-embed/dist/styles/textLayer.css'
- import * as ExcelIO from "@grapecity-software/spread-excelio"
- import SpreadDesigner from '@/components/SpreadDesigner/index.vue'
- import { Printer, Download, Refresh, DocumentAdd, Picture, ArrowDown } from '@element-plus/icons-vue'
- import { useTagsViewStore } from '@/store/modules/tagsView'
- import {
- getPDF
- } from '@/api/laboratory/standard/template';
- import {
- getPDFByInspectionLocal,
- getStandardTemplateInfo,
- } from '@/api/pressure2/standard/template'
- import {ref, onMounted,onUnmounted} from "vue";
- import {InitParams} from './SpreadInterface';
- import { buildFileUrl } from '@/utils'
- import axios from 'axios'
- import { DynamicTbApi } from '@/api/pressure2/dynamictb'
- import {DynamicTbColApi} from "@/api/pressure2/dynamictbcol";
- import { ExcelApi } from '@/api/pressure2/excel'
- import { DynamicTbValApi } from '@/api/pressure2/dynamictbval'
- import BatchUploadFile from "./BatchUploadFile.vue";
- import {editReport, handleCopy, collectSheetDataSource, collectShapesIntoDataSource, serializeDataSourceForStorage} from "@/utils/reportUtil";
- import Designer from '@grapecity-software/spread-sheets-designer-vue'
- import { ElLoading } from "element-plus"
- import {getPDF2, getPDFByInspection} from "@/api/pressure2/standard/template";
- import { useDictStore } from '@/store/modules/dict'
- defineOptions({ name: 'SpreadViewer' });
- const props = defineProps({
- initData: {
- type: Object as PropType<InitParams>,
- default: ()=>({}),
- required: true
- },
- isFullscreen:{
- type: Boolean,
- default: false,
- required: false
- }
- });
- const insId=ref<string>(null);
- const colListData=ref([]);
- const addPic = (picData) => {
- if (previewSpread) {
- if (props.initData.refName == '红外热成像检测') {
- picData.name.sheet = previewSpread.getActiveSheet().name()
- previewSpread.getActiveSheet().shapes.addPictureShape(JSON.stringify(picData.name), picData.base, picData.name.x, picData.name.y, 240, 320);
- return
- }
- picData.name.sheet = previewSpread.getActiveSheet().name()
- previewSpread.getActiveSheet().shapes.addPictureShape(JSON.stringify(picData.name), picData.base, picData.name.x, picData.name.y, picData.name.width, picData.name.height);
- }
- }
- /*
- watch(()=>[props.initData.templateId,props.initData.refId],([newTide,newRid],[oldTid,oldRid])=>{
- if(newTide && newRid){
- initPreview();
- }
- }); */
- //const route = useRoute()
- // spread相关
- const tagsViewStore = useTagsViewStore()
- const dictStore = useDictStore()
- const loading = ref(true)
- const showSpread=ref(true)
- const previewContainer = ref(null)
- let sheetData = {};
- const designerKey = import.meta.env.VITE_SPREADJS_DESIGNER_KEY
- const nodeEnv = import.meta.env.VITE_NODE_ENV
- const licenseKey = import.meta.env.VITE_SPREADJS_LICENSE_KEY
- GC.Spread.Sheets.Designer.LicenseKey = designerKey
- //同时对SpreadJS与ExcelIO进行授权
- GC.Spread.Sheets.LicenseKey = licenseKey
- // pdf相关
- let previewSpread = null
- const pdfLoading = ref(false);
- const showPdf=ref(false);
- const recordSource = ref('')
- const pdfViewer = ref()
- const pdfContentWidth = ref(800)
- const pdfViewerHeight = ref(600)
- const pdfTimestamp = ref('')
- // Wingdings 弹窗相关
- const wingdingsDialogVisible = ref(false)
- const currentCell = ref(null)
- const getWingdingsList = computed(
- () => dictStore.getDictMap['Wingdings']
- )
- const handlePdfRendered = () => {
- pdfLoading.value = false
- }
- // 获取模版详情
- const fetchTemplateData = async () => {
- console.log(this)
- if(!props.initData.templateId){
- return;
- }
- const templateRes = await getStandardTemplateInfo({ id: props.initData.templateId })
- if (templateRes && templateRes.fileUrl) {
- const fileUrl = buildFileUrl(templateRes.fileUrl)
- const response = await axios.get(fileUrl, { responseType: 'blob' });
- return new Blob([response.data], { type: response.headers['content-type'] });
- }
- return;
- }
- const initPreview = async () => {
- const { refId, opType, templateId } = props.initData;
-
- if (!refId) {
- return false;
- }
-
- loading.value = true;
-
- if (opType === 0) {
- showSpread.value = true;
- pdfLoading.value = false;
- }
- previewSpread?.destroy();
- previewSpread = new GC.Spread.Sheets.Workbook(previewContainer.value);
- const handleOpenError = (error: any) => {
- console.error('文件打开失败:', error);
- loading.value = false;
- };
- const handleCopyIfNeeded = () => {
- if (opType === 0) {
- handleCopy(previewSpread);
- }
- };
- const initCommonData = async () => {
- const res = await DynamicTbValApi.getDynamicTbInsAndValByRefId(props.initData);
- insId.value = res.dynamicTbInsRespVO.id;
- dynamicTbColRespVOList = res.dynamicTbColRespVOList;
- colListData.value = res.dynamicTbColRespVOList.filter(i => i.colValType === 4);
- return res;
- };
- if (nodeEnv === 'dev' || nodeEnv === 'hst' || nodeEnv === 'uat') {
- try {
- // 1. 获取实例和字段数据(数据绑定留给前端处理,后端只做图片/高亮/续页处理)
- const res = await initCommonData();
- // 2. 调用后端接口获取处理好的 SJS
- const apiMethod = nodeEnv === 'dev' ? ExcelApi.process : ExcelApi.excel;
- const sjsBlob: any = await apiMethod({ templateId, refId });
- // 3. 打开 SJS 后在回调中绑定数据源
- previewSpread.open(sjsBlob, () => {
- console.log('后端处理完成,预览加载完成');
- const sheets = previewSpread.sheets;
- // 构建数据源并绑定(后端不 setDataSource,交由前端处理)
- // 后端已将 .jpg/.png 路径处理为背景图片,此处设 null 避免覆盖
- if (res.dynamicTbValRespVOList && res.dynamicTbValRespVOList.length > 0) {
- sheetData = {};
- res.dynamicTbValRespVOList.forEach(i => {
- const val = i.valValue;
- if (typeof val === 'string') {
- const trimmed = val.trim();
- // 后端已将图片路径值处理为背景图片,跳过避免文字覆盖
- if (trimmed.endsWith('.jpg') || trimmed.endsWith('.png')) {
- // sheetData[i.colCode] = null;
- } else if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
- if(i.colCode === 'Illustration'){
- sheetData[i.colCode] = null
- }else{
- try { sheetData[i.colCode] = JSON.parse(val); } catch { sheetData[i.colCode] = val; }
- }
- } else if (val == 'false' || val == 'true') {
- sheetData[i.colCode] = val == 'true' ? 'true' : null;
- } else {
- sheetData[i.colCode] = val;
- }
- } else {
- sheetData[i.colCode] = val;
- }
- });
- }
- let dataSource1 = new GC.Spread.Sheets.Bindings.CellBindingSource(sheetData);
- if (props.initData.dataSource) {
- dataSource1 = new GC.Spread.Sheets.Bindings.CellBindingSource(props.initData.dataSource);
- }
- sheets.forEach(sheet => sheet.setDataSource(dataSource1));
- // 表格行样式同步(数据绑定后前端处理)
- handleTableRowsAfterDataBind(sheets);
- // 续页处理
- hiddenPage().then(() => {
- if (opType === 1) {
- showSpread.value = false;
- pdfLoading.value = true;
- openPdf();
- } else {
- showPdf.value = false;
- loading.value = false;
- }
- });
- }, handleOpenError);
- handleCopyIfNeeded();
- } catch (error) {
- console.error('后端处理 Excel 失败:', error);
- loading.value = false;
- }
- return;
- }
- try {
- const blob = await fetchTemplateData();
- if (!blob) return;
- previewSpread.open(blob, async () => {
- console.log('预览加载完成');
- const sheets = previewSpread.sheets;
-
- const res = await DynamicTbValApi.getDynamicTbInsAndValByRefId(props.initData);
- insId.value = res.dynamicTbInsRespVO.id;
- if (res.dynamicTbValRespVOList && res.dynamicTbValRespVOList.length > 0) {
- sheetData = {};
- res.dynamicTbValRespVOList.forEach(i => {
- const val = i.valValue;
- if (typeof val === 'string') {
- const trimmed = val.trim();
- if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
- try {
- sheetData[i.colCode] = JSON.parse(val);
- } catch {
- sheetData[i.colCode] = val;
- }
- } else if (val == 'false' || val == 'true') {
- sheetData[i.colCode] = val == 'true' ? 'true' : null;
- } else {
- sheetData[i.colCode] = val;
- }
- } else {
- sheetData[i.colCode] = val;
- }
- });
- }
- let dataSource1 = new GC.Spread.Sheets.Bindings.CellBindingSource(sheetData);
- if (props.initData.dataSource) {
- dataSource1 = new GC.Spread.Sheets.Bindings.CellBindingSource(props.initData.dataSource);
- }
- sheets.forEach(sheet => sheet.setDataSource(dataSource1));
- dynamicTbColRespVOList = res.dynamicTbColRespVOList;
- const editCols = res.dynamicTbColRespVOList.filter(i => i.isEdit).map(i => i.colCode);
- const imgCols = res.dynamicTbColRespVOList.filter(i => i.colValType === 4).map(i => i.colCode);
- colListData.value = res.dynamicTbColRespVOList.filter(i => i.colValType === 4);
- for (const sheet of sheets) {
- await editReport(sheet, editCols, imgCols, opType);
- }
-
- handleTableRowsAfterDataBind(sheets);
- await hiddenPage();
- if (opType === 1) {
- showSpread.value = false;
- pdfLoading.value = true;
- await openPdf();
- } else {
- showPdf.value = false;
- loading.value = false;
- }
- }, handleOpenError);
- handleCopyIfNeeded();
- } catch (error) {
- console.error('预览加载失败:', error);
- }
- }
- let dynamicTbColRespVOList;
- const openPdf = async () => {
- const formData = new FormData()
- const rules = []
- previewSpread.sheets.forEach((sheet) => {
- sheet.conditionalFormats.getRules().forEach(rule => {
- if (rule.condition() && rule.condition().formula() == 'TRUE') {
- console.log(rule)
- rules.push({
- sheet,
- rule
- })
- }
- })
- rules.forEach(rule => {
- sheet.conditionalFormats.removeRule(rule.rule)
- })
- });
- await previewSpread.save(async function (blob) {
- formData.append('file', blob)
- let response = null
- //uat环境葡萄城pdf接口
- if (nodeEnv == 'uat') {
- // 手动拼接pdf的url,适配检验方案
- if (props.initData.manualUrl){
- formData.append('manualUrl', props.initData.manualUrl)
- response = await getPDFByInspection(formData)
- }else{
- response = await getPDF2(formData)
- }
- } else {
- //本地测试环境葡萄城pdf接口
- if (props.initData.manualUrl){
- // 手动拼接pdf的url,适配检验方案
- formData.append('manualUrl', props.initData.manualUrl)
- response = await getPDFByInspectionLocal(formData)
- }else{
- response = await getPDF2(formData)
- }
- }
- if (response) {
- const flow = new Blob([response], { type: 'application/pdf' })
- recordSource.value = window.URL.createObjectURL(flow);
- if(props.initData.opType==0){
- loading.value = false;
- window.open(recordSource.value, '_blank');
- }
- if(props.initData.opType==1){
- //showSpread.value=false;
- pdfTimestamp.value = Date.now();
- showPdf.value=true;
- pdfLoading.value=true;
- setTimeout(() => {
- calculatePdfSize()
- },100)
- }
- }
- }, (e) => {
- }, {
- includeBindingSource: true
- })
- previewSpread.sheets.forEach((sheet) => {
- rules.forEach(rule => {
- if (rule.sheet === sheet){
- sheet.conditionalFormats.addRule(rule.rule)
- }
- })
- });
- }
- const handleSpreadPrint = () => {
- if (!previewSpread) return
- previewSpread.print()
- }
- /**
- * 打开 Wingdings 符号选择弹窗
- */
- const openWingdingsDialog = () => {
- if (!previewSpread) {
- ElMessage.warning('请先加载文档')
- return
- }
-
- // 获取当前选中的单元格
- const activeSheet = previewSpread.getActiveSheet()
- const selections = activeSheet.getSelections()
-
- if (!selections || selections.length === 0) {
- ElMessage.warning('请先选择一个单元格')
- return
- }
-
- // 保存当前单元格信息
- const selection = selections[0]
- currentCell.value = {
- sheet: activeSheet,
- row: selection.row,
- col: selection.col
- }
-
- wingdingsDialogVisible.value = true
- }
- /**
- * 选择 Wingdings 符号并插入到单元格
- */
- const selectWingdings = (item) => {
- if (!currentCell.value) {
- ElMessage.error('未找到目标单元格')
- return
- }
-
- const { sheet, row, col } = currentCell.value
- const cell = sheet.getCell(row, col)
- // if (!cell.allowEditInCell()){
- // wingdingsDialogVisible.value = false
- // return;
- // }
- // 获取当前单元格的值
- const currentValue = cell.value() || ''
-
- // 追加符号到单元格
- const newValue = currentValue + item.value
- cell.value(newValue)
-
- ElMessage.success(`已插入符号: ${item.label}`)
- wingdingsDialogVisible.value = false
- }
- const handleSave = () => {
- loading.value = true;
- let dataSource = {};
- previewSpread.sheets.forEach((sheet) => {
- if (sheet.name() && sheet.name() !== '试用版警告') {
- // 收集 sheet 的数据源(兼容 table 绑定字段)
- const sheetData = collectSheetDataSource(sheet);
- console.log(sheetData)
- for (const key in sheetData) {
- if (dataSource[key] && sheetData[key] == sheetData[key]) {
- } else {
- dataSource[key] = sheetData[key];
- }
- }
- // 收集形状(浮动图片)数据
- collectShapesIntoDataSource(sheet, dataSource);
- }
- });
- if (Object.keys(dataSource).length > 0) {
- // 序列化为后端存储格式(对象/数组 → JSON 字符串)
- const serializedData = serializeDataSourceForStorage(dataSource);
- console.log(serializedData);
-
- DynamicTbValApi.saveAllColValue(insId.value, serializedData).then(res => {
- if (res) {
- ElMessage.success('保存成功')
- emit('saveSuccess', {insId:insId.value,dataSource:serializedData,refId:props.initData.refId});
- } else{
- ElMessage.error('保存失败')
- emit('saveFail');
- }
- }).catch(e => {
- ElMessage.error('保存失败')
- emit('saveFail');
- }).finally(() => {
- loading.value = false;
- });
- } else {
- loading.value = false;
- }
- }
- const handleCopyRow = () => {
- if (!previewSpread) return ElMessage.warning('请先加载文档')
- const activeSheet = previewSpread.getActiveSheet()
- const selections = activeSheet.getSelections()
- if (!selections || selections.length === 0) return ElMessage.warning('请先选择一个单元格')
- const { row, col } = selections[0]
- // 找到包含当前单元格的 table
- const tables = activeSheet.tables?.all() || []
- let targetTable = null
- for (const table of tables) {
- const range = table.range()
- if (row >= range.row && row < range.row + range.rowCount && col >= range.col && col < range.col + range.colCount) {
- targetTable = table
- break
- }
- }
- if (!targetTable) return ElMessage.warning('当前单元格不在表格内,请选中表格内的单元格')
- const range = targetTable.range()
- if (row < range.row + 1) return ElMessage.warning('请选中表格内的数据行')
- // 绑定源中追加空行,让 table 自动扩展一行
- const dataSource = activeSheet.getDataSource()
- const sourceData = dataSource?.getSource()
- const bindingPath = targetTable.bindingPath()
- if (!bindingPath || !sourceData?.[bindingPath] || !Array.isArray(sourceData[bindingPath])) {
- return ElMessage.warning('表格未绑定数据源')
- }
- // 在刷新数据源前,先收集第一行的合并规则(刷新时会清空)
- const { col: tableCol, colCount: tableColCount } = range
- const firstRowSpans = collectFirstRowSpans(activeSheet, range.row + 1, tableCol, tableColCount)
- // 记录新行位置(当前 rowCount 就是新增行的索引偏移)
- const newRowIndex = range.row + range.rowCount
- // 在绑定数组中追加空对象
- sourceData[bindingPath].push({})
- activeSheet.setDataSource(null)
- activeSheet.setDataSource(dataSource)
- // 复制样式/行高/合并到新行
- previewSpread.suspendPaint()
- try {
- const copyOps = GC.Spread.Sheets.CopyToOptions.style | GC.Spread.Sheets.CopyToOptions.formula
- activeSheet.copyTo(row, tableCol, newRowIndex, tableCol, 1, tableColCount, copyOps)
- activeSheet.setRowHeight(newRowIndex, activeSheet.getRowHeight(row))
- if (firstRowSpans.length > 0) {
- applyMergeRules(activeSheet, newRowIndex, firstRowSpans)
- }
- } finally {
- previewSpread.resumePaint()
- }
- ElMessage.success('已复制行')
- }
- const close = () => {
- emit('close')
- }
- let generateData
- let sheetNum = ref(2)
- /**
- * 生成续页
- * @param {boolean} isSetTimeout 是否设置定时器
- */
- const handleGenerate = async (isSetTimeout) => {
- if (!generateData || !generateData.sheetName){
- ElMessage.error('请配置模板续页名称')
- return
- }
- /**
- * 生成续页
- * @param isAdd 是否继续生成
- */
- const generate = async (isAdd) => {
- const sheet = previewSpread.getSheetFromName(generateData.sheetName)
- if (sheet.visible()) {
- previewSpread.commandManager().execute({
- cmd: "copySheet",
- sheetName: generateData.sheetName,
- targetIndex: 999,
- newName: generateData.sheetName + " (" + sheetNum.value + ")",
- includeBindingSource: true
- });
- } else {
- sheet.visible(true)
- return
- }
- // 范围内的字段叠加
- let col = parseInt(generateData.copyRange.topLeft.split(',')[0])
- let row = parseInt(generateData.copyRange.topLeft.split(',')[1])
- let colCount = parseInt(generateData.copyRange.topRight.split(',')[0]) - col
- let rowCount = parseInt(generateData.copyRange.bottomLeft.split(',')[1]) - row
- const sheet2 = previewSpread.getSheetFromName(generateData.sheetName + " (" + sheetNum.value + ")")
- let colPathList = []
- let dynamicTbColRespVOListCode = dynamicTbColRespVOList.map(i => i.colCode)
- for (let i = 0; i <= colCount; i++) {
- for (let j = 0; j <= rowCount; j++) {
- const bindingPath = sheet.getBindingPath(row + j, col + i)
- if (bindingPath) {
- let newPath = bindingPath.split('_')[0] + "_" + (parseInt(bindingPath.split('_')[1]) + (rowCount + 1) * (sheetNum.value - 1))
- sheet2.setBindingPath(row + j, col + i, newPath)
- if (!dynamicTbColRespVOListCode.includes(newPath)){
- colPathList.push(newPath)
- }
- }
- }
- }
- sheetNum.value = sheetNum.value + 1
- if (colPathList.length != 0){
- await DynamicTbColApi.createBatchByCodes(colPathList,props.initData.templateId)
- }
- if (isAdd){
- let col = parseInt(generateData.copyRange.topLeft.split(',')[0])
- let row = parseInt(generateData.copyRange.topLeft.split(',')[1])
- let colCount = parseInt(generateData.copyRange.topRight.split(',')[0]) - col
- let rowCount = parseInt(generateData.copyRange.bottomLeft.split(',')[1]) - row
- if (!sheet2) {
- return;
- }
- if (generateData.hidden) {
- // 判断第一行,如果全部为空,则隐藏
- let isEmpty = true;
- for (let i = 0; i < colCount; i++) {
- const range = sheet2.getCell(row, col + i)
- if (range.value()) {
- isEmpty = false;
- break;
- }
- }
- if (isEmpty) {
- sheet2.visible(false)
- }
- }
- // 判断最后一行,如果不为空,自动续页
- let isNotEmpty = false;
- for (let i = 0; i < colCount; i++) {
- const range = sheet2.getCell(row + rowCount, col + i)
- if (range.value()) {
- isNotEmpty = true;
- break;
- }
- }
- if (isNotEmpty) {
- await generate(true)
- }
- }
- }
- if (isSetTimeout) {
- const loading = ElLoading.service({text: '正在生成中'})
- setTimeout(async () => {
- try {
- await generate(false)
- } finally {
- loading.close()
- }
- }, 20)
- } else {
- const loading = ElLoading.service({text: '生成续页中,数据量较大'})
- await generate(true)
- loading.close()
- }
- }
- /**
- * 隐藏续页
- */
- const hiddenPage = async () => {
- sheetNum.value = 2
- const data = await DynamicTbApi.getDynamicTb(props.initData.templateId)
- if (data && data.copyConfig) {
- generateData = data.copyConfig
- if (!generateData.isContinuePage){
- return
- }
- } else {
- return
- }
- // 计算坐标
- let col = parseInt(generateData.copyRange.topLeft.split(',')[0])
- let row = parseInt(generateData.copyRange.topLeft.split(',')[1])
- let colCount = parseInt(generateData.copyRange.topRight.split(',')[0]) - col
- let rowCount = parseInt(generateData.copyRange.bottomLeft.split(',')[1]) - row
- const sheet = previewSpread.getSheetFromName(generateData.sheetName)
- if (!sheet) {
- return;
- }
- if (generateData.hidden) {
- // 判断第一行,如果全部为空,则隐藏
- let isEmpty = true;
- for (let i = 0; i < colCount; i++) {
- const range = sheet.getCell(row, col + i)
- if (range.value()) {
- isEmpty = false;
- break;
- }
- }
- if (isEmpty) {
- sheet.visible(false)
- }
- }
- // 判断最后一行,如果不为空,自动续页
- let isNotEmpty = false;
- for (let i = 0; i < colCount; i++) {
- const range = sheet.getCell(row + rowCount, col + i)
- if (range.value()) {
- isNotEmpty = true;
- break;
- }
- }
- if (isNotEmpty) {
- handleGenerate(false)
- }
- }
- /**
- * 获取数据
- */
- const getData = () => {
- let dataSource = {};
- previewSpread.sheets.forEach((sheet) => {
- const sheetData = collectSheetDataSource(sheet);
- for (const key in sheetData) {
- if (dataSource[key] && sheetData[key] == sheetData[key]) {
- } else {
- dataSource[key] = sheetData[key];
- }
- }
- collectShapesIntoDataSource(sheet, dataSource);
- });
- return dataSource;
- }
- /**
- * 数据绑定后同步表格样式——将第一行(模板行)的合并规则、样式、行高复制到所有自动扩展的数据行
- * 参考 SpreadDesigner 的 handleSheetTableCopyToOptimized
- */
- const copyOptions = GC.Spread.Sheets.CopyToOptions.style | GC.Spread.Sheets.CopyToOptions.formula
- const handleTableRowsAfterDataBind = (sheets) => {
- previewSpread.suspendPaint()
- try {
- for (const sheet of sheets) {
- const tables = sheet.tables?.all() || []
- for (const table of tables) {
- const range = table.range()
- const { row, rowCount, col, colCount } = range
- if (rowCount <= 1) continue
- // 批量设置自动换行
- sheet.getRange(row, col, rowCount, colCount).wordWrap(true)
- // 表格内所有单元格设为可编辑
- sheet.getRange(row, col, rowCount, colCount).allowEditInCell(true)
- // 收集第一行的合并规则
- const firstRowSpans = collectFirstRowSpans(sheet, row, col, colCount)
- // 复制样式+行高 并 应用合并
- const firstRowHeight = sheet.getRowHeight(row)
- for (let r = 1; r < rowCount; r++) {
- const currentRow = row + r
- // 复制第一行样式和公式到当前数据行
- sheet.copyTo(row, col, currentRow, col, 1, colCount, copyOptions)
- // 复制行高
- sheet.setRowHeight(currentRow, firstRowHeight)
- // 应用合并规则
- if (firstRowSpans.length > 0) {
- applyMergeRules(sheet, currentRow, firstRowSpans)
- }
- }
- }
- }
- } finally {
- previewSpread.resumePaint()
- }
- }
- /** 收集第一行(模板行)的合并单元格规则 */
- const collectFirstRowSpans = (sheet, row, col, colCount) => {
- const firstRowSpans = []
- for (let c = col; c < col + colCount; c++) {
- const span = sheet.getSpan(row, c)
- if (span && span.row === row) {
- firstRowSpans.push({ startCol: span.col, colCount: span.colCount })
- c += span.colCount - 1
- }
- }
- // 如果没有预置合并单元格,根据第一行内容自动识别(相邻同值列合并)
- if (firstRowSpans.length === 0) {
- const firstRowValues = []
- for (let c = col; c < col + colCount; c++) {
- firstRowValues.push(sheet.getValue(row, c))
- }
- let startMergeCol = 0
- for (let c = 1; c <= colCount; c++) {
- if (c === colCount || firstRowValues[c] !== firstRowValues[c - 1]) {
- if (c - startMergeCol > 1) {
- const startCol = col + startMergeCol
- const mergeColCount = c - startMergeCol
- firstRowSpans.push({ startCol, colCount: mergeColCount })
- sheet.addSpan(row, startCol, 1, mergeColCount)
- }
- startMergeCol = c
- }
- }
- }
- return firstRowSpans
- }
- /** 将合并规则应用到指定行(直接按模板列的 span 结构合并,不做内容相等校验) */
- const applyMergeRules = (sheet, currentRow, firstRowSpans) => {
- firstRowSpans.forEach((mergeInfo) => {
- try {
- sheet.addSpan(currentRow, mergeInfo.startCol, 1, mergeInfo.colCount)
- } catch (error) {
- // 忽略合并失败
- }
- })
- }
- defineExpose({reloadView:initPreview, handleSave, getWorkbook: () => previewSpread,getData});
- const emit = defineEmits(['saveSuccess','docReady','docCreate','saveFail','close'])
- // 动态计算PDF查看器尺寸
- const calculatePdfSize = () => {
- pdfContentWidth.value = pdfViewer.value.clientWidth - 45
- pdfViewerHeight.value = pdfViewer.value.clientHeight
- console.log('props.pdfUrl', pdfContentWidth.value, pdfViewerHeight.value)
- }
- onMounted(() => {
- window.addEventListener('resize', calculatePdfSize)
- console.log("SpreadViewer,onMounted:"+JSON.stringify(props.initData));
- //initPreview();
- })
- onUnmounted(()=>{
- window.removeEventListener('resize', calculatePdfSize)
- previewSpread?.destroy();
- })
- </script>
- <style lang="scss" scoped>
- :deep(.default-toolbar) {
- padding-top: 0;
- }
- .lab-designer-container {
- position: relative;
- display: flex;
- flex-direction: column;
- align-items: stretch;
- /*height: calc(100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 140px);*/
- height: calc(100vh - var(--top-tool-height) - 5px );
- .spread-designer-container {
- width: calc(100% - 440px);
- height: 100%;
- padding: 0;
- }
- }
- .lab-designer-container-fullscreen {
- position: relative;
- display: flex;
- flex-direction: column;
- align-items: stretch;
- /*height: calc(100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 140px);*/
- height: 100%;
- .spread-designer-container {
- width: calc(100% - 440px);
- height: 100%;
- padding: 0;
- }
- }
- .header-row {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 0px 20px 16px 20px;
- background: #fff;
- border-bottom: 1px solid #e8e8e8;
- }
- .header-row .title {
- font-size: 16px;
- font-weight: 600;
- color: #303133;
- }
- .unit-footer-btns {
- align-items: center;
- }
- .dropdown-item-content {
- display: flex;
- align-items: center;
- gap: 8px;
- justify-content: center;
- width: 100%;
- }
- .spread-container {
- flex: 1;
- background: #f5f7fa;
- overflow: auto;
- padding: 0;
- }
- .pdf-viewer-container {
- width: 100%;
- height: calc(100% - 40px);
- //background-color: #8E8E9D;
- padding: 5px;
- box-sizing: border-box;
- overflow-y: auto;
- }
- .pdf-viewer-content {
- display: flex;
- justify-content: center;
- align-items: center;
- }
- // Wingdings 符号选择样式
- .wingdings-container {
- max-height: 500px;
- overflow-y: auto;
- }
- .wingdings-grid {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
- gap: 12px;
- padding: 10px;
- }
- .wingdings-item {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- padding: 12px 8px;
- border: 1px solid #dcdfe6;
- border-radius: 4px;
- cursor: pointer;
- transition: all 0.3s;
-
- &:hover {
- border-color: #409eff;
- background-color: #ecf5ff;
- transform: translateY(-2px);
- box-shadow: 0 2px 8px rgba(64, 158, 255, 0.2);
- }
- }
- .wingdings-symbol {
- font-size: 24px;
- margin-bottom: 4px;
- color: #303133;
- }
- .wingdings-label {
- font-size: 12px;
- color: #909399;
- text-align: center;
- word-break: break-all;
- }
- </style>
|