SpreadEditor.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532
  1. <template>
  2. <div class="lab-designer-container">
  3. <SpreadDesigner class="spread-designer-container" v-loading="loading" businessType="2" :businessId="props.formData.id" ref="spreadDesignerRef"
  4. @init="handleDesignerInit" style="width: 100%;padding: 10px 20px 10px 10px;height: 650px">
  5. <!-- @init="handleDesignerInit" style="width: 100%;padding: 10px 20px 10px 10px;height: 750px">-->
  6. <template #toolbar>
  7. <el-button type="warning" @click="handleConfig">模版配置</el-button>
  8. <el-button type="primary" @click="handlePreview">模版预览</el-button>
  9. <el-button type="primary" v-if="formType !== 'view'" @click="handleSave">保存</el-button>
  10. <el-button type="default" @click="handleCancel">取消</el-button>
  11. <!-- <el-switch v-model="autoCopy" active-text="复制模式" />-->
  12. </template>
  13. </SpreadDesigner>
  14. <el-dialog v-model="isConfig" width="380px" height="80%" title="模版配置" :modal="false"
  15. modal-class="config-dialog-modal" !close-on-click-modal draggable overflow>
  16. <!-- 续页配置 -->
  17. <el-form>
  18. <el-form-item label="是否存在续页" required>
  19. <el-switch v-model="isContinuePage" active-text="是否续页"/>
  20. </el-form-item>
  21. <el-form-item label="续页工作表名称" required>
  22. <el-input v-model="continuePageSheetName" :disabled="!isContinuePage"
  23. placeholder="请输入续页工作表"/>
  24. </el-form-item>
  25. <el-form-item label="隐藏空白续页">
  26. <el-switch v-model="isHiddenContinuePage" :disabled="!isContinuePage" active-text="隐藏续页"/>
  27. </el-form-item>
  28. </el-form>
  29. <el-card header="续页复制范围" style="margin: 20px;" v-if="isContinuePage">
  30. <el-alert title="选择需要复制表格的4个角,点击复制坐标" type="info" style="margin-bottom: 10px;"
  31. :closable="false"/>
  32. <el-form inline>
  33. <el-row :gutter="10">
  34. <el-col :span="12">
  35. <el-input v-model="copyRangeTopLeft" placeholder="左上角">
  36. <template #append>
  37. <el-button type="primary" @click="handleCopy('lt')">复制</el-button>
  38. </template>
  39. </el-input>
  40. </el-col>
  41. <el-col :span="12">
  42. <el-input v-model="copyRangeTopRight" placeholder="右上角">
  43. <template #append>
  44. <el-button type="primary" @click="handleCopy('rt')">复制</el-button>
  45. </template>
  46. </el-input>
  47. </el-col>
  48. </el-row>
  49. <p style="height: 5px;"></p>
  50. <el-row :gutter="10">
  51. <el-col :span="12">
  52. <el-input v-model="copyRangeBottomLeft" placeholder="左下角">
  53. <template #append>
  54. <el-button type="primary" @click="handleCopy('lb')">复制</el-button>
  55. </template>
  56. </el-input>
  57. </el-col>
  58. <el-col :span="12">
  59. <el-input v-model="copyRangeBottomRight" placeholder="右下角">
  60. <template #append>
  61. <el-button type="primary" @click="handleCopy('rb')">复制</el-button>
  62. </template>
  63. </el-input>
  64. </el-col>
  65. </el-row>
  66. </el-form>
  67. </el-card>
  68. <template #footer>
  69. <el-button type="primary" @click="saveConfig">保存</el-button>
  70. </template>
  71. </el-dialog>
  72. </div>
  73. </template>
  74. <script setup lang="ts">
  75. import '@grapecity-software/spread-sheets-designer-resources-cn'
  76. import * as GC from '@grapecity-software/spread-sheets'
  77. import SpreadDesigner from '@/views/pressure2/dynamictb/SpreadDesigner/index.vue'
  78. import {useTagsViewStore} from '@/store/modules/tagsView'
  79. import {
  80. createStandardTemplateV2,
  81. getStandardTemplateInfo,
  82. updateStandardTemplate
  83. } from '@/api/pressure2/standard/template'
  84. import {useRoute, useRouter} from 'vue-router'
  85. import {buildFileUrl} from '@/utils'
  86. import {DynamicTbApi} from '@/api/pressure2/dynamictb'
  87. import {DynamicTbInsApi} from '@/api/pressure2/dynamictbins'
  88. import {getSimpleDictDataList} from '@/api/system/dict/dict.data'
  89. import axios from 'axios'
  90. const route = useRoute()
  91. const router = useRouter();
  92. const tagsViewStore = useTagsViewStore()
  93. defineOptions({ name: 'SpreadEditor' });
  94. let designer = null
  95. const spreadDesignerRef = ref(null)
  96. const loading = ref(false)
  97. const isConfig = ref(false)
  98. // 续页配置相关变量
  99. const isContinuePage = ref(false)
  100. const continuePageSheetName = ref('续页')
  101. const isAutoCopy = ref(false)
  102. const isHiddenContinuePage = ref(false)
  103. // 复制范围相关变量
  104. const copyRangeTopLeft = ref('')
  105. const copyRangeTopRight = ref('')
  106. const copyRangeBottomLeft = ref('')
  107. const copyRangeBottomRight = ref('')
  108. const props = defineProps({
  109. dtParams: {
  110. type: Object,
  111. default: () => ({}),
  112. required: false
  113. },
  114. formData: {
  115. type: Object,
  116. default: () => ({}),
  117. required: false
  118. },formType:{
  119. type: String,
  120. default: '',
  121. required: false
  122. }
  123. });
  124. const getTbCols = async () => {
  125. return await DynamicTbApi.getDtCols(props.dtParams);
  126. }
  127. const handleDesignerInit = (instance) => {
  128. designer = instance
  129. if (props.formData.id) {
  130. fetchTemplateData()
  131. }
  132. }
  133. // 取消编辑
  134. const handleCancel = () => {
  135. tagsViewStore.closeSelectedTag(route)
  136. }
  137. // 保存编辑
  138. const handleSave = () => {
  139. loading.value = true
  140. const formData = new FormData()
  141. const spread = designer.getWorkbook()
  142. // 工作表数据
  143. let bindingPathSchema = spreadDesignerRef.value?.getDefaultSchema()
  144. // 字段列表释义
  145. let bindingPathNameJson = spreadDesignerRef.value?.handleUpdateDesignerState.get('bindingPathDataJSON')
  146. bindingPathNameJson = !bindingPathNameJson ? '[]' : JSON.stringify(bindingPathNameJson)
  147. spread.save(async function (blob) {
  148. loading.value = false
  149. let result = null
  150. formData.append('file', blob)
  151. formData.append('id', props.formData.id)
  152. formData.append('name', props.formData.tbName)
  153. formData.append('signType', '')
  154. formData.append('classId', props.formData.tbType)
  155. formData.append('type', '2')
  156. // formData.append('code', props.formData.tbCode)
  157. formData.append('versionNumber', props.formData.tbCode)
  158. formData.append('status', 0)
  159. formData.append('bindingPathSchema', !bindingPathSchema ? '' : bindingPathSchema)
  160. formData.append('bindingPathNameJson', bindingPathNameJson)
  161. if (isNewTemplate) {
  162. // let params = {
  163. // file: blob,
  164. // id: props.formData.id,
  165. // name: props.formData.tbName,
  166. // signType: '',
  167. // classId: props.formData.tbType,
  168. // type: '2',
  169. // // code: props.formData.tbCode,
  170. // versionNumber: props.formData.tbCode,
  171. // status: 0,
  172. // bindingPathSchema: !bindingPathSchema ? '' : bindingPathSchema,
  173. // bindingPathNameJson: bindingPathNameJson,
  174. // }
  175. result = await createStandardTemplateV2(formData)
  176. isNewTemplate = false;
  177. } else {
  178. result = await updateStandardTemplate(formData);
  179. }
  180. if (result) {
  181. loading.value = false
  182. ElMessage.success('保存成功')
  183. // tagsViewStore.closeSelectedTag(route)
  184. }
  185. })
  186. }
  187. // 预览
  188. const handlePreview = () => {
  189. DynamicTbInsApi.getOrCreatePreviewData(props.formData.id).then(res => {
  190. router.push({
  191. path: `/cybggl/preview/${props.formData.id}/${res.refId}`
  192. });
  193. })
  194. }
  195. let isNewTemplate = false;
  196. // 获取模版详情
  197. const fetchTemplateData = async () => {
  198. loading.value = true
  199. const templateRes = await getStandardTemplateInfo({ id: props.formData.id })
  200. const spread = designer.getWorkbook()
  201. if (templateRes && templateRes.id) {
  202. // 已有模板的情况
  203. let bindingPathSchema = JSON.parse(templateRes.bindingPathSchema);
  204. let properties = {}; //bindingPathSchema.properties;
  205. let bindingPathName = [];
  206. let schemaSources = await getTbCols();
  207. // 获取字典数据,供组合框使用
  208. const dictDataList = await getSimpleDictDataList();
  209. schemaSources?.forEach(element => {
  210. if (element.colValType == 5){
  211. // 列表
  212. let items = {
  213. type: 'object',
  214. properties: {
  215. },
  216. }
  217. element.note.split(',').forEach(element2 => {
  218. items.properties[element2] = {
  219. type: 'string',
  220. }
  221. });
  222. properties[element.colCode] = {
  223. dataFieldType: 'table',
  224. type: 'array',
  225. items: items
  226. }
  227. } else if(element.colValType == 6){
  228. // 复选框
  229. properties[element.colCode] = {
  230. dataFieldType: 'checkbox',
  231. type: 'string'
  232. }
  233. } else if (element.colValType == 7) {
  234. // 下拉字典
  235. let items = {
  236. type: 'array',
  237. properties: {}
  238. }
  239. // 根据备注中的字典 code,从字典数据中获取下拉选项
  240. const dictCode = element.note
  241. const matchedDict = (dictDataList || []).filter(item => item.dictType === dictCode)
  242. matchedDict.forEach(dict => {
  243. items.properties[dict.label] = {
  244. type: 'string',
  245. }
  246. })
  247. properties[element.colCode] = {
  248. dataFieldType: 'combox',
  249. type: 'string',
  250. items: items,
  251. }
  252. } else if (element.colValType == 8) {
  253. // 下拉选项
  254. let items = {
  255. type: 'array',
  256. properties: {}
  257. }
  258. element.note.split(',').forEach(dict => {
  259. items.properties[dict] = {
  260. type: 'string',
  261. }
  262. })
  263. properties[element.colCode] = {
  264. dataFieldType: 'combox',
  265. type: 'string',
  266. items: items,
  267. }
  268. } else {
  269. properties[element.colCode] = {
  270. dataFieldType: 'text',
  271. type: 'string'
  272. }
  273. }
  274. bindingPathName.push(
  275. {
  276. "dataFieldType": "text",
  277. "field": element.colCode,
  278. "displayName": element.colName,
  279. "type": "string",
  280. "isVerify": "0"
  281. }
  282. )
  283. });
  284. spread.options.allowDynamicArray = true;
  285. bindingPathSchema.properties = properties;
  286. spreadDesignerRef.value.setDefaultSchema(JSON.stringify(bindingPathSchema), JSON.stringify(bindingPathName))
  287. if (!templateRes.fileUrl) return loading.value = false
  288. // 获取模板文件流
  289. const fileUrl = !templateRes.fileUrl ? '' : buildFileUrl(templateRes.fileUrl)
  290. const response = await axios.get(fileUrl, { responseType: 'blob' });
  291. const blob = new Blob([response.data], { type: response.headers['content-type'] });
  292. // 渲染葡萄城文件
  293. spread.open(blob, () => {
  294. spread.getActiveSheet().zoom(1)
  295. // 设置数据源的值
  296. spreadDesignerRef.value.newSetDataSource(JSON.stringify(bindingPathSchema), {})
  297. loading.value = false
  298. console.log('加载成功')
  299. //helpCopy(spread)
  300. handleBing()
  301. }, (err) => {
  302. loading.value = false
  303. console.log('加载失败', err)
  304. })
  305. } else {
  306. // 第一次创建模板的情况
  307. await initNewTemplate()
  308. }
  309. loading.value = false;
  310. }
  311. let temp = ref<string | null>(null)
  312. let index = ref<number>()
  313. const helpCopy = (spread) => {
  314. spread.bind(GC.Spread.Sheets.Events.ClipboardPasting, function (_e, info) {
  315. const cellRange = info.fromRange;
  316. if (info.sheet.getBindingPath(cellRange.row, cellRange.col)) {
  317. if (temp.value && temp.value.split('_')[0] !== info.sheet.getBindingPath(cellRange.row, cellRange.col).split('_')[0]) {
  318. index.value = undefined
  319. }
  320. temp.value = info.sheet.getBindingPath(cellRange.row, cellRange.col)
  321. if (!index.value){
  322. index.value = parseInt(info.sheet.getBindingPath(cellRange.row, cellRange.col).split('_')[1]|| 1 )+1
  323. }
  324. }
  325. })
  326. spread.bind(GC.Spread.Sheets.Events.ClipboardPasted, function (_e, info) {
  327. if (temp.value) {
  328. const arr = temp.value.split('_')
  329. const cellRange = info.cellRange;
  330. temp.value = arr[0] + '_' + index.value
  331. index.value = index.value + 1
  332. info.sheet.setBindingPath(cellRange.row, cellRange.col, temp.value)
  333. }
  334. })
  335. }
  336. const autoCopy = ref(false)
  337. watch(autoCopy,()=>{
  338. if (autoCopy.value){
  339. helpCopy(designer.getWorkbook())
  340. }else {
  341. handleUnbind()
  342. }
  343. })
  344. const handleUnbind = () => {
  345. const spread = designer.getWorkbook()
  346. spread.unbindAll()
  347. }
  348. // 初始化新模板
  349. const initNewTemplate = async () => {
  350. try {
  351. // 获取动态表格列数据
  352. const schemaSources = await getTbCols();
  353. // 构建初始的 bindingPathSchema
  354. const initialSchema = {
  355. type: "object",
  356. $schema: 'http://json-schema.org/draft-04/schema#',
  357. properties: {}
  358. };
  359. // 将动态表格列添加到 schema
  360. schemaSources?.forEach(element => {
  361. initialSchema.properties[element.col_code] = {
  362. dataFieldType: 'text',
  363. type: 'string'
  364. };
  365. });
  366. // 设置默认 schema 和空的绑定路径名称
  367. spreadDesignerRef.value.setDefaultSchema(JSON.stringify(initialSchema), '[]');
  368. // 设置空的数据源
  369. spreadDesignerRef.value.newSetDataSource(JSON.stringify(initialSchema), {});
  370. console.log('新模板初始化成功,动态列已添加到数据区');
  371. isNewTemplate = true;
  372. } catch (error) {
  373. console.error('初始化新模板失败:', error);
  374. ElMessage.error('初始化模板失败');
  375. }
  376. }
  377. let sheetTemp,colTemp,rowTemp
  378. const handleBing = () => {
  379. const spread = designer.getWorkbook()
  380. spread.sheets.forEach(sheet => {
  381. sheet.bind(GC.Spread.Sheets.Events.CellClick, (s, args) => {
  382. sheetTemp = args.sheetName
  383. colTemp = args.col
  384. rowTemp = args.row
  385. })
  386. });
  387. }
  388. const handleCopy = (value) => {
  389. if (sheetTemp != continuePageSheetName.value) {
  390. ElMessage.error('当前工作表不是续页');
  391. return
  392. }
  393. switch (value) {
  394. case 'lt':
  395. copyRangeTopLeft.value = colTemp + ',' + rowTemp
  396. break;
  397. case 'rt':
  398. copyRangeTopRight.value = colTemp + ',' + rowTemp
  399. break;
  400. case 'lb':
  401. copyRangeBottomLeft.value = colTemp + ',' + rowTemp
  402. break;
  403. case 'rb':
  404. copyRangeBottomRight.value = colTemp + ',' + rowTemp
  405. break;
  406. }
  407. }
  408. const handleConfig = async () => {
  409. isConfig.value = true
  410. // 查询配置
  411. let data = await DynamicTbApi.getDynamicTb(props.formData.id);
  412. console.log(data.copyConfig)
  413. if (data.copyConfig){
  414. const config = data.copyConfig
  415. isContinuePage.value = config.isContinuePage
  416. continuePageSheetName.value = config.sheetName
  417. copyRangeTopLeft.value = config.copyRange.topLeft
  418. copyRangeTopRight.value = config.copyRange.topRight
  419. copyRangeBottomLeft.value = config.copyRange.bottomLeft
  420. copyRangeBottomRight.value = config.copyRange.bottomRight
  421. isHiddenContinuePage.value = config.hidden
  422. }
  423. }
  424. const saveConfig = () => {
  425. if (!isContinuePage.value){
  426. DynamicTbApi.updateDynamicTb({
  427. id: props.formData.id,
  428. copyConfig: ""
  429. }).then(() => {
  430. ElMessage.success('保存成功');
  431. isConfig.value = false
  432. }).catch(() => {
  433. ElMessage.error('保存失败');
  434. })
  435. return;
  436. }
  437. // 检查判空
  438. if (!continuePageSheetName.value) {
  439. ElMessage.error('请选择续页工作表');
  440. return
  441. }
  442. if (!copyRangeTopLeft.value || !copyRangeTopRight.value || !copyRangeBottomLeft.value || !copyRangeBottomRight.value) {
  443. ElMessage.error('请选择复制范围');
  444. return;
  445. }
  446. DynamicTbApi.updateDynamicTb({
  447. id: props.formData.id,
  448. copyConfig: JSON.stringify({
  449. copyRange: {
  450. topLeft: copyRangeTopLeft.value,
  451. topRight: copyRangeTopRight.value,
  452. bottomLeft: copyRangeBottomLeft.value,
  453. bottomRight: copyRangeBottomRight.value
  454. },
  455. sheetName: continuePageSheetName.value,
  456. hidden: isHiddenContinuePage.value,
  457. isContinuePage: true
  458. })
  459. }).then(() => {
  460. ElMessage.success('保存成功');
  461. isConfig.value = false
  462. }).catch(() => {
  463. ElMessage.error('保存失败');
  464. })
  465. }
  466. </script>
  467. <style lang="scss" scoped>
  468. :deep(.default-toolbar) {
  469. padding-top: 0;
  470. }
  471. .lab-designer-container {
  472. position: relative;
  473. display: flex;
  474. flex-direction: row;
  475. align-items: stretch;
  476. height: calc(100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height));
  477. .spread-designer-container {
  478. width: calc(100% - 440px);
  479. height: 100%;
  480. padding: 0;
  481. }
  482. }
  483. // 配置弹窗的遮罩层允许点击穿透
  484. :deep(.config-dialog-modal) {
  485. pointer-events: none;
  486. // 确保弹窗本身可以接收鼠标事件
  487. .el-dialog {
  488. pointer-events: auto;
  489. }
  490. }
  491. </style>