| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402 |
- <template>
- <view class="spread-designer-generic">
- <NavBar>
- <template #title>
- <text class="nav-title">{{ navTitle }}</text>
- </template>
- <template #right>
- <button
- v-for="(button, index) in navButtons"
- :key="index"
- :class="['nav-btn', button.className]"
- @click="handleNavButtonClick(button)"
- >
- {{ button.text }}
- </button>
- </template>
- </NavBar>
- <view class="main-container">
- <Designer
- ref="designerRef"
- :config="designerConfig"
- :style="styleInfo"
- @designerInitialized="handleWorkbookInitialized"
- />
- </view>
- <view
- v-if="floatingInputVisible"
- class="floating-input-container"
- :class="{ 'keyboard-visible': keyboardHeight > 0 }"
- :style="{ '--keyboard-height': `${keyboardHeight - 32}px` }"
- >
- <view class="floating-toolbar">
- <view class="floating-field-name-display">{{ currentFieldName }}</view>
- <view class="button-group">
- <button
- id="prevCellBtn"
- class="prev-cell-btn"
- :disabled="!currentCell"
- @click="findNextEmptyBoundCell('prev')"
- >
- <text class="btn-text">上一项</text>
- </button>
- <button
- id="nextCellBtn"
- class="next-cell-btn"
- :disabled="!currentCell"
- @click="findNextEmptyBoundCell('next')"
- >
- <text class="btn-text">下一项</text>
- </button>
- </view>
- </view>
- <view class="floating-input-wrapper">
- <textarea
- id="floatingInput"
- v-model="floatingInputValue"
- placeholder="请选择单元格"
- @input="backfillValueToCell"
- />
- <button id="floatingClearBtn" class="clear-btn" @click="clearInput">×</button>
- </view>
- </view>
- <view v-if="dialogVisible" class="dialog-overlay" @click="dialogVisible = false">
- <view class="dialog-content" @click.stop>
- <view class="modal-message">{{ dialogMessage }}</view>
- </view>
- </view>
- </view>
- </template>
- <script setup>
- import { ref, computed, watch, nextTick } from 'vue'
- import GC from './tools/gc'
- import dayjs from 'dayjs'
- import '@grapecity-software/spread-sheets/styles/gc.spread.sheets.excel2013white.css'
- import '@grapecity-software/spread-sheets-designer/styles/gc.spread.sheets.designer.min.css'
- import '@grapecity-software/spread-sheets-print'
- import '@grapecity-software/spread-sheets-shapes'
- import '@grapecity-software/spread-sheets-charts'
- import '@grapecity-software/spread-sheets-slicers'
- import '@grapecity-software/spread-sheets-pivot-addon'
- import '@grapecity-software/spread-sheets-tablesheet'
- import '@grapecity-software/spread-sheets-ganttsheet'
- import '@grapecity-software/spread-sheets-reportsheet-addon'
- import '@grapecity-software/spread-sheets-formula-panel'
- import '@grapecity-software/spread-sheets-io'
- import '@grapecity-software/spread-sheets-designer-resources-cn'
- import Designer from '@grapecity-software/spread-sheets-designer-vue'
- import NavBar from '@/components/NavBar/NavBar.vue'
- const props = defineProps({
- businessConfig: {
- type: Object,
- default: null,
- },
- templateData: {
- type: Object,
- default: null,
- },
- templateBlob: {
- type: String,
- default: '',
- },
- })
- const emit = defineEmits(['save', 'cancel', 'customAction', 'close', 'openCamera', 'calcCompleted'])
- const designerKey = import.meta.env.VITE_SPREADJS_DESIGNER_KEY
- const licenseKey = import.meta.env.VITE_SPREADJS_LICENSE_KEY
- GC.Spread.Sheets.Designer.LicenseKey = designerKey
- GC.Spread.Sheets.LicenseKey = licenseKey
- const designer = ref(null)
- const navTitle = ref('通用编辑器')
- const navButtons = ref([])
- const floatingInputVisible = ref(false)
- const floatingInputValue = ref('')
- const currentCell = ref(null)
- const currentCellInfo = ref(null)
- const keyboardHeight = ref(0)
- const selectedPicture = ref(null)
- const currentFieldName = ref('-')
- const fieldNameMapping = ref([])
- const dialogVisible = ref(false)
- const dialogMessage = ref('')
- const designerConfig = computed(() => {
- const config = GC.Spread.Sheets.Designer.DefaultConfig
- config.ribbon = []
- const commandsToDelete = ['formulaBarPanel']
- config.sidePanels = config.sidePanels.filter((item) => !commandsToDelete.includes(item.command))
- return config
- })
- const styleInfo = computed(() => {
- return {
- width: '100%',
- height: '100%',
- }
- })
- function is(val, type) {
- return Object.prototype.toString.call(val) === `[object ${type}]`
- }
- function changeStringToObject(json) {
- if (is(json, 'String') && json?.length) {
- return changeStringToObject(JSON.parse(json))
- } else if (is(json, 'Object')) {
- return json
- } else {
- console.error('数据类型不正确!')
- return null
- }
- }
- function base64ToBlob(base64Data, contentType = '') {
- const byteCharacters = atob(base64Data)
- const byteNumbers = new Array(byteCharacters.length)
- for (let i = 0; i < byteCharacters.length; i++) {
- byteNumbers[i] = byteCharacters.charCodeAt(i)
- }
- const byteArray = new Uint8Array(byteNumbers)
- return new Blob([byteArray], { type: contentType })
- }
- function blobToBase64(blob) {
- return new Promise((resolve, reject) => {
- const reader = new FileReader()
- reader.onload = () => {
- const base64 = reader.result.split(',')[1]
- resolve(base64)
- }
- reader.onerror = reject
- reader.readAsDataURL(blob)
- })
- }
- function formatterOADateNumber(input) {
- const regex = /OADate\(([\d.]+)\)/
- const match = input.match(regex)
- let OADate = null
- if (match && match[1]) {
- OADate = Number(match[1])
- OADate = isNaN(OADate) ? null : parseInt(OADate)
- }
- return OADate !== null ? new Date(Math.round(OADate - 25569) * 86400000) : null
- }
- function getSheetBindingPathData(sheet, dataSource) {
- const boundFields = new Set()
- const tableMap = new Map()
- // 扫描模板文件中的数据填充槽标识(键)
- for (let row = 0; row < sheet.getRowCount(); row++) {
- for (let col = 0; col < sheet.getColumnCount(); col++) {
- const bindingPath = sheet.getBindingPath(row, col)
- if (bindingPath) {
- boundFields.add(bindingPath)
- }
- }
- }
- // 扫描模板文件中所有子表及其对应的数据填充槽标识
- const tables = sheet.tables.all() || []
- if (tables.length > 0) {
- tables.forEach(function (table) {
- const tableName = table.bindingPath()
- boundFields.add(tableName)
- const tableBoundFields = new Set()
- const columnCount = table.range().colCount
- for (let i = 0; i < columnCount; i++) {
- const field = table.getColumnDataField(i)
- if (field) {
- tableBoundFields.add(field)
- }
- }
- tableMap.set(tableName, [...tableBoundFields])
- })
- }
- const boundFieldsArray = [...boundFields].filter(Boolean)
- if (boundFieldsArray.length === 0) return {}
- // 根据模板文件数据值槽的需要,过滤数据值容器的数据(而且是按整个数据结构层次的过滤)
- function recombineDataSource(dataSourceObj, prekey, boundFieldsArray) {
- const filterResult = {}
- for (const key in dataSourceObj) {
- // 如果key对应属性值是对象,那么按需向下层处理
- if (is(dataSourceObj[key], 'Object')) {
- const nextKey = !prekey ? key : `${prekey}.${key}`
- if (boundFieldsArray.findIndex((field) => field.indexOf(nextKey) >= 0) >= 0) {
- filterResult[key] = recombineDataSource(dataSourceObj[key], nextKey, boundFieldsArray)
- }
- continue
- }
- // 如果key对应属性值是对象数组,那么对sheet中的子表数据填充槽进行处理
- // 也就是说,数组处理上,基本数据值类型一般就是一个单元格的填充,但是对象类型说明是一张子表填充
- if (is(dataSourceObj[key], 'Array') && dataSourceObj[key].every((x) => is(x, 'Object'))) {
- if (boundFieldsArray.includes(key)) {
- filterResult[key] = Object.entries(dataSourceObj[key]).map(([i, item]) => {
- if (is(item, 'String')) return item
- const tableBoundFieldArrays = tableMap.get(key)
- return recombineDataSource(item, '', tableBoundFieldArrays)
- })
- }
- continue
- }
- if (boundFieldsArray.includes(!prekey ? key : `${prekey}.${key}`)) {
- filterResult[key] = dataSourceObj[key]
- }
- }
- return filterResult
- }
- const newDataSource = recombineDataSource(dataSource, '', boundFieldsArray)
- boundFields.clear()
- tableMap.clear()
- return newDataSource
- }
- // 生成属性键,并填充对应默认值
- function generateDefaultData(schema) {
- const result = {}
- for (const key in schema.properties) {
- result[key] = generatePropertyDefaultValue(schema.properties[key])
- }
- return result
- }
- // 为属性填充默认值
- function generatePropertyDefaultValue(property) {
- if (property.type === 'array' && property.items) {
- if (property.items.type !== 'object' || !property.items.properties) {
- return ['']
- }
- const itemValue = {}
- for (const itemKey in property.items.properties) {
- itemValue[itemKey] = generatePropertyDefaultValue(property.items.properties[itemKey])
- }
- return [itemValue]
- }
- if (property.properties) {
- const objValue = {}
- for (const objKey in property.properties) {
- objValue[objKey] = generatePropertyDefaultValue(property.properties[objKey])
- }
- return objValue
- }
- return ''
- }
- /**
- * 为属性填充非默认数据值
- * @param target 属性值为默认值的键值数据容器
- * @param source 填充数据到键值容器的数据源头
- */
- function deepMergeSchemaValue(target, source) {
- const result = { ...target }
- for (const key in source) {
- if (source.hasOwnProperty(key)) {
- const targetValue = target[key]
- const sourceValue = source[key]
- if (targetValue === undefined) {
- continue
- }
- if (Array.isArray(targetValue) && Array.isArray(sourceValue)) {
- result[key] = sourceValue.length > 0 ? sourceValue : targetValue
- } else if (
- typeof targetValue === 'object' &&
- targetValue !== null &&
- typeof sourceValue === 'object' &&
- sourceValue !== null
- ) {
- result[key] =
- Object.keys(sourceValue).length === 0
- ? targetValue
- : deepMergeSchemaValue(targetValue, sourceValue)
- } else if (sourceValue !== undefined && sourceValue !== '') {
- result[key] = sourceValue
- }
- }
- }
- return result
- }
- function generateAndReturnDataSourceOADate(dataSource) {
- if (!is(dataSource, 'Object') && !is(dataSource, 'Array')) return {}
- if (is(dataSource, 'Object')) {
- for (const key in dataSource) {
- if (is(dataSource[key], 'String') && dataSource[key].indexOf('OADate') >= 0) {
- dataSource[key] = dayjs(formatterOADateNumber(dataSource[key])).format('YYYY年MM月DD日')
- continue
- } else if (is(dataSource[key], 'Date')) {
- dataSource[key] = dayjs(dataSource[key]).format('YYYY年MM月DD日')
- continue
- }
- if (is(dataSource[key], 'Object') || is(dataSource[key], 'Array')) {
- dataSource[key] = generateAndReturnDataSourceOADate(dataSource[key])
- continue
- }
- }
- }
- if (is(dataSource, 'Array')) {
- return dataSource.map((keyItem) => {
- if (is(keyItem, 'String') && keyItem.indexOf('OADate') >= 0) {
- return dayjs(formatterOADateNumber(keyItem)).format('YYYY年MM月DD日')
- }
- if (is(keyItem, 'Object') || is(keyItem, 'Array')) {
- return generateAndReturnDataSourceOADate(keyItem)
- }
- return keyItem
- })
- }
- return dataSource
- }
- // 监听值变化(用户通过UI操作变化值会被监听,代码去变化值不会被监听),进行dataSource的同步(发生事件以外的sheet)
- // 因为每个sheet用了同一个dataSource(这里不是同一个实例,但是是数据结构和值相同),为了保持这个dataSource的一致,所以要更新其他sheet的datasource
- function registerCellValuesChangeEventHandlerForEverySheet(sheets) {
- sheets.forEach((activedSheet) => {
- activedSheet.bind(GC.Spread.Sheets.Events.ValueChanged, function (sender, args) {
- const bindingPathName = activedSheet.getBindingPath(args.row, args.col)
- if (bindingPathName) {
- try {
- // 处理其他sheet的dataSource
- for (const sheet of sheets) {
- const dataSource = sheet.getDataSource()?.getSource() || {}
- if (
- !dataSource.hasOwnProperty(bindingPathName) ||
- (activedSheet.name() === sheet.name() && activedSheet.__ID__ === sheet.__ID__)
- )
- continue
- const tables = sheet.tables?.all() || []
- for (let i = 0; i < tables.length; i++) {
- const table = sheet.tables.all()[i]
- table.expandBoundRows(true)
- }
- const newDataSource = new GC.Spread.Sheets.Bindings.CellBindingSource({
- ...dataSource,
- [bindingPathName]: args.newValue,
- })
- sheet.setDataSource(newDataSource)
- }
- } catch (err) {
- console.error('监听单元格出错啦', err)
- }
- }
- })
- })
- }
- function registerZoomEventHandler(designerInstance) {
- if (!designerInstance) return
- const spreadInstance = designerInstance?.getWorkbook()
- if (Object.prototype.toString.call(spreadInstance) !== '[object Object]') return
- // 缩放事件
- spreadInstance.bind(GC.Spread.Sheets.Events.ViewZooming, function (sender, args) {
- const activedSheet = spreadInstance.getActiveSheet()
- const minZoom = calcSheetZoom(activedSheet)
- // 如果缩小,限制其最小是sheet铺满屏幕,否则不再缩小
- if (args.newZoomFactor < minZoom) args.cancel = true
- })
- }
- // 计算sheet铺满屏幕的缩放比例
- function calcSheetZoom(sheet) {
- const screenWidth = window.screen.width
- const usedRange = sheet.getUsedRange(GC.Spread.Sheets.UsedRangeType.style)
- sheet.showCell(usedRange.row, usedRange.col)
- const scrollbarWidth = 40
- let totalWidth = 0
- for (let col = usedRange.col; col <= usedRange.colCount - 1; col++) {
- totalWidth += sheet.getColumnWidth(col)
- }
- return 1 + (screenWidth - scrollbarWidth - totalWidth) / totalWidth
- }
- function initDesignerSheetConfig(sheet) {
- sheet?.zoom(1)
- sheet?.zoom(calcSheetZoom(sheet))
- sheet.options.rowHeaderVisible = false
- sheet.options.colHeaderVisible = false
- sheet.setActiveCell(null)
- }
- async function setDefaultSchema(designerInstance, bindingPathSchema) {
- const initBindingPathSchema = !bindingPathSchema ? {} : changeStringToObject(bindingPathSchema)
- await designerInstance.setData('treeNodeFromJson', JSON.stringify(initBindingPathSchema))
- }
- function getDefaultSchema(designerInstance) {
- return (
- designerInstance.getData('updatedTreeNode') ||
- designerInstance.getData('treeNodeFromJson') ||
- designerInstance.getData('oldTreeNodeFromJson')
- )
- }
- // 设置数据到模板文件,渲染对应数据
- function setDataSource(designerInstance, schemaData, dataSourceValues) {
- dataSourceValues =
- !is(dataSourceValues, 'Object') && is(dataSourceValues, 'String')
- ? JSON.parse(dataSourceValues)
- : dataSourceValues || {}
- schemaData =
- !is(schemaData, 'Object') && is(schemaData, 'String')
- ? JSON.parse(schemaData)
- : schemaData || {}
- const formatterSource = generateDefaultData(schemaData)
- const resultDataSource = deepMergeSchemaValue(formatterSource, dataSourceValues)
- const spreadInstance = designerInstance.getWorkbook()
- registerCellValuesChangeEventHandlerForEverySheet(spreadInstance.sheets)
- for (const sheet of spreadInstance.sheets) {
- const tables = sheet.tables?.all() || []
- for (let i = 0; i < tables.length; i++) {
- const table = tables[i]
- table.expandBoundRows(true)
- }
- const filterResultDataSource = getSheetBindingPathData(sheet, resultDataSource)
- const dataSource = new GC.Spread.Sheets.Bindings.CellBindingSource(filterResultDataSource)
- sheet.setDataSource(dataSource)
- }
- }
- function getDataSource(designerInstance) {
- const spreadInstance = designerInstance.getWorkbook()
- let dataSource = {}
- for (const sheet of spreadInstance.sheets) {
- const source = sheet.getDataSource()?.getSource()
- if (source) {
- for (const key in source) {
- if (is(source[key], 'Object') && is(dataSource[key], 'Object')) {
- source[key] = Object.assign(dataSource[key], source[key])
- }
- }
- dataSource = {
- ...dataSource,
- ...source,
- }
- }
- }
- dataSource = generateAndReturnDataSourceOADate(dataSource)
- return dataSource
- }
- function handleSheetTableCopyTo(designerInstance, isRowMerage) {
- if (!designerInstance) return
- const spreadInstance = designerInstance.getWorkbook()
- for (const sheet of spreadInstance.sheets) {
- const tables = sheet.tables?.all() || []
- for (let tableIndex = 0; tableIndex < tables.length; tableIndex++) {
- const range = tables[tableIndex].range()
- const { row, rowCount, col, colCount } = range
- sheet.getRange(row, col, rowCount, colCount).wordWrap(true)
- if (isRowMerage) {
- 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) {
- firstRowSpans.push({
- startCol: col + startMergeCol,
- colCount: c - startMergeCol,
- })
- sheet.addSpan(row, col + startMergeCol, 1, c - startMergeCol)
- }
- startMergeCol = c
- }
- }
- }
- for (let r = 1; r < rowCount; r++) {
- const currentRow = row + r
- firstRowSpans.forEach((mergeInfo) => {
- try {
- const firstCellValue = sheet.getValue(currentRow, mergeInfo.startCol)
- let shouldMerge = true
- for (let c = 1; c < mergeInfo.colCount; c++) {
- const currentCellValue = sheet.getValue(currentRow, mergeInfo.startCol + c)
- if (currentCellValue !== firstCellValue) {
- shouldMerge = false
- break
- }
- }
- if (shouldMerge) {
- sheet.addSpan(currentRow, mergeInfo.startCol, 1, mergeInfo.colCount)
- }
- } catch (error) {
- console.warn(
- `合并单元格失败 at (${currentRow}, ${mergeInfo.startCol}):`,
- error.message,
- )
- }
- })
- }
- }
- for (let rowIndex = 1; rowIndex < rowCount; rowIndex++) {
- const currentRow = row + rowIndex
- sheet?.copyTo(
- row,
- col,
- currentRow,
- col,
- 1,
- colCount,
- GC.Spread.Sheets.CopyToOptions.style | GC.Spread.Sheets.CopyToOptions.formula,
- )
- }
- setTimeout(() => {
- for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {
- const currentRow = row + rowIndex
- sheet?.autoFitRow(currentRow)
- const currentHeight = sheet.getRowHeight(currentRow)
- const newHeight = Math.max(currentHeight * 1.2, 25)
- sheet.setRowHeight(currentRow, newHeight)
- }
- }, 100)
- }
- const currentTheme = sheet.currentTheme()
- currentTheme.bodyFont('仿宋_GB2312')
- }
- }
- function removeSheetCellFocus(designerInstance) {
- const spreadInstance = designerInstance.getWorkbook()
- if (spreadInstance) {
- spreadInstance.focus(false)
- for (const sheet of spreadInstance?.sheets) {
- sheet.clearSelection()
- }
- }
- }
- function updateFieldNameDisplay(fieldName) {
- let displayName = '-'
- if (fieldNameMapping.value && Array.isArray(fieldNameMapping.value)) {
- const fieldMapping = fieldNameMapping.value.find((item) => item.field === fieldName)
- if (fieldMapping && fieldMapping.displayName) {
- displayName = fieldMapping.displayName
- }
- }
- currentFieldName.value = displayName
- }
- function showFloatingInput() {
- floatingInputValue.value = currentCellInfo.value.value
- floatingInputVisible.value = true
- }
- function backfillValueToCell() {
- if (!currentCell.value || !currentCellInfo.value) return
- const { sheet, row, col } = currentCell.value
- sheet.setValue(row, col, floatingInputValue.value)
- }
- function clearInput() {
- floatingInputValue.value = ''
- }
- function findNextEmptyBoundCell(direction = 'next') {
- if (!currentCell.value || !currentCell.value.sheet) {
- console.warn('没有当前活动单元格')
- return
- }
- try {
- const sheet = currentCell.value.sheet
- const usedRange = sheet.getUsedRange(GC.Spread.Sheets.UsedRangeType.data)
- if (!usedRange) {
- console.warn('没有找到使用范围')
- return
- }
- const { rowCount, colCount } = usedRange
- const startRow = currentCell.value.row
- const startCol = currentCell.value.col
- const targetCell = findNextEmptyCellFromPosition(
- sheet,
- startRow,
- startCol,
- rowCount,
- colCount,
- direction,
- )
- if (targetCell) {
- currentCell.value = { sheet, row: targetCell.row, col: targetCell.col }
- const cell = sheet.getCell(targetCell.row, targetCell.col)
- const bindingPath = sheet.getBindingPath(targetCell.row, targetCell.col) || '-'
- currentCellInfo.value = {
- value: cell.value() || '',
- isLocked: false,
- formatter: cell.formatter(),
- fieldName: bindingPath,
- }
- updateFieldNameDisplay(bindingPath)
- showFloatingInput()
- sheet.setSelection(targetCell.row, targetCell.col, 1, 1)
- const directionText = direction === 'next' ? '下一个' : '上一个'
- console.log(
- `跳转到${directionText}未输入单元格: 行${targetCell.row + 1}, 列${targetCell.col + 1}`,
- )
- } else {
- console.log(`没有找到更多${direction === 'next' ? '下一个' : '上一个'}未输入的绑定字段单元格`)
- }
- } catch (error) {
- console.error('查找过程中出错:', error)
- }
- }
- function findNextEmptyCellFromPosition(
- sheet,
- startRow,
- startCol,
- maxRow,
- maxCol,
- direction = 'next',
- ) {
- const currentCellRange = getMergedCellRange(sheet, startRow, startCol)
- const targetCell = searchInRange(
- sheet,
- currentCellRange,
- startRow,
- startCol,
- maxRow,
- maxCol,
- direction,
- true,
- )
- if (targetCell) return targetCell
- return searchInRange(
- sheet,
- currentCellRange,
- startRow,
- startCol,
- maxRow,
- maxCol,
- direction,
- false,
- )
- }
- function searchInRange(
- sheet,
- currentCellRange,
- startRow,
- startCol,
- maxRow,
- maxCol,
- direction,
- fromCurrentPosition,
- ) {
- const rowStart = fromCurrentPosition ? startRow : direction === 'next' ? 0 : maxRow - 1
- const rowEnd = fromCurrentPosition
- ? direction === 'next'
- ? maxRow
- : -1
- : direction === 'next'
- ? startRow
- : startRow
- const rowStep = direction === 'next' ? 1 : -1
- for (let row = rowStart; direction === 'next' ? row < rowEnd : row >= rowEnd; row += rowStep) {
- let colStart, colEnd, colStep
- if (fromCurrentPosition) {
- colStart =
- row === startRow
- ? direction === 'next'
- ? startCol + 1
- : startCol - 1
- : direction === 'next'
- ? 0
- : maxCol - 1
- colEnd = direction === 'next' ? maxCol : -1
- colStep = direction === 'next' ? 1 : -1
- } else {
- colStart = direction === 'next' ? 0 : maxCol - 1
- colEnd = row === startRow ? startCol : direction === 'next' ? maxCol : -1
- colStep = direction === 'next' ? 1 : -1
- }
- for (let col = colStart; direction === 'next' ? col < colEnd : col >= colEnd; col += colStep) {
- if (!fromCurrentPosition && row === startRow && col === startCol) {
- continue
- }
- if (isInMergedRange(currentCellRange, row, col)) {
- continue
- }
- const bindingPath = sheet.getBindingPath(row, col)
- const cellValue = sheet.getValue(row, col)
- if (bindingPath && (cellValue === null || cellValue === undefined || cellValue === '')) {
- return { row, col }
- }
- }
- }
- return null
- }
- function getMergedCellRange(sheet, row, col) {
- try {
- const span = sheet.getSpan(row, col)
- if (span) {
- return {
- row: span.row,
- col: span.col,
- rowCount: span.rowCount,
- colCount: span.colCount,
- }
- }
- } catch (error) {
- console.log('获取合并范围失败:', error)
- }
- return null
- }
- function isInMergedRange(mergedRange, row, col) {
- if (!mergedRange) {
- return false
- }
- const { row: rangeRow, col: rangeCol, rowCount, colCount } = mergedRange
- return (
- row >= rangeRow && row < rangeRow + rowCount && col >= rangeCol && col < rangeCol + colCount
- )
- }
- function handleWorkbookInitialized(spreadInstance) {
- designer.value = spreadInstance
- registerZoomEventHandler(designer.value)
- designer.value.setData('isRibbonCollapse', true)
- const templateData = props.templateData
- const businessConfig = props.businessConfig
- if (businessConfig && templateData) {
- initGenericEditorUI(businessConfig.ui)
- loadTemplateData(props.templateBlob, templateData)
- } else {
- console.error('没有获取到业务配置和模板数据')
- }
- if (templateData && templateData.pathNameMapping) {
- fieldNameMapping.value = templateData.pathNameMapping
- console.log('字段名映射已初始化:', fieldNameMapping.value)
- } else {
- fieldNameMapping.value = []
- console.log('未找到字段名映射,使用空数组')
- }
- }
- function initGenericEditorUI(uiConfig) {
- navTitle.value = uiConfig.title
- navButtons.value = []
- const cancelBtn = {
- text: uiConfig.cancelButtonText || '取消',
- className: '',
- action: 'cancel',
- }
- navButtons.value.push(cancelBtn)
- if (!uiConfig.hideSaveButton) {
- const saveBtn = {
- text: uiConfig.saveButtonText || '保存',
- className: 'primary',
- action: 'save',
- }
- navButtons.value.push(saveBtn)
- }
- if (uiConfig.customButtons && uiConfig.customButtons.length > 0) {
- uiConfig.customButtons.forEach((button) => {
- const customBtn = {
- text: button.text,
- className: button.className || '',
- action: button.action,
- }
- navButtons.value.push(customBtn)
- })
- }
- }
- function handleNavButtonClick(button) {
- if (button.action === 'cancel') {
- cancelEdit()
- } else if (button.action === 'save') {
- saveRecord()
- } else {
- handleCustomAction(button.action)
- }
- }
- /**
- * 加载模板文件和数据,
- * @param templateBlob 模板文件,是模板数据的基础容器
- * @param templateData 模板数据,填充模板文件中对应的数据格
- */
- function loadTemplateData(templateBlob, templateData) {
- console.log('触发模板渲染加载。。。')
- if (!designer.value || !templateData) return
- const spreadInstance = designer.value.getWorkbook()
- spreadInstance.touchToolStrip.clear()
- if (templateBlob) {
- console.log('开始加载模板文件')
- try {
- const blob = base64ToBlob(templateBlob, 'application/ssjson')
- spreadInstance.open(
- blob,
- () => {
- console.log('模板文件加载成功!')
- initDesignerSheetConfig(spreadInstance.getActiveSheet())
- if (templateData.schema) {
- initDataSource(designer.value, templateData)
- }
- handleSheetTableCopyTo(designer.value, true)
- initSpreadInputEvents(spreadInstance)
- },
- (error) => {
- console.error('模板文件加载失败:', error)
- },
- )
- } catch (error) {
- console.error('处理模板文件时发生错误:', error)
- }
- } else if (templateData.schema) {
- console.log(
- '开始加载schema模板:',
- JSON.stringify(templateData.schema).substring(0, 200) + '...',
- )
- console.log('开始加载数据:', JSON.stringify(templateData.data).substring(0, 200) + '...')
- if (spreadInstance.getSheetCount() === 0) {
- const sheet = spreadInstance.addSheet(0)
- sheet.name('Sheet1')
- initDesignerSheetConfig(sheet)
- console.log('创建了新工作表')
- }
- initDataSource(designer.value, templateData)
- spreadInstance.repaint()
- handleSheetTableCopyTo(designer.value, true)
- initSpreadInputEvents(spreadInstance)
- }
- console.log('模板数据加载完成。。。')
- }
- function initDataSource(designer, templateData) {
- setDefaultSchema(designer, templateData.schema)
- setDataSource(designer, templateData.schema, templateData.data)
- }
- function initSpreadInputEvents(spreadInstance) {
- spreadInstance.bind(GC.Spread.Sheets.Events.CellClick, function (sender, args) {
- const sheet = args.sheet
- const row = args.row
- const col = args.col
- selectedPicture.value = null
- currentCell.value = { sheet, row, col }
- const cell = sheet.getCell(row, col)
- const bindingPath = sheet.getBindingPath(row, col) || '-'
- currentCellInfo.value = {
- value: cell.value() || '',
- isLocked: false,
- formatter: cell.formatter(),
- fieldName: bindingPath,
- }
- updateFieldNameDisplay(bindingPath)
- showFloatingInput()
- })
- console.log('悬浮输入框事件已绑定')
- }
- function handleCustomAction(action) {
- if (!designer.value) return
- const spreadInstance = designer.value.getWorkbook()
- spreadInstance.save(async (blob) => {
- removeSheetCellFocus(designer.value)
- const base64 = await blobToBase64(blob)
- const dataJSON = getDataSource(designer.value)
- const schemaJSON = getDefaultSchema(designer.value)
- emit('customAction', {
- action,
- data: {
- blob: base64,
- dataJSON,
- schemaJSON,
- businessType: props.businessConfig.businessType,
- templateId: props.templateData.data?.templateId || props.templateData.template,
- reportUrl: props.templateData.data?.templateUrl || props.templateData.templateUrl || '',
- },
- })
- })
- }
- function saveRecord() {
- if (!designer.value || !props.businessConfig || !props.templateData) return
- const spreadInstance = designer.value.getWorkbook()
- spreadInstance.save(async (blob) => {
- removeSheetCellFocus(designer.value)
- const base64 = await blobToBase64(blob)
- const dataJSON = getDataSource(designer.value)
- const schemaJSON = getDefaultSchema(designer.value)
- emit('save', {
- blob: base64,
- dataJSON,
- schemaJSON,
- businessType: props.businessConfig.businessType,
- templateId: props.templateData.data?.templateId || props.templateData.template,
- reportUrl: props.templateData.data?.templateUrl || props.templateData.templateUrl || '',
- })
- })
- }
- function cancelEdit() {
- emit('cancel')
- }
- function goBack() {
- uni.navigateBack({
- delta: 1,
- success: () => {
- // 返回成功后的回调函数
- },
- fail: () => {
- // 返回失败后的回调函数
- },
- })
- }
- function showDialog(message, timer = 3000) {
- dialogMessage.value = message
- dialogVisible.value = true
- setTimeout(() => {
- dialogVisible.value = false
- }, timer)
- }
- function updateDataSource(dataSource) {
- const templateData = props.templateData
- if (!designer.value || !templateData || !templateData.schema) return
- setDataSource(designer.value, templateData.schema, dataSource)
- showDialog('数据更新完成')
- }
- function onWebViewResize(newWidth) {
- console.log('收到屏幕尺寸变化通知,新宽度:', newWidth)
- if (designer.value) {
- const spreadInstance = designer.value.getWorkbook()
- spreadInstance.touchToolStrip.clear()
- const activedSheet = spreadInstance.getActiveSheet()
- initDesignerSheetConfig(activedSheet)
- }
- }
- function setKeyboardHeight(height) {
- keyboardHeight.value = height
- }
- function handleCameraResult(result) {
- if (result.success && result.imageData) {
- console.log('收到相机拍摄结果:', result)
- } else {
- console.log('相机拍摄失败或用户取消')
- }
- }
- watch(
- () => props.templateBlob,
- (newBlob, oldBlob) => {
- if (newBlob && newBlob !== oldBlob && designer.value && props.templateData) {
- console.log('templateBlob 发生变化,重新加载模板,数据:', props.templateData)
- loadTemplateData(props.templateBlob, props.templateData)
- }
- },
- )
- defineExpose({
- onWebViewResize,
- updateDataSource,
- handleCameraResult,
- setKeyboardHeight,
- })
- </script>
- <style scoped>
- * {
- box-sizing: border-box;
- padding: 0;
- margin: 0;
- }
- .spread-designer-generic {
- position: relative;
- height: 100vh;
- overflow: hidden;
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
- }
- .main-container {
- width: 100%;
- height: calc(100vh - clamp(44px, 8vw, 56px) - 95px);
- }
- .navigator-bar {
- box-sizing: border-box;
- display: flex;
- align-items: center;
- justify-content: space-between;
- height: clamp(44px, 8vw, 56px);
- min-height: 44px;
- padding: 0 16px;
- background-color: #f8f9fa;
- border-bottom: 1px solid #e9ecef;
- }
- .nav-left {
- display: flex;
- flex: 1;
- align-items: center;
- }
- .back-btn {
- padding: 0 8px;
- font-size: 18px;
- cursor: pointer;
- background: none;
- border: 0;
- border-radius: 4px;
- transition: background-color 0.2s;
- }
- .back-btn:hover {
- background-color: #e9ecef;
- }
- .nav-title {
- flex: 1;
- font-size: 17px;
- font-weight: 600;
- color: #333;
- text-align: center;
- }
- .nav-right {
- display: flex;
- flex: 1;
- gap: 8px;
- justify-content: flex-end;
- }
- .nav-btn {
- padding: 0 12px;
- font-size: 12px;
- color: #495057;
- white-space: nowrap;
- cursor: pointer;
- background-color: #fff;
- border: 1px solid #dee2e6;
- border-radius: 4px;
- transition: all 0.2s;
- }
- .nav-btn:hover {
- background-color: #f8f9fa;
- }
- .nav-btn.primary {
- color: white;
- background-color: #007bff;
- border-color: #007bff;
- }
- .nav-btn.primary:hover {
- background-color: #0056b3;
- }
- .nav-btn.warning {
- color: white;
- background-color: #e6a23c;
- border-color: #e6a23c;
- }
- .floating-input-wrapper {
- position: relative;
- width: 100%;
- }
- .floating-input-wrapper textarea {
- width: 100%;
- height: 40px;
- padding: 8px 32px 8px 12px;
- font-size: 14px;
- resize: none;
- border: 1px solid #d9d9d9;
- border-radius: 4px;
- outline: none;
- }
- .floating-input-wrapper textarea:focus {
- border-color: #007aff;
- }
- .clear-btn {
- position: absolute;
- top: 50%;
- right: 8px;
- z-index: 2;
- display: flex;
- align-items: center;
- justify-content: center;
- width: 24px;
- height: 24px;
- font-size: 16px;
- font-weight: bold;
- color: #666;
- cursor: pointer;
- background: #f0f0f0;
- border: none;
- border-radius: 12px;
- transform: translateY(-50%);
- }
- .clear-btn:hover {
- color: #333;
- background: #e0e0e0;
- }
- .floating-field-name-display {
- display: flex;
- flex: 1;
- align-items: center;
- height: 35px;
- padding: 6px 12px;
- margin-right: 8px;
- overflow: hidden;
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
- font-size: 12px;
- line-height: 1.2;
- color: #666;
- text-overflow: ellipsis;
- white-space: nowrap;
- background: #f5f5f5;
- border-radius: 4px;
- }
- .floating-input-container {
- position: fixed;
- right: 0;
- bottom: 0;
- left: 0;
- z-index: 9999;
- display: flex;
- flex-direction: column;
- width: 100%;
- min-height: 85px;
- padding: 8px 12px 12px 12px;
- background: white;
- border-top: 1px solid #e0e0e0;
- transition: all 0.3s ease;
- }
- .floating-input-container.keyboard-visible {
- position: fixed !important;
- bottom: var(--keyboard-height, 0);
- z-index: 10000;
- min-height: 105px;
- max-height: 345px;
- }
- .floating-toolbar {
- display: flex;
- align-items: center;
- justify-content: space-between;
- height: 35px;
- padding: 0 2px;
- margin-top: 4px;
- margin-bottom: 10px;
- }
- .button-group {
- display: flex;
- gap: 8px;
- align-items: center;
- }
- .prev-cell-btn {
- position: relative;
- display: flex;
- align-items: center;
- justify-content: center;
- min-width: 60px;
- height: 35px;
- padding: 6px 12px;
- font-size: 12px;
- font-weight: 500;
- line-height: 1;
- color: white;
- cursor: pointer;
- background: #007aff;
- border: none;
- border-radius: 4px;
- transition: all 0.2s ease;
- }
- .prev-cell-btn:hover:not(:disabled) {
- background: #0056b3;
- transform: translateY(-1px);
- }
- .prev-cell-btn:active:not(:disabled) {
- transform: translateY(0);
- }
- .prev-cell-btn:focus {
- outline: none;
- box-shadow: 0 0 0 1px rgba(0, 122, 255, 0.3);
- }
- .prev-cell-btn:disabled {
- color: #999999;
- cursor: not-allowed;
- background: #cccccc;
- opacity: 0.7;
- }
- .next-cell-btn {
- position: relative;
- display: flex;
- align-items: center;
- justify-content: center;
- min-width: 60px;
- height: 35px;
- padding: 6px 12px;
- font-size: 12px;
- font-weight: 500;
- line-height: 1;
- color: white;
- cursor: pointer;
- background: #007aff;
- border: none;
- border-radius: 4px;
- transition: all 0.2s ease;
- }
- .next-cell-btn:hover:not(:disabled) {
- background: #0056b3;
- transform: translateY(-1px);
- }
- .next-cell-btn:active:not(:disabled) {
- transform: translateY(0);
- }
- .next-cell-btn:focus {
- outline: none;
- box-shadow: 0 0 0 1px rgba(0, 122, 255, 0.3);
- }
- .next-cell-btn:disabled {
- color: #999999;
- cursor: not-allowed;
- background: #cccccc;
- opacity: 0.7;
- }
- .dialog-overlay {
- position: fixed;
- top: 0;
- right: 0;
- bottom: 0;
- left: 0;
- z-index: 10001;
- display: flex;
- align-items: center;
- justify-content: center;
- background-color: rgba(0, 0, 0, 0.5);
- }
- .dialog-content {
- max-width: 80%;
- padding: 20px;
- background-color: white;
- border-radius: 8px;
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
- }
- .modal-message {
- font-size: 16px;
- line-height: 1.5;
- color: #333;
- text-align: center;
- }
- </style>
|