|
|
@@ -0,0 +1,327 @@
|
|
|
+<template>
|
|
|
+ <view class="spread-designer-custom-generic">
|
|
|
+ <slot name="navBar">
|
|
|
+ <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>
|
|
|
+ </slot>
|
|
|
+
|
|
|
+ <view class="main-container">
|
|
|
+ <SpreadCustomRender
|
|
|
+ ref="renderRef"
|
|
|
+ @ready="onRenderReady"
|
|
|
+ @save="onRenderSave"
|
|
|
+ />
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+import { ref, watch, onMounted } from 'vue'
|
|
|
+import GC from './tools/gc'
|
|
|
+import '@grapecity-software/spread-sheets/styles/gc.spread.sheets.excel2013white.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 NavBar from '@/components/NavBar/NavBar.vue'
|
|
|
+import { SpreadCustomRender, extractConfig } from '@/components/SpreadCustomRender'
|
|
|
+import { base64ToBlob } from './tools/spreadUtils'
|
|
|
+import type { SpreadWorkbookConfig } from '@/components/SpreadCustomRender'
|
|
|
+
|
|
|
+// ==================== Props ====================
|
|
|
+
|
|
|
+const props = defineProps({
|
|
|
+ businessConfig: {
|
|
|
+ type: Object as () => Record<string, any> | null,
|
|
|
+ default: null,
|
|
|
+ },
|
|
|
+ templateData: {
|
|
|
+ type: Object as () => Record<string, any> | null,
|
|
|
+ default: null,
|
|
|
+ },
|
|
|
+ templateBlob: {
|
|
|
+ type: String,
|
|
|
+ default: '',
|
|
|
+ },
|
|
|
+})
|
|
|
+
|
|
|
+const emit = defineEmits(['save', 'cancel', 'customAction', 'close'])
|
|
|
+
|
|
|
+// ==================== SpreadJS License ====================
|
|
|
+
|
|
|
+GC.Spread.Sheets.LicenseKey = import.meta.env.VITE_SPREADJS_LICENSE_KEY
|
|
|
+
|
|
|
+// ==================== 状态 ====================
|
|
|
+
|
|
|
+const renderRef = ref<InstanceType<typeof SpreadCustomRender>>()
|
|
|
+const navTitle = ref('通用渲染器')
|
|
|
+const navButtons = ref<Array<{ text: string; className: string; action: string }>>([])
|
|
|
+const workbookConfig = ref<SpreadWorkbookConfig | null>(null)
|
|
|
+const formData = ref<Record<string, any>>({})
|
|
|
+const renderReady = ref(false)
|
|
|
+
|
|
|
+// 隐藏 Workbook(仅 fallback 时才创建)
|
|
|
+let workbook: any = null
|
|
|
+
|
|
|
+// ==================== 初始化 ====================
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ if (props.businessConfig?.ui) {
|
|
|
+ initUI(props.businessConfig.ui)
|
|
|
+ }
|
|
|
+ if (props.templateBlob) {
|
|
|
+ loadTemplate(props.templateBlob)
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+// ==================== UI 初始化 ====================
|
|
|
+
|
|
|
+function initUI(uiConfig: Record<string, any>) {
|
|
|
+ navTitle.value = uiConfig.title || '通用渲染器'
|
|
|
+ navButtons.value = []
|
|
|
+
|
|
|
+ navButtons.value.push({
|
|
|
+ text: uiConfig.cancelButtonText || '取消',
|
|
|
+ className: '',
|
|
|
+ action: 'cancel',
|
|
|
+ })
|
|
|
+
|
|
|
+ if (!uiConfig.hideSaveButton) {
|
|
|
+ navButtons.value.push({
|
|
|
+ text: uiConfig.saveButtonText || '保存',
|
|
|
+ className: 'primary',
|
|
|
+ action: 'save',
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ if (uiConfig.customButtons && Array.isArray(uiConfig.customButtons)) {
|
|
|
+ uiConfig.customButtons.forEach((button: any) => {
|
|
|
+ navButtons.value.push({
|
|
|
+ text: button.text,
|
|
|
+ className: button.className || '',
|
|
|
+ action: button.action,
|
|
|
+ })
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// ==================== 模板加载 ====================
|
|
|
+
|
|
|
+/**
|
|
|
+ * 加载模板:
|
|
|
+ * 主方案 — GC.Spread.Sheets.IO.open() 直接返回 JSON,传给 extractConfig 处理
|
|
|
+ * 回退 — workbook.open() 加载后 toJSON() 再 extractConfig
|
|
|
+ */
|
|
|
+function loadTemplate(blobBase64: string) {
|
|
|
+ const blob = base64ToBlob(blobBase64, '')
|
|
|
+ console.log('[spreadDesignerCustomGeneric] 开始加载模板, blob size:', blob.size)
|
|
|
+
|
|
|
+ if (GC.Spread.Sheets.IO && typeof GC.Spread.Sheets.IO.open === 'function') {
|
|
|
+ // 主方案:IO.open 返回 JSON,无需 Workbook 实例
|
|
|
+ GC.Spread.Sheets.IO.open(
|
|
|
+ blob,
|
|
|
+ (json: any) => {
|
|
|
+ console.log('[spreadDesignerCustomGeneric] IO.open 成功')
|
|
|
+ onConfigReady(json as SpreadWorkbookConfig)
|
|
|
+ },
|
|
|
+ (error: any) => {
|
|
|
+ console.error('[spreadDesignerCustomGeneric] IO.open 失败, 回退到 workbook.open:', error)
|
|
|
+ loadViaWorkbook(blobBase64)
|
|
|
+ },
|
|
|
+ )
|
|
|
+ } else {
|
|
|
+ loadViaWorkbook(blobBase64)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 回退方案:创建 Workbook 实例,open 后 toJSON
|
|
|
+ */
|
|
|
+function loadViaWorkbook(blobBase64: string) {
|
|
|
+ if (!workbook) {
|
|
|
+ const host = document.createElement('div')
|
|
|
+ host.style.cssText = 'position:absolute;left:-9999px;top:-9999px;width:1px;height:1px;'
|
|
|
+ document.body.appendChild(host)
|
|
|
+ try {
|
|
|
+ workbook = new GC.Spread.Sheets.Workbook(host)
|
|
|
+ } catch (e) {
|
|
|
+ console.error('[spreadDesignerCustomGeneric] Workbook 创建失败:', e)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const blob = base64ToBlob(blobBase64, '')
|
|
|
+ workbook.open(
|
|
|
+ blob,
|
|
|
+ () => {
|
|
|
+ console.log('[spreadDesignerCustomGeneric] workbook.open 成功')
|
|
|
+ onConfigReady(workbook.toJSON() as SpreadWorkbookConfig)
|
|
|
+ },
|
|
|
+ (error: any) => {
|
|
|
+ console.error('[spreadDesignerCustomGeneric] workbook.open 也失败:', error)
|
|
|
+ },
|
|
|
+ )
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 配置就绪:extractConfig 后处理 -> 设置 formData -> 注册渲染器
|
|
|
+ */
|
|
|
+function onConfigReady(rawConfig: SpreadWorkbookConfig) {
|
|
|
+ // extractConfig: 清理字段、标准化样式、处理富文本、压缩默认值
|
|
|
+ const config = extractConfig(rawConfig)
|
|
|
+ workbookConfig.value = config
|
|
|
+
|
|
|
+ if (props.templateData?.data) {
|
|
|
+ formData.value = { ...props.templateData.data }
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log('[spreadDesignerCustomGeneric] 配置处理完成, sheets:', Object.keys(config.sheets || {}))
|
|
|
+ registerRender()
|
|
|
+}
|
|
|
+
|
|
|
+function registerRender() {
|
|
|
+ if (!workbookConfig.value || !renderReady.value || !renderRef.value) return
|
|
|
+ renderRef.value.register({
|
|
|
+ config: workbookConfig.value!,
|
|
|
+ formData: formData.value,
|
|
|
+ showToolbar: false,
|
|
|
+ readonly: false,
|
|
|
+ })
|
|
|
+ console.log('[spreadDesignerCustomGeneric] 已注册到 SpreadCustomRender')
|
|
|
+}
|
|
|
+
|
|
|
+// ==================== 事件处理 ====================
|
|
|
+
|
|
|
+function onRenderReady() {
|
|
|
+ renderReady.value = true
|
|
|
+ registerRender()
|
|
|
+}
|
|
|
+
|
|
|
+function onRenderSave(data: Record<string, any>) {
|
|
|
+ saveRecord(data)
|
|
|
+}
|
|
|
+
|
|
|
+function handleNavButtonClick(button: { action: string }) {
|
|
|
+ if (button.action === 'cancel') {
|
|
|
+ emit('cancel')
|
|
|
+ } else if (button.action === 'save') {
|
|
|
+ saveRecord()
|
|
|
+ } else {
|
|
|
+ handleCustomAction(button.action)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function saveRecord(renderData?: Record<string, any>) {
|
|
|
+ if (!renderRef.value) return
|
|
|
+ const data = renderData || renderRef.value.getBoundFormData()
|
|
|
+ emit('save', {
|
|
|
+ dataJSON: data,
|
|
|
+ businessType: props.businessConfig?.businessType,
|
|
|
+ templateId: props.templateData?.data?.templateId || props.templateData?.template,
|
|
|
+ reportUrl: props.templateData?.data?.templateUrl || props.templateData?.templateUrl || '',
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+function handleCustomAction(action: string) {
|
|
|
+ if (!renderRef.value) return
|
|
|
+ emit('customAction', { action, data: renderRef.value.getFormData() })
|
|
|
+}
|
|
|
+
|
|
|
+// ==================== Watch ====================
|
|
|
+
|
|
|
+watch(
|
|
|
+ () => props.templateBlob,
|
|
|
+ (newBlob) => {
|
|
|
+ if (newBlob) loadTemplate(newBlob)
|
|
|
+ },
|
|
|
+)
|
|
|
+
|
|
|
+watch(
|
|
|
+ () => props.businessConfig,
|
|
|
+ (config) => {
|
|
|
+ if (config?.ui) initUI(config.ui)
|
|
|
+ },
|
|
|
+ { immediate: true },
|
|
|
+)
|
|
|
+
|
|
|
+// ==================== 暴露方法 ====================
|
|
|
+
|
|
|
+defineExpose({
|
|
|
+ getRenderData: () => renderRef.value?.getBoundFormData() || {},
|
|
|
+ getFormData: () => renderRef.value?.getFormData() || {},
|
|
|
+ setReadonly: (readonly: boolean) => renderRef.value?.setReadonly(readonly),
|
|
|
+})
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+* {
|
|
|
+ box-sizing: border-box;
|
|
|
+ padding: 0;
|
|
|
+ margin: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.spread-designer-custom-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) - env(safe-area-inset-top, 0px));
|
|
|
+ overflow: auto;
|
|
|
+}
|
|
|
+
|
|
|
+.nav-title {
|
|
|
+ flex: 1;
|
|
|
+ font-size: 17px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #333;
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+
|
|
|
+.nav-btn {
|
|
|
+ margin: 0 4px;
|
|
|
+ padding: 0 12px;
|
|
|
+ font-size: 12px;
|
|
|
+ color: #495057;
|
|
|
+ white-space: nowrap;
|
|
|
+ background-color: #fff;
|
|
|
+ border: 1px solid #dee2e6;
|
|
|
+ border-radius: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.nav-btn:active {
|
|
|
+ background-color: #f8f9fa;
|
|
|
+}
|
|
|
+
|
|
|
+.nav-btn.primary {
|
|
|
+ color: white;
|
|
|
+ background-color: #007bff;
|
|
|
+ border-color: #007bff;
|
|
|
+}
|
|
|
+
|
|
|
+.nav-btn.primary:active {
|
|
|
+ background-color: #0056b3;
|
|
|
+}
|
|
|
+</style>
|