SpreadEditor.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477
  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" @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 axios from 'axios'
  89. const route = useRoute()
  90. const router = useRouter();
  91. const tagsViewStore = useTagsViewStore()
  92. defineOptions({ name: 'SpreadEditor' });
  93. let designer = null
  94. const spreadDesignerRef = ref(null)
  95. const loading = ref(false)
  96. const isConfig = ref(false)
  97. // 续页配置相关变量
  98. const isContinuePage = ref(false)
  99. const continuePageSheetName = ref('续页')
  100. const isAutoCopy = ref(false)
  101. const isHiddenContinuePage = ref(false)
  102. // 复制范围相关变量
  103. const copyRangeTopLeft = ref('')
  104. const copyRangeTopRight = ref('')
  105. const copyRangeBottomLeft = ref('')
  106. const copyRangeBottomRight = ref('')
  107. const props = defineProps({
  108. dtParams: {
  109. type: Object,
  110. default: () => ({}),
  111. required: false
  112. },
  113. formData: {
  114. type: Object,
  115. default: () => ({}),
  116. required: false
  117. }
  118. });
  119. const getTbCols = async () => {
  120. return await DynamicTbApi.getDtCols(props.dtParams);
  121. }
  122. const handleDesignerInit = (instance) => {
  123. designer = instance
  124. if (props.formData.id) {
  125. fetchTemplateData()
  126. }
  127. }
  128. // 取消编辑
  129. const handleCancel = () => {
  130. tagsViewStore.closeSelectedTag(route)
  131. }
  132. // 保存编辑
  133. const handleSave = () => {
  134. loading.value = true
  135. const formData = new FormData()
  136. const spread = designer.getWorkbook()
  137. // 工作表数据
  138. let bindingPathSchema = spreadDesignerRef.value?.getDefaultSchema()
  139. // 字段列表释义
  140. let bindingPathNameJson = spreadDesignerRef.value?.handleUpdateDesignerState.get('bindingPathDataJSON')
  141. bindingPathNameJson = !bindingPathNameJson ? '[]' : JSON.stringify(bindingPathNameJson)
  142. spread.save(async function (blob) {
  143. loading.value = false
  144. let result = null
  145. formData.append('file', blob)
  146. formData.append('id', props.formData.id)
  147. formData.append('name', props.formData.tbName)
  148. formData.append('signType', '')
  149. formData.append('classId', props.formData.tbType)
  150. formData.append('type', '2')
  151. // formData.append('code', props.formData.tbCode)
  152. formData.append('versionNumber', props.formData.tbCode)
  153. formData.append('status', 0)
  154. formData.append('bindingPathSchema', !bindingPathSchema ? '' : bindingPathSchema)
  155. formData.append('bindingPathNameJson', bindingPathNameJson)
  156. if (isNewTemplate) {
  157. // let params = {
  158. // file: blob,
  159. // id: props.formData.id,
  160. // name: props.formData.tbName,
  161. // signType: '',
  162. // classId: props.formData.tbType,
  163. // type: '2',
  164. // // code: props.formData.tbCode,
  165. // versionNumber: props.formData.tbCode,
  166. // status: 0,
  167. // bindingPathSchema: !bindingPathSchema ? '' : bindingPathSchema,
  168. // bindingPathNameJson: bindingPathNameJson,
  169. // }
  170. result = await createStandardTemplateV2(formData)
  171. isNewTemplate = false;
  172. } else {
  173. result = await updateStandardTemplate(formData);
  174. }
  175. if (result) {
  176. loading.value = false
  177. ElMessage.success('保存成功')
  178. // tagsViewStore.closeSelectedTag(route)
  179. }
  180. })
  181. }
  182. // 预览
  183. const handlePreview = () => {
  184. DynamicTbInsApi.getOrCreatePreviewData(props.formData.id).then(res => {
  185. router.push({
  186. path: `/cybggl/preview/${props.formData.id}/${res.refId}`
  187. });
  188. })
  189. }
  190. let isNewTemplate = false;
  191. // 获取模版详情
  192. const fetchTemplateData = async () => {
  193. loading.value = true
  194. const templateRes = await getStandardTemplateInfo({ id: props.formData.id })
  195. const spread = designer.getWorkbook()
  196. if (templateRes && templateRes.id) {
  197. // 已有模板的情况
  198. let bindingPathSchema = JSON.parse(templateRes.bindingPathSchema);
  199. let properties = {}; //bindingPathSchema.properties;
  200. let bindingPathName = [];
  201. let schemaSources = await getTbCols();
  202. schemaSources?.forEach(element => {
  203. /*if (element.col_val_type == 5){
  204. let items = {
  205. type: 'object',
  206. properties: {
  207. },
  208. }
  209. element.note.split(',').forEach(element2 => {
  210. items.properties[element2] = {
  211. type: 'string',
  212. }
  213. });
  214. properties[element.col_code] = {
  215. dataFieldType: 'table',
  216. type: 'array',
  217. items: items
  218. }
  219. }else {*/
  220. properties[element.colCode] = {
  221. dataFieldType: 'text',
  222. type: 'string'
  223. }
  224. // }
  225. bindingPathName.push(
  226. {
  227. "dataFieldType": "text",
  228. "field": element.colCode,
  229. "displayName": element.colName,
  230. "type": "string",
  231. "isVerify": "0"
  232. }
  233. )
  234. });
  235. bindingPathSchema.properties = properties;
  236. spreadDesignerRef.value.setDefaultSchema(JSON.stringify(bindingPathSchema), JSON.stringify(bindingPathName))
  237. if (!templateRes.fileUrl) return loading.value = false
  238. // 获取模板文件流
  239. const fileUrl = !templateRes.fileUrl ? '' : buildFileUrl(templateRes.fileUrl)
  240. const response = await axios.get(fileUrl, { responseType: 'blob' });
  241. const blob = new Blob([response.data], { type: response.headers['content-type'] });
  242. // 渲染葡萄城文件
  243. spread.open(blob, () => {
  244. spread.getActiveSheet().zoom(1)
  245. // 设置数据源的值
  246. spreadDesignerRef.value.newSetDataSource(JSON.stringify(bindingPathSchema), {})
  247. loading.value = false
  248. console.log('加载成功')
  249. //helpCopy(spread)
  250. handleBing()
  251. }, (err) => {
  252. loading.value = false
  253. console.log('加载失败', err)
  254. })
  255. } else {
  256. // 第一次创建模板的情况
  257. await initNewTemplate()
  258. }
  259. loading.value = false;
  260. }
  261. let temp = ref<string | null>(null)
  262. let index = ref<number>()
  263. const helpCopy = (spread) => {
  264. spread.bind(GC.Spread.Sheets.Events.ClipboardPasting, function (_e, info) {
  265. const cellRange = info.fromRange;
  266. if (info.sheet.getBindingPath(cellRange.row, cellRange.col)) {
  267. if (temp.value && temp.value.split('_')[0] !== info.sheet.getBindingPath(cellRange.row, cellRange.col).split('_')[0]) {
  268. index.value = undefined
  269. }
  270. temp.value = info.sheet.getBindingPath(cellRange.row, cellRange.col)
  271. if (!index.value){
  272. index.value = parseInt(info.sheet.getBindingPath(cellRange.row, cellRange.col).split('_')[1]|| 1 )+1
  273. }
  274. }
  275. })
  276. spread.bind(GC.Spread.Sheets.Events.ClipboardPasted, function (_e, info) {
  277. if (temp.value) {
  278. const arr = temp.value.split('_')
  279. const cellRange = info.cellRange;
  280. temp.value = arr[0] + '_' + index.value
  281. index.value = index.value + 1
  282. info.sheet.setBindingPath(cellRange.row, cellRange.col, temp.value)
  283. }
  284. })
  285. }
  286. const autoCopy = ref(false)
  287. watch(autoCopy,()=>{
  288. if (autoCopy.value){
  289. helpCopy(designer.getWorkbook())
  290. }else {
  291. handleUnbind()
  292. }
  293. })
  294. const handleUnbind = () => {
  295. const spread = designer.getWorkbook()
  296. spread.unbindAll()
  297. }
  298. // 初始化新模板
  299. const initNewTemplate = async () => {
  300. try {
  301. // 获取动态表格列数据
  302. const schemaSources = await getTbCols();
  303. // 构建初始的 bindingPathSchema
  304. const initialSchema = {
  305. type: "object",
  306. $schema: 'http://json-schema.org/draft-04/schema#',
  307. properties: {}
  308. };
  309. // 将动态表格列添加到 schema
  310. schemaSources?.forEach(element => {
  311. initialSchema.properties[element.col_code] = {
  312. dataFieldType: 'text',
  313. type: 'string'
  314. };
  315. });
  316. // 设置默认 schema 和空的绑定路径名称
  317. spreadDesignerRef.value.setDefaultSchema(JSON.stringify(initialSchema), '[]');
  318. // 设置空的数据源
  319. spreadDesignerRef.value.newSetDataSource(JSON.stringify(initialSchema), {});
  320. console.log('新模板初始化成功,动态列已添加到数据区');
  321. isNewTemplate = true;
  322. } catch (error) {
  323. console.error('初始化新模板失败:', error);
  324. ElMessage.error('初始化模板失败');
  325. }
  326. }
  327. let sheetTemp,colTemp,rowTemp
  328. const handleBing = () => {
  329. const spread = designer.getWorkbook()
  330. spread.sheets.forEach(sheet => {
  331. sheet.bind(GC.Spread.Sheets.Events.CellClick, (s, args) => {
  332. sheetTemp = args.sheetName
  333. colTemp = args.col
  334. rowTemp = args.row
  335. })
  336. });
  337. }
  338. const handleCopy = (value) => {
  339. if (sheetTemp != continuePageSheetName.value) {
  340. ElMessage.error('当前工作表不是续页');
  341. return
  342. }
  343. switch (value) {
  344. case 'lt':
  345. copyRangeTopLeft.value = colTemp + ',' + rowTemp
  346. break;
  347. case 'rt':
  348. copyRangeTopRight.value = colTemp + ',' + rowTemp
  349. break;
  350. case 'lb':
  351. copyRangeBottomLeft.value = colTemp + ',' + rowTemp
  352. break;
  353. case 'rb':
  354. copyRangeBottomRight.value = colTemp + ',' + rowTemp
  355. break;
  356. }
  357. }
  358. const handleConfig = async () => {
  359. isConfig.value = true
  360. // 查询配置
  361. let data = await DynamicTbApi.getDynamicTb(props.formData.id);
  362. console.log(data.copyConfig)
  363. if (data.copyConfig){
  364. const config = JSON.parse(data.copyConfig)
  365. isContinuePage.value = config.isContinuePage
  366. continuePageSheetName.value = config.sheetName
  367. copyRangeTopLeft.value = config.copyRange.topLeft
  368. copyRangeTopRight.value = config.copyRange.topRight
  369. copyRangeBottomLeft.value = config.copyRange.bottomLeft
  370. copyRangeBottomRight.value = config.copyRange.bottomRight
  371. isHiddenContinuePage.value = config.hidden
  372. }
  373. }
  374. const saveConfig = () => {
  375. if (!isContinuePage.value){
  376. DynamicTbApi.updateDynamicTb({
  377. id: props.formData.id,
  378. copyConfig: ""
  379. }).then(() => {
  380. ElMessage.success('保存成功');
  381. isConfig.value = false
  382. }).catch(() => {
  383. ElMessage.error('保存失败');
  384. })
  385. return;
  386. }
  387. // 检查判空
  388. if (!continuePageSheetName.value) {
  389. ElMessage.error('请选择续页工作表');
  390. return
  391. }
  392. if (!copyRangeTopLeft.value || !copyRangeTopRight.value || !copyRangeBottomLeft.value || !copyRangeBottomRight.value) {
  393. ElMessage.error('请选择复制范围');
  394. return;
  395. }
  396. DynamicTbApi.updateDynamicTb({
  397. id: props.formData.id,
  398. copyConfig: JSON.stringify({
  399. copyRange: {
  400. topLeft: copyRangeTopLeft.value,
  401. topRight: copyRangeTopRight.value,
  402. bottomLeft: copyRangeBottomLeft.value,
  403. bottomRight: copyRangeBottomRight.value
  404. },
  405. sheetName: continuePageSheetName.value,
  406. hidden: isHiddenContinuePage.value,
  407. isContinuePage: true
  408. })
  409. }).then(() => {
  410. ElMessage.success('保存成功');
  411. isConfig.value = false
  412. }).catch(() => {
  413. ElMessage.error('保存失败');
  414. })
  415. }
  416. </script>
  417. <style lang="scss" scoped>
  418. :deep(.default-toolbar) {
  419. padding-top: 0;
  420. }
  421. .lab-designer-container {
  422. position: relative;
  423. display: flex;
  424. flex-direction: row;
  425. align-items: stretch;
  426. height: calc(100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height));
  427. .spread-designer-container {
  428. width: calc(100% - 440px);
  429. height: 100%;
  430. padding: 0;
  431. }
  432. }
  433. // 配置弹窗的遮罩层允许点击穿透
  434. :deep(.config-dialog-modal) {
  435. pointer-events: none;
  436. // 确保弹窗本身可以接收鼠标事件
  437. .el-dialog {
  438. pointer-events: auto;
  439. }
  440. }
  441. </style>