spreadDesignerGeneric.vue 38 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402
  1. <template>
  2. <view class="spread-designer-generic">
  3. <NavBar>
  4. <template #title>
  5. <text class="nav-title">{{ navTitle }}</text>
  6. </template>
  7. <template #right>
  8. <button
  9. v-for="(button, index) in navButtons"
  10. :key="index"
  11. :class="['nav-btn', button.className]"
  12. @click="handleNavButtonClick(button)"
  13. >
  14. {{ button.text }}
  15. </button>
  16. </template>
  17. </NavBar>
  18. <view class="main-container">
  19. <Designer
  20. ref="designerRef"
  21. :config="designerConfig"
  22. :style="styleInfo"
  23. @designerInitialized="handleWorkbookInitialized"
  24. />
  25. </view>
  26. <view
  27. v-if="floatingInputVisible"
  28. class="floating-input-container"
  29. :class="{ 'keyboard-visible': keyboardHeight > 0 }"
  30. :style="{ '--keyboard-height': `${keyboardHeight - 32}px` }"
  31. >
  32. <view class="floating-toolbar">
  33. <view class="floating-field-name-display">{{ currentFieldName }}</view>
  34. <view class="button-group">
  35. <button
  36. id="prevCellBtn"
  37. class="prev-cell-btn"
  38. :disabled="!currentCell"
  39. @click="findNextEmptyBoundCell('prev')"
  40. >
  41. <text class="btn-text">上一项</text>
  42. </button>
  43. <button
  44. id="nextCellBtn"
  45. class="next-cell-btn"
  46. :disabled="!currentCell"
  47. @click="findNextEmptyBoundCell('next')"
  48. >
  49. <text class="btn-text">下一项</text>
  50. </button>
  51. </view>
  52. </view>
  53. <view class="floating-input-wrapper">
  54. <textarea
  55. id="floatingInput"
  56. v-model="floatingInputValue"
  57. placeholder="请选择单元格"
  58. @input="backfillValueToCell"
  59. />
  60. <button id="floatingClearBtn" class="clear-btn" @click="clearInput">×</button>
  61. </view>
  62. </view>
  63. <view v-if="dialogVisible" class="dialog-overlay" @click="dialogVisible = false">
  64. <view class="dialog-content" @click.stop>
  65. <view class="modal-message">{{ dialogMessage }}</view>
  66. </view>
  67. </view>
  68. </view>
  69. </template>
  70. <script setup>
  71. import { ref, computed, watch, nextTick } from 'vue'
  72. import GC from './tools/gc'
  73. import dayjs from 'dayjs'
  74. import '@grapecity-software/spread-sheets/styles/gc.spread.sheets.excel2013white.css'
  75. import '@grapecity-software/spread-sheets-designer/styles/gc.spread.sheets.designer.min.css'
  76. import '@grapecity-software/spread-sheets-print'
  77. import '@grapecity-software/spread-sheets-shapes'
  78. import '@grapecity-software/spread-sheets-charts'
  79. import '@grapecity-software/spread-sheets-slicers'
  80. import '@grapecity-software/spread-sheets-pivot-addon'
  81. import '@grapecity-software/spread-sheets-tablesheet'
  82. import '@grapecity-software/spread-sheets-ganttsheet'
  83. import '@grapecity-software/spread-sheets-reportsheet-addon'
  84. import '@grapecity-software/spread-sheets-formula-panel'
  85. import '@grapecity-software/spread-sheets-io'
  86. import '@grapecity-software/spread-sheets-designer-resources-cn'
  87. import Designer from '@grapecity-software/spread-sheets-designer-vue'
  88. import NavBar from '@/components/NavBar/NavBar.vue'
  89. const props = defineProps({
  90. businessConfig: {
  91. type: Object,
  92. default: null,
  93. },
  94. templateData: {
  95. type: Object,
  96. default: null,
  97. },
  98. templateBlob: {
  99. type: String,
  100. default: '',
  101. },
  102. })
  103. const emit = defineEmits(['save', 'cancel', 'customAction', 'close', 'openCamera', 'calcCompleted'])
  104. const designerKey = import.meta.env.VITE_SPREADJS_DESIGNER_KEY
  105. const licenseKey = import.meta.env.VITE_SPREADJS_LICENSE_KEY
  106. GC.Spread.Sheets.Designer.LicenseKey = designerKey
  107. GC.Spread.Sheets.LicenseKey = licenseKey
  108. const designer = ref(null)
  109. const navTitle = ref('通用编辑器')
  110. const navButtons = ref([])
  111. const floatingInputVisible = ref(false)
  112. const floatingInputValue = ref('')
  113. const currentCell = ref(null)
  114. const currentCellInfo = ref(null)
  115. const keyboardHeight = ref(0)
  116. const selectedPicture = ref(null)
  117. const currentFieldName = ref('-')
  118. const fieldNameMapping = ref([])
  119. const dialogVisible = ref(false)
  120. const dialogMessage = ref('')
  121. const designerConfig = computed(() => {
  122. const config = GC.Spread.Sheets.Designer.DefaultConfig
  123. config.ribbon = []
  124. const commandsToDelete = ['formulaBarPanel']
  125. config.sidePanels = config.sidePanels.filter((item) => !commandsToDelete.includes(item.command))
  126. return config
  127. })
  128. const styleInfo = computed(() => {
  129. return {
  130. width: '100%',
  131. height: '100%',
  132. }
  133. })
  134. function is(val, type) {
  135. return Object.prototype.toString.call(val) === `[object ${type}]`
  136. }
  137. function changeStringToObject(json) {
  138. if (is(json, 'String') && json?.length) {
  139. return changeStringToObject(JSON.parse(json))
  140. } else if (is(json, 'Object')) {
  141. return json
  142. } else {
  143. console.error('数据类型不正确!')
  144. return null
  145. }
  146. }
  147. function base64ToBlob(base64Data, contentType = '') {
  148. const byteCharacters = atob(base64Data)
  149. const byteNumbers = new Array(byteCharacters.length)
  150. for (let i = 0; i < byteCharacters.length; i++) {
  151. byteNumbers[i] = byteCharacters.charCodeAt(i)
  152. }
  153. const byteArray = new Uint8Array(byteNumbers)
  154. return new Blob([byteArray], { type: contentType })
  155. }
  156. function blobToBase64(blob) {
  157. return new Promise((resolve, reject) => {
  158. const reader = new FileReader()
  159. reader.onload = () => {
  160. const base64 = reader.result.split(',')[1]
  161. resolve(base64)
  162. }
  163. reader.onerror = reject
  164. reader.readAsDataURL(blob)
  165. })
  166. }
  167. function formatterOADateNumber(input) {
  168. const regex = /OADate\(([\d.]+)\)/
  169. const match = input.match(regex)
  170. let OADate = null
  171. if (match && match[1]) {
  172. OADate = Number(match[1])
  173. OADate = isNaN(OADate) ? null : parseInt(OADate)
  174. }
  175. return OADate !== null ? new Date(Math.round(OADate - 25569) * 86400000) : null
  176. }
  177. function getSheetBindingPathData(sheet, dataSource) {
  178. const boundFields = new Set()
  179. const tableMap = new Map()
  180. // 扫描模板文件中的数据填充槽标识(键)
  181. for (let row = 0; row < sheet.getRowCount(); row++) {
  182. for (let col = 0; col < sheet.getColumnCount(); col++) {
  183. const bindingPath = sheet.getBindingPath(row, col)
  184. if (bindingPath) {
  185. boundFields.add(bindingPath)
  186. }
  187. }
  188. }
  189. // 扫描模板文件中所有子表及其对应的数据填充槽标识
  190. const tables = sheet.tables.all() || []
  191. if (tables.length > 0) {
  192. tables.forEach(function (table) {
  193. const tableName = table.bindingPath()
  194. boundFields.add(tableName)
  195. const tableBoundFields = new Set()
  196. const columnCount = table.range().colCount
  197. for (let i = 0; i < columnCount; i++) {
  198. const field = table.getColumnDataField(i)
  199. if (field) {
  200. tableBoundFields.add(field)
  201. }
  202. }
  203. tableMap.set(tableName, [...tableBoundFields])
  204. })
  205. }
  206. const boundFieldsArray = [...boundFields].filter(Boolean)
  207. if (boundFieldsArray.length === 0) return {}
  208. // 根据模板文件数据值槽的需要,过滤数据值容器的数据(而且是按整个数据结构层次的过滤)
  209. function recombineDataSource(dataSourceObj, prekey, boundFieldsArray) {
  210. const filterResult = {}
  211. for (const key in dataSourceObj) {
  212. // 如果key对应属性值是对象,那么按需向下层处理
  213. if (is(dataSourceObj[key], 'Object')) {
  214. const nextKey = !prekey ? key : `${prekey}.${key}`
  215. if (boundFieldsArray.findIndex((field) => field.indexOf(nextKey) >= 0) >= 0) {
  216. filterResult[key] = recombineDataSource(dataSourceObj[key], nextKey, boundFieldsArray)
  217. }
  218. continue
  219. }
  220. // 如果key对应属性值是对象数组,那么对sheet中的子表数据填充槽进行处理
  221. // 也就是说,数组处理上,基本数据值类型一般就是一个单元格的填充,但是对象类型说明是一张子表填充
  222. if (is(dataSourceObj[key], 'Array') && dataSourceObj[key].every((x) => is(x, 'Object'))) {
  223. if (boundFieldsArray.includes(key)) {
  224. filterResult[key] = Object.entries(dataSourceObj[key]).map(([i, item]) => {
  225. if (is(item, 'String')) return item
  226. const tableBoundFieldArrays = tableMap.get(key)
  227. return recombineDataSource(item, '', tableBoundFieldArrays)
  228. })
  229. }
  230. continue
  231. }
  232. if (boundFieldsArray.includes(!prekey ? key : `${prekey}.${key}`)) {
  233. filterResult[key] = dataSourceObj[key]
  234. }
  235. }
  236. return filterResult
  237. }
  238. const newDataSource = recombineDataSource(dataSource, '', boundFieldsArray)
  239. boundFields.clear()
  240. tableMap.clear()
  241. return newDataSource
  242. }
  243. // 生成属性键,并填充对应默认值
  244. function generateDefaultData(schema) {
  245. const result = {}
  246. for (const key in schema.properties) {
  247. result[key] = generatePropertyDefaultValue(schema.properties[key])
  248. }
  249. return result
  250. }
  251. // 为属性填充默认值
  252. function generatePropertyDefaultValue(property) {
  253. if (property.type === 'array' && property.items) {
  254. if (property.items.type !== 'object' || !property.items.properties) {
  255. return ['']
  256. }
  257. const itemValue = {}
  258. for (const itemKey in property.items.properties) {
  259. itemValue[itemKey] = generatePropertyDefaultValue(property.items.properties[itemKey])
  260. }
  261. return [itemValue]
  262. }
  263. if (property.properties) {
  264. const objValue = {}
  265. for (const objKey in property.properties) {
  266. objValue[objKey] = generatePropertyDefaultValue(property.properties[objKey])
  267. }
  268. return objValue
  269. }
  270. return ''
  271. }
  272. /**
  273. * 为属性填充非默认数据值
  274. * @param target 属性值为默认值的键值数据容器
  275. * @param source 填充数据到键值容器的数据源头
  276. */
  277. function deepMergeSchemaValue(target, source) {
  278. const result = { ...target }
  279. for (const key in source) {
  280. if (source.hasOwnProperty(key)) {
  281. const targetValue = target[key]
  282. const sourceValue = source[key]
  283. if (targetValue === undefined) {
  284. continue
  285. }
  286. if (Array.isArray(targetValue) && Array.isArray(sourceValue)) {
  287. result[key] = sourceValue.length > 0 ? sourceValue : targetValue
  288. } else if (
  289. typeof targetValue === 'object' &&
  290. targetValue !== null &&
  291. typeof sourceValue === 'object' &&
  292. sourceValue !== null
  293. ) {
  294. result[key] =
  295. Object.keys(sourceValue).length === 0
  296. ? targetValue
  297. : deepMergeSchemaValue(targetValue, sourceValue)
  298. } else if (sourceValue !== undefined && sourceValue !== '') {
  299. result[key] = sourceValue
  300. }
  301. }
  302. }
  303. return result
  304. }
  305. function generateAndReturnDataSourceOADate(dataSource) {
  306. if (!is(dataSource, 'Object') && !is(dataSource, 'Array')) return {}
  307. if (is(dataSource, 'Object')) {
  308. for (const key in dataSource) {
  309. if (is(dataSource[key], 'String') && dataSource[key].indexOf('OADate') >= 0) {
  310. dataSource[key] = dayjs(formatterOADateNumber(dataSource[key])).format('YYYY年MM月DD日')
  311. continue
  312. } else if (is(dataSource[key], 'Date')) {
  313. dataSource[key] = dayjs(dataSource[key]).format('YYYY年MM月DD日')
  314. continue
  315. }
  316. if (is(dataSource[key], 'Object') || is(dataSource[key], 'Array')) {
  317. dataSource[key] = generateAndReturnDataSourceOADate(dataSource[key])
  318. continue
  319. }
  320. }
  321. }
  322. if (is(dataSource, 'Array')) {
  323. return dataSource.map((keyItem) => {
  324. if (is(keyItem, 'String') && keyItem.indexOf('OADate') >= 0) {
  325. return dayjs(formatterOADateNumber(keyItem)).format('YYYY年MM月DD日')
  326. }
  327. if (is(keyItem, 'Object') || is(keyItem, 'Array')) {
  328. return generateAndReturnDataSourceOADate(keyItem)
  329. }
  330. return keyItem
  331. })
  332. }
  333. return dataSource
  334. }
  335. // 监听值变化(用户通过UI操作变化值会被监听,代码去变化值不会被监听),进行dataSource的同步(发生事件以外的sheet)
  336. // 因为每个sheet用了同一个dataSource(这里不是同一个实例,但是是数据结构和值相同),为了保持这个dataSource的一致,所以要更新其他sheet的datasource
  337. function registerCellValuesChangeEventHandlerForEverySheet(sheets) {
  338. sheets.forEach((activedSheet) => {
  339. activedSheet.bind(GC.Spread.Sheets.Events.ValueChanged, function (sender, args) {
  340. const bindingPathName = activedSheet.getBindingPath(args.row, args.col)
  341. if (bindingPathName) {
  342. try {
  343. // 处理其他sheet的dataSource
  344. for (const sheet of sheets) {
  345. const dataSource = sheet.getDataSource()?.getSource() || {}
  346. if (
  347. !dataSource.hasOwnProperty(bindingPathName) ||
  348. (activedSheet.name() === sheet.name() && activedSheet.__ID__ === sheet.__ID__)
  349. )
  350. continue
  351. const tables = sheet.tables?.all() || []
  352. for (let i = 0; i < tables.length; i++) {
  353. const table = sheet.tables.all()[i]
  354. table.expandBoundRows(true)
  355. }
  356. const newDataSource = new GC.Spread.Sheets.Bindings.CellBindingSource({
  357. ...dataSource,
  358. [bindingPathName]: args.newValue,
  359. })
  360. sheet.setDataSource(newDataSource)
  361. }
  362. } catch (err) {
  363. console.error('监听单元格出错啦', err)
  364. }
  365. }
  366. })
  367. })
  368. }
  369. function registerZoomEventHandler(designerInstance) {
  370. if (!designerInstance) return
  371. const spreadInstance = designerInstance?.getWorkbook()
  372. if (Object.prototype.toString.call(spreadInstance) !== '[object Object]') return
  373. // 缩放事件
  374. spreadInstance.bind(GC.Spread.Sheets.Events.ViewZooming, function (sender, args) {
  375. const activedSheet = spreadInstance.getActiveSheet()
  376. const minZoom = calcSheetZoom(activedSheet)
  377. // 如果缩小,限制其最小是sheet铺满屏幕,否则不再缩小
  378. if (args.newZoomFactor < minZoom) args.cancel = true
  379. })
  380. }
  381. // 计算sheet铺满屏幕的缩放比例
  382. function calcSheetZoom(sheet) {
  383. const screenWidth = window.screen.width
  384. const usedRange = sheet.getUsedRange(GC.Spread.Sheets.UsedRangeType.style)
  385. sheet.showCell(usedRange.row, usedRange.col)
  386. const scrollbarWidth = 40
  387. let totalWidth = 0
  388. for (let col = usedRange.col; col <= usedRange.colCount - 1; col++) {
  389. totalWidth += sheet.getColumnWidth(col)
  390. }
  391. return 1 + (screenWidth - scrollbarWidth - totalWidth) / totalWidth
  392. }
  393. function initDesignerSheetConfig(sheet) {
  394. sheet?.zoom(1)
  395. sheet?.zoom(calcSheetZoom(sheet))
  396. sheet.options.rowHeaderVisible = false
  397. sheet.options.colHeaderVisible = false
  398. sheet.setActiveCell(null)
  399. }
  400. async function setDefaultSchema(designerInstance, bindingPathSchema) {
  401. const initBindingPathSchema = !bindingPathSchema ? {} : changeStringToObject(bindingPathSchema)
  402. await designerInstance.setData('treeNodeFromJson', JSON.stringify(initBindingPathSchema))
  403. }
  404. function getDefaultSchema(designerInstance) {
  405. return (
  406. designerInstance.getData('updatedTreeNode') ||
  407. designerInstance.getData('treeNodeFromJson') ||
  408. designerInstance.getData('oldTreeNodeFromJson')
  409. )
  410. }
  411. // 设置数据到模板文件,渲染对应数据
  412. function setDataSource(designerInstance, schemaData, dataSourceValues) {
  413. dataSourceValues =
  414. !is(dataSourceValues, 'Object') && is(dataSourceValues, 'String')
  415. ? JSON.parse(dataSourceValues)
  416. : dataSourceValues || {}
  417. schemaData =
  418. !is(schemaData, 'Object') && is(schemaData, 'String')
  419. ? JSON.parse(schemaData)
  420. : schemaData || {}
  421. const formatterSource = generateDefaultData(schemaData)
  422. const resultDataSource = deepMergeSchemaValue(formatterSource, dataSourceValues)
  423. const spreadInstance = designerInstance.getWorkbook()
  424. registerCellValuesChangeEventHandlerForEverySheet(spreadInstance.sheets)
  425. for (const sheet of spreadInstance.sheets) {
  426. const tables = sheet.tables?.all() || []
  427. for (let i = 0; i < tables.length; i++) {
  428. const table = tables[i]
  429. table.expandBoundRows(true)
  430. }
  431. const filterResultDataSource = getSheetBindingPathData(sheet, resultDataSource)
  432. const dataSource = new GC.Spread.Sheets.Bindings.CellBindingSource(filterResultDataSource)
  433. sheet.setDataSource(dataSource)
  434. }
  435. }
  436. function getDataSource(designerInstance) {
  437. const spreadInstance = designerInstance.getWorkbook()
  438. let dataSource = {}
  439. for (const sheet of spreadInstance.sheets) {
  440. const source = sheet.getDataSource()?.getSource()
  441. if (source) {
  442. for (const key in source) {
  443. if (is(source[key], 'Object') && is(dataSource[key], 'Object')) {
  444. source[key] = Object.assign(dataSource[key], source[key])
  445. }
  446. }
  447. dataSource = {
  448. ...dataSource,
  449. ...source,
  450. }
  451. }
  452. }
  453. dataSource = generateAndReturnDataSourceOADate(dataSource)
  454. return dataSource
  455. }
  456. function handleSheetTableCopyTo(designerInstance, isRowMerage) {
  457. if (!designerInstance) return
  458. const spreadInstance = designerInstance.getWorkbook()
  459. for (const sheet of spreadInstance.sheets) {
  460. const tables = sheet.tables?.all() || []
  461. for (let tableIndex = 0; tableIndex < tables.length; tableIndex++) {
  462. const range = tables[tableIndex].range()
  463. const { row, rowCount, col, colCount } = range
  464. sheet.getRange(row, col, rowCount, colCount).wordWrap(true)
  465. if (isRowMerage) {
  466. const firstRowSpans = []
  467. for (let c = col; c < col + colCount; c++) {
  468. const span = sheet.getSpan(row, c)
  469. if (span && span.row === row) {
  470. firstRowSpans.push({
  471. startCol: span.col,
  472. colCount: span.colCount,
  473. })
  474. c += span.colCount - 1
  475. }
  476. }
  477. if (firstRowSpans.length === 0) {
  478. const firstRowValues = []
  479. for (let c = col; c < col + colCount; c++) {
  480. firstRowValues.push(sheet.getValue(row, c))
  481. }
  482. let startMergeCol = 0
  483. for (let c = 1; c <= colCount; c++) {
  484. if (c === colCount || firstRowValues[c] !== firstRowValues[c - 1]) {
  485. if (c - startMergeCol > 1) {
  486. firstRowSpans.push({
  487. startCol: col + startMergeCol,
  488. colCount: c - startMergeCol,
  489. })
  490. sheet.addSpan(row, col + startMergeCol, 1, c - startMergeCol)
  491. }
  492. startMergeCol = c
  493. }
  494. }
  495. }
  496. for (let r = 1; r < rowCount; r++) {
  497. const currentRow = row + r
  498. firstRowSpans.forEach((mergeInfo) => {
  499. try {
  500. const firstCellValue = sheet.getValue(currentRow, mergeInfo.startCol)
  501. let shouldMerge = true
  502. for (let c = 1; c < mergeInfo.colCount; c++) {
  503. const currentCellValue = sheet.getValue(currentRow, mergeInfo.startCol + c)
  504. if (currentCellValue !== firstCellValue) {
  505. shouldMerge = false
  506. break
  507. }
  508. }
  509. if (shouldMerge) {
  510. sheet.addSpan(currentRow, mergeInfo.startCol, 1, mergeInfo.colCount)
  511. }
  512. } catch (error) {
  513. console.warn(
  514. `合并单元格失败 at (${currentRow}, ${mergeInfo.startCol}):`,
  515. error.message,
  516. )
  517. }
  518. })
  519. }
  520. }
  521. for (let rowIndex = 1; rowIndex < rowCount; rowIndex++) {
  522. const currentRow = row + rowIndex
  523. sheet?.copyTo(
  524. row,
  525. col,
  526. currentRow,
  527. col,
  528. 1,
  529. colCount,
  530. GC.Spread.Sheets.CopyToOptions.style | GC.Spread.Sheets.CopyToOptions.formula,
  531. )
  532. }
  533. setTimeout(() => {
  534. for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {
  535. const currentRow = row + rowIndex
  536. sheet?.autoFitRow(currentRow)
  537. const currentHeight = sheet.getRowHeight(currentRow)
  538. const newHeight = Math.max(currentHeight * 1.2, 25)
  539. sheet.setRowHeight(currentRow, newHeight)
  540. }
  541. }, 100)
  542. }
  543. const currentTheme = sheet.currentTheme()
  544. currentTheme.bodyFont('仿宋_GB2312')
  545. }
  546. }
  547. function removeSheetCellFocus(designerInstance) {
  548. const spreadInstance = designerInstance.getWorkbook()
  549. if (spreadInstance) {
  550. spreadInstance.focus(false)
  551. for (const sheet of spreadInstance?.sheets) {
  552. sheet.clearSelection()
  553. }
  554. }
  555. }
  556. function updateFieldNameDisplay(fieldName) {
  557. let displayName = '-'
  558. if (fieldNameMapping.value && Array.isArray(fieldNameMapping.value)) {
  559. const fieldMapping = fieldNameMapping.value.find((item) => item.field === fieldName)
  560. if (fieldMapping && fieldMapping.displayName) {
  561. displayName = fieldMapping.displayName
  562. }
  563. }
  564. currentFieldName.value = displayName
  565. }
  566. function showFloatingInput() {
  567. floatingInputValue.value = currentCellInfo.value.value
  568. floatingInputVisible.value = true
  569. }
  570. function backfillValueToCell() {
  571. if (!currentCell.value || !currentCellInfo.value) return
  572. const { sheet, row, col } = currentCell.value
  573. sheet.setValue(row, col, floatingInputValue.value)
  574. }
  575. function clearInput() {
  576. floatingInputValue.value = ''
  577. }
  578. function findNextEmptyBoundCell(direction = 'next') {
  579. if (!currentCell.value || !currentCell.value.sheet) {
  580. console.warn('没有当前活动单元格')
  581. return
  582. }
  583. try {
  584. const sheet = currentCell.value.sheet
  585. const usedRange = sheet.getUsedRange(GC.Spread.Sheets.UsedRangeType.data)
  586. if (!usedRange) {
  587. console.warn('没有找到使用范围')
  588. return
  589. }
  590. const { rowCount, colCount } = usedRange
  591. const startRow = currentCell.value.row
  592. const startCol = currentCell.value.col
  593. const targetCell = findNextEmptyCellFromPosition(
  594. sheet,
  595. startRow,
  596. startCol,
  597. rowCount,
  598. colCount,
  599. direction,
  600. )
  601. if (targetCell) {
  602. currentCell.value = { sheet, row: targetCell.row, col: targetCell.col }
  603. const cell = sheet.getCell(targetCell.row, targetCell.col)
  604. const bindingPath = sheet.getBindingPath(targetCell.row, targetCell.col) || '-'
  605. currentCellInfo.value = {
  606. value: cell.value() || '',
  607. isLocked: false,
  608. formatter: cell.formatter(),
  609. fieldName: bindingPath,
  610. }
  611. updateFieldNameDisplay(bindingPath)
  612. showFloatingInput()
  613. sheet.setSelection(targetCell.row, targetCell.col, 1, 1)
  614. const directionText = direction === 'next' ? '下一个' : '上一个'
  615. console.log(
  616. `跳转到${directionText}未输入单元格: 行${targetCell.row + 1}, 列${targetCell.col + 1}`,
  617. )
  618. } else {
  619. console.log(`没有找到更多${direction === 'next' ? '下一个' : '上一个'}未输入的绑定字段单元格`)
  620. }
  621. } catch (error) {
  622. console.error('查找过程中出错:', error)
  623. }
  624. }
  625. function findNextEmptyCellFromPosition(
  626. sheet,
  627. startRow,
  628. startCol,
  629. maxRow,
  630. maxCol,
  631. direction = 'next',
  632. ) {
  633. const currentCellRange = getMergedCellRange(sheet, startRow, startCol)
  634. const targetCell = searchInRange(
  635. sheet,
  636. currentCellRange,
  637. startRow,
  638. startCol,
  639. maxRow,
  640. maxCol,
  641. direction,
  642. true,
  643. )
  644. if (targetCell) return targetCell
  645. return searchInRange(
  646. sheet,
  647. currentCellRange,
  648. startRow,
  649. startCol,
  650. maxRow,
  651. maxCol,
  652. direction,
  653. false,
  654. )
  655. }
  656. function searchInRange(
  657. sheet,
  658. currentCellRange,
  659. startRow,
  660. startCol,
  661. maxRow,
  662. maxCol,
  663. direction,
  664. fromCurrentPosition,
  665. ) {
  666. const rowStart = fromCurrentPosition ? startRow : direction === 'next' ? 0 : maxRow - 1
  667. const rowEnd = fromCurrentPosition
  668. ? direction === 'next'
  669. ? maxRow
  670. : -1
  671. : direction === 'next'
  672. ? startRow
  673. : startRow
  674. const rowStep = direction === 'next' ? 1 : -1
  675. for (let row = rowStart; direction === 'next' ? row < rowEnd : row >= rowEnd; row += rowStep) {
  676. let colStart, colEnd, colStep
  677. if (fromCurrentPosition) {
  678. colStart =
  679. row === startRow
  680. ? direction === 'next'
  681. ? startCol + 1
  682. : startCol - 1
  683. : direction === 'next'
  684. ? 0
  685. : maxCol - 1
  686. colEnd = direction === 'next' ? maxCol : -1
  687. colStep = direction === 'next' ? 1 : -1
  688. } else {
  689. colStart = direction === 'next' ? 0 : maxCol - 1
  690. colEnd = row === startRow ? startCol : direction === 'next' ? maxCol : -1
  691. colStep = direction === 'next' ? 1 : -1
  692. }
  693. for (let col = colStart; direction === 'next' ? col < colEnd : col >= colEnd; col += colStep) {
  694. if (!fromCurrentPosition && row === startRow && col === startCol) {
  695. continue
  696. }
  697. if (isInMergedRange(currentCellRange, row, col)) {
  698. continue
  699. }
  700. const bindingPath = sheet.getBindingPath(row, col)
  701. const cellValue = sheet.getValue(row, col)
  702. if (bindingPath && (cellValue === null || cellValue === undefined || cellValue === '')) {
  703. return { row, col }
  704. }
  705. }
  706. }
  707. return null
  708. }
  709. function getMergedCellRange(sheet, row, col) {
  710. try {
  711. const span = sheet.getSpan(row, col)
  712. if (span) {
  713. return {
  714. row: span.row,
  715. col: span.col,
  716. rowCount: span.rowCount,
  717. colCount: span.colCount,
  718. }
  719. }
  720. } catch (error) {
  721. console.log('获取合并范围失败:', error)
  722. }
  723. return null
  724. }
  725. function isInMergedRange(mergedRange, row, col) {
  726. if (!mergedRange) {
  727. return false
  728. }
  729. const { row: rangeRow, col: rangeCol, rowCount, colCount } = mergedRange
  730. return (
  731. row >= rangeRow && row < rangeRow + rowCount && col >= rangeCol && col < rangeCol + colCount
  732. )
  733. }
  734. function handleWorkbookInitialized(spreadInstance) {
  735. designer.value = spreadInstance
  736. registerZoomEventHandler(designer.value)
  737. designer.value.setData('isRibbonCollapse', true)
  738. const templateData = props.templateData
  739. const businessConfig = props.businessConfig
  740. if (businessConfig && templateData) {
  741. initGenericEditorUI(businessConfig.ui)
  742. loadTemplateData(props.templateBlob, templateData)
  743. } else {
  744. console.error('没有获取到业务配置和模板数据')
  745. }
  746. if (templateData && templateData.pathNameMapping) {
  747. fieldNameMapping.value = templateData.pathNameMapping
  748. console.log('字段名映射已初始化:', fieldNameMapping.value)
  749. } else {
  750. fieldNameMapping.value = []
  751. console.log('未找到字段名映射,使用空数组')
  752. }
  753. }
  754. function initGenericEditorUI(uiConfig) {
  755. navTitle.value = uiConfig.title
  756. navButtons.value = []
  757. const cancelBtn = {
  758. text: uiConfig.cancelButtonText || '取消',
  759. className: '',
  760. action: 'cancel',
  761. }
  762. navButtons.value.push(cancelBtn)
  763. if (!uiConfig.hideSaveButton) {
  764. const saveBtn = {
  765. text: uiConfig.saveButtonText || '保存',
  766. className: 'primary',
  767. action: 'save',
  768. }
  769. navButtons.value.push(saveBtn)
  770. }
  771. if (uiConfig.customButtons && uiConfig.customButtons.length > 0) {
  772. uiConfig.customButtons.forEach((button) => {
  773. const customBtn = {
  774. text: button.text,
  775. className: button.className || '',
  776. action: button.action,
  777. }
  778. navButtons.value.push(customBtn)
  779. })
  780. }
  781. }
  782. function handleNavButtonClick(button) {
  783. if (button.action === 'cancel') {
  784. cancelEdit()
  785. } else if (button.action === 'save') {
  786. saveRecord()
  787. } else {
  788. handleCustomAction(button.action)
  789. }
  790. }
  791. /**
  792. * 加载模板文件和数据,
  793. * @param templateBlob 模板文件,是模板数据的基础容器
  794. * @param templateData 模板数据,填充模板文件中对应的数据格
  795. */
  796. function loadTemplateData(templateBlob, templateData) {
  797. console.log('触发模板渲染加载。。。')
  798. if (!designer.value || !templateData) return
  799. const spreadInstance = designer.value.getWorkbook()
  800. spreadInstance.touchToolStrip.clear()
  801. if (templateBlob) {
  802. console.log('开始加载模板文件')
  803. try {
  804. const blob = base64ToBlob(templateBlob, 'application/ssjson')
  805. spreadInstance.open(
  806. blob,
  807. () => {
  808. console.log('模板文件加载成功!')
  809. initDesignerSheetConfig(spreadInstance.getActiveSheet())
  810. if (templateData.schema) {
  811. initDataSource(designer.value, templateData)
  812. }
  813. handleSheetTableCopyTo(designer.value, true)
  814. initSpreadInputEvents(spreadInstance)
  815. },
  816. (error) => {
  817. console.error('模板文件加载失败:', error)
  818. },
  819. )
  820. } catch (error) {
  821. console.error('处理模板文件时发生错误:', error)
  822. }
  823. } else if (templateData.schema) {
  824. console.log(
  825. '开始加载schema模板:',
  826. JSON.stringify(templateData.schema).substring(0, 200) + '...',
  827. )
  828. console.log('开始加载数据:', JSON.stringify(templateData.data).substring(0, 200) + '...')
  829. if (spreadInstance.getSheetCount() === 0) {
  830. const sheet = spreadInstance.addSheet(0)
  831. sheet.name('Sheet1')
  832. initDesignerSheetConfig(sheet)
  833. console.log('创建了新工作表')
  834. }
  835. initDataSource(designer.value, templateData)
  836. spreadInstance.repaint()
  837. handleSheetTableCopyTo(designer.value, true)
  838. initSpreadInputEvents(spreadInstance)
  839. }
  840. console.log('模板数据加载完成。。。')
  841. }
  842. function initDataSource(designer, templateData) {
  843. setDefaultSchema(designer, templateData.schema)
  844. setDataSource(designer, templateData.schema, templateData.data)
  845. }
  846. function initSpreadInputEvents(spreadInstance) {
  847. spreadInstance.bind(GC.Spread.Sheets.Events.CellClick, function (sender, args) {
  848. const sheet = args.sheet
  849. const row = args.row
  850. const col = args.col
  851. selectedPicture.value = null
  852. currentCell.value = { sheet, row, col }
  853. const cell = sheet.getCell(row, col)
  854. const bindingPath = sheet.getBindingPath(row, col) || '-'
  855. currentCellInfo.value = {
  856. value: cell.value() || '',
  857. isLocked: false,
  858. formatter: cell.formatter(),
  859. fieldName: bindingPath,
  860. }
  861. updateFieldNameDisplay(bindingPath)
  862. showFloatingInput()
  863. })
  864. console.log('悬浮输入框事件已绑定')
  865. }
  866. function handleCustomAction(action) {
  867. if (!designer.value) return
  868. const spreadInstance = designer.value.getWorkbook()
  869. spreadInstance.save(async (blob) => {
  870. removeSheetCellFocus(designer.value)
  871. const base64 = await blobToBase64(blob)
  872. const dataJSON = getDataSource(designer.value)
  873. const schemaJSON = getDefaultSchema(designer.value)
  874. emit('customAction', {
  875. action,
  876. data: {
  877. blob: base64,
  878. dataJSON,
  879. schemaJSON,
  880. businessType: props.businessConfig.businessType,
  881. templateId: props.templateData.data?.templateId || props.templateData.template,
  882. reportUrl: props.templateData.data?.templateUrl || props.templateData.templateUrl || '',
  883. },
  884. })
  885. })
  886. }
  887. function saveRecord() {
  888. if (!designer.value || !props.businessConfig || !props.templateData) return
  889. const spreadInstance = designer.value.getWorkbook()
  890. spreadInstance.save(async (blob) => {
  891. removeSheetCellFocus(designer.value)
  892. const base64 = await blobToBase64(blob)
  893. const dataJSON = getDataSource(designer.value)
  894. const schemaJSON = getDefaultSchema(designer.value)
  895. emit('save', {
  896. blob: base64,
  897. dataJSON,
  898. schemaJSON,
  899. businessType: props.businessConfig.businessType,
  900. templateId: props.templateData.data?.templateId || props.templateData.template,
  901. reportUrl: props.templateData.data?.templateUrl || props.templateData.templateUrl || '',
  902. })
  903. })
  904. }
  905. function cancelEdit() {
  906. emit('cancel')
  907. }
  908. function goBack() {
  909. uni.navigateBack({
  910. delta: 1,
  911. success: () => {
  912. // 返回成功后的回调函数
  913. },
  914. fail: () => {
  915. // 返回失败后的回调函数
  916. },
  917. })
  918. }
  919. function showDialog(message, timer = 3000) {
  920. dialogMessage.value = message
  921. dialogVisible.value = true
  922. setTimeout(() => {
  923. dialogVisible.value = false
  924. }, timer)
  925. }
  926. function updateDataSource(dataSource) {
  927. const templateData = props.templateData
  928. if (!designer.value || !templateData || !templateData.schema) return
  929. setDataSource(designer.value, templateData.schema, dataSource)
  930. showDialog('数据更新完成')
  931. }
  932. function onWebViewResize(newWidth) {
  933. console.log('收到屏幕尺寸变化通知,新宽度:', newWidth)
  934. if (designer.value) {
  935. const spreadInstance = designer.value.getWorkbook()
  936. spreadInstance.touchToolStrip.clear()
  937. const activedSheet = spreadInstance.getActiveSheet()
  938. initDesignerSheetConfig(activedSheet)
  939. }
  940. }
  941. function setKeyboardHeight(height) {
  942. keyboardHeight.value = height
  943. }
  944. function handleCameraResult(result) {
  945. if (result.success && result.imageData) {
  946. console.log('收到相机拍摄结果:', result)
  947. } else {
  948. console.log('相机拍摄失败或用户取消')
  949. }
  950. }
  951. watch(
  952. () => props.templateBlob,
  953. (newBlob, oldBlob) => {
  954. if (newBlob && newBlob !== oldBlob && designer.value && props.templateData) {
  955. console.log('templateBlob 发生变化,重新加载模板,数据:', props.templateData)
  956. loadTemplateData(props.templateBlob, props.templateData)
  957. }
  958. },
  959. )
  960. defineExpose({
  961. onWebViewResize,
  962. updateDataSource,
  963. handleCameraResult,
  964. setKeyboardHeight,
  965. })
  966. </script>
  967. <style scoped>
  968. * {
  969. box-sizing: border-box;
  970. padding: 0;
  971. margin: 0;
  972. }
  973. .spread-designer-generic {
  974. position: relative;
  975. height: 100vh;
  976. overflow: hidden;
  977. font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  978. }
  979. .main-container {
  980. width: 100%;
  981. height: calc(100vh - clamp(44px, 8vw, 56px) - 95px);
  982. }
  983. .navigator-bar {
  984. box-sizing: border-box;
  985. display: flex;
  986. align-items: center;
  987. justify-content: space-between;
  988. height: clamp(44px, 8vw, 56px);
  989. min-height: 44px;
  990. padding: 0 16px;
  991. background-color: #f8f9fa;
  992. border-bottom: 1px solid #e9ecef;
  993. }
  994. .nav-left {
  995. display: flex;
  996. flex: 1;
  997. align-items: center;
  998. }
  999. .back-btn {
  1000. padding: 0 8px;
  1001. font-size: 18px;
  1002. cursor: pointer;
  1003. background: none;
  1004. border: 0;
  1005. border-radius: 4px;
  1006. transition: background-color 0.2s;
  1007. }
  1008. .back-btn:hover {
  1009. background-color: #e9ecef;
  1010. }
  1011. .nav-title {
  1012. flex: 1;
  1013. font-size: 17px;
  1014. font-weight: 600;
  1015. color: #333;
  1016. text-align: center;
  1017. }
  1018. .nav-right {
  1019. display: flex;
  1020. flex: 1;
  1021. gap: 8px;
  1022. justify-content: flex-end;
  1023. }
  1024. .nav-btn {
  1025. padding: 0 12px;
  1026. font-size: 12px;
  1027. color: #495057;
  1028. white-space: nowrap;
  1029. cursor: pointer;
  1030. background-color: #fff;
  1031. border: 1px solid #dee2e6;
  1032. border-radius: 4px;
  1033. transition: all 0.2s;
  1034. }
  1035. .nav-btn:hover {
  1036. background-color: #f8f9fa;
  1037. }
  1038. .nav-btn.primary {
  1039. color: white;
  1040. background-color: #007bff;
  1041. border-color: #007bff;
  1042. }
  1043. .nav-btn.primary:hover {
  1044. background-color: #0056b3;
  1045. }
  1046. .nav-btn.warning {
  1047. color: white;
  1048. background-color: #e6a23c;
  1049. border-color: #e6a23c;
  1050. }
  1051. .floating-input-wrapper {
  1052. position: relative;
  1053. width: 100%;
  1054. }
  1055. .floating-input-wrapper textarea {
  1056. width: 100%;
  1057. height: 40px;
  1058. padding: 8px 32px 8px 12px;
  1059. font-size: 14px;
  1060. resize: none;
  1061. border: 1px solid #d9d9d9;
  1062. border-radius: 4px;
  1063. outline: none;
  1064. }
  1065. .floating-input-wrapper textarea:focus {
  1066. border-color: #007aff;
  1067. }
  1068. .clear-btn {
  1069. position: absolute;
  1070. top: 50%;
  1071. right: 8px;
  1072. z-index: 2;
  1073. display: flex;
  1074. align-items: center;
  1075. justify-content: center;
  1076. width: 24px;
  1077. height: 24px;
  1078. font-size: 16px;
  1079. font-weight: bold;
  1080. color: #666;
  1081. cursor: pointer;
  1082. background: #f0f0f0;
  1083. border: none;
  1084. border-radius: 12px;
  1085. transform: translateY(-50%);
  1086. }
  1087. .clear-btn:hover {
  1088. color: #333;
  1089. background: #e0e0e0;
  1090. }
  1091. .floating-field-name-display {
  1092. display: flex;
  1093. flex: 1;
  1094. align-items: center;
  1095. height: 35px;
  1096. padding: 6px 12px;
  1097. margin-right: 8px;
  1098. overflow: hidden;
  1099. font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  1100. font-size: 12px;
  1101. line-height: 1.2;
  1102. color: #666;
  1103. text-overflow: ellipsis;
  1104. white-space: nowrap;
  1105. background: #f5f5f5;
  1106. border-radius: 4px;
  1107. }
  1108. .floating-input-container {
  1109. position: fixed;
  1110. right: 0;
  1111. bottom: 0;
  1112. left: 0;
  1113. z-index: 9999;
  1114. display: flex;
  1115. flex-direction: column;
  1116. width: 100%;
  1117. min-height: 85px;
  1118. padding: 8px 12px 12px 12px;
  1119. background: white;
  1120. border-top: 1px solid #e0e0e0;
  1121. transition: all 0.3s ease;
  1122. }
  1123. .floating-input-container.keyboard-visible {
  1124. position: fixed !important;
  1125. bottom: var(--keyboard-height, 0);
  1126. z-index: 10000;
  1127. min-height: 105px;
  1128. max-height: 345px;
  1129. }
  1130. .floating-toolbar {
  1131. display: flex;
  1132. align-items: center;
  1133. justify-content: space-between;
  1134. height: 35px;
  1135. padding: 0 2px;
  1136. margin-top: 4px;
  1137. margin-bottom: 10px;
  1138. }
  1139. .button-group {
  1140. display: flex;
  1141. gap: 8px;
  1142. align-items: center;
  1143. }
  1144. .prev-cell-btn {
  1145. position: relative;
  1146. display: flex;
  1147. align-items: center;
  1148. justify-content: center;
  1149. min-width: 60px;
  1150. height: 35px;
  1151. padding: 6px 12px;
  1152. font-size: 12px;
  1153. font-weight: 500;
  1154. line-height: 1;
  1155. color: white;
  1156. cursor: pointer;
  1157. background: #007aff;
  1158. border: none;
  1159. border-radius: 4px;
  1160. transition: all 0.2s ease;
  1161. }
  1162. .prev-cell-btn:hover:not(:disabled) {
  1163. background: #0056b3;
  1164. transform: translateY(-1px);
  1165. }
  1166. .prev-cell-btn:active:not(:disabled) {
  1167. transform: translateY(0);
  1168. }
  1169. .prev-cell-btn:focus {
  1170. outline: none;
  1171. box-shadow: 0 0 0 1px rgba(0, 122, 255, 0.3);
  1172. }
  1173. .prev-cell-btn:disabled {
  1174. color: #999999;
  1175. cursor: not-allowed;
  1176. background: #cccccc;
  1177. opacity: 0.7;
  1178. }
  1179. .next-cell-btn {
  1180. position: relative;
  1181. display: flex;
  1182. align-items: center;
  1183. justify-content: center;
  1184. min-width: 60px;
  1185. height: 35px;
  1186. padding: 6px 12px;
  1187. font-size: 12px;
  1188. font-weight: 500;
  1189. line-height: 1;
  1190. color: white;
  1191. cursor: pointer;
  1192. background: #007aff;
  1193. border: none;
  1194. border-radius: 4px;
  1195. transition: all 0.2s ease;
  1196. }
  1197. .next-cell-btn:hover:not(:disabled) {
  1198. background: #0056b3;
  1199. transform: translateY(-1px);
  1200. }
  1201. .next-cell-btn:active:not(:disabled) {
  1202. transform: translateY(0);
  1203. }
  1204. .next-cell-btn:focus {
  1205. outline: none;
  1206. box-shadow: 0 0 0 1px rgba(0, 122, 255, 0.3);
  1207. }
  1208. .next-cell-btn:disabled {
  1209. color: #999999;
  1210. cursor: not-allowed;
  1211. background: #cccccc;
  1212. opacity: 0.7;
  1213. }
  1214. .dialog-overlay {
  1215. position: fixed;
  1216. top: 0;
  1217. right: 0;
  1218. bottom: 0;
  1219. left: 0;
  1220. z-index: 10001;
  1221. display: flex;
  1222. align-items: center;
  1223. justify-content: center;
  1224. background-color: rgba(0, 0, 0, 0.5);
  1225. }
  1226. .dialog-content {
  1227. max-width: 80%;
  1228. padding: 20px;
  1229. background-color: white;
  1230. border-radius: 8px;
  1231. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  1232. }
  1233. .modal-message {
  1234. font-size: 16px;
  1235. line-height: 1.5;
  1236. color: #333;
  1237. text-align: center;
  1238. }
  1239. </style>