@lemonppt/renderer 0.1.8 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,2 +1,2 @@
1
- export declare const editorScript = "\n(function () {\n const STORAGE_KEY = 'lemonppt:editor:' + (window.__lemonPPT_goal?.randomSeed || window.__lemonPPT_goal?.title || 'default');\n const MAX_HISTORY = 50;\n\n let goal = window.__lemonPPT_goal;\n if (!goal) return;\n\n // \u4F18\u5148\u4ECE localStorage \u6062\u590D\n try {\n const saved = localStorage.getItem(STORAGE_KEY);\n if (saved) {\n const parsed = JSON.parse(saved);\n if (parsed && parsed.slides) {\n goal = parsed;\n window.__lemonPPT_goal = goal;\n syncDomFromGoal();\n }\n }\n } catch (err) {\n console.warn('\u81EA\u52A8\u6062\u590D\u5931\u8D25', err);\n }\n\n const history = [deepClone(goal)];\n let historyIndex = 0;\n\n function deepClone(obj) {\n return JSON.parse(JSON.stringify(obj));\n }\n\n function recordHistory() {\n // \u622A\u65AD redo \u5206\u652F\n if (historyIndex < history.length - 1) {\n history.splice(historyIndex + 1);\n }\n history.push(deepClone(goal));\n if (history.length > MAX_HISTORY) {\n history.shift();\n } else {\n historyIndex++;\n }\n updateUndoRedoButtons();\n }\n\n function undo() {\n if (historyIndex <= 0) return;\n historyIndex--;\n restoreGoal(history[historyIndex]);\n }\n\n function redo() {\n if (historyIndex >= history.length - 1) return;\n historyIndex++;\n restoreGoal(history[historyIndex]);\n }\n\n function restoreGoal(newGoal) {\n goal = newGoal;\n window.__lemonPPT_goal = goal;\n Object.assign(window.__lemonPPT_goal, newGoal);\n syncDomFromGoal();\n autoSave();\n updateUndoRedoButtons();\n }\n\n function autoSave() {\n try {\n localStorage.setItem(STORAGE_KEY, JSON.stringify(goal));\n } catch (err) {\n console.warn('\u81EA\u52A8\u4FDD\u5B58\u5931\u8D25', err);\n }\n }\n\n function syncDomFromGoal() {\n // \u6587\u672C\n document.querySelectorAll('[data-lp-editable=\"true\"]').forEach((el) => {\n const slideIdx = Number(el.getAttribute('data-lp-slide-idx'));\n const prop = el.getAttribute('data-lp-prop');\n if (Number.isNaN(slideIdx) || !prop) return;\n const slide = goal.slides[slideIdx];\n if (!slide) return;\n const value = getProp(slide.props, prop);\n el.textContent = value == null ? '' : String(value);\n });\n\n // \u56FE\u7247\n document.querySelectorAll('[data-lp-editable-image=\"true\"]').forEach((img) => {\n const slideIdx = Number(img.getAttribute('data-lp-slide-idx'));\n const prop = img.getAttribute('data-lp-prop') || 'image';\n if (Number.isNaN(slideIdx)) return;\n const slide = goal.slides[slideIdx];\n if (!slide) return;\n const value = getProp(slide.props, prop);\n if (value) {\n img.setAttribute('src', String(value));\n } else {\n img.remove();\n }\n });\n }\n\n function getProp(obj, path) {\n const keys = path.split('.');\n let target = obj;\n for (const k of keys) {\n if (target == null || typeof target !== 'object') return undefined;\n target = target[k];\n }\n return target;\n }\n\n function setProp(obj, path, value) {\n const keys = path.split('.');\n let target = obj;\n for (let i = 0; i < keys.length - 1; i++) {\n const k = keys[i];\n if (!(k in target)) target[k] = [];\n target = target[k];\n }\n target[keys[keys.length - 1]] = value;\n }\n\n // \u56FE\u7247\u7F16\u8F91\u5F39\u7A97\n function openImageEditor(img, slideIdx, prop) {\n const slide = goal.slides[slideIdx];\n if (!slide) return;\n\n const overlay = document.createElement('div');\n overlay.className = 'lp-image-editor-overlay';\n overlay.innerHTML = `\n <div class=\"lp-image-editor\">\n <h3>\u7F16\u8F91\u56FE\u7247</h3>\n <label>\n <span>\u56FE\u7247 URL</span>\n <input type=\"text\" class=\"lp-image-url\" placeholder=\"https://...\" value=\"${escapeHtml(img.getAttribute('src') || '')}\">\n </label>\n <div class=\"lp-image-or\">\u6216</div>\n <label class=\"lp-image-file-label\">\n <span>\u4E0A\u4F20\u672C\u5730\u56FE\u7247</span>\n <input type=\"file\" class=\"lp-image-file\" accept=\"image/*\">\n </label>\n <div class=\"lp-image-preview-wrap\">\n <img class=\"lp-image-preview\" src=\"${escapeHtml(img.getAttribute('src') || '')}\" alt=\"\">\n </div>\n <div class=\"lp-image-actions\">\n <button class=\"lp-image-delete lp-image-btn-secondary\">\u5220\u9664\u56FE\u7247</button>\n <div class=\"lp-image-actions-right\">\n <button class=\"lp-image-cancel lp-image-btn-secondary\">\u53D6\u6D88</button>\n <button class=\"lp-image-confirm\">\u786E\u8BA4</button>\n </div>\n </div>\n </div>\n `;\n\n document.body.appendChild(overlay);\n\n const urlInput = overlay.querySelector('.lp-image-url');\n const fileInput = overlay.querySelector('.lp-image-file');\n const preview = overlay.querySelector('.lp-image-preview');\n const confirmBtn = overlay.querySelector('.lp-image-confirm');\n const cancelBtn = overlay.querySelector('.lp-image-cancel');\n const deleteBtn = overlay.querySelector('.lp-image-delete');\n\n let pendingValue = img.getAttribute('src') || '';\n\n urlInput.addEventListener('input', () => {\n pendingValue = urlInput.value;\n preview.src = pendingValue;\n });\n\n fileInput.addEventListener('change', () => {\n const file = fileInput.files[0];\n if (!file) return;\n const reader = new FileReader();\n reader.onload = (e) => {\n pendingValue = e.target.result;\n urlInput.value = '';\n preview.src = pendingValue;\n };\n reader.readAsDataURL(file);\n });\n\n function close() {\n overlay.remove();\n }\n\n function apply() {\n recordHistory();\n if (pendingValue) {\n img.setAttribute('src', pendingValue);\n setProp(slide.props, prop, pendingValue);\n } else {\n img.remove();\n setProp(slide.props, prop, undefined);\n }\n autoSave();\n close();\n }\n\n function remove() {\n recordHistory();\n img.remove();\n setProp(slide.props, prop, undefined);\n autoSave();\n close();\n }\n\n confirmBtn.addEventListener('click', apply);\n cancelBtn.addEventListener('click', close);\n deleteBtn.addEventListener('click', remove);\n overlay.addEventListener('click', (e) => {\n if (e.target === overlay) close();\n });\n }\n\n function escapeHtml(str) {\n return String(str)\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&#39;');\n }\n\n const editableEls = document.querySelectorAll('[data-lp-editable=\"true\"]');\n\n editableEls.forEach((el) => {\n el.setAttribute('contenteditable', 'true');\n\n el.addEventListener('focus', () => {\n recordHistory();\n });\n\n el.addEventListener('blur', () => {\n const slideIdx = Number(el.getAttribute('data-lp-slide-idx'));\n const prop = el.getAttribute('data-lp-prop');\n if (Number.isNaN(slideIdx) || !prop) return;\n\n const slide = goal.slides[slideIdx];\n if (!slide) return;\n\n const value = el.textContent || '';\n setProp(slide.props, prop, value);\n autoSave();\n });\n\n el.addEventListener('keydown', (e) => {\n if (e.key === 'Enter') {\n e.preventDefault();\n el.blur();\n }\n });\n });\n\n // \u56FE\u7247\u6362\u56FE\n const imageEls = document.querySelectorAll('[data-lp-editable-image=\"true\"]');\n imageEls.forEach((img) => {\n img.addEventListener('click', () => {\n const slideIdx = Number(img.getAttribute('data-lp-slide-idx'));\n const prop = img.getAttribute('data-lp-prop') || 'image';\n if (Number.isNaN(slideIdx)) return;\n openImageEditor(img, slideIdx, prop);\n });\n });\n\n const downloadBtn = document.getElementById('lp-download-goal');\n if (downloadBtn) {\n downloadBtn.addEventListener('click', () => {\n const blob = new Blob([JSON.stringify(goal, null, 2)], { type: 'application/json' });\n const url = URL.createObjectURL(blob);\n const a = document.createElement('a');\n a.href = url;\n a.download = 'goal.json';\n a.click();\n URL.revokeObjectURL(url);\n });\n }\n\n const exportPptxBtn = document.getElementById('lp-export-pptx');\n if (exportPptxBtn) {\n exportPptxBtn.addEventListener('click', async () => {\n exportPptxBtn.textContent = '\u5BFC\u51FA\u4E2D...';\n exportPptxBtn.disabled = true;\n try {\n const res = await fetch('/api/export/pptx', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(goal),\n });\n if (!res.ok) throw new Error('\u5BFC\u51FA\u5931\u8D25: ' + res.status);\n const blob = await res.blob();\n const url = URL.createObjectURL(blob);\n const a = document.createElement('a');\n a.href = url;\n a.download = 'presentation.pptx';\n a.click();\n URL.revokeObjectURL(url);\n } catch (err) {\n alert(err instanceof Error ? err.message : String(err));\n } finally {\n exportPptxBtn.textContent = '\u5BFC\u51FA PPTX';\n exportPptxBtn.disabled = false;\n }\n });\n }\n\n const exportPdfBtn = document.getElementById('lp-export-pdf');\n if (exportPdfBtn) {\n exportPdfBtn.addEventListener('click', async () => {\n exportPdfBtn.textContent = '\u5BFC\u51FA\u4E2D...';\n exportPdfBtn.disabled = true;\n try {\n const res = await fetch('/api/export/pdf', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(goal),\n });\n if (!res.ok) throw new Error('\u5BFC\u51FA\u5931\u8D25: ' + res.status);\n const blob = await res.blob();\n const url = URL.createObjectURL(blob);\n const a = document.createElement('a');\n a.href = url;\n a.download = 'presentation.pdf';\n a.click();\n URL.revokeObjectURL(url);\n } catch (err) {\n alert(err instanceof Error ? err.message : String(err));\n } finally {\n exportPdfBtn.textContent = '\u5BFC\u51FA PDF';\n exportPdfBtn.disabled = false;\n }\n });\n }\n\n // \u64A4\u9500 / \u91CD\u505A\n const undoBtn = document.getElementById('lp-undo');\n const redoBtn = document.getElementById('lp-redo');\n\n function updateUndoRedoButtons() {\n if (undoBtn) undoBtn.disabled = historyIndex <= 0;\n if (redoBtn) redoBtn.disabled = historyIndex >= history.length - 1;\n }\n\n if (undoBtn) undoBtn.addEventListener('click', undo);\n if (redoBtn) redoBtn.addEventListener('click', redo);\n\n // \u4E3B\u9898\u5207\u6362\n const themeSwitcher = document.getElementById('lp-theme-switcher');\n if (themeSwitcher) {\n themeSwitcher.value = goal.theme || 'base';\n themeSwitcher.addEventListener('change', () => {\n const newTheme = themeSwitcher.value;\n if (newTheme === goal.theme) return;\n goal.theme = newTheme;\n autoSave();\n window.location.href = '/editor?theme=' + encodeURIComponent(newTheme);\n });\n }\n\n document.addEventListener('keydown', (e) => {\n const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;\n const mod = isMac ? e.metaKey : e.ctrlKey;\n if (mod && e.key.toLowerCase() === 'z') {\n e.preventDefault();\n if (e.shiftKey) redo();\n else undo();\n }\n if (mod && e.key.toLowerCase() === 'y') {\n e.preventDefault();\n redo();\n }\n });\n\n updateUndoRedoButtons();\n\n // \u7FFB\u9875\u811A\u672C\n const slides = document.querySelectorAll('.lp-slide-wrapper');\n const thumbnails = document.querySelectorAll('.lp-thumbnail');\n const prevBtn = document.getElementById('lp-prev');\n const nextBtn = document.getElementById('lp-next');\n const currentLabel = document.getElementById('lp-current');\n let current = 0;\n\n function updateClasses() {\n slides.forEach((slide, index) => {\n slide.classList.remove('active', 'prev');\n if (index === current) slide.classList.add('active');\n else if (index < current) slide.classList.add('prev');\n });\n thumbnails.forEach((thumb, index) => {\n thumb.classList.toggle('active', index === current);\n });\n const activeThumb = thumbnails[current];\n if (activeThumb) activeThumb.scrollIntoView({ block: 'nearest', behavior: 'smooth' });\n if (currentLabel) currentLabel.textContent = String(current + 1);\n if (prevBtn) prevBtn.disabled = current === 0;\n if (nextBtn) nextBtn.disabled = current === slides.length - 1;\n }\n\n function goTo(index) {\n if (index < 0 || index >= slides.length) return;\n current = index;\n updateClasses();\n if (typeof selectSlide === 'function') selectSlide(current);\n }\n\n if (prevBtn) prevBtn.addEventListener('click', () => goTo(current - 1));\n if (nextBtn) nextBtn.addEventListener('click', () => goTo(current + 1));\n thumbnails.forEach((thumb) => {\n thumb.addEventListener('click', () => goTo(Number(thumb.dataset.index)));\n });\n function isEditingTarget(target) {\n if (!target || !target.closest) return false;\n return target.closest('[contenteditable=\"true\"]') || target.closest('.lp-image-editor');\n }\n\n document.addEventListener('keydown', (e) => {\n if (isEditingTarget(e.target)) return;\n if (e.key === 'ArrowRight' || e.key === ' ' || e.key === 'PageDown') goTo(current + 1);\n if (e.key === 'ArrowLeft' || e.key === 'PageUp') goTo(current - 1);\n if (e.key === 'Home') goTo(0);\n if (e.key === 'End') goTo(slides.length - 1);\n });\n\n updateClasses();\n\n // \u7F29\u653E\u63A7\u5236\n const stage = document.querySelector('.lp-editor-stage');\n const scaler = document.querySelector('.lp-editor-stage-scaler');\n const zoomSlider = document.getElementById('lp-zoom-slider');\n const zoomValue = document.getElementById('lp-zoom-value');\n const zoomOutBtn = document.getElementById('lp-zoom-out');\n const zoomInBtn = document.getElementById('lp-zoom-in');\n const zoomFitBtn = document.getElementById('lp-zoom-fit');\n let userZoom = null;\n\n function fitScale() {\n if (!stage || !scaler) return 1;\n return Math.min(stage.clientWidth / 1280, stage.clientHeight / 720) * 0.92;\n }\n\n function updateScale() {\n if (!stage || !scaler) return;\n const scale = userZoom == null ? fitScale() : userZoom;\n scaler.style.transform = 'scale(' + Math.max(scale, 0.35) + ')';\n if (zoomValue) zoomValue.textContent = Math.round(scale * 100) + '%';\n if (zoomSlider && userZoom != null) zoomSlider.value = String(Math.round(scale * 100));\n }\n\n if (zoomSlider) {\n zoomSlider.addEventListener('input', () => {\n userZoom = Number(zoomSlider.value) / 100;\n updateScale();\n });\n }\n if (zoomOutBtn) {\n zoomOutBtn.addEventListener('click', () => {\n userZoom = (userZoom == null ? fitScale() : userZoom) - 0.1;\n if (userZoom < 0.35) userZoom = 0.35;\n updateScale();\n });\n }\n if (zoomInBtn) {\n zoomInBtn.addEventListener('click', () => {\n userZoom = (userZoom == null ? fitScale() : userZoom) + 0.1;\n if (userZoom > 1.5) userZoom = 1.5;\n updateScale();\n });\n }\n if (zoomFitBtn) {\n zoomFitBtn.addEventListener('click', () => {\n userZoom = null;\n updateScale();\n });\n }\n window.addEventListener('resize', updateScale);\n updateScale();\n\n // \u64AD\u653E\uFF1A\u5148\u6E32\u67D3\u9759\u6001\u7248\u672C\u518D\u6253\u5F00\n const playBtn = document.getElementById('lp-play');\n if (playBtn) {\n playBtn.addEventListener('click', async () => {\n playBtn.textContent = '\u51C6\u5907\u4E2D...';\n playBtn.disabled = true;\n try {\n const res = await fetch('/api/render', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(goal),\n });\n if (!res.ok) throw new Error('\u6E32\u67D3\u5931\u8D25: ' + res.status);\n window.open('/deck/index.html', '_blank');\n } catch (err) {\n alert(err instanceof Error ? err.message : String(err));\n } finally {\n playBtn.textContent = '\u25B6 \u64AD\u653E';\n playBtn.disabled = false;\n }\n });\n }\n\n // \u5185\u5BB9\u7ED3\u6784\u53D8\u66F4\u540E\u91CD\u65B0\u6E32\u67D3\u5F53\u524D\u7F16\u8F91\u5668\uFF08\u4EC5\u7528\u4E8E\u6570\u7EC4\u589E\u5220\u7B49\u6A21\u677F\u5185\u5BB9\u8C03\u6574\uFF09\n function saveCurrentForReload() {\n try {\n localStorage.setItem('lemonppt:editor:currentSlide', String(current));\n } catch (e) {}\n }\n\n async function reloadEditor() {\n saveCurrentForReload();\n try {\n const res = await fetch('/api/render-editor', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(goal),\n });\n if (!res.ok) throw new Error('\u91CD\u65B0\u6E32\u67D3\u5931\u8D25: ' + res.status);\n window.location.reload();\n } catch (err) {\n alert(err instanceof Error ? err.message : String(err));\n }\n }\n\n // \u5143\u7D20\u9009\u4E2D\u4E0E\u53F3\u4FA7\u5C5E\u6027\u9762\u677F\n let selectedEl = null;\n let selectedSlideIdx = 0;\n const propertyContent = document.getElementById('lp-property-content');\n\n const FIELD_LABELS = {\n kicker: '\u6807\u7B7E',\n title: '\u6807\u9898',\n subtitle: '\u526F\u6807\u9898',\n date: '\u65E5\u671F',\n quote: '\u5F15\u7528',\n author: '\u4F5C\u8005',\n role: '\u804C\u4F4D',\n company: '\u516C\u53F8',\n value: '\u6570\u503C',\n unit: '\u5355\u4F4D',\n label: '\u6307\u6807\u540D',\n description: '\u8BF4\u660E',\n change: '\u53D8\u5316',\n imageUrl: '\u56FE\u7247',\n url: '\u56FE\u7247',\n logoUrl: 'Logo',\n avatarUrl: '\u5934\u50CF',\n caption: '\u8BF4\u660E',\n q: '\u95EE\u9898',\n a: '\u56DE\u7B54',\n question: '\u95EE\u9898',\n answer: '\u56DE\u7B54',\n name: '\u540D\u79F0',\n price: '\u4EF7\u683C',\n period: '\u5468\u671F',\n cta: '\u6309\u94AE\u6587\u6848',\n type: '\u56FE\u8868\u7C7B\u578B',\n status: '\u72B6\u6001',\n bio: '\u7B80\u4ECB',\n };\n\n function getFieldLabel(path) {\n const key = String(path).split('.').pop() || '';\n return FIELD_LABELS[key] || key;\n }\n\n function inferFieldType(path, value) {\n const key = String(path).split('.').pop() || '';\n if (/image|url|logo|avatar/i.test(key) && typeof value === 'string') return 'image';\n if (typeof value === 'boolean') return 'boolean';\n if (typeof value === 'number') return 'number';\n if (key === 'type' && ['bar', 'line', 'pie'].includes(String(value))) return 'select';\n if (key === 'status') return 'select';\n if (['description', 'bio', 'quote', 'answer', 'a', 'subtitle'].includes(key)) return 'textarea';\n return 'text';\n }\n\n function getSelectOptions(path) {\n const key = String(path).split('.').pop() || '';\n if (key === 'type') return [{ value: 'bar', label: '\u67F1\u72B6' }, { value: 'line', label: '\u6298\u7EBF' }, { value: 'pie', label: '\u997C\u56FE' }];\n if (key === 'status') return [{ value: '\u5DF2\u5B8C\u6210', label: '\u5DF2\u5B8C\u6210' }, { value: '\u8FDB\u884C\u4E2D', label: '\u8FDB\u884C\u4E2D' }, { value: '\u89C4\u5212\u4E2D', label: '\u89C4\u5212\u4E2D' }];\n return [];\n }\n\n function clearSelection() {\n if (selectedEl) {\n selectedEl.classList.remove('lp-selected');\n selectedEl = null;\n }\n selectedSlideIdx = current;\n renderSlidePanel();\n }\n\n function setField(path, value) {\n const slide = goal.slides[selectedSlideIdx];\n if (!slide) return;\n recordHistory();\n setProp(slide.props, path, value);\n syncDomFromGoal();\n autoSave();\n }\n\n function createEl(tag, className, parent) {\n const el = document.createElement(tag);\n if (className) el.className = className;\n if (parent) parent.appendChild(el);\n return el;\n }\n\n function createTextField(label, value, onChange) {\n const wrap = createEl('div', 'lp-property-field');\n createEl('label', 'lp-property-label', wrap).textContent = label;\n const input = createEl('input', 'lp-property-input', wrap);\n input.type = 'text';\n input.value = value == null ? '' : String(value);\n input.addEventListener('input', () => onChange(input.value));\n return wrap;\n }\n\n function createTextareaField(label, value, onChange) {\n const wrap = createEl('div', 'lp-property-field');\n createEl('label', 'lp-property-label', wrap).textContent = label;\n const textarea = createEl('textarea', 'lp-property-textarea', wrap);\n textarea.value = value == null ? '' : String(value);\n textarea.addEventListener('input', () => onChange(textarea.value));\n return wrap;\n }\n\n function createNumberField(label, value, onChange) {\n const wrap = createEl('div', 'lp-property-field');\n createEl('label', 'lp-property-label', wrap).textContent = label;\n const input = createEl('input', 'lp-property-input', wrap);\n input.type = 'number';\n input.value = value == null ? '' : String(value);\n input.addEventListener('input', () => onChange(Number(input.value)));\n return wrap;\n }\n\n function createToggleField(label, value, onChange) {\n const wrap = createEl('div', 'lp-property-field');\n const labelEl = createEl('label', 'lp-property-toggle', wrap);\n const text = createEl('span', '', labelEl);\n text.textContent = label;\n const input = createEl('input', '', labelEl);\n input.type = 'checkbox';\n input.checked = !!value;\n const track = createEl('div', 'lp-property-toggle-track', labelEl);\n createEl('div', 'lp-property-toggle-thumb', track);\n input.addEventListener('change', () => onChange(input.checked));\n return wrap;\n }\n\n function createSelectField(label, value, options, onChange) {\n const wrap = createEl('div', 'lp-property-field');\n createEl('label', 'lp-property-label', wrap).textContent = label;\n const segmented = createEl('div', 'lp-property-segmented', wrap);\n options.forEach((opt) => {\n const btn = createEl('button', '', segmented);\n btn.textContent = opt.label;\n btn.type = 'button';\n if (opt.value === value) btn.classList.add('active');\n btn.addEventListener('click', () => {\n Array.from(segmented.children).forEach((b) => b.classList.remove('active'));\n btn.classList.add('active');\n onChange(opt.value);\n });\n });\n return wrap;\n }\n\n function createImageField(label, value, onChange) {\n const wrap = createEl('div', 'lp-property-field');\n createEl('label', 'lp-property-label', wrap).textContent = label;\n const urlInput = createEl('input', 'lp-property-input', wrap);\n urlInput.type = 'text';\n urlInput.value = value == null ? '' : String(value);\n urlInput.placeholder = 'https://...';\n urlInput.addEventListener('input', () => onChange(urlInput.value));\n\n const fileWrap = createEl('div', 'lp-property-field');\n fileWrap.style.marginTop = '8px';\n const fileInput = createEl('input', 'lp-property-input', fileWrap);\n fileInput.type = 'file';\n fileInput.accept = 'image/*';\n fileInput.addEventListener('change', () => {\n const file = fileInput.files[0];\n if (!file) return;\n const reader = new FileReader();\n reader.onload = (e) => {\n const dataUrl = e.target.result;\n urlInput.value = dataUrl;\n onChange(dataUrl);\n };\n reader.readAsDataURL(file);\n });\n wrap.appendChild(fileWrap);\n return wrap;\n }\n\n function createFieldControl(path, value) {\n const type = inferFieldType(path, value);\n const label = getFieldLabel(path);\n if (type === 'image') return createImageField(label, value, (v) => setField(path, v || undefined));\n if (type === 'textarea') return createTextareaField(label, value, (v) => setField(path, v || undefined));\n if (type === 'number') return createNumberField(label, value, (v) => setField(path, v));\n if (type === 'boolean') return createToggleField(label, value, (v) => setField(path, v));\n if (type === 'select') return createSelectField(label, value, getSelectOptions(path), (v) => setField(path, v));\n return createTextField(label, value, (v) => setField(path, v || undefined));\n }\n\n function inferEmptyItem(array) {\n if (!array.length) return {};\n const sample = array[0];\n if (typeof sample === 'string') return '';\n if (typeof sample === 'object' && sample !== null) {\n const item = {};\n Object.keys(sample).forEach((k) => {\n const v = sample[k];\n item[k] = typeof v === 'boolean' ? false : (typeof v === 'number' ? 0 : '');\n });\n return item;\n }\n return '';\n }\n\n function createArraySection(path, array, parent) {\n const section = createEl('div', 'lp-property-section', parent);\n createEl('div', 'lp-property-section-title', section).textContent = getFieldLabel(path) + ' (' + array.length + ')';\n\n const list = createEl('div', 'lp-property-array', section);\n array.forEach((item, index) => {\n const itemWrap = createEl('div', 'lp-property-array-item', list);\n const header = createEl('div', 'lp-property-array-header', itemWrap);\n header.textContent = '\u7B2C ' + (index + 1) + ' \u9879';\n const removeBtn = createEl('button', 'lp-property-btn lp-property-btn-sm lp-property-btn-danger', header);\n removeBtn.textContent = '\u5220\u9664';\n removeBtn.type = 'button';\n removeBtn.addEventListener('click', () => {\n recordHistory();\n const arr = getProp(goal.slides[selectedSlideIdx].props, path);\n if (Array.isArray(arr)) {\n arr.splice(index, 1);\n autoSave();\n reloadEditor();\n }\n });\n\n if (typeof item === 'string') {\n const textarea = createEl('textarea', 'lp-property-textarea', itemWrap);\n textarea.value = item;\n textarea.addEventListener('input', () => {\n setField(path + '.' + index, textarea.value);\n });\n } else if (typeof item === 'object' && item !== null) {\n Object.keys(item).forEach((key) => {\n itemWrap.appendChild(createFieldControl(path + '.' + index + '.' + key, item[key]));\n });\n }\n });\n\n const addBtn = createEl('button', 'lp-property-btn lp-property-btn-primary', section);\n addBtn.textContent = '\uFF0B \u6DFB\u52A0\u4E00\u9879';\n addBtn.type = 'button';\n addBtn.addEventListener('click', () => {\n recordHistory();\n const arr = getProp(goal.slides[selectedSlideIdx].props, path);\n if (Array.isArray(arr)) {\n arr.push(inferEmptyItem(arr));\n autoSave();\n reloadEditor();\n }\n });\n }\n\n function renderSlideFields(props, parent) {\n Object.keys(props).forEach((key) => {\n if (key === '_style') return;\n const value = props[key];\n if (Array.isArray(value)) {\n createArraySection(key, value, parent);\n } else if (value !== null && typeof value === 'object') {\n // \u5D4C\u5957\u5BF9\u8C61\u76F4\u63A5\u5C55\u5F00\uFF08\u76EE\u524D\u8F83\u5C11\uFF09\n const section = createEl('div', 'lp-property-section', parent);\n createEl('div', 'lp-property-section-title', section).textContent = getFieldLabel(key);\n Object.keys(value).forEach((subKey) => {\n section.appendChild(createFieldControl(key + '.' + subKey, value[subKey]));\n });\n } else {\n parent.appendChild(createFieldControl(key, value));\n }\n });\n }\n\n function renderSlidePanel() {\n if (!propertyContent) return;\n propertyContent.innerHTML = '';\n const slide = goal.slides[selectedSlideIdx];\n if (!slide) return;\n\n const info = createEl('div', 'lp-property-section', propertyContent);\n createEl('div', 'lp-property-section-title', info).textContent = '\u5E7B\u706F\u7247 ' + (selectedSlideIdx + 1);\n const layoutLabel = createEl('div', 'lp-property-help', info);\n layoutLabel.textContent = '\u7248\u5F0F\uFF1A' + slide.layout;\n\n if (selectedEl) {\n const prop = selectedEl.getAttribute('data-lp-prop');\n const quickSection = createEl('div', 'lp-property-section', propertyContent);\n createEl('div', 'lp-property-section-title', quickSection).textContent = '\u5F53\u524D\u9009\u4E2D';\n const fieldValue = getProp(slide.props, prop);\n quickSection.appendChild(createFieldControl(prop, fieldValue));\n }\n\n const fieldsSection = createEl('div', 'lp-property-section', propertyContent);\n createEl('div', 'lp-property-section-title', fieldsSection).textContent = '\u5185\u5BB9\u5C5E\u6027';\n renderSlideFields(slide.props, fieldsSection);\n }\n\n function selectEl(el) {\n if (selectedEl === el) return;\n if (selectedEl) selectedEl.classList.remove('lp-selected');\n selectedEl = el;\n selectedEl.classList.add('lp-selected');\n selectedSlideIdx = Number(el.getAttribute('data-lp-slide-idx')) || current;\n renderSlidePanel();\n // \u6EDA\u52A8\u5230\u5F53\u524D\u9009\u4E2D\u5B57\u6BB5\n const path = el.getAttribute('data-lp-prop');\n if (path && propertyContent) {\n const label = propertyContent.querySelector('.lp-property-label');\n // \u7B80\u5355\u9AD8\u4EAE\uFF1A\u6682\u65F6\u4E0D\u505A\u81EA\u52A8\u6EDA\u52A8\uFF0C\u907F\u514D\u590D\u6742\u5EA6\n }\n }\n\n function selectSlide(index) {\n if (selectedEl) {\n selectedEl.classList.remove('lp-selected');\n selectedEl = null;\n }\n selectedSlideIdx = index;\n renderSlidePanel();\n }\n\n document.addEventListener('focusin', (e) => {\n const el = e.target.closest && e.target.closest('[data-lp-editable=\"true\"], [data-lp-editable-image=\"true\"]');\n if (el) selectEl(el);\n });\n\n document.addEventListener('click', (e) => {\n const editableEl = e.target.closest && e.target.closest('[data-lp-editable=\"true\"], [data-lp-editable-image=\"true\"]');\n if (editableEl) {\n selectEl(editableEl);\n return;\n }\n const wrapper = e.target.closest && e.target.closest('.lp-slide-wrapper');\n if (wrapper) {\n selectSlide(Number(wrapper.getAttribute('data-slide-index')));\n return;\n }\n if (e.target.closest && !e.target.closest('.lp-editor-right-panel')) {\n clearSelection();\n }\n });\n\n // \u6062\u590D\u4E0A\u6B21\u505C\u7559\u7684\u5E7B\u706F\u7247\uFF08\u7ED3\u6784\u53D8\u66F4\u540E\u91CD\u8F7D\u7528\uFF09\n const savedCurrent = localStorage.getItem('lemonppt:editor:currentSlide');\n if (savedCurrent) {\n const savedIndex = Number(savedCurrent);\n if (!Number.isNaN(savedIndex) && savedIndex >= 0 && savedIndex < slides.length) {\n goTo(savedIndex);\n }\n localStorage.removeItem('lemonppt:editor:currentSlide');\n }\n\n // \u9ED8\u8BA4\u9009\u4E2D\u5F53\u524D\u5E7B\u706F\u7247\n selectSlide(current);\n})();\n";
1
+ export declare const editorScript = "\n(function () {\n // #region debug-point logging-helper\n const __dbgUrl = 'http://127.0.0.1:7777/event';\n const __dbgSession = 'add-slide-null-regression';\n function __dbg(hypothesisId, location, msg, data) {\n try {\n fetch(__dbgUrl, {\n method: 'POST',\n body: JSON.stringify({ sessionId: __dbgSession, runId: 'pre-fix', hypothesisId, location, msg: '[DEBUG] ' + msg, data: data || {}, ts: Date.now() })\n }).catch(function () {});\n } catch (e) {}\n }\n window.addEventListener('error', (e) => {\n __dbg('ERR', 'window.onerror', e.message, { filename: e.filename, lineno: e.lineno, colno: e.colno, stack: e.error && e.error.stack });\n });\n window.addEventListener('unhandledrejection', (e) => {\n __dbg('ERR', 'window.unhandledrejection', String(e.reason), { stack: e.reason && e.reason.stack });\n });\n // #endregion\n\n // \u7528 jQuery \u4E3A\u6240\u6709\u5E26 data-scrollbar \u7684\u5BB9\u5668\u6DFB\u52A0\u81EA\u5B9A\u4E49\u6EDA\u52A8\u6761\u6837\u5F0F\u7C7B\n if (typeof jQuery !== 'undefined') {\n jQuery(function ($) {\n $('[data-scrollbar]').addClass('lp-custom-scrollbar');\n });\n }\n\n const STORAGE_KEY = 'lemonppt:editor:v2:' + (window.__lemonPPT_goal?.randomSeed || window.__lemonPPT_goal?.title || 'default');\n const MIGRATION_KEY = 'lemonppt:editor:migration:showInsight:v1';\n const MAX_HISTORY = 50;\n\n let goal = window.__lemonPPT_goal;\n if (!goal) return;\n\n // \u4F18\u5148\u4ECE localStorage \u6062\u590D\n let needsDomRebuild = false;\n try {\n const saved = localStorage.getItem(STORAGE_KEY);\n if (saved) {\n const parsed = JSON.parse(saved);\n if (parsed && parsed.slides) {\n // \u4E00\u6B21\u6027\u8FC1\u79FB\uFF1A\u5C06\u56FE\u8868\u9875\u7684\u91CD\u70B9\u5F3A\u8C03\u5F00\u5173\u91CD\u7F6E\u4E3A\u9ED8\u8BA4\u5F00\u542F\n if (!localStorage.getItem(MIGRATION_KEY)) {\n parsed.slides.forEach((slide) => {\n if (slide.role === 'chart') {\n slide.props = slide.props || {};\n slide.props.showInsight = true;\n }\n });\n localStorage.setItem(MIGRATION_KEY, '1');\n }\n // \u82E5\u7ED3\u6784\u6570\u91CF\u6216\u5E7B\u706F\u7247\u987A\u5E8F\u53D1\u751F\u53D8\u5316\uFF0C\u9700\u8981\u91CD\u5EFA DOM\uFF1B\u4EC5\u5185\u5BB9\u53D8\u5316\u5219\u540C\u6B65\u6587\u672C\u5373\u53EF\n const orderChanged = parsed.slides.length !== goal.slides.length ||\n parsed.slides.some((slide, i) => slide.layout !== goal.slides[i].layout);\n goal = parsed;\n window.__lemonPPT_goal = goal;\n if (orderChanged) {\n needsDomRebuild = true;\n } else {\n syncDomFromGoal();\n }\n }\n }\n } catch (err) {\n console.warn('\u81EA\u52A8\u6062\u590D\u5931\u8D25', err);\n }\n\n // \u521D\u59CB\u5316\u5916\u89C2\u6A21\u5F0F\uFF08\u540C\u4E00\u4E3B\u9898\u4E0B\u7684\u6D45\u8272/\u6DF1\u8272\u5207\u6362\uFF0C\u901A\u8FC7 data-theme / data-appearance \u5207\u6362 CSS \u53D8\u91CF\uFF09\n syncAppearanceFromGoal();\n\n const TRANSITIONS = [\n { key: 'none', label: '\u65E0\u52A8\u753B' },\n { key: 'slide', label: '\u6A2A\u6ED1' },\n { key: 'fade', label: '\u6DE1\u5165\u6DE1\u51FA' },\n { key: 'scale', label: '\u7F29\u653E' },\n { key: 'cut-in', label: '\u5207\u5165' },\n { key: 'cube', label: '\u7ACB\u65B9\u4F53' },\n { key: 'flip', label: '\u7FFB\u9875' },\n { key: 'cover', label: '\u8986\u76D6' },\n { key: 'wipe', label: '\u64E6\u9664' },\n ];\n\n const history = [deepClone(goal)];\n let historyIndex = 0;\n\n function deepClone(obj) {\n return JSON.parse(JSON.stringify(obj));\n }\n\n function recordHistory() {\n // \u622A\u65AD redo \u5206\u652F\n if (historyIndex < history.length - 1) {\n history.splice(historyIndex + 1);\n }\n history.push(deepClone(goal));\n if (history.length > MAX_HISTORY) {\n history.shift();\n } else {\n historyIndex++;\n }\n updateUndoRedoButtons();\n }\n\n function undo() {\n if (historyIndex <= 0) return;\n historyIndex--;\n restoreGoal(history[historyIndex]);\n }\n\n function redo() {\n if (historyIndex >= history.length - 1) return;\n historyIndex++;\n restoreGoal(history[historyIndex]);\n }\n\n function restoreGoal(newGoal) {\n const oldSlides = goal && goal.slides ? goal.slides : [];\n goal = newGoal;\n window.__lemonPPT_goal = goal;\n Object.assign(window.__lemonPPT_goal, newGoal);\n const orderChanged = oldSlides.length !== goal.slides.length ||\n oldSlides.some((slide, i) => slide.layout !== goal.slides[i].layout);\n if (orderChanged) {\n current = 0;\n selectedSlideIdx = 0;\n rebuildSlidesAndThumbnails();\n if (typeof selectSlide === 'function') selectSlide(current);\n } else {\n syncDomFromGoal();\n }\n syncAppearanceFromGoal();\n autoSave();\n updateUndoRedoButtons();\n }\n\n let autoSaveTimer = null;\n function autoSave() {\n if (autoSaveTimer) {\n clearTimeout(autoSaveTimer);\n }\n autoSaveTimer = setTimeout(() => {\n autoSaveTimer = null;\n try {\n localStorage.setItem(STORAGE_KEY, JSON.stringify(goal));\n } catch (err) {\n console.warn('\u81EA\u52A8\u4FDD\u5B58\u5931\u8D25', err);\n }\n }, 400);\n }\n\n // \u83B7\u53D6\u53EF\u7F16\u8F91\u5143\u7D20\uFF1B\u517C\u5BB9 Text \u8282\u70B9\u3001Shadow DOM \u7B49\u573A\u666F\n function getEditingElement(target) {\n if (!target) return null;\n if (target.nodeType === Node.TEXT_NODE) {\n target = target.parentElement;\n }\n if (!target || typeof target.closest !== 'function') return null;\n return target.closest('[data-lp-editable=\"true\"], [contenteditable=\"true\"], input, textarea, select');\n }\n\n function isEditingTarget(target) {\n return !!getEditingElement(target);\n }\n\n function propMatchesChangedPath(prop, changedPath) {\n if (!changedPath) return true;\n return prop === changedPath || prop.startsWith(changedPath + '.') || changedPath.startsWith(prop + '.');\n }\n\n function syncDomFromGoal(changedPath) {\n // \u6587\u672C\uFF1A\u5982\u679C\u6307\u5B9A\u4E86 changedPath\uFF0C\u53EA\u540C\u6B65\u53D7\u5F71\u54CD\u7684\u8DEF\u5F84\uFF0C\u907F\u514D\u8986\u76D6 React \u6E32\u67D3\u7684\u7279\u6B8A\u7ED3\u6784\uFF08\u5982\u6807\u9898\u9AD8\u4EAE\uFF09\n document.querySelectorAll('[data-lp-editable=\"true\"]').forEach((el) => {\n const slideIdx = Number(el.getAttribute('data-lp-slide-idx'));\n const prop = el.getAttribute('data-lp-prop');\n if (Number.isNaN(slideIdx) || !prop) return;\n if (!propMatchesChangedPath(prop, changedPath)) return;\n const slide = goal.slides[slideIdx];\n if (!slide) return;\n const value = getProp(slide.props, prop);\n el.textContent = value == null ? '' : String(value);\n });\n\n // \u56FE\u7247\n document.querySelectorAll('[data-lp-editable-image=\"true\"]').forEach((img) => {\n const slideIdx = Number(img.getAttribute('data-lp-slide-idx'));\n const prop = img.getAttribute('data-lp-prop') || 'image';\n if (Number.isNaN(slideIdx)) return;\n if (!propMatchesChangedPath(prop, changedPath)) return;\n const slide = goal.slides[slideIdx];\n if (!slide) return;\n const value = getProp(slide.props, prop);\n if (value) {\n if (img.tagName === 'IMG') {\n img.setAttribute('src', String(value));\n } else {\n // \u5360\u4F4D\u5BB9\u5668\u9700\u8981\u66FF\u6362\u4E3A\u771F\u5B9E\u56FE\u7247\n const newImg = document.createElement('img');\n newImg.className = img.className.replace(/\blp-editable-image-placeholder\b/g, '').trim();\n newImg.src = String(value);\n newImg.alt = img.getAttribute('alt') || '';\n newImg.setAttribute('data-lp-editable-image', 'true');\n newImg.setAttribute('data-lp-slide-idx', String(slideIdx));\n newImg.setAttribute('data-lp-prop', prop);\n img.parentNode.replaceChild(newImg, img);\n }\n }\n // \u65E0\u56FE\u7247\u65F6\u4FDD\u7559\u5360\u4F4D\u5BB9\u5668\uFF0C\u786E\u4FDD\u4ECD\u53EF\u70B9\u51FB\u4E0A\u4F20\n });\n }\n\n function getProp(obj, path) {\n const keys = path.split('.');\n let target = obj;\n for (const k of keys) {\n if (target == null || typeof target !== 'object') return undefined;\n target = target[k];\n }\n return target;\n }\n\n function setProp(obj, path, value) {\n const keys = path.split('.');\n let target = obj;\n for (let i = 0; i < keys.length - 1; i++) {\n const k = keys[i];\n if (!(k in target)) target[k] = [];\n target = target[k];\n }\n target[keys[keys.length - 1]] = value;\n }\n\n // \u70B9\u51FB\u56FE\u7247\u56FA\u5B9A\u533A\u57DF\u76F4\u63A5\u4E0A\u4F20\u672C\u5730\u56FE\u7247\n function handleImageUpload(target, slideIdx, prop) {\n const slide = goal.slides[slideIdx];\n if (!slide) return;\n\n const input = document.createElement('input');\n input.type = 'file';\n input.accept = 'image/*';\n input.style.display = 'none';\n document.body.appendChild(input);\n\n input.addEventListener('change', () => {\n const file = input.files?.[0];\n if (!file) {\n input.remove();\n return;\n }\n const reader = new FileReader();\n reader.onload = (e) => {\n const dataUrl = e.target?.result;\n if (typeof dataUrl !== 'string') {\n input.remove();\n return;\n }\n recordHistory();\n setProp(slide.props, prop, dataUrl);\n autoSave();\n renderCurrentSlideToRoot();\n input.remove();\n };\n reader.readAsDataURL(file);\n });\n\n input.addEventListener('cancel', () => input.remove());\n input.click();\n }\n\n function escapeHtml(str) {\n return String(str)\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&#39;');\n }\n\n // \u521D\u59CB\u5316 contenteditable \u5C5E\u6027\uFF1B\u4F7F\u7528 MutationObserver \u76D1\u542C React \u91CD\u65B0\u6E32\u67D3\u540E\u65B0\u589E\u7684\u7F16\u8F91\u5143\u7D20\n function initEditableElements(container) {\n const scope = container || document;\n scope.querySelectorAll('[data-lp-editable=\"true\"]').forEach((el) => {\n if (el.getAttribute('contenteditable') !== 'true') {\n el.setAttribute('contenteditable', 'true');\n }\n });\n }\n initEditableElements();\n\n const editableObserver = new MutationObserver((mutations) => {\n mutations.forEach((mutation) => {\n mutation.addedNodes.forEach((node) => {\n if (node.nodeType === Node.ELEMENT_NODE) {\n initEditableElements(node);\n }\n });\n });\n });\n editableObserver.observe(document.body, { childList: true, subtree: true });\n\n // \u4F7F\u7528\u4E8B\u4EF6\u59D4\u6258\u5904\u7406\u6587\u672C\u7F16\u8F91\uFF0C\u652F\u6301 React \u5C40\u90E8\u6E32\u67D3\u540E\u81EA\u52A8\u751F\u6548\n document.addEventListener('focus', (e) => {\n const el = e.target.closest('[data-lp-editable=\"true\"]');\n if (!el) return;\n recordHistory();\n }, true);\n\n document.addEventListener('blur', (e) => {\n const el = e.target.closest('[data-lp-editable=\"true\"]');\n if (!el) return;\n const slideIdx = Number(el.getAttribute('data-lp-slide-idx'));\n const prop = el.getAttribute('data-lp-prop');\n if (Number.isNaN(slideIdx) || !prop) return;\n\n const slide = goal.slides[slideIdx];\n if (!slide) return;\n\n const rawValue = el.textContent || '';\n const fieldType = el.getAttribute('data-lp-editable-type');\n const value = fieldType === 'number' ? Number(rawValue) : rawValue;\n setProp(slide.props, prop, value);\n autoSave();\n // \u56FE\u8868\u6570\u636E\u5B57\u6BB5\u5728\u753B\u5E03\u4E0A\u76F4\u63A5\u7F16\u8F91\u540E\uFF0C\u9700\u8981\u91CD\u65B0\u6E32\u67D3\u5E7B\u706F\u7247\u4EE5\u66F4\u65B0\u56FE\u8868\n if (el.getAttribute('data-lp-chart-data') === 'true' && typeof window.__lemonPPT_initECharts === 'function') {\n refreshCurrentSlide();\n }\n }, true);\n\n document.addEventListener('keydown', (e) => {\n const el = getEditingElement(e.target) || getEditingElement(document.activeElement);\n if (!el) return;\n if (e.key === 'Enter') {\n e.preventDefault();\n el.blur();\n }\n });\n\n // \u56FE\u7247\u6362\u56FE\uFF1A\u70B9\u51FB\u56FA\u5B9A\u533A\u57DF\u76F4\u63A5\u5524\u8D77\u6587\u4EF6\u9009\u62E9\u5668\u4E0A\u4F20\n document.addEventListener('click', (e) => {\n const target = e.target.closest('[data-lp-editable-image=\"true\"]');\n if (!target) return;\n const slideIdx = Number(target.getAttribute('data-lp-slide-idx'));\n const prop = target.getAttribute('data-lp-prop') || 'image';\n if (Number.isNaN(slideIdx)) return;\n handleImageUpload(target, slideIdx, prop);\n });\n\n const exportToggle = document.getElementById('lp-export-toggle');\n const exportMenu = document.getElementById('lp-export-menu');\n const exportWrapper = document.getElementById('lp-editor-export');\n if (exportToggle && exportMenu) {\n exportToggle.addEventListener('click', (e) => {\n e.stopPropagation();\n const hidden = exportMenu.hasAttribute('hidden');\n if (hidden) {\n exportMenu.removeAttribute('hidden');\n exportToggle.setAttribute('aria-expanded', 'true');\n } else {\n exportMenu.setAttribute('hidden', '');\n exportToggle.setAttribute('aria-expanded', 'false');\n }\n });\n document.addEventListener('click', (e) => {\n if (!exportWrapper || exportWrapper.contains(e.target)) return;\n exportMenu.setAttribute('hidden', '');\n exportToggle.setAttribute('aria-expanded', 'false');\n });\n }\n\n const exportHtmlBtn = document.getElementById('lp-export-html');\n if (exportHtmlBtn) {\n exportHtmlBtn.addEventListener('click', () => {\n const html = '<!DOCTYPE html>\\n' + document.documentElement.outerHTML;\n const blob = new Blob([html], { type: 'text/html' });\n const url = URL.createObjectURL(blob);\n const a = document.createElement('a');\n a.href = url;\n a.download = 'presentation.html';\n a.click();\n URL.revokeObjectURL(url);\n if (exportMenu) exportMenu.setAttribute('hidden', '');\n });\n }\n\n const exportPptxBtn = document.getElementById('lp-export-pptx');\n if (exportPptxBtn) {\n const pptxLabel = exportPptxBtn.querySelector('.lp-editor-export-label');\n exportPptxBtn.addEventListener('click', async () => {\n if (window.location.protocol === 'file:') {\n alert('\u9759\u6001\u6587\u4EF6\u6A21\u5F0F\u4E0B\u65E0\u6CD5\u76F4\u63A5\u5BFC\u51FA PPTX\u3002\\n\\n\u8BF7\u901A\u8FC7\u672C\u5730\u670D\u52A1\u5668\u8BBF\u95EE\u540E\u5BFC\u51FA\uFF1A\\n pnpm dev\\n\u6216\u8FD0\u884C\uFF1A\\n node scripts/export-pptx.mjs <goal.json> out.pptx');\n if (exportMenu) exportMenu.setAttribute('hidden', '');\n return;\n }\n if (pptxLabel) pptxLabel.textContent = '\u5BFC\u51FA\u4E2D...';\n exportPptxBtn.disabled = true;\n try {\n const res = await fetch('/api/export/pptx', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(goal),\n });\n if (!res.ok) throw new Error('\u5BFC\u51FA\u5931\u8D25: ' + res.status);\n const blob = await res.blob();\n const url = URL.createObjectURL(blob);\n const a = document.createElement('a');\n a.href = url;\n a.download = 'presentation.pptx';\n a.click();\n URL.revokeObjectURL(url);\n } catch (err) {\n alert(err instanceof Error ? err.message : String(err));\n } finally {\n if (pptxLabel) pptxLabel.textContent = 'PPTX';\n exportPptxBtn.disabled = false;\n if (exportMenu) exportMenu.setAttribute('hidden', '');\n }\n });\n }\n\n const exportPdfBtn = document.getElementById('lp-export-pdf');\n if (exportPdfBtn) {\n const pdfLabel = exportPdfBtn.querySelector('.lp-editor-export-label');\n exportPdfBtn.addEventListener('click', async () => {\n if (window.location.protocol === 'file:') {\n alert('\u9759\u6001\u6587\u4EF6\u6A21\u5F0F\u4E0B\u65E0\u6CD5\u76F4\u63A5\u5BFC\u51FA PDF\u3002\\n\\n\u8BF7\u901A\u8FC7\u672C\u5730\u670D\u52A1\u5668\u8BBF\u95EE\u540E\u5BFC\u51FA\uFF1A\\n pnpm dev\\n\u6216\u8FD0\u884C\uFF1A\\n node scripts/export-pdf.mjs <goal.json> out.pdf');\n if (exportMenu) exportMenu.setAttribute('hidden', '');\n return;\n }\n if (pdfLabel) pdfLabel.textContent = '\u5BFC\u51FA\u4E2D...';\n exportPdfBtn.disabled = true;\n try {\n const res = await fetch('/api/export/pdf', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(goal),\n });\n if (!res.ok) throw new Error('\u5BFC\u51FA\u5931\u8D25: ' + res.status);\n const blob = await res.blob();\n const url = URL.createObjectURL(blob);\n const a = document.createElement('a');\n a.href = url;\n a.download = 'presentation.pdf';\n a.click();\n URL.revokeObjectURL(url);\n } catch (err) {\n alert(err instanceof Error ? err.message : String(err));\n } finally {\n if (pdfLabel) pdfLabel.textContent = 'PDF';\n exportPdfBtn.disabled = false;\n if (exportMenu) exportMenu.setAttribute('hidden', '');\n }\n });\n }\n\n // \u64A4\u9500 / \u91CD\u505A\n const undoBtn = document.getElementById('lp-undo');\n const redoBtn = document.getElementById('lp-redo');\n\n function updateUndoRedoButtons() {\n if (undoBtn) undoBtn.disabled = historyIndex <= 0;\n if (redoBtn) redoBtn.disabled = historyIndex >= history.length - 1;\n }\n\n if (undoBtn) undoBtn.addEventListener('click', undo);\n if (redoBtn) redoBtn.addEventListener('click', redo);\n\n function syncAppearanceFromGoal() {\n const t = goal.theme || 'theme01';\n if (t === 'theme04' || t === 'theme05' || t === 'theme06') {\n const scheme = goal.colorScheme || (t === 'theme05' ? 'coral' : t === 'theme06' ? 'volt' : 'green');\n const appearance = goal.appearance || 'dark';\n document.documentElement.setAttribute('data-theme', scheme);\n document.documentElement.setAttribute('data-appearance', appearance);\n } else if (t === 'theme03') {\n const scheme = goal.colorScheme || 'scheme-a';\n const appearance = goal.appearance || 'dark';\n document.documentElement.setAttribute('data-theme', scheme);\n document.documentElement.setAttribute('data-appearance', appearance);\n } else {\n const scheme = goal.colorScheme || (t === 'theme02' ? 'scheme-a' : 'light');\n document.documentElement.setAttribute('data-theme', scheme);\n document.documentElement.removeAttribute('data-appearance');\n }\n\n document.querySelectorAll('.lp-appearance-btn').forEach((btn) => {\n const appearanceValue = btn.getAttribute('data-appearance');\n const themeValue = btn.getAttribute('data-theme');\n if (!appearanceValue && !themeValue) return;\n\n let active = false;\n if (t === 'theme04' || t === 'theme05' || t === 'theme06') {\n if (themeValue) {\n active = themeValue === (goal.colorScheme || (t === 'theme05' ? 'coral' : t === 'theme06' ? 'volt' : 'green'));\n } else if (appearanceValue) {\n active = appearanceValue === (goal.appearance || 'dark');\n }\n } else if (t === 'theme03') {\n active = appearanceValue === (goal.appearance || 'dark');\n } else {\n active = appearanceValue === (goal.colorScheme || (t === 'theme02' ? 'scheme-a' : 'light'));\n }\n btn.classList.toggle('lp-appearance-active', active);\n btn.setAttribute('aria-pressed', String(active));\n });\n }\n\n function applyAppearanceClientSide(newMode) {\n const t = goal.theme || 'theme01';\n\n if (t === 'theme04' || t === 'theme05' || t === 'theme06') {\n if (newMode === goal.appearance) return;\n recordHistory();\n goal.appearance = newMode;\n autoSave();\n document.documentElement.setAttribute('data-appearance', newMode);\n } else if (t === 'theme03') {\n if (newMode === goal.appearance) return;\n recordHistory();\n goal.appearance = newMode;\n autoSave();\n document.documentElement.setAttribute('data-appearance', newMode);\n } else {\n if (newMode === goal.colorScheme) return;\n recordHistory();\n goal.colorScheme = newMode;\n autoSave();\n document.documentElement.setAttribute('data-theme', newMode);\n }\n\n syncAppearanceFromGoal();\n\n // \u5207\u6362\u5916\u89C2\u540E CSS \u53D8\u91CF\u503C\u53D8\u5316\uFF0C\u91CA\u653E\u5168\u90E8\u5B9E\u4F8B\u5E76\u91CD\u65B0\u521D\u59CB\u5316\u5F53\u524D active slide \u7684 ECharts\u3002\n if (typeof window.__lemonPPT_disposeECharts === 'function') {\n window.__lemonPPT_disposeECharts();\n }\n initActiveSlideECharts();\n }\n\n function applyToneClientSide(newTone) {\n const t = goal.theme || 'theme01';\n if (t !== 'theme04' && t !== 'theme05' && t !== 'theme06') return;\n if (newTone === goal.colorScheme) return;\n recordHistory();\n goal.colorScheme = newTone;\n autoSave();\n document.documentElement.setAttribute('data-theme', newTone);\n syncAppearanceFromGoal();\n if (typeof window.__lemonPPT_disposeECharts === 'function') {\n window.__lemonPPT_disposeECharts();\n }\n initActiveSlideECharts();\n }\n\n // \u5916\u89C2\u6A21\u5F0F\u5207\u6362\uFF08\u6D45\u8272 / \u6DF1\u8272 / \u4E3B\u9898 04 \u8272\u8C03\uFF09\n document.querySelectorAll('.lp-appearance-btn').forEach((btn) => {\n btn.addEventListener('click', () => {\n const appearance = btn.getAttribute('data-appearance');\n const tone = btn.getAttribute('data-theme');\n if (tone) applyToneClientSide(tone);\n if (appearance) applyAppearanceClientSide(appearance);\n });\n });\n\n // \u6DFB\u52A0\u5E7B\u706F\u7247\n function createDefaultSlide(layoutId, role) {\n const slide = { layout: layoutId, role, props: { title: '\u65B0\u5E7B\u706F\u7247' } };\n switch (role) {\n case 'cover': {\n if (layoutId === 'theme02_cover_v1') {\n slide.props = {\n kicker: '\u9713\u8679\u4E3B\u9898\u6F14\u793A',\n title: 'Neon Pitch',\n subtitle: '\u6DF1\u8272\u80CC\u666F + \u53CC\u914D\u8272\u65B9\u6848 + \u79D1\u6280\u611F\u7248\u5F0F',\n date: '2026.07'\n };\n } else {\n slide.props = { title: '\u5C01\u9762\u6807\u9898', subtitle: '\u526F\u6807\u9898' };\n }\n break;\n }\n case 'closing': {\n if (layoutId === 'theme01_conclusion_v1') {\n slide.props = {\n title: '\u6838\u5FC3\u7ED3\u8BBA',\n subtitle: 'CONCLUSION',\n points: [\n { item: 'AI \u6F14\u793A\u751F\u6210\u5DF2\u8FC8\u5165\u5B9E\u7528\u9636\u6BB5' },\n { item: '\u4E3B\u9898\u7CFB\u7EDF\u4E0E\u5E03\u5C40\u5F15\u64CE\u662F\u62A4\u57CE\u6CB3' },\n { item: '\u5F00\u653E\u751F\u6001\u5C06\u52A0\u901F\u884C\u4E1A\u843D\u5730' }\n ]\n };\n } else if (layoutId === 'theme02_closing_v1') {\n slide.props = {\n kicker: '\u611F\u8C22\u89C2\u770B',\n title: \"Let's Build Neon\",\n subtitle: '\u7528\u6DF1\u8272\u9713\u8679\u98CE\u683C\u70B9\u4EAE\u4E0B\u4E00\u573A\u6F14\u793A',\n cta: '\u5F00\u59CB\u4F7F\u7528',\n contact: '\u67E0\u6AAC\u56E2\u961F',\n email: 'hello@lemonppt.dev',\n link: 'lemonppt.dev'\n };\n } else {\n slide.props = {\n kicker: '\u611F\u8C22\u89C2\u770B',\n title: '\u671F\u5F85\u4E0E\u60A8\u5408\u4F5C',\n subtitle: '\u6709\u4EFB\u4F55\u95EE\u9898\uFF0C\u6B22\u8FCE\u968F\u65F6\u4EA4\u6D41',\n cta: '\u8054\u7CFB\u6211\u4EEC',\n contact: '\u67E0\u6AAC\u56E2\u961F',\n email: 'hello@lemonppt.dev',\n link: 'lemonppt.dev'\n };\n }\n break;\n }\n case 'quote': {\n if (layoutId === 'theme02_quote_v1') {\n slide.props = {\n quote: '\u8BBE\u8BA1\u7684\u672C\u8D28\u662F\u89E3\u51B3\u95EE\u9898\uFF0C\u800C\u597D\u7684\u8BBE\u8BA1\u662F\u8BA9\u590D\u6742\u53D8\u5F97\u7B80\u5355\u3002',\n author: '\u67E0\u6AAC\u56E2\u961F',\n role: '\u4EA7\u54C1\u7406\u5FF5'\n };\n } else {\n slide.props = {\n quote: '\u7B80\u6D01\u662F\u7EC8\u6781\u7684\u590D\u6742\u3002',\n author: '\u83B1\u6602\u7EB3\u591A\u00B7\u8FBE\u00B7\u82AC\u5947'\n };\n }\n break;\n }\n case 'content': {\n if (layoutId === 'theme01_risk_v1') {\n slide.props = {\n kicker: '\u98CE\u9669\u7814\u5224',\n title: '\u98CE\u9669\u6807\u9898',\n items: [\n { risk: '\u98CE\u9669 1', impact: '\u9AD8', response: '\u5E94\u5BF9\u7B56\u7565' },\n { risk: '\u98CE\u9669 2', impact: '\u4E2D', response: '\u5E94\u5BF9\u7B56\u7565' }\n ]\n };\n } else if (layoutId === 'theme01_outlook_v1') {\n slide.props = {\n kicker: '\u6295\u8D44\u5C55\u671B',\n title: '\u5C55\u671B\u6807\u9898',\n items: [\n { title: '\u8D8B\u52BF\u4E00', trend: '\u8D8B\u52BF\u5224\u65AD', action: '\u884C\u52A8\u5EFA\u8BAE' },\n { title: '\u8D8B\u52BF\u4E8C', trend: '\u8D8B\u52BF\u5224\u65AD', action: '\u884C\u52A8\u5EFA\u8BAE' }\n ]\n };\n } else if (layoutId === 'theme01_region_v1') {\n slide.props = {\n kicker: '\u5730\u533A\u5206\u5E03',\n title: '\u5E02\u573A\u5206\u5E03',\n regions: [\n { name: '\u5730\u533A\u4E00', value: '35%', change: '+2%', note: '\u8BF4\u660E\u6587\u5B57' },\n { name: '\u5730\u533A\u4E8C', value: '25%', change: '+1%', note: '\u8BF4\u660E\u6587\u5B57' }\n ]\n };\n } else if (layoutId === 'theme01_case_study') {\n slide.props = {\n kicker: '\u5178\u578B\u6848\u4F8B',\n title: '\u67E0\u6AAC\u79D1\u6280',\n subtitle: 'FROM CHALLENGER TO FRONTRUNNER',\n intro: '\u67E0\u6AAC\u79D1\u6280\u6210\u7ACB\u4E8E 2022 \u5E74\uFF0C\u4E13\u6CE8 AI \u6F14\u793A\u6587\u7A3F\u751F\u6210\u3002\u51ED\u501F\u81EA\u7814\u5E03\u5C40\u5F15\u64CE\u4E0E\u4E3B\u9898\u7CFB\u7EDF\uFF0C\u5728 18 \u4E2A\u6708\u5185\u5B8C\u6210\u4ECE 0 \u5230 1 \u7684\u8DE8\u8D8A\u3002',\n rounds: [\n { date: '2022\u00B706', round: '\u5929\u4F7F\u8F6E', valuation: '\u4F30\u503C \u00A51 \u4EBF', amount: '\u00A51,000\u4E07' },\n { date: '2023\u00B703', round: 'Pre-A', valuation: '\u4F30\u503C \u00A55 \u4EBF', amount: '\u00A55,000\u4E07' },\n { date: '2024\u00B701', round: 'A \u8F6E', valuation: '\u4F30\u503C \u00A520 \u4EBF', amount: '\u00A51.5\u4EBF' }\n ],\n quote: 'AI \u4E0D\u662F\u8981\u66FF\u4EE3\u4EBA\u505A PPT\uFF0C\u800C\u662F\u628A\u521B\u610F\u4ECE\u683C\u5F0F\u4E2D\u89E3\u653E\u51FA\u6765\u3002',\n quoteAuthor: '\u674E\u96F7\uFF0C\u8054\u5408\u521B\u59CB\u4EBA\u517C CEO',\n footnote: '\u6570\u636E\u6765\u6E90\uFF1A\u516C\u53F8\u516C\u5F00\u62AB\u9732\u4E0E\u5A92\u4F53\u6574\u7406\u3002'\n };\n } else if (layoutId === 'theme01_spotlight_grid') {\n slide.props = {\n kicker: '\u4E3B\u9898\u805A\u7126',\n title: '\u94B1\uFF0C\u6700\u7EC8\u7838\u5411\u4E86\u8FD9\u4E09\u4EF6\u4E8B',\n subtitle: '\u8D44\u91D1\u96C6\u4E2D\u6D41\u5411\u6A21\u578B\u80FD\u529B\u3001\u5E94\u7528\u573A\u666F\u4E0E\u57FA\u7840\u8BBE\u65BD',\n columns: [\n { tag: '\u6A21\u578B\u80FD\u529B', title: '\u901A\u7528\u5927\u6A21\u578B', description: '\u5934\u90E8\u5382\u5546\u6301\u7EED\u52A0\u7801\u57FA\u5EA7\u6A21\u578B\uFF0C\u89C4\u6A21\u6548\u5E94\u663E\u73B0\u3002' },\n { tag: '\u5E94\u7528\u573A\u666F', title: '\u5782\u76F4\u5E94\u7528', description: '\u529E\u516C\u3001\u8BBE\u8BA1\u3001\u7F16\u7A0B\u7B49\u573A\u666F\u7387\u5148\u8DD1\u51FA\u5546\u4E1A\u5316\u8DEF\u5F84\u3002' },\n { tag: '\u57FA\u7840\u8BBE\u65BD', title: 'AI \u57FA\u7840\u8BBE\u65BD', description: '\u7B97\u529B\u3001\u6570\u636E\u4E0E\u5DE5\u5177\u94FE\u6210\u4E3A\u652F\u6491\u4E0A\u5C42\u521B\u65B0\u7684\u5173\u952E\u5E95\u5EA7\u3002' }\n ]\n };\n } else if (layoutId === 'theme02_chapter_v1') {\n slide.props = {\n kicker: '\u7AE0\u8282',\n number: '01',\n title: '\u6838\u5FC3\u53D1\u73B0',\n subtitle: '\u4ECE\u6570\u636E\u5230\u6D1E\u5BDF\u7684\u5173\u952E\u8F6C\u6298'\n };\n } else if (layoutId === 'theme02_content_v1') {\n slide.props = {\n kicker: '\u8981\u70B9\u603B\u7ED3',\n title: '\u5185\u5BB9\u6807\u9898',\n subtitle: '\u7528\u9713\u8679 bullet \u5217\u8868\u7A81\u51FA\u5173\u952E\u4FE1\u606F',\n bullets: [\n '\u5168\u6808 AI \u6F14\u793A\u751F\u6210\uFF0C\u4ECE\u5927\u7EB2\u5230\u6210\u7A3F\u4E00\u952E\u5B8C\u6210',\n '\u53CC\u914D\u8272\u65B9\u6848\u81EA\u7531\u5207\u6362\uFF0C\u9002\u914D\u4E0D\u540C\u54C1\u724C\u8C03\u6027',\n 'SVG \u56FE\u8868 + \u6D1E\u5BDF\u9762\u677F\uFF0C\u6570\u636E\u8868\u8FBE\u66F4\u805A\u7126'\n ]\n };\n } else {\n slide.props = { title: '\u5185\u5BB9\u6807\u9898', points: ['\u65B0\u8981\u70B9'] };\n }\n break;\n }\n case 'chart': {\n if (layoutId === 'theme01_chart_treemap') {\n slide.props = {\n title: '\u56FE\u8868\u6807\u9898',\n kicker: '\u77E9\u5F62\u6811\u56FE',\n unit: '',\n data: [\n { name: '\u5206\u7C7B A', value: 400, children: [\n { name: 'A-1', value: 200 },\n { name: 'A-2', value: 150 },\n { name: 'A-3', value: 50 }\n ]},\n { name: '\u5206\u7C7B B', value: 300, children: [\n { name: 'B-1', value: 180 },\n { name: 'B-2', value: 120 }\n ]}\n ]\n };\n } else if (layoutId === 'theme01_chart_sankey') {\n slide.props = {\n title: '\u56FE\u8868\u6807\u9898',\n kicker: '\u6851\u57FA\u56FE',\n data: [\n { source: '\u8BBF\u95EE', target: '\u70B9\u51FB', value: 5000 },\n { source: '\u70B9\u51FB', target: '\u54A8\u8BE2', value: 3500 },\n { source: '\u54A8\u8BE2', target: '\u8BA2\u5355', value: 2200 },\n { source: '\u8BBF\u95EE', target: '\u8DF3\u51FA', value: 1500 },\n { source: '\u70B9\u51FB', target: '\u79BB\u5F00', value: 1000 },\n { source: '\u54A8\u8BE2', target: '\u6D41\u5931', value: 1300 }\n ]\n };\n } else if (layoutId === 'theme01_chart_sunburst') {\n slide.props = {\n title: '\u56FE\u8868\u6807\u9898',\n kicker: '\u65ED\u65E5\u56FE',\n data: [\n { name: '\u7EBF\u4E0A', value: 600, children: [\n { name: '\u641C\u7D22', value: 300 },\n { name: '\u793E\u4EA4', value: 200 },\n { name: '\u76F4\u8BBF', value: 100 }\n ]},\n { name: '\u7EBF\u4E0B', value: 400, children: [\n { name: '\u95E8\u5E97', value: 250 },\n { name: '\u4EE3\u7406', value: 150 }\n ]}\n ]\n };\n } else if (layoutId === 'theme01_chart_gauge') {\n slide.props = {\n title: '\u56FE\u8868\u6807\u9898',\n kicker: '\u4EEA\u8868\u76D8',\n value: 78,\n min: 0,\n max: 100,\n unit: '%'\n };\n } else if (layoutId === 'theme01_chart_heatmap') {\n slide.props = {\n title: '\u56FE\u8868\u6807\u9898',\n kicker: '\u70ED\u529B\u56FE',\n xAxis: ['A', 'B', 'C', 'D'],\n yAxis: ['W1', 'W2', 'W3'],\n data: [\n ['A', 'W1', 12], ['B', 'W1', 45], ['C', 'W1', 78], ['D', 'W1', 23],\n ['A', 'W2', 67], ['B', 'W2', 89], ['C', 'W2', 34], ['D', 'W2', 56],\n ['A', 'W3', 91], ['B', 'W3', 15], ['C', 'W3', 66], ['D', 'W3', 40]\n ]\n };\n } else if (layoutId === 'theme01_chart_funnel') {\n slide.props = {\n title: '\u56FE\u8868\u6807\u9898',\n kicker: '\u6F0F\u6597\u56FE',\n data: [\n { name: '\u8BBF\u95EE', value: 10000 },\n { name: '\u610F\u5411', value: 6500 },\n { name: '\u8BE2\u4EF7', value: 4200 },\n { name: '\u6210\u4EA4', value: 2800 }\n ]\n };\n } else if (layoutId === 'theme01_chart_radar') {\n slide.props = {\n title: '\u56FE\u8868\u6807\u9898',\n kicker: '\u96F7\u8FBE\u56FE',\n indicators: [\n { name: '\u6027\u80FD', max: 100 },\n { name: '\u7A33\u5B9A\u6027', max: 100 },\n { name: '\u6613\u7528\u6027', max: 100 },\n { name: '\u6269\u5C55\u6027', max: 100 },\n { name: '\u5B89\u5168\u6027', max: 100 }\n ],\n data: [\n { name: '\u5F53\u524D', value: [85, 90, 78, 88, 82] },\n { name: '\u76EE\u6807', value: [95, 95, 90, 92, 95] }\n ]\n };\n } else if (layoutId === 'theme01_chart_graph') {\n slide.props = {\n title: '\u56FE\u8868\u6807\u9898',\n kicker: '\u5173\u7CFB\u56FE',\n categories: [\n { name: '\u6838\u5FC3' },\n { name: '\u4EA7\u54C1' },\n { name: '\u6280\u672F' }\n ],\n nodes: [\n { name: '\u5E73\u53F0', value: 80, category: 0 },\n { name: '\u7528\u6237\u7AEF', value: 50, category: 1 },\n { name: '\u7BA1\u7406\u7AEF', value: 45, category: 1 },\n { name: '\u6570\u636E\u4E2D\u53F0', value: 60, category: 2 },\n { name: 'AI \u5F15\u64CE', value: 70, category: 2 },\n { name: '\u6E32\u67D3\u5668', value: 55, category: 2 },\n { name: '\u6A21\u677F\u5E93', value: 40, category: 1 }\n ],\n links: [\n { source: '\u5E73\u53F0', target: '\u7528\u6237\u7AEF', value: 5 },\n { source: '\u5E73\u53F0', target: '\u7BA1\u7406\u7AEF', value: 5 },\n { source: '\u5E73\u53F0', target: '\u6570\u636E\u4E2D\u53F0', value: 8 },\n { source: '\u6570\u636E\u4E2D\u53F0', target: 'AI \u5F15\u64CE', value: 6 },\n { source: 'AI \u5F15\u64CE', target: '\u6E32\u67D3\u5668', value: 4 },\n { source: '\u7BA1\u7406\u7AEF', target: '\u6A21\u677F\u5E93', value: 3 },\n { source: '\u7528\u6237\u7AEF', target: '\u6E32\u67D3\u5668', value: 4 }\n ]\n };\n } else if (layoutId === 'theme01_chart_bar3d') {\n slide.props = {\n title: '\u56FE\u8868\u6807\u9898',\n kicker: '3D \u67F1\u72B6\u56FE',\n labels: ['Q1', 'Q2', 'Q3', 'Q4'],\n data: [120, 200, 150, 80]\n };\n } else if (layoutId === 'theme01_chart_wordcloud') {\n slide.props = {\n title: '\u56FE\u8868\u6807\u9898',\n kicker: '\u8BCD\u4E91',\n words: [\n { name: 'AI', value: 100 },\n { name: 'PPT', value: 90 },\n { name: '\u6570\u636E\u53EF\u89C6\u5316', value: 85 },\n { name: '\u81EA\u52A8\u5316', value: 80 },\n { name: '\u5927\u5C4F', value: 75 },\n { name: '\u4E3B\u9898', value: 70 },\n { name: '\u6A21\u677F', value: 65 },\n { name: '\u667A\u80FD\u751F\u6210', value: 60 }\n ]\n };\n } else if (layoutId === 'theme01_chart_donut') {\n slide.props = {\n title: '\u8D5B\u9053\u5206\u5E03',\n kicker: '\u73AF\u5F62\u56FE',\n subtitle: '\u878D\u8D44\u989D\u5360\u6BD4',\n total: { value: '970 \u4EBF\u7F8E\u5143', label: '\u5168\u5E74\u5408\u8BA1' },\n segments: [\n { label: '\u901A\u7528\u5927\u6A21\u578B', labelEn: 'General LLM', value: '420', percent: '43.3%', color: 'var(--lp-blue)' },\n { label: '\u5782\u76F4\u5E94\u7528', labelEn: 'Vertical Apps', value: '245', percent: '25.3%', color: 'var(--lp-green)' },\n { label: 'AI \u57FA\u7840\u8BBE\u65BD', labelEn: 'AI Infra', value: '198', percent: '20.4%', color: 'var(--lp-amber)' },\n { label: 'AI \u82AF\u7247', labelEn: 'AI Chips', value: '97', percent: '10.0%', color: 'var(--lp-red)' },\n { label: '\u5176\u4ED6', labelEn: 'Others', value: '10', percent: '1.0%', color: 'var(--lp-violet)' }\n ]\n };\n } else if (layoutId === 'theme02_chart_funnel') {\n slide.props = {\n title: '\u8D44\u91D1\u96C6\u4E2D\u5EA6',\n kicker: '\u9713\u8679\u6F0F\u6597',\n subtitle: '\u8D44\u672C\u5982\u4F55\u5411\u5934\u90E8\u6536\u655B',\n data: [\n { name: '\u5168\u5E02\u573A', value: 970 },\n { name: '\u5934\u90E8 10 \u5BB6', value: 232 },\n { name: 'Top 3', value: 181 },\n { name: '\u5355\u7B14\u6700\u5927', value: 66 }\n ],\n showInsight: true,\n insight: {\n value: '24%',\n label: '\u5934\u90E8 10 \u5BB6 = \u5168\u5E02\u573A\u8FD1 1/4',\n description: '\u5C11\u6570\u72EC\u89D2\u517D\u53CD\u590D\u83B7\u5F97\u5DE8\u989D\u8FFD\u52A0\u6295\u8D44\uFF0C\u5934\u90E8\u683C\u5C40\u5DF2\u7136\u786E\u7ACB\u3002'\n },\n footnote: '\u6570\u636E\u6765\u6E90\uFF1AlemonPPT \u5185\u90E8\u7EDF\u8BA1 \u00B7 2026'\n };\n } else if (layoutId === 'theme02_chart_donut') {\n slide.props = {\n title: '\u8D5B\u9053\u5206\u5E03',\n kicker: '\u9713\u8679\u73AF\u5F62',\n subtitle: '\u878D\u8D44\u989D\u5360\u6BD4',\n total: { value: '970 \u4EBF\u7F8E\u5143', label: '\u5168\u5E74\u5408\u8BA1' },\n segments: [\n { label: '\u901A\u7528\u5927\u6A21\u578B', labelEn: 'General LLM', value: '420', percent: '43.3%', color: 'var(--lp-accent)' },\n { label: '\u5782\u76F4\u5E94\u7528', labelEn: 'Vertical Apps', value: '245', percent: '25.3%', color: 'var(--lp-accent-cool)' },\n { label: 'AI \u57FA\u7840\u8BBE\u65BD', labelEn: 'AI Infra', value: '198', percent: '20.4%', color: 'var(--lp-accent-2)' },\n { label: 'AI \u82AF\u7247', labelEn: 'AI Chips', value: '97', percent: '10.0%', color: 'var(--lp-violet)' },\n { label: '\u5176\u4ED6', labelEn: 'Others', value: '10', percent: '1.0%', color: 'var(--lp-cyan)' }\n ],\n footnote: '\u6570\u636E\u6765\u6E90\uFF1AlemonPPT \u5185\u90E8\u7EDF\u8BA1 \u00B7 2026',\n showInsight: true,\n insight: {\n value: '43.3%',\n label: '\u901A\u7528\u5927\u6A21\u578B\u5360\u6BD4\u6700\u9AD8',\n description: '\u901A\u7528\u5927\u6A21\u578B\u4E0E\u5782\u76F4\u5E94\u7528\u5408\u8BA1\u8D21\u732E\u8FD1\u4E03\u6210\u878D\u8D44\u989D\uFF0C\u8D5B\u9053\u5934\u90E8\u6548\u5E94\u660E\u663E\u3002'\n }\n };\n } else if (layoutId === 'theme02_chart_heatmap') {\n slide.props = {\n title: '\u7528\u6237\u6D3B\u8DC3\u5EA6\u70ED\u529B',\n kicker: '\u9713\u8679\u70ED\u529B',\n xAxis: ['\u5468\u4E00', '\u5468\u4E8C', '\u5468\u4E09', '\u5468\u56DB', '\u5468\u4E94'],\n yAxis: ['00:00', '06:00', '12:00', '18:00'],\n data: [\n ['\u5468\u4E00', '00:00', 120], ['\u5468\u4E00', '06:00', 80], ['\u5468\u4E00', '12:00', 320], ['\u5468\u4E00', '18:00', 280],\n ['\u5468\u4E8C', '00:00', 90], ['\u5468\u4E8C', '06:00', 60], ['\u5468\u4E8C', '12:00', 350], ['\u5468\u4E8C', '18:00', 300],\n ['\u5468\u4E09', '00:00', 100], ['\u5468\u4E09', '06:00', 70], ['\u5468\u4E09', '12:00', 380], ['\u5468\u4E09', '18:00', 340],\n ['\u5468\u56DB', '00:00', 110], ['\u5468\u56DB', '06:00', 75], ['\u5468\u56DB', '12:00', 360], ['\u5468\u56DB', '18:00', 310],\n ['\u5468\u4E94', '00:00', 130], ['\u5468\u4E94', '06:00', 85], ['\u5468\u4E94', '12:00', 400], ['\u5468\u4E94', '18:00', 360]\n ],\n showInsight: true,\n insight: {\n value: '400',\n label: '\u5468\u4E94 12:00 \u6D3B\u8DC3\u5EA6\u5CF0\u503C',\n description: '\u5DE5\u4F5C\u65E5\u5348\u95F4\u662F\u7528\u6237\u6D3B\u8DC3\u9AD8\u5CF0\uFF0C\u9002\u5408\u63A8\u9001\u5173\u952E\u5185\u5BB9\u4E0E\u8FD0\u8425\u6D3B\u52A8\u3002'\n }\n };\n } else if (layoutId === 'theme02_chart_radar') {\n slide.props = {\n title: '\u80FD\u529B\u96F7\u8FBE\u5BF9\u6BD4',\n kicker: '\u9713\u8679\u96F7\u8FBE',\n indicators: [\n { name: '\u6027\u80FD', max: 100 },\n { name: '\u7A33\u5B9A\u6027', max: 100 },\n { name: '\u6613\u7528\u6027', max: 100 },\n { name: '\u6269\u5C55\u6027', max: 100 },\n { name: '\u5B89\u5168\u6027', max: 100 }\n ],\n data: [\n { name: '\u5F53\u524D', value: [85, 90, 78, 88, 82] },\n { name: '\u76EE\u6807', value: [95, 95, 90, 92, 95] }\n ],\n showInsight: true,\n insight: {\n value: '84.6',\n label: '\u5F53\u524D\u7EFC\u5408\u80FD\u529B\u5747\u5206',\n description: '\u6027\u80FD\u4E0E\u7A33\u5B9A\u6027\u8868\u73B0\u7A81\u51FA\uFF0C\u6613\u7528\u6027\u4ECD\u6709\u63D0\u5347\u7A7A\u95F4\uFF0C\u5EFA\u8BAE\u4F18\u5148\u4F18\u5316\u4EA4\u4E92\u6D41\u7A0B\u3002'\n }\n };\n } else if (layoutId === 'theme02_chart_gauge') {\n slide.props = {\n title: '\u5E74\u5EA6\u76EE\u6807\u8FBE\u6210\u7387',\n kicker: '\u9713\u8679\u4EEA\u8868',\n value: 78,\n min: 0,\n max: 100,\n unit: '%',\n showInsight: true,\n insight: {\n value: '+22%',\n label: '\u8F83\u4E0A\u6708\u63D0\u5347',\n description: '\u5E74\u5EA6\u76EE\u6807\u5B8C\u6210\u7387\u5DF2\u63A5\u8FD1 80%\uFF0C\u6309\u5F53\u524D\u589E\u901F\u9884\u8BA1\u4E0B\u5B63\u5EA6\u53EF\u8D85\u989D\u8FBE\u6210\u3002'\n }\n };\n } else if (layoutId === 'theme02_chart_v1') {\n slide.props = {\n title: '\u5B63\u5EA6\u8425\u6536\u589E\u957F',\n kicker: '\u9713\u8679\u56FE\u8868',\n subtitle: '\u5168\u5E74\u56DB\u4E2A\u5B63\u5EA6\u6301\u7EED\u4E0A\u626C',\n type: 'bar',\n labels: ['Q1', 'Q2', 'Q3', 'Q4'],\n data: [1800, 2450, 3200, 4150],\n unit: '\u4E07\u5143',\n showInsight: true,\n insight: {\n headline: '11,600',\n subheadline: 'FULL-YEAR TOTAL',\n items: [\n { label: 'Q4 \u73AF\u6BD4', value: '+30%' },\n { label: '\u5E73\u5747\u5B63\u5EA6', value: '2,900' }\n ],\n badge: { text: 'Q4 \u589E\u957F\u6700\u5FEB', tone: 'accent' }\n }\n };\n } else {\n slide.props = { title: '\u56FE\u8868\u6807\u9898', labels: ['Q1', 'Q2', 'Q3', 'Q4'], data: [20, 45, 70, 95] };\n }\n break;\n }\n case 'bento': {\n if (layoutId === 'theme02_bento_v1') {\n slide.props = {\n kicker: 'Bento',\n title: '\u6A21\u5757\u5316\u6570\u636E\u5C55\u793A',\n subtitle: '\u7528\u4E0D\u89C4\u5219\u7F51\u683C\u7A81\u51FA\u5173\u952E\u6570\u5B57',\n items: [\n { label: '\u5E74\u5EA6\u8425\u6536', value: '1.2', unit: '\u4EBF\u5143', size: 'large' },\n { label: '\u7528\u6237\u589E\u957F', value: '+320', unit: '%', size: 'medium' },\n { label: 'NPS', value: '72', unit: '\u5206', size: 'small' },\n { label: '\u4ED8\u8D39\u5BA2\u6237', value: '1,200', unit: '+', size: 'small' },\n { label: '\u6A21\u677F\u6570', value: '150', unit: '+', size: 'small' }\n ]\n };\n } else {\n slide.props = {\n kicker: 'Bento',\n title: '\u6A21\u5757\u5316\u5C55\u793A',\n items: [\n { title: '\u6838\u5FC3\u6307\u6807', description: '\u5173\u952E\u6570\u636E\u4E00\u89C8', span: 'large' },\n { title: '\u7528\u6237\u589E\u957F', description: '\u6708\u6D3B\u8DC3\u7528\u6237\u6301\u7EED\u63D0\u5347', span: 'medium' },\n { title: '\u4EA7\u54C1\u7279\u6027', description: '\u6838\u5FC3\u80FD\u529B', span: 'small' },\n { title: '\u6280\u672F\u67B6\u6784', description: '\u7A33\u5B9A\u53EF\u9760', span: 'small' }\n ]\n };\n }\n break;\n }\n case 'gallery': {\n if (layoutId === 'theme01_filmstrip_v1') {\n slide.props = {\n kicker: '\u5F71\u50CF\u957F\u5377',\n title: '\u6545\u4E8B\u7EBF',\n images: [\n { url: '', caption: '\u9636\u6BB5\u4E00' },\n { url: '', caption: '\u9636\u6BB5\u4E8C' },\n { url: '', caption: '\u9636\u6BB5\u4E09' },\n { url: '', caption: '\u9636\u6BB5\u56DB' }\n ]\n };\n } else if (layoutId === 'theme02_gallery_v1') {\n slide.props = {\n kicker: '\u56FE\u96C6',\n title: '\u7CBE\u9009\u56FE\u7247',\n subtitle: '\u7528\u9713\u8679\u8FB9\u6846\u7F51\u683C\u5C55\u793A\u4EA7\u54C1\u573A\u666F',\n images: [\n { url: '', caption: '\u56FE\u7247\u8BF4\u660E 1' },\n { url: '', caption: '\u56FE\u7247\u8BF4\u660E 2' },\n { url: '', caption: '\u56FE\u7247\u8BF4\u660E 3' },\n { url: '', caption: '\u56FE\u7247\u8BF4\u660E 4' }\n ]\n };\n } else {\n slide.props = {\n kicker: '\u56FE\u96C6',\n title: '\u7CBE\u9009\u56FE\u7247',\n images: [\n { url: '', caption: '\u56FE\u7247\u8BF4\u660E 1' },\n { url: '', caption: '\u56FE\u7247\u8BF4\u660E 2' },\n { url: '', caption: '\u56FE\u7247\u8BF4\u660E 3' },\n { url: '', caption: '\u56FE\u7247\u8BF4\u660E 4' }\n ]\n };\n }\n break;\n }\n case 'table': {\n if (layoutId === 'theme01_table_data') {\n slide.props = {\n kicker: '\u6570\u636E\u8868',\n title: '\u5BF9\u6BD4\u4E00\u89C8',\n columns: [\n { key: 'rank', label: '\u6392\u540D', align: 'left' },\n { key: 'company', label: '\u516C\u53F8', align: 'left' },\n { key: 'track', label: '\u8D5B\u9053', align: 'left' },\n { key: 'amount', label: '\u878D\u8D44\u989D', align: 'right' }\n ],\n rows: [\n { rank: '1', company: '\u67E0\u6AAC\u79D1\u6280', track: 'SaaS', amount: '\u00A55,000\u4E07' },\n { rank: '2', company: '\u9752\u4E91\u6570\u636E', track: '\u4E91\u539F\u751F', amount: '\u00A53,200\u4E07' },\n { rank: '3', company: '\u7EA2\u661F\u667A\u80FD', track: 'AI \u786C\u4EF6', amount: '\u00A52,800\u4E07' },\n { rank: '4', company: '\u84DD\u6D77\u8D44\u672C', track: '\u91D1\u878D\u79D1\u6280', amount: '\u00A51,500\u4E07' }\n ],\n highlightRow: 0\n };\n } else if (layoutId === 'theme02_table_v1') {\n slide.props = {\n kicker: '\u6570\u636E\u8868',\n title: '\u65B9\u6848\u5BF9\u6BD4\u4E00\u89C8',\n subtitle: '\u591A\u7EF4\u5EA6\u8BC4\u4F30\uFF0C\u8F85\u52A9\u51B3\u7B56',\n headers: ['\u7EF4\u5EA6', '\u4F20\u7EDF\u65B9\u5F0F', '\u67E0\u6AAC PPT'],\n rows: [\n ['\u5236\u4F5C\u65F6\u95F4', '2-3 \u5929', '10 \u5206\u949F'],\n ['\u98CE\u683C\u4E00\u81F4\u6027', '\u4F9D\u8D56\u8BBE\u8BA1\u5E08', '\u4E3B\u9898\u7CFB\u7EDF\u4FDD\u8BC1'],\n ['\u6570\u636E\u66F4\u65B0', '\u624B\u52A8\u4FEE\u6539', '\u8054\u52A8\u5237\u65B0'],\n ['\u534F\u4F5C\u6548\u7387', '\u7248\u672C\u6DF7\u4E71', '\u5B9E\u65F6\u534F\u4F5C'],\n ['\u5BFC\u51FA\u683C\u5F0F', '\u5355\u4E00', 'PPTX / PDF / \u56FE\u7247']\n ],\n highlightFirstColumn: true\n };\n } else {\n slide.props = {\n kicker: '\u6570\u636E\u8868',\n title: '\u5BF9\u6BD4\u4E00\u89C8',\n headers: ['\u7EF4\u5EA6', '\u65B9\u6848 A', '\u65B9\u6848 B', '\u65B9\u6848 C'],\n rows: [\n ['\u6210\u672C', '\u4F4E', '\u4E2D', '\u9AD8'],\n ['\u6027\u80FD', '\u4E2D', '\u9AD8', '\u9AD8'],\n ['\u6613\u7528\u6027', '\u9AD8', '\u4E2D', '\u4F4E'],\n ['\u6269\u5C55\u6027', '\u4E2D', '\u9AD8', '\u9AD8']\n ],\n highlightFirstColumn: true\n };\n }\n break;\n }\n case 'tags': {\n if (layoutId === 'theme02_tags_v1') {\n slide.props = {\n kicker: '\u6807\u7B7E\u5899',\n title: '\u5173\u952E\u8BCD\u4E91',\n subtitle: '\u4E00\u773C\u770B\u61C2\u7528\u6237\u6700\u5173\u5FC3\u7684\u80FD\u529B',\n tags: [\n { label: 'AI', value: 98, tone: 'accent' },\n { label: '\u81EA\u52A8\u5316', value: 85, tone: 'positive' },\n { label: '\u53EF\u89C6\u5316', value: 72 },\n { label: '\u534F\u4F5C', value: 60 },\n { label: '\u4E91\u7AEF', value: 55 },\n { label: '\u6548\u7387', value: 80, tone: 'positive' },\n { label: '\u8BBE\u8BA1', value: 45 },\n { label: '\u54C1\u724C', value: 38 },\n { label: 'PPTX', value: 65, tone: 'accent' },\n { label: '\u667A\u80FD\u751F\u6210', value: 70 }\n ]\n };\n } else {\n slide.props = {\n kicker: '\u6807\u7B7E\u5899',\n title: '\u5173\u952E\u8BCD\u4E91',\n tags: [\n { label: 'AI', value: 98, tone: 'accent' },\n { label: '\u81EA\u52A8\u5316', value: 85, tone: 'positive' },\n { label: '\u53EF\u89C6\u5316', value: 72 },\n { label: '\u534F\u4F5C', value: 60 },\n { label: '\u4E91\u7AEF', value: 55 },\n { label: '\u6548\u7387', value: 80, tone: 'positive' },\n { label: '\u8BBE\u8BA1', value: 45 },\n { label: '\u54C1\u724C', value: 38 },\n { label: 'PPTX', value: 65, tone: 'accent' },\n { label: '\u667A\u80FD\u751F\u6210', value: 70 }\n ]\n };\n }\n break;\n }\n case 'process': {\n if (layoutId === 'theme02_process_v1') {\n slide.props = {\n kicker: '\u5B9E\u65BD\u8DEF\u5F84',\n title: '\u56DB\u6B65\u4E0A\u7EBF',\n subtitle: '\u4ECE\u9700\u6C42\u5230\u4EA4\u4ED8\u7684\u6807\u51C6\u5316\u6D41\u7A0B',\n steps: [\n { title: '\u9700\u6C42\u68B3\u7406', description: '\u660E\u786E\u53D7\u4F17\u3001\u76EE\u6807\u4E0E\u6838\u5FC3\u4FE1\u606F\u3002' },\n { title: 'AI \u751F\u6210', description: '\u8F93\u5165\u4E3B\u9898\uFF0C\u81EA\u52A8\u751F\u6210\u5927\u7EB2\u4E0E\u7248\u5F0F\u3002' },\n { title: '\u89C6\u89C9\u8C03\u4F18', description: '\u5207\u6362\u914D\u8272\u3001\u66FF\u6362\u56FE\u7247\u3001\u5FAE\u8C03\u6587\u6848\u3002' },\n { title: '\u5BFC\u51FA\u4EA4\u4ED8', description: '\u4E00\u952E\u5BFC\u51FA PPTX / PDF / \u56FE\u7247\u3002' }\n ]\n };\n } else {\n slide.props = { title: '\u6D41\u7A0B\u6807\u9898', steps: ['\u65B0\u6B65\u9AA4'] };\n }\n break;\n }\n case 'feature': {\n if (layoutId === 'theme02_feature_v1') {\n slide.props = {\n kicker: '\u4EA7\u54C1\u7279\u6027',\n title: '\u4E3A\u4EC0\u4E48\u9009\u62E9\u67E0\u6AAC PPT',\n subtitle: '\u4ECE\u751F\u6210\u5230\u4EA4\u4ED8\uFF0C\u6BCF\u4E2A\u73AF\u8282\u90FD\u66F4\u9AD8\u6548',\n features: [\n { title: 'AI \u4E00\u952E\u751F\u6210', description: '\u57FA\u4E8E\u5927\u6A21\u578B\u81EA\u52A8\u7406\u89E3\u9700\u6C42\uFF0C\u751F\u6210\u5B8C\u6574\u5927\u7EB2\u4E0E\u9875\u9762\u3002' },\n { title: '\u4E3B\u9898\u7CFB\u7EDF', description: '\u6DF1\u8272\u9713\u8679\u3001\u6D45\u8272\u73BB\u7483\u7B49\u591A\u79CD\u98CE\u683C\uFF0C\u5168\u5C40\u4E00\u952E\u5207\u6362\u3002' },\n { title: '\u6570\u636E\u53EF\u89C6\u5316', description: '\u5185\u7F6E SVG \u56FE\u8868\u4E0E\u6D1E\u5BDF\u9762\u677F\uFF0C\u6570\u636E\u8868\u8FBE\u66F4\u805A\u7126\u3002' }\n ]\n };\n } else {\n slide.props = { title: '\u7279\u6027\u6807\u9898', features: [{ title: '\u65B0\u7279\u6027', description: '' }] };\n }\n break;\n }\n case 'stats': {\n if (layoutId === 'theme02_metrics_v1') {\n slide.props = {\n kicker: '\u6570\u636E\u5899',\n title: '\u6838\u5FC3\u6307\u6807\u4E00\u89C8',\n subtitle: '\u7528\u9713\u8679\u5361\u7247\u5448\u73B0\u5173\u952E\u4E1A\u52A1\u6570\u636E',\n stats: [\n { label: '\u5E74\u5EA6\u8425\u6536', value: '1.2', unit: '\u4EBF\u5143', tone: 'accent' },\n { label: '\u7528\u6237\u589E\u957F', value: '+320', unit: '%', tone: 'cool' },\n { label: '\u5BA2\u6237\u6EE1\u610F\u5EA6', value: '98', unit: '%', tone: 'accent2' },\n { label: '\u56E2\u961F\u89C4\u6A21', value: '86', unit: '\u4EBA', tone: 'default' }\n ]\n };\n } else if (layoutId === 'theme02_stats_v1') {\n slide.props = {\n kicker: '\u6838\u5FC3\u6307\u6807',\n title: '\u589E\u957F\u6570\u636E\u4E00\u89C8',\n subtitle: '\u7528\u9713\u8679\u6570\u5B57\u5361\u7247\u7A81\u51FA\u5173\u952E\u6210\u679C',\n stats: [\n { label: '\u6CE8\u518C\u7528\u6237', value: '120K', unit: '+' },\n { label: '\u6708\u6D3B\u8DC3\u7528\u6237', value: '45K', unit: '+' },\n { label: '\u4ED8\u8D39\u8F6C\u5316\u7387', value: '8.5', unit: '%' },\n { label: '\u5BA2\u6237\u7EED\u8D39\u7387', value: '92', unit: '%' },\n { label: '\u6A21\u677F\u4F7F\u7528\u91CF', value: '2.4M', unit: '+' },\n { label: 'NPS \u8BC4\u5206', value: '72', unit: '' }\n ]\n };\n } else {\n slide.props = { title: '\u6570\u636E\u6807\u9898', stats: [{ label: '\u6307\u6807', value: 0, unit: '', change: '' }] };\n }\n break;\n }\n case 'metric': {\n if (layoutId === 'theme01_metric_big') {\n slide.props = {\n kicker: '\u6838\u5FC3\u6307\u6807',\n title: '\u5E74\u5EA6\u878D\u8D44\u603B\u989D',\n subtitle: '\u5168\u5E74\u5927\u989D\u878D\u8D44\u4E8B\u4EF6\u6C47\u603B',\n value: '970',\n unit: '\u4EBF\u7F8E\u5143',\n context: '\u521B\u5386\u53F2\u65B0\u9AD8\uFF0C\u5360\u5168\u7F8E\u98CE\u9669\u6295\u8D44\u8FD1\u4E09\u5206\u4E4B\u4E00',\n metrics: [\n { value: '97', label: '\u4E8B\u4EF6\u7B14\u6570', accent: false },\n { value: '\u224810 \u4EBF', label: '\u5E73\u5747\u5355\u7B14', accent: false },\n { value: '+41%', label: 'Q4 \u73AF\u6BD4', accent: true },\n { value: 'Q2-Q3', label: '\u9AD8\u5CF0\u533A\u95F4', accent: false }\n ]\n };\n } else if (layoutId === 'theme01_metric_triptych') {\n slide.props = {\n kicker: '\u6307\u6807\u603B\u89C8',\n title: '\u5173\u952E\u6570\u636E\u4E09\u89C6\u56FE',\n subtitle: '\u89C4\u6A21\u3001\u589E\u901F\u4E0E\u8D28\u91CF\u7684\u7EFC\u5408\u89C6\u89D2',\n panels: [\n { index: '01', title: '\u878D\u8D44\u603B\u989D', value: '970 \u4EBF', subtitle: '\u5168\u5E74\u7D2F\u8BA1\u89C4\u6A21\u521B\u65B0\u9AD8', chartType: 'bar', chartData: [120, 180, 320, 350] },\n { index: '02', title: '\u4E8B\u4EF6\u7B14\u6570', value: '97 \u7B14', subtitle: '\u5927\u989D\u4EA4\u6613\u6D3B\u8DC3\u5EA6\u63D0\u5347', chartType: 'line', chartData: [18, 22, 28, 29] },\n { index: '03', title: '\u5E73\u5747\u5355\u7B14', value: '\u224810 \u4EBF', subtitle: '\u5355\u7B14\u91D1\u989D\u6301\u7EED\u8D70\u9AD8', chartType: 'area', chartData: [6.7, 8.2, 11.4, 12.1] }\n ]\n };\n } else if (layoutId === 'theme02_metric_big') {\n slide.props = {\n kicker: '\u6838\u5FC3\u6307\u6807',\n title: '\u5E74\u5EA6\u878D\u8D44\u603B\u989D',\n subtitle: '\u5168\u5E74\u5927\u989D\u878D\u8D44\u4E8B\u4EF6\u6C47\u603B',\n value: '970',\n unit: '\u4EBF\u7F8E\u5143',\n context: '\u521B\u5386\u53F2\u65B0\u9AD8\uFF0C\u5360\u5168\u7F8E\u98CE\u9669\u6295\u8D44\u8FD1\u4E09\u5206\u4E4B\u4E00',\n metrics: [\n { value: '97', label: '\u4E8B\u4EF6\u7B14\u6570', accent: false },\n { value: '\u224810 \u4EBF', label: '\u5E73\u5747\u5355\u7B14', accent: false },\n { value: '+41%', label: 'Q4 \u73AF\u6BD4', accent: true },\n { value: 'Q2-Q3', label: '\u9AD8\u5CF0\u533A\u95F4', accent: false }\n ]\n };\n } else if (layoutId === 'theme02_progress_v1') {\n slide.props = {\n kicker: '\u8FBE\u6210\u5EA6',\n title: '\u5E74\u5EA6\u76EE\u6807\u5B8C\u6210\u8FDB\u5EA6',\n subtitle: '\u5173\u952E OKR \u5F53\u524D\u5B8C\u6210\u5EA6',\n items: [\n { label: 'ARR \u8425\u6536\u76EE\u6807', value: 8400, max: 12000, unit: '\u4E07\u5143' },\n { label: '\u4F01\u4E1A\u5BA2\u6237\u7B7E\u7EA6', value: 320, max: 500, unit: '\u5BB6' },\n { label: '\u4EA7\u54C1 NPS', value: 72, max: 100, unit: '\u5206' },\n { label: '\u56E2\u961F\u89C4\u6A21', value: 86, max: 120, unit: '\u4EBA' }\n ]\n };\n } else if (layoutId === 'theme02_delta_v1') {\n slide.props = {\n kicker: '\u4ECA\u6614\u5BF9\u7167',\n title: '\u5173\u952E\u6307\u6807\u53D8\u5316',\n subtitle: '\u53BB\u5E74\u540C\u671F vs \u5F53\u524D\u8868\u73B0',\n items: [\n { label: 'ARR \u8425\u6536', previous: 4200, current: 8400, unit: '\u4E07\u5143' },\n { label: '\u4F01\u4E1A\u5BA2\u6237', previous: 120, current: 320, unit: '\u5BB6' },\n { label: '\u4EA7\u54C1 NPS', previous: 58, current: 72, unit: '\u5206' },\n { label: '\u56E2\u961F\u89C4\u6A21', previous: 45, current: 86, unit: '\u4EBA' }\n ],\n footnote: '\u6570\u636E\u6765\u6E90\uFF1A\u516C\u53F8\u8D22\u52A1\u4E0E\u8FD0\u8425\u7CFB\u7EDF \u00B7 2026'\n };\n } else {\n slide.props = { title: '\u6307\u6807\u6807\u9898', metrics: [{ label: '\u6307\u6807', value: '0', unit: '', change: '' }] };\n }\n break;\n }\n case 'comparison': {\n if (layoutId === 'theme01_diptych_contrast') {\n slide.props = {\n kicker: '\u53CC\u8054\u5BF9\u6BD4',\n title: '\u6A21\u578B\u80FD\u529B vs \u5E94\u7528\u843D\u5730',\n left: { label: '\u6A21\u578B\u80FD\u529B', labelEn: 'FOUNDATION MODEL', imageUrl: '' },\n right: { label: '\u5E94\u7528\u843D\u5730', labelEn: 'APPLICATION', imageUrl: '' },\n centerCard: {\n title: '\u6838\u5FC3\u5DEE\u5F02',\n comparisons: [\n { leftValue: '66%', leftLabel: '\u8D44\u672C\u5360\u6BD4', rightValue: '25%', rightLabel: '\u8D44\u672C\u5360\u6BD4' },\n { leftValue: '420 \u4EBF', leftLabel: '\u878D\u8D44\u989D', rightValue: '245 \u4EBF', rightLabel: '\u878D\u8D44\u989D' },\n { leftValue: '\u5934\u90E8\u96C6\u4E2D', leftLabel: '\u7ADE\u4E89\u683C\u5C40', rightValue: '\u957F\u5C3E\u5206\u6563', rightLabel: '\u7ADE\u4E89\u683C\u5C40' }\n ],\n conclusion: '\u5E95\u5C42\u6A21\u578B\u4ECD\u662F\u8D44\u91D1\u4E3B\u6218\u573A\uFF0C\u4F46\u5782\u76F4\u5E94\u7528\u7684\u53D8\u73B0\u8DEF\u5F84\u66F4\u6E05\u6670\u3002'\n }\n };\n } else if (layoutId === 'theme02_comparison_v1') {\n slide.props = {\n kicker: '\u65B9\u6848\u5BF9\u6BD4',\n title: '\u4F20\u7EDF\u65B9\u5F0F vs \u67E0\u6AAC PPT',\n subtitle: '\u4ECE\u5C0F\u65F6\u7EA7\u5230\u5206\u949F\u7EA7\u7684\u6548\u7387\u8DC3\u8FC1',\n leftTitle: '\u4F20\u7EDF PPT \u5236\u4F5C',\n rightTitle: '\u67E0\u6AAC PPT \u751F\u6210',\n leftItems: ['\u624B\u52A8\u6392\u7248\u8017\u65F6\u4E45', '\u98CE\u683C\u96BE\u4EE5\u7EDF\u4E00', '\u534F\u4F5C\u7248\u672C\u6DF7\u4E71', '\u6570\u636E\u56FE\u8868\u66F4\u65B0\u6162'],\n rightItems: ['AI \u4E00\u952E\u751F\u6210\u7248\u5F0F', '\u4E3B\u9898\u7CFB\u7EDF\u5168\u5C40\u4E00\u81F4', '\u4E91\u7AEF\u5B9E\u65F6\u534F\u4F5C', '\u6570\u636E\u8054\u52A8\u81EA\u52A8\u5237\u65B0']\n };\n } else {\n slide.props = { title: '\u5BF9\u6BD4\u6807\u9898', left: '\u5DE6\u4FA7', right: '\u53F3\u4FA7' };\n }\n break;\n }\n case 'team': {\n if (layoutId === 'theme02_team_v1') {\n slide.props = {\n kicker: '\u6838\u5FC3\u56E2\u961F',\n title: '\u6211\u4EEC\u662F\u8C01',\n subtitle: '\u4E00\u652F\u76F8\u4FE1\u6280\u672F\u4E0E\u8BBE\u8BA1\u80FD\u6539\u53D8\u6F14\u793A\u7684\u56E2\u961F',\n members: [\n { name: '\u674E\u96F7', role: 'CEO', bio: '\u8FDE\u7EED\u521B\u4E1A\u8005\uFF0C\u524D\u5934\u90E8 SaaS \u4EA7\u54C1\u8D1F\u8D23\u4EBA\u3002' },\n { name: '\u97E9\u6885\u6885', role: 'CTO', bio: '\u5168\u6808\u5DE5\u7A0B\u5E08\uFF0C\u4E13\u6CE8 AI \u4E0E\u6E32\u67D3\u5F15\u64CE\u3002' },\n { name: '\u6797\u6D9B', role: '\u8BBE\u8BA1\u603B\u76D1', bio: '\u5341\u5E74\u54C1\u724C\u8BBE\u8BA1\u7ECF\u9A8C\uFF0C\u6DF1\u8015\u89C6\u89C9\u7CFB\u7EDF\u3002' },\n { name: '\u5409\u59C6', role: '\u589E\u957F\u8D1F\u8D23\u4EBA', bio: '\u6570\u636E\u9A71\u52A8\u589E\u957F\uFF0C\u64C5\u957F B2B \u89C4\u6A21\u5316\u83B7\u5BA2\u3002' }\n ]\n };\n } else {\n slide.props = { title: '\u56E2\u961F', members: [{ name: '\u65B0\u6210\u5458', role: '', bio: '', imageUrl: '' }] };\n }\n break;\n }\n case 'timeline': {\n if (layoutId === 'theme02_timeline_v1') {\n slide.props = {\n kicker: '\u91CC\u7A0B\u7891',\n title: '\u53D1\u5C55\u5386\u7A0B',\n subtitle: '\u4ECE\u60F3\u6CD5\u5230\u4EA7\u54C1\u7684\u5173\u952E\u8282\u70B9',\n milestones: [\n { date: '2022.06', title: '\u9879\u76EE\u542F\u52A8', description: '\u67E0\u6AAC PPT \u7ACB\u9879\uFF0C\u805A\u7126 AI \u6F14\u793A\u751F\u6210\u3002' },\n { date: '2023.03', title: '\u4EA7\u54C1\u4E0A\u7EBF', description: '\u9996\u4E2A\u7248\u672C\u53D1\u5E03\uFF0C\u652F\u6301\u4E00\u952E\u751F\u6210\u5B8C\u6574\u5927\u7EB2\u3002' },\n { date: '2024.01', title: '\u89C4\u6A21\u589E\u957F', description: '\u7D2F\u8BA1\u7528\u6237\u7A81\u7834 10 \u4E07\uFF0C\u4F01\u4E1A\u5BA2\u6237\u7B7E\u7EA6\u3002' },\n { date: '2025.09', title: '\u751F\u6001\u5F00\u653E', description: '\u5F00\u653E\u4E3B\u9898\u4E0E\u6A21\u677F\u5E02\u573A\uFF0C\u5171\u5EFA\u521B\u4F5C\u8005\u751F\u6001\u3002' }\n ]\n };\n } else {\n slide.props = { title: '\u65F6\u95F4\u7EBF', events: [{ date: '', title: '\u65B0\u91CC\u7A0B\u7891', description: '' }] };\n }\n break;\n }\n case 'faq': {\n if (layoutId === 'theme02_faq_v1') {\n slide.props = {\n kicker: '\u5E38\u89C1\u95EE\u9898',\n title: '\u4F60\u53EF\u80FD\u60F3\u77E5\u9053\u7684',\n subtitle: '\u5FEB\u901F\u4E86\u89E3\u67E0\u6AAC PPT \u7684\u6838\u5FC3\u80FD\u529B\u4E0E\u4F7F\u7528\u65B9\u5F0F',\n items: [\n { q: '\u67E0\u6AAC PPT \u9002\u5408\u4EC0\u4E48\u573A\u666F\uFF1F', a: '\u878D\u8D44\u8DEF\u6F14\u3001\u4EA7\u54C1\u53D1\u5E03\u3001\u5B63\u5EA6\u6C47\u62A5\u3001\u57F9\u8BAD\u8BFE\u4EF6\u7B49\u9700\u8981\u9AD8\u8D28\u91CF\u6F14\u793A\u7684\u573A\u5408\u3002' },\n { q: '\u751F\u6210\u540E\u80FD\u5426\u7EE7\u7EED\u7F16\u8F91\uFF1F', a: '\u53EF\u4EE5\u3002\u7F16\u8F91\u5668\u652F\u6301\u76F4\u63A5\u4FEE\u6539\u6587\u5B57\u3001\u66FF\u6362\u56FE\u7247\u3001\u8C03\u6574\u914D\u8272\u4E0E\u7248\u5F0F\u3002' },\n { q: '\u5BFC\u51FA\u683C\u5F0F\u6709\u54EA\u4E9B\uFF1F', a: '\u652F\u6301 PPTX\u3001PDF \u548C\u9AD8\u6E05\u56FE\u7247\u5BFC\u51FA\uFF0C\u6EE1\u8DB3\u4E0D\u540C\u5206\u53D1\u9700\u6C42\u3002' }\n ]\n };\n } else {\n slide.props = { title: '\u5E38\u89C1\u95EE\u9898', items: [{ q: '\u95EE\u9898', a: '\u56DE\u7B54' }] };\n }\n break;\n }\n case 'partners': {\n if (layoutId === 'theme02_partners_v1') {\n slide.props = {\n kicker: '\u5408\u4F5C\u4F19\u4F34',\n title: '\u4ED6\u4EEC\u90FD\u5728\u4F7F\u7528',\n subtitle: '\u4E0E\u9886\u5148\u4F01\u4E1A\u5171\u540C\u63A8\u52A8\u6F14\u793A\u65B9\u5F0F\u5347\u7EA7',\n partners: [\n { name: '\u4E91\u667A\u79D1\u6280', logoUrl: '' },\n { name: '\u672A\u6765\u8D44\u672C', logoUrl: '' },\n { name: '\u661F\u8FB0\u6570\u636E', logoUrl: '' },\n { name: '\u84DD\u6D77\u4F20\u5A92', logoUrl: '' },\n { name: '\u7EA2\u77F3\u54A8\u8BE2', logoUrl: '' },\n { name: '\u6781\u5149\u8BBE\u8BA1', logoUrl: '' },\n { name: '\u4E07\u8C61\u96C6\u56E2', logoUrl: '' },\n { name: '\u94F6\u6CB3\u521B\u6295', logoUrl: '' }\n ]\n };\n } else {\n slide.props = { title: '\u5408\u4F5C\u4F19\u4F34', partners: [{ name: '', logoUrl: '' }] };\n }\n break;\n }\n case 'roadmap': {\n if (layoutId === 'theme02_roadmap_v1') {\n slide.props = {\n kicker: '\u4EA7\u54C1\u8DEF\u7EBF',\n title: '\u672A\u6765 12 \u4E2A\u6708\u89C4\u5212',\n subtitle: '\u4ECE\u57FA\u7840\u80FD\u529B\u5230\u751F\u6001\u5F00\u653E\u7684\u6E05\u6670\u8282\u594F',\n phases: [\n { phase: 'Q1 \u57FA\u7840\u80FD\u529B', items: ['\u7F16\u8F91\u5668\u5185\u6838\u91CD\u6784', '\u4E3B\u9898\u7CFB\u7EDF 2.0', 'PPTX \u5BFC\u51FA\u4F18\u5316'] },\n { phase: 'Q2 \u667A\u80FD\u5347\u7EA7', items: ['AI \u591A\u8F6E\u5BF9\u8BDD\u751F\u6210', '\u81EA\u52A8\u914D\u56FE\u4E0E\u6570\u636E\u8054\u52A8', '\u534F\u4F5C\u8BC4\u8BBA'] },\n { phase: 'Q3 \u4F01\u4E1A\u573A\u666F', items: ['\u54C1\u724C\u8D44\u4EA7\u4E2D\u5FC3', '\u6743\u9650\u4E0E\u7248\u672C\u7BA1\u7406', '\u79C1\u6709\u90E8\u7F72'] },\n { phase: 'Q4 \u751F\u6001\u5F00\u653E', items: ['\u6A21\u677F\u5E02\u573A', '\u5F00\u53D1\u8005 SDK', '\u5F00\u653E\u4E3B\u9898\u534F\u8BAE'] }\n ]\n };\n } else {\n slide.props = { title: '\u8DEF\u7EBF\u56FE', phases: [{ phase: '\u9636\u6BB5', items: ['\u91CC\u7A0B\u7891'] }] };\n }\n break;\n }\n case 'pricing': {\n if (layoutId === 'theme02_pricing_v1') {\n slide.props = {\n kicker: '\u5B9A\u4EF7\u65B9\u6848',\n title: '\u9009\u62E9\u9002\u5408\u4F60\u7684\u65B9\u6848',\n subtitle: '\u4ECE\u4E2A\u4EBA\u521B\u4F5C\u8005\u5230\u4F01\u4E1A\u56E2\u961F\uFF0C\u7075\u6D3B\u6269\u5C55',\n tiers: [\n {\n name: '\u514D\u8D39\u7248',\n price: '\u00A50',\n period: '/ \u6708',\n features: ['\u6BCF\u6708 10 \u6B21\u751F\u6210', '3 \u79CD\u57FA\u7840\u4E3B\u9898', 'PNG \u5BFC\u51FA'],\n cta: '\u5F00\u59CB\u4F7F\u7528',\n highlight: false\n },\n {\n name: '\u4E13\u4E1A\u7248',\n price: '\u00A599',\n period: '/ \u6708',\n features: ['\u65E0\u9650\u6B21\u751F\u6210', '\u5168\u90E8\u4E3B\u9898\u4E0E\u7248\u5F0F', 'PPTX / PDF \u5BFC\u51FA', '\u4F18\u5148\u5BA2\u670D'],\n cta: '\u7ACB\u5373\u5347\u7EA7',\n highlight: true\n },\n {\n name: '\u56E2\u961F\u7248',\n price: '\u00A5399',\n period: '/ \u6708',\n features: ['5 \u4EBA\u534F\u4F5C', '\u54C1\u724C\u8D44\u4EA7\u4E2D\u5FC3', 'API \u63A5\u5165', '\u4E13\u5C5E\u5BA2\u6237\u6210\u529F'],\n cta: '\u8054\u7CFB\u9500\u552E',\n highlight: false\n }\n ]\n };\n } else {\n slide.props = { title: '\u5B9A\u4EF7', tiers: [{ name: '\u65B9\u6848', price: '', period: '', features: [''], cta: '' }] };\n }\n break;\n }\n case 'image': {\n if (layoutId === 'theme02_image_v1') {\n slide.props = {\n kicker: '\u89C6\u89C9\u5448\u73B0',\n title: '\u4E00\u5F20\u56FE\u8BB2\u6E05\u4EA7\u54C1\u4EF7\u503C',\n subtitle: '\u4E0A\u4F20\u9AD8\u6E05\u5927\u56FE\uFF0C\u7528\u9713\u8679\u6E10\u53D8\u53E0\u52A0\u6253\u9020\u79D1\u6280\u611F\u5C01\u9762\u3002',\n image: '',\n caption: '\u56FE\u6CE8\uFF1A\u4EA7\u54C1\u754C\u9762\u793A\u610F\u56FE'\n };\n } else {\n slide.props = { title: '\u56FE\u7247\u6807\u9898', subtitle: '\u526F\u6807\u9898' };\n }\n break;\n }\n case 'swot': {\n if (layoutId === 'theme02_swot_v1') {\n slide.props = {\n kicker: '\u6218\u7565\u5206\u6790',\n title: 'SWOT \u5206\u6790',\n subtitle: '\u770B\u6E05\u5185\u5916\u90E8\u73AF\u5883\uFF0C\u5236\u5B9A\u4E0B\u4E00\u6B65\u7B56\u7565',\n strength: 'AI \u751F\u6210\u80FD\u529B\u9886\u5148\uFF0C\u4E3B\u9898\u7CFB\u7EDF\u53EF\u6269\u5C55\uFF0C\u793E\u533A\u6D3B\u8DC3\u5EA6\u9AD8\u3002',\n weakness: '\u54C1\u724C\u77E5\u540D\u5EA6\u4ECD\u5728\u5EFA\u7ACB\uFF0C\u90E8\u5206\u9AD8\u7EA7\u7248\u5F0F\u4F9D\u8D56\u4EBA\u5DE5\u8BBE\u8BA1\u3002',\n opportunity: '\u4F01\u4E1A\u6570\u5B57\u5316\u6C47\u62A5\u9700\u6C42\u589E\u957F\uFF0CAI \u5DE5\u5177\u63A5\u53D7\u5EA6\u63D0\u5347\u3002',\n threat: '\u5927\u5382\u540C\u7C7B\u4EA7\u54C1\u5E03\u5C40\u52A0\u901F\uFF0C\u7528\u6237\u5BA1\u7F8E\u8981\u6C42\u6301\u7EED\u63D0\u9AD8\u3002'\n };\n } else {\n slide.props = { title: 'SWOT \u5206\u6790', strength: '\u4F18\u52BF', weakness: '\u52A3\u52BF', opportunity: '\u673A\u4F1A', threat: '\u5A01\u80C1' };\n }\n break;\n }\n case 'pest': {\n if (layoutId === 'theme02_pest_v1') {\n slide.props = {\n kicker: '\u5B8F\u89C2\u5206\u6790',\n title: 'PEST \u5206\u6790',\n subtitle: '\u4ECE\u653F\u7B56\u3001\u7ECF\u6D4E\u3001\u793E\u4F1A\u3001\u6280\u672F\u56DB\u7EF4\u5EA6\u6D1E\u5BDF\u73AF\u5883',\n political: 'AI \u5185\u5BB9\u751F\u6210\u76D1\u7BA1\u6846\u67B6\u9010\u6B65\u5B8C\u5584\uFF0C\u5408\u89C4\u5316\u6210\u4E3A\u4EA7\u54C1\u51C6\u5165\u95E8\u69DB\u3002',\n economic: '\u4F01\u4E1A\u964D\u672C\u589E\u6548\u9700\u6C42\u5F3A\u70C8\uFF0C\u6570\u5B57\u5316\u5DE5\u5177\u9884\u7B97\u5360\u6BD4\u6301\u7EED\u63D0\u5347\u3002',\n social: '\u8FDC\u7A0B\u534F\u4F5C\u5E38\u6001\u5316\uFF0C\u5BF9\u8F7B\u91CF\u5316\u3001\u53EF\u5206\u4EAB\u6F14\u793A\u5185\u5BB9\u7684\u9700\u6C42\u6FC0\u589E\u3002',\n technological: '\u5927\u6A21\u578B\u591A\u6A21\u6001\u80FD\u529B\u5FEB\u901F\u8FDB\u6B65\uFF0C\u6587\u672C\u5230\u5E7B\u706F\u7247\u7684\u751F\u6210\u8D28\u91CF\u663E\u8457\u63D0\u9AD8\u3002'\n };\n } else {\n slide.props = { title: 'PEST \u5206\u6790', political: '\u653F\u6CBB\u73AF\u5883', economic: '\u7ECF\u6D4E\u73AF\u5883', social: '\u793E\u4F1A\u73AF\u5883', technological: '\u6280\u672F\u73AF\u5883' };\n }\n break;\n }\n case 'testimonial': {\n if (layoutId === 'theme02_testimonial_v1') {\n slide.props = {\n kicker: '\u5BA2\u6237\u8BC4\u4EF7',\n quote: '\u67E0\u6AAC PPT \u5E2E\u52A9\u6211\u4EEC\u628A\u6C47\u62A5\u5236\u4F5C\u65F6\u95F4\u4ECE 2 \u5929\u7F29\u77ED\u5230 2 \u5C0F\u65F6\uFF0C\u56E2\u961F\u53EF\u4EE5\u628A\u66F4\u591A\u7CBE\u529B\u653E\u5728\u4E1A\u52A1\u601D\u8003\u4E0A\u3002',\n author: '\u9648\u6653\u660E',\n role: '\u4EA7\u54C1\u603B\u76D1',\n company: '\u672A\u6765\u79D1\u6280\u6709\u9650\u516C\u53F8',\n avatarUrl: ''\n };\n } else {\n slide.props = { kicker: '\u5BA2\u6237\u8BC4\u4EF7', quote: '\u4EA7\u54C1\u5E2E\u52A9\u6211\u4EEC\u628A\u6C47\u62A5\u6548\u7387\u63D0\u5347\u4E86 3 \u500D\u3002', author: '\u5F20\u4E09', role: '\u4EA7\u54C1\u7ECF\u7406', company: '\u793A\u4F8B\u79D1\u6280' };\n }\n break;\n }\n case 'tableOfContents': {\n if (layoutId === 'theme02_table_of_contents_v1') {\n slide.props = {\n title: '\u76EE\u5F55',\n subtitle: 'CONTENTS',\n items: [\n { title: '\u5E02\u573A\u80CC\u666F\u4E0E\u673A\u4F1A', page: '02' },\n { title: '\u4EA7\u54C1\u65B9\u6848\u4E0E\u4F18\u52BF', page: '04' },\n { title: '\u5546\u4E1A\u6A21\u5F0F\u4E0E\u589E\u957F', page: '06' },\n { title: '\u56E2\u961F\u4E0E\u878D\u8D44\u8BA1\u5212', page: '08' }\n ]\n };\n } else {\n slide.props = { title: '\u76EE\u5F55', items: ['\u76EE\u5F55\u9879'] };\n }\n break;\n }\n }\n return slide;\n }\n\n const addSlideBtn = document.getElementById('lp-add-slide');\n const addSlideModal = document.getElementById('lp-add-slide-modal');\n const addSlideConfirm = document.getElementById('lp-add-slide-confirm');\n let selectedLayoutId = null;\n let selectedRole = null;\n\n function closeAddSlideModal() {\n if (addSlideModal) addSlideModal.setAttribute('hidden', '');\n addSlideModal?.querySelectorAll('.lp-add-slide-option').forEach((el) => el.classList.remove('selected'));\n selectedLayoutId = null;\n selectedRole = null;\n if (addSlideConfirm) addSlideConfirm.disabled = true;\n }\n\n function openAddSlideModal() {\n if (addSlideModal) addSlideModal.removeAttribute('hidden');\n }\n\n function confirmAddSlide() {\n // #region debug-point C:add-slide\n __dbg('C', 'editor-script.ts:confirmAddSlide', 'start', { selectedLayoutId, selectedRole, slidesLength: goal.slides.length });\n // #endregion\n if (!selectedLayoutId) return;\n const newSlide = createDefaultSlide(selectedLayoutId, selectedRole || selectedLayoutId.split('_')[0]);\n recordHistory();\n goal.slides.push(newSlide);\n selectedSlideIdx = goal.slides.length - 1;\n // current \u4F1A\u5728 rebuildSlidesAndThumbnails \u4E2D\u540C\u6B65\uFF0C\u907F\u514D\u5728 DOM \u91CD\u5EFA\u524D\u8BBF\u95EE\u8D8A\u754C\n // #region debug-point C:add-slide\n __dbg('C', 'editor-script.ts:confirmAddSlide', 'pushed', { newIndex: selectedSlideIdx, newSlideLayout: newSlide.layout, newSlideProps: JSON.parse(JSON.stringify(newSlide.props)) });\n // #endregion\n autoSave();\n closeAddSlideModal();\n reloadEditor();\n }\n\n if (addSlideBtn) {\n addSlideBtn.addEventListener('click', openAddSlideModal);\n }\n\n if (addSlideModal) {\n addSlideModal.addEventListener('click', (e) => {\n const target = e.target;\n if (target.closest('[data-close-modal]')) {\n closeAddSlideModal();\n return;\n }\n const option = target.closest('.lp-add-slide-option');\n if (option) {\n addSlideModal.querySelectorAll('.lp-add-slide-option').forEach((el) => el.classList.remove('selected'));\n option.classList.add('selected');\n selectedLayoutId = option.getAttribute('data-layout');\n selectedRole = option.getAttribute('data-role');\n if (addSlideConfirm) addSlideConfirm.disabled = false;\n }\n });\n }\n\n if (addSlideConfirm) {\n addSlideConfirm.addEventListener('click', confirmAddSlide);\n }\n\n updateUndoRedoButtons();\n\n // \u7FFB\u9875\u811A\u672C\n let slides = Array.from(document.querySelectorAll('.lp-slide-wrapper'));\n let thumbnails = Array.from(document.querySelectorAll('.lp-thumbnail'));\n const prevBtn = document.getElementById('lp-prev');\n const nextBtn = document.getElementById('lp-next');\n const currentLabel = document.getElementById('lp-current');\n const deck = document.querySelector('.lp-deck');\n let current = 0;\n let isTransitioning = false;\n let cleanupTimer = null;\n let dragSrcIndex = null;\n let dragDropIndicator = null;\n\n function refreshSlidesAndThumbnails() {\n slides = Array.from(document.querySelectorAll('.lp-slide-wrapper'));\n thumbnails = Array.from(document.querySelectorAll('.lp-thumbnail'));\n }\n\n function updateEditorUI() {\n thumbnails.forEach((thumb, index) => {\n thumb.classList.toggle('active', index === current);\n });\n const activeThumb = thumbnails[current];\n if (activeThumb) activeThumb.scrollIntoView({ block: 'nearest', behavior: 'smooth' });\n const labelEl = document.getElementById('lp-current');\n if (labelEl) labelEl.textContent = String(current + 1);\n if (prevBtn) prevBtn.disabled = current === 0 || isTransitioning;\n if (nextBtn) nextBtn.disabled = current === slides.length - 1 || isTransitioning;\n }\n\n function resetTransitionState() {\n slides.forEach((slide) => slide.classList.remove('enter', 'leave'));\n if (deck) {\n deck.setAttribute('data-lp-transition', 'none');\n deck.removeAttribute('data-lp-direction');\n }\n isTransitioning = false;\n updateClasses();\n }\n\n function updateClasses() {\n // #region debug-point B:update-classes\n const activeBefore = Array.from(slides).map((s, i) => ({ i, active: s.classList.contains('active'), prev: s.classList.contains('prev') }));\n slides.forEach((slide, index) => {\n slide.classList.remove('active', 'prev', 'enter', 'leave');\n if (index === current) slide.classList.add('active');\n else if (index < current) slide.classList.add('prev');\n });\n const activeAfter = Array.from(slides).map((s, i) => ({ i, active: s.classList.contains('active'), prev: s.classList.contains('prev'), layout: s.dataset.layout }));\n const computed = Array.from(slides).map((s, i) => {\n const st = window.getComputedStyle(s);\n return { i, opacity: st.opacity, transform: st.transform, zIndex: st.zIndex, display: st.display, visibility: st.visibility };\n });\n __dbg('B', 'editor-script.ts:updateClasses', 'classes updated', { current, activeBefore, activeAfter, computed });\n // #endregion\n updateEditorUI();\n }\n\n function goTo(index) {\n // #region debug-point A:goto-entry\n __dbg('A', 'editor-script.ts:goTo', 'goTo called', { from: current, to: index, slidesLength: slides.length, isTransitioning, deckExists: !!deck });\n // #endregion\n if (index === current || index < 0 || index >= slides.length || isTransitioning) {\n // #region debug-point A:goto-bail\n __dbg('A', 'editor-script.ts:goTo', 'goTo bailed', { reason: index === current ? 'same' : index < 0 ? 'negative' : index >= slides.length ? 'overflow' : 'transitioning', current, index, slidesLength: slides.length });\n // #endregion\n return;\n }\n const leaving = slides[current];\n const entering = slides[index];\n const transition = entering.dataset.lpTransition || 'none';\n const direction = index > current ? 'next' : 'prev';\n\n if (transition === 'none') {\n current = index;\n if (deck) {\n deck.setAttribute('data-lp-transition', 'none');\n deck.removeAttribute('data-lp-direction');\n }\n leaving.classList.remove('active');\n entering.classList.add('active');\n updateClasses();\n if (typeof selectSlide === 'function') selectSlide(current);\n // \u5207\u9875\u540E\u91CA\u653E\u79BB\u5F00 slide \u7684\u56FE\u8868\u5E76\u4EC5\u521D\u59CB\u5316\u65B0 active slide \u7684 ECharts\u3002\n requestAnimationFrame(() => {\n disposeEChartsInWrapper(leaving);\n initActiveSlideECharts();\n });\n return;\n }\n\n isTransitioning = true;\n current = index;\n updateEditorUI();\n\n if (deck) {\n deck.setAttribute('data-lp-transition', transition);\n deck.setAttribute('data-lp-direction', direction);\n }\n\n leaving.classList.add('leave');\n entering.classList.add('enter');\n // \u5148\u7981\u7528 transition\uFF0C\u8BA9 leave/enter \u7684\u521D\u59CB\u72B6\u6001\u76F4\u63A5\u843D\u4F4D\uFF0C\n // \u907F\u514D\u4ECE\u4E0A\u4E00\u72B6\u6001\u7684 translateX \u63D2\u503C\uFF1B\u968F\u540E\u6062\u590D transition \u518D\u5207\u6362 active\u3002\n leaving.style.transition = 'none';\n entering.style.transition = 'none';\n void entering.offsetWidth;\n leaving.style.transition = '';\n entering.style.transition = '';\n leaving.classList.remove('active');\n entering.classList.add('active');\n\n function finish() {\n entering.removeEventListener('transitionend', finish);\n clearTimeout(cleanupTimer);\n resetTransitionState();\n // \u52A8\u753B\u7ED3\u675F\u540E\u91CA\u653E\u79BB\u5F00 slide \u7684\u56FE\u8868\uFF0C\u5E76\u4EC5\u5BF9\u5F53\u524D active slide \u7684 ECharts \u8FDB\u884C resize/\u521D\u59CB\u5316\n requestAnimationFrame(() => {\n disposeEChartsInWrapper(leaving);\n initActiveSlideECharts();\n });\n }\n entering.addEventListener('transitionend', finish);\n cleanupTimer = setTimeout(finish, 600);\n\n if (typeof selectSlide === 'function') selectSlide(current);\n }\n\n if (prevBtn) prevBtn.addEventListener('click', () => goTo(current - 1));\n if (nextBtn) nextBtn.addEventListener('click', () => goTo(current + 1));\n const deleteConfirmToast = document.getElementById('lp-delete-confirm-toast');\n const deleteConfirmText = document.getElementById('lp-delete-confirm-text');\n const deleteConfirmCancel = document.getElementById('lp-delete-confirm-cancel');\n const deleteConfirmConfirm = document.getElementById('lp-delete-confirm-confirm');\n let pendingDeleteIndex = null;\n\n function showDeleteConfirm(index, triggerEl) {\n if (goal.slides.length <= 1) {\n alert('\u81F3\u5C11\u4FDD\u7559\u4E00\u9875\u5E7B\u706F\u7247\u3002');\n return;\n }\n pendingDeleteIndex = index;\n if (deleteConfirmText) {\n deleteConfirmText.textContent = '\u786E\u5B9A\u5220\u9664\u7B2C ' + (index + 1) + ' \u9875\u5E7B\u706F\u7247\uFF1F';\n }\n if (deleteConfirmToast) {\n deleteConfirmToast.removeAttribute('hidden');\n positionDeleteConfirmToast(triggerEl);\n }\n }\n\n function positionDeleteConfirmToast(triggerEl) {\n if (!deleteConfirmToast || !triggerEl) return;\n const rect = triggerEl.getBoundingClientRect();\n const toastRect = deleteConfirmToast.getBoundingClientRect();\n const viewportWidth = window.innerWidth;\n const viewportHeight = window.innerHeight;\n const gap = 8;\n let top = rect.bottom + gap;\n let left = rect.left + rect.width / 2 - toastRect.width / 2;\n if (left < gap) left = gap;\n if (left + toastRect.width > viewportWidth - gap) {\n left = viewportWidth - toastRect.width - gap;\n }\n if (top + toastRect.height > viewportHeight - gap) {\n top = rect.top - toastRect.height - gap;\n }\n deleteConfirmToast.style.top = top + 'px';\n deleteConfirmToast.style.left = left + 'px';\n }\n\n function hideDeleteConfirm() {\n pendingDeleteIndex = null;\n if (deleteConfirmToast) {\n deleteConfirmToast.setAttribute('hidden', '');\n deleteConfirmToast.style.top = '';\n deleteConfirmToast.style.left = '';\n }\n }\n\n function confirmDelete() {\n if (pendingDeleteIndex == null) return;\n const index = pendingDeleteIndex;\n hideDeleteConfirm();\n recordHistory();\n goal.slides.splice(index, 1);\n selectedSlideIdx = Math.min(index, goal.slides.length - 1);\n // current \u4F1A\u5728 rebuildSlidesAndThumbnails \u4E2D\u540C\u6B65\n autoSave();\n reloadEditor();\n }\n\n if (deleteConfirmCancel) {\n deleteConfirmCancel.addEventListener('click', hideDeleteConfirm);\n }\n if (deleteConfirmConfirm) {\n deleteConfirmConfirm.addEventListener('click', confirmDelete);\n }\n if (deleteConfirmToast) {\n deleteConfirmToast.addEventListener('click', (e) => {\n if (e.target === deleteConfirmToast) {\n hideDeleteConfirm();\n }\n });\n }\n\n function attachThumbnailListeners() {\n thumbnails.forEach((thumb) => {\n thumb.addEventListener('click', (e) => {\n const deleteBtn = e.target.closest && e.target.closest('.lp-thumbnail-delete');\n if (deleteBtn) {\n e.stopPropagation();\n const index = Number(deleteBtn.getAttribute('data-index'));\n showDeleteConfirm(index, deleteBtn);\n return;\n }\n goTo(Number(thumb.dataset.index));\n });\n\n // \u7F29\u7565\u56FE\u6539\u4E3A div \u540E\uFF0C\u8865\u5145\u952E\u76D8\u9009\u4E2D\u652F\u6301\n thumb.addEventListener('keydown', (e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n goTo(Number(thumb.dataset.index));\n }\n });\n });\n }\n attachThumbnailListeners();\n\n // \u7F29\u7565\u56FE\u865A\u62DF\u6EDA\u52A8\uFF1A\u7528 IntersectionObserver \u5378\u8F7D\u89C6\u53E3\u5916 slide \u6E32\u67D3 DOM\uFF0C\u4EC5\u4FDD\u7559\u5360\u4F4D\u6846\u67B6\u4E0E\u6587\u5B57\u4FE1\u606F\u3002\n const thumbnailRenderCache = {};\n let thumbnailObserver = null;\n\n function getThumbnailCacheKey(thumb) {\n return 'thumb-' + thumb.dataset.index;\n }\n\n function mountThumbnailRender(thumb) {\n const render = thumb.querySelector('.lp-thumbnail-render');\n if (!render) return;\n if (thumb.dataset.lpThumbMounted === 'true') return;\n const key = getThumbnailCacheKey(thumb);\n const html = thumbnailRenderCache[key];\n if (html != null) {\n render.innerHTML = html;\n }\n thumb.dataset.lpThumbMounted = 'true';\n }\n\n function unmountThumbnailRender(thumb) {\n const render = thumb.querySelector('.lp-thumbnail-render');\n if (!render) return;\n if (thumb.dataset.lpThumbMounted === 'false') return;\n const key = getThumbnailCacheKey(thumb);\n thumbnailRenderCache[key] = render.innerHTML;\n render.innerHTML = '';\n thumb.dataset.lpThumbMounted = 'false';\n }\n\n function initThumbnailVirtualScroll() {\n const container = document.querySelector('.lp-editor-left-panel');\n if (!container || !('IntersectionObserver' in window)) return;\n\n if (thumbnailObserver) {\n thumbnailObserver.disconnect();\n thumbnailObserver = null;\n }\n\n // \u9ED8\u8BA4\u6240\u6709\u7F29\u7565\u56FE\u5904\u4E8E\u6302\u8F7D\u72B6\u6001\n thumbnails.forEach((thumb) => {\n thumb.dataset.lpThumbMounted = 'true';\n });\n\n thumbnailObserver = new IntersectionObserver((entries) => {\n entries.forEach((entry) => {\n const thumb = entry.target;\n const index = Number(thumb.dataset.index);\n // \u5F53\u524D\u6FC0\u6D3B\u9875\u59CB\u7EC8\u4FDD\u7559\u6E32\u67D3\uFF0C\u907F\u514D\u5207\u6362\u65F6\u767D\u5C4F\n if (index === current || entry.isIntersecting) {\n mountThumbnailRender(thumb);\n } else {\n unmountThumbnailRender(thumb);\n }\n });\n }, {\n root: container,\n rootMargin: '200px 0px 200px 0px',\n threshold: 0,\n });\n\n thumbnails.forEach((thumb) => thumbnailObserver.observe(thumb));\n }\n\n function disconnectThumbnailVirtualScroll() {\n if (thumbnailObserver) {\n thumbnailObserver.disconnect();\n thumbnailObserver = null;\n }\n }\n initThumbnailVirtualScroll();\n\n function attachDragAndDropListeners() {\n const container = document.querySelector('.lp-editor-thumbnails');\n if (!container) return;\n\n function getOrCreateDropIndicator() {\n if (dragDropIndicator && dragDropIndicator.parentElement) return dragDropIndicator;\n dragDropIndicator = document.createElement('div');\n dragDropIndicator.className = 'lp-thumbnail-drop-indicator';\n container.appendChild(dragDropIndicator);\n return dragDropIndicator;\n }\n\n function clearDragState() {\n dragSrcIndex = null;\n thumbnails.forEach((t) => t.classList.remove('dragging', 'drag-over'));\n if (dragDropIndicator) dragDropIndicator.classList.remove('visible');\n }\n\n function positionDropIndicator(target, before) {\n const indicator = getOrCreateDropIndicator();\n if (!indicator || !target) return;\n const rect = target.getBoundingClientRect();\n const containerRect = container.getBoundingClientRect();\n const top = before ? rect.top - containerRect.top - 1 : rect.bottom - containerRect.top - 1;\n indicator.style.top = top + 'px';\n indicator.classList.add('visible');\n }\n\n container.addEventListener('dragstart', (e) => {\n const thumb = e.target && e.target.closest && e.target.closest('.lp-thumbnail');\n if (!thumb) return;\n // \u70B9\u51FB\u5220\u9664\u6309\u94AE\u65F6\u4E0D\u5E94\u89E6\u53D1\u62D6\u62FD\n const deleteBtn = e.target.closest && e.target.closest('.lp-thumbnail-delete');\n if (deleteBtn) {\n e.preventDefault();\n return;\n }\n if (isTransitioning || goal.slides.length <= 1) {\n e.preventDefault();\n return;\n }\n const src = Number(thumb.dataset.index);\n if (Number.isNaN(src)) return;\n dragSrcIndex = src;\n hideDeleteConfirm();\n thumb.classList.add('dragging');\n if (e.dataTransfer) {\n e.dataTransfer.effectAllowed = 'move';\n e.dataTransfer.setData('text/plain', String(src));\n try {\n e.dataTransfer.setDragImage(thumb, 16, 16);\n } catch (_) {}\n }\n });\n\n container.addEventListener('dragover', (e) => {\n if (dragSrcIndex == null) return;\n e.preventDefault();\n if (e.dataTransfer) e.dataTransfer.dropEffect = 'move';\n const targetThumb = e.target && e.target.closest && e.target.closest('.lp-thumbnail');\n const indicator = getOrCreateDropIndicator();\n if (!targetThumb) {\n const lastThumb = container.querySelector('.lp-thumbnail:last-child');\n if (lastThumb && dragSrcIndex !== goal.slides.length - 1) {\n positionDropIndicator(lastThumb, false);\n } else {\n indicator.classList.remove('visible');\n }\n thumbnails.forEach((t) => t.classList.remove('drag-over'));\n return;\n }\n const targetIndex = Number(targetThumb.dataset.index);\n if (Number.isNaN(targetIndex)) return;\n const rect = targetThumb.getBoundingClientRect();\n const before = e.clientY < rect.top + rect.height / 2;\n if (targetIndex === dragSrcIndex) {\n indicator.classList.remove('visible');\n targetThumb.classList.remove('drag-over');\n return;\n }\n thumbnails.forEach((t) => t.classList.remove('drag-over'));\n targetThumb.classList.add('drag-over');\n positionDropIndicator(targetThumb, before);\n });\n\n container.addEventListener('dragleave', (e) => {\n if (dragSrcIndex == null) return;\n if (!container.contains(e.relatedTarget)) {\n if (dragDropIndicator) dragDropIndicator.classList.remove('visible');\n thumbnails.forEach((t) => t.classList.remove('drag-over'));\n }\n });\n\n container.addEventListener('drop', (e) => {\n if (dragSrcIndex == null) return;\n e.preventDefault();\n const src = dragSrcIndex;\n const targetThumb = e.target && e.target.closest && e.target.closest('.lp-thumbnail');\n let targetIndex = 0;\n let insertBefore = true;\n if (targetThumb) {\n targetIndex = Number(targetThumb.dataset.index);\n const rect = targetThumb.getBoundingClientRect();\n insertBefore = e.clientY < rect.top + rect.height / 2;\n } else {\n targetIndex = goal.slides.length - 1;\n insertBefore = false;\n }\n if (Number.isNaN(targetIndex) || src === targetIndex) {\n clearDragState();\n return;\n }\n let insertAt = targetIndex;\n if (insertBefore) {\n if (src < targetIndex) insertAt = targetIndex - 1;\n } else {\n if (src > targetIndex) insertAt = targetIndex + 1;\n }\n const activeSlide = goal.slides[current];\n const movingSlide = goal.slides[src];\n const newSlides = goal.slides.filter((_, i) => i !== src);\n insertAt = Math.max(0, Math.min(insertAt, newSlides.length));\n newSlides.splice(insertAt, 0, movingSlide);\n goal.slides = newSlides;\n recordHistory();\n selectedSlideIdx = goal.slides.indexOf(activeSlide);\n current = selectedSlideIdx;\n autoSave();\n rebuildSlidesAndThumbnails();\n if (typeof selectSlide === 'function') selectSlide(current);\n clearDragState();\n });\n\n container.addEventListener('dragend', () => {\n clearDragState();\n });\n }\n attachDragAndDropListeners();\n\n function deleteSlide(index) {\n if (goal.slides.length <= 1) {\n alert('\u81F3\u5C11\u4FDD\u7559\u4E00\u9875\u5E7B\u706F\u7247\u3002');\n return;\n }\n const deleteBtn = document.querySelector('.lp-thumbnail-delete[data-index=\"' + index + '\"]');\n showDeleteConfirm(index, deleteBtn);\n }\n\n document.addEventListener('keydown', (e) => {\n if (isEditingTarget(e.target) || isEditingTarget(document.activeElement)) return;\n const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;\n const mod = isMac ? e.metaKey : e.ctrlKey;\n\n // \u5173\u95ED\u5220\u9664\u786E\u8BA4\u6C14\u6CE1\n if (e.key === 'Escape' && pendingDeleteIndex != null) {\n e.preventDefault();\n hideDeleteConfirm();\n return;\n }\n\n // \u64A4\u9500 / \u91CD\u505A\n if (mod && e.key.toLowerCase() === 'z') {\n e.preventDefault();\n if (e.shiftKey) redo();\n else undo();\n return;\n }\n if (mod && e.key.toLowerCase() === 'y') {\n e.preventDefault();\n redo();\n return;\n }\n\n // \u7FFB\u9875\u5BFC\u822A\n if (isTransitioning || slides.length === 0) return;\n if (e.key === 'ArrowRight' || e.key === ' ' || e.key === 'PageDown') {\n e.preventDefault();\n goTo(current + 1);\n }\n if (e.key === 'ArrowLeft' || e.key === 'PageUp') {\n e.preventDefault();\n goTo(current - 1);\n }\n if (e.key === 'Home') {\n e.preventDefault();\n goTo(0);\n }\n if (e.key === 'End') {\n e.preventDefault();\n goTo(slides.length - 1);\n }\n });\n\n updateClasses();\n\n // \u7F29\u653E\u63A7\u5236\n const stage = document.querySelector('.lp-editor-stage');\n const scaler = document.querySelector('.lp-editor-stage-scaler');\n const editorRoot = document.querySelector('.lp-editor-root');\n const zoomSlider = document.getElementById('lp-zoom-slider');\n const zoomValue = document.getElementById('lp-zoom-value');\n const zoomOutBtn = document.getElementById('lp-zoom-out');\n const zoomInBtn = document.getElementById('lp-zoom-in');\n const zoomFitBtn = document.getElementById('lp-zoom-fit');\n let userZoom = null;\n\n const SLIDE_WIDTH = 1280;\n const SLIDE_HEIGHT = 720;\n\n function fitScale() {\n if (!stage || !scaler) return 1;\n return Math.min(stage.clientWidth / SLIDE_WIDTH, stage.clientHeight / SLIDE_HEIGHT) * 0.92;\n }\n\n function updateScale() {\n if (!stage || !scaler) return;\n if (editorRoot && editorRoot.classList.contains('lp-editor-presentation-mode')) {\n const scale = Math.min(window.innerWidth / SLIDE_WIDTH, window.innerHeight / SLIDE_HEIGHT);\n scaler.style.setProperty('--lp-presentation-scale', String(Math.max(scale, 0.35)));\n scaler.style.transform = '';\n return;\n }\n const scale = userZoom == null ? fitScale() : userZoom;\n scaler.style.transform = 'scale(' + Math.max(scale, 0.35) + ')';\n scaler.style.removeProperty('--lp-presentation-scale');\n if (zoomValue) zoomValue.textContent = Math.round(scale * 100) + '%';\n if (zoomSlider && userZoom != null) zoomSlider.value = String(Math.round(scale * 100));\n }\n\n if (zoomSlider) {\n zoomSlider.addEventListener('input', () => {\n userZoom = Number(zoomSlider.value) / 100;\n updateScale();\n });\n }\n if (zoomOutBtn) {\n zoomOutBtn.addEventListener('click', () => {\n userZoom = (userZoom == null ? fitScale() : userZoom) - 0.1;\n if (userZoom < 0.35) userZoom = 0.35;\n updateScale();\n });\n }\n if (zoomInBtn) {\n zoomInBtn.addEventListener('click', () => {\n userZoom = (userZoom == null ? fitScale() : userZoom) + 0.1;\n if (userZoom > 1.5) userZoom = 1.5;\n updateScale();\n });\n }\n if (zoomFitBtn) {\n zoomFitBtn.addEventListener('click', () => {\n userZoom = null;\n updateScale();\n });\n }\n window.addEventListener('resize', updateScale);\n updateScale();\n\n // \u64AD\u653E\uFF1A\u8FDB\u5165\u5168\u5C4F\u6F14\u793A\u6A21\u5F0F\uFF0C\u4F7F\u7528\u5F53\u524D\u7F16\u8F91\u72B6\u6001\u76F4\u63A5\u64AD\u653E\n const playBtn = document.getElementById('lp-play');\n let presentationExitBtn = null;\n let presentationKeyHandler = null;\n\n let isExitingPresentation = false;\n\n function exitPresentationMode() {\n if (!editorRoot || isExitingPresentation) return;\n isExitingPresentation = true;\n editorRoot.classList.remove('lp-editor-presentation-mode');\n if (presentationExitBtn && presentationExitBtn.parentNode) {\n presentationExitBtn.parentNode.removeChild(presentationExitBtn);\n presentationExitBtn = null;\n }\n if (presentationKeyHandler) {\n document.removeEventListener('keydown', presentationKeyHandler);\n presentationKeyHandler = null;\n }\n const done = () => {\n isExitingPresentation = false;\n updateScale();\n };\n if (document.fullscreenElement && document.exitFullscreen) {\n document.exitFullscreen().catch(() => {}).finally(done);\n } else {\n done();\n }\n }\n\n async function enterPresentationMode() {\n if (!editorRoot) return;\n editorRoot.classList.add('lp-editor-presentation-mode');\n updateScale();\n try {\n const target = editorRoot.requestFullscreen ? editorRoot : document.documentElement;\n if (target.requestFullscreen) {\n await target.requestFullscreen();\n }\n } catch (err) {\n console.warn('\u8FDB\u5165\u5168\u5C4F\u5931\u8D25\uFF0C\u5C06\u4EE5\u7A97\u53E3\u6F14\u793A\u6A21\u5F0F\u64AD\u653E', err);\n }\n if (!presentationExitBtn) {\n presentationExitBtn = document.createElement('button');\n presentationExitBtn.className = 'lp-editor-presentation-exit';\n presentationExitBtn.textContent = '\u9000\u51FA\u6F14\u793A (Esc)';\n presentationExitBtn.type = 'button';\n presentationExitBtn.addEventListener('click', exitPresentationMode);\n document.body.appendChild(presentationExitBtn);\n }\n if (!presentationKeyHandler) {\n presentationKeyHandler = (e) => {\n if (e.key === 'Escape') {\n e.preventDefault();\n exitPresentationMode();\n }\n };\n document.addEventListener('keydown', presentationKeyHandler);\n }\n }\n\n document.addEventListener('fullscreenchange', () => {\n if (!document.fullscreenElement && editorRoot && editorRoot.classList.contains('lp-editor-presentation-mode')) {\n exitPresentationMode();\n }\n });\n\n if (playBtn) {\n playBtn.addEventListener('click', enterPresentationMode);\n }\n\n // \u5185\u5BB9\u7ED3\u6784\u53D8\u66F4\u540E\u91CD\u65B0\u6E32\u67D3\u5F53\u524D\u7F16\u8F91\u5668\uFF08\u4EC5\u7528\u4E8E\u6570\u7EC4\u589E\u5220\u7B49\u6A21\u677F\u5185\u5BB9\u8C03\u6574\uFF09\n function saveCurrentForReload() {\n try {\n localStorage.setItem('lemonppt:editor:currentSlide', String(selectedSlideIdx));\n } catch (e) {}\n }\n\n function getSlideLabel(slide) {\n const props = slide.props || {};\n const candidates = [\n props.title,\n props.name,\n props.kicker,\n props.quote,\n props.heading,\n props.items && Array.isArray(props.items) ? props.items[0] : undefined,\n props.metric,\n props.value,\n ];\n for (const c of candidates) {\n if (c !== undefined && c !== null && String(c).trim()) return String(c).trim();\n }\n return slide.layout;\n }\n\n // \u9759\u6001\u6587\u4EF6\u6A21\u5F0F\u4E0B\uFF0C\u589E\u5220\u5E7B\u706F\u7247\u540E\u4E0D\u518D\u4F7F\u7528 document.write \u91CD\u5199\u6574\u9875\uFF08\u4F1A\u4E2D\u65AD CSS/\u8D44\u6E90\u52A0\u8F7D\uFF09\uFF0C\n // \u800C\u662F\u76F4\u63A5\u5728\u73B0\u6709 DOM \u4E2D\u91CD\u5EFA slide \u5BB9\u5668\u548C\u7F29\u7565\u56FE\uFF0C\u5E76\u590D\u7528 React root \u91CD\u65B0\u6E32\u67D3\u5185\u5BB9\u3002\n function rebuildSlidesAndThumbnails() {\n // #region debug-point D:rebuild\n __dbg('D', 'editor-script.ts:rebuildSlidesAndThumbnails', 'start', { current, selectedSlideIdx, slidesLength: goal.slides.length });\n // #endregion\n if (!deck) return;\n if (typeof window.__lemonPPT_renderSlideHtml !== 'function') return;\n\n // DOM \u91CD\u5EFA\u524D\u5C06 current \u540C\u6B65\u5230\u5408\u6CD5\u8303\u56F4\uFF0C\u907F\u514D\u540E\u7EED goTo/updateClasses \u8D8A\u754C\n current = Math.max(0, Math.min(selectedSlideIdx, goal.slides.length - 1));\n\n const firstWrapper = slides[0];\n const width = firstWrapper ? parseInt(firstWrapper.style.width, 10) || 1280 : 1280;\n const height = firstWrapper ? parseInt(firstWrapper.style.height, 10) || 720 : 720;\n const theme = goal.theme || 'theme01';\n\n // \u91CD\u5EFA deck \u4E2D\u7684 slide \u5BB9\u5668\n const wrappersHtml = goal.slides.map((slide, index) => {\n const slideHtml = window.__lemonPPT_renderSlideHtml(slide, { slideIdx: index, editable: true, theme });\n const stateClass = index === current ? 'active' : (index < current ? 'prev' : '');\n const transition = String((slide.props && slide.props.transition) || 'none');\n return '<div class=\"lp-slide-wrapper ' + stateClass + '\" data-slide-index=\"' + index + '\" data-layout=\"' + slide.layout + '\" data-lp-transition=\"' + transition + '\" style=\"width:' + width + 'px;height:' + height + 'px;box-sizing:border-box;\">' + slideHtml + '</div>';\n }).join('');\n deck.innerHTML = wrappersHtml;\n\n // \u91CD\u5EFA\u5DE6\u4FA7\u7F29\u7565\u56FE\n const thumbnailsContainer = document.querySelector('.lp-editor-thumbnails');\n if (thumbnailsContainer) {\n const thumbInnerWidth = 156;\n const scale = thumbInnerWidth / width;\n const showDragHandle = goal.slides.length > 1;\n const dragHandleHtml = showDragHandle\n ? '<span class=\"lp-thumbnail-drag-handle\" data-lp-action=\"drag-handle\" aria-hidden=\"true\">' +\n '<svg width=\"12\" height=\"16\" viewBox=\"0 0 12 16\" fill=\"currentColor\" aria-hidden=\"true\">' +\n '<circle cx=\"2.5\" cy=\"2.5\" r=\"1.5\"/>' +\n '<circle cx=\"9.5\" cy=\"2.5\" r=\"1.5\"/>' +\n '<circle cx=\"2.5\" cy=\"8\" r=\"1.5\"/>' +\n '<circle cx=\"9.5\" cy=\"8\" r=\"1.5\"/>' +\n '<circle cx=\"2.5\" cy=\"13.5\" r=\"1.5\"/>' +\n '<circle cx=\"9.5\" cy=\"13.5\" r=\"1.5\"/>' +\n '</svg>' +\n '</span>'\n : '';\n const buttonsHtml = goal.slides.map((slide, index) => {\n const label = getSlideLabel(slide);\n const activeClass = index === current ? 'active' : '';\n const slideHtml = window.__lemonPPT_renderSlideHtml(slide, { slideIdx: index, editable: true, theme });\n const draggableAttr = goal.slides.length > 1 ? 'draggable=\"true\"' : '';\n return '<div class=\"lp-thumbnail ' + activeClass + '\" data-index=\"' + index + '\" role=\"button\" tabindex=\"0\" ' + draggableAttr + ' aria-label=\"\u5E7B\u706F\u7247 ' + (index + 1) + '\uFF0C\u62D6\u52A8\u53EF\u8C03\u6574\u987A\u5E8F\">' +\n dragHandleHtml +\n '<div class=\"lp-thumbnail-render\">' +\n '<div class=\"lp-thumbnail-scaler\" style=\"width:' + width + 'px;height:' + height + 'px;transform:scale(' + scale + ');transform-origin:top left;\">' + slideHtml + '</div>' +\n '</div>' +\n '<div class=\"lp-thumbnail-scrim\"></div>' +\n '<div class=\"lp-thumbnail-content\">' +\n '<div class=\"lp-thumbnail-index\">' + (index + 1) + ' / ' + goal.slides.length + '</div>' +\n '<div class=\"lp-thumbnail-title\">' + escapeHtml(label) + '</div>' +\n '<div class=\"lp-thumbnail-layout\">' + escapeHtml(slide.layout) + '</div>' +\n '</div>' +\n '<span class=\"lp-thumbnail-delete\" data-lp-action=\"delete-slide\" data-index=\"' + index + '\" title=\"\u5220\u9664\u5E7B\u706F\u7247\" aria-label=\"\u5220\u9664\u5E7B\u706F\u7247\">\u00D7</span>' +\n '</div>';\n }).join('');\n thumbnailsContainer.innerHTML = buttonsHtml;\n // \u7F29\u7565\u56FE DOM \u91CD\u5EFA\u540E\u6E05\u7A7A\u865A\u62DF\u6EDA\u52A8\u7F13\u5B58\uFF0C\u907F\u514D\u65E7 HTML \u4E0E\u65B0\u7D22\u5F15\u9519\u4F4D\n Object.keys(thumbnailRenderCache).forEach((key) => {\n delete thumbnailRenderCache[key];\n });\n }\n\n // \u540C\u6B65\u9875\u7801\u8BA1\u6570\u5668\n const pageCounter = document.querySelector('.lp-editor-page-counter');\n if (pageCounter) {\n pageCounter.innerHTML = '<span id=\"lp-current\">' + (current + 1) + '</span> / ' + goal.slides.length;\n }\n\n refreshSlidesAndThumbnails();\n // #region debug-point D:rebuild\n __dbg('D', 'editor-script.ts:rebuildSlidesAndThumbnails', 'refreshed', { current, slidesLength: slides.length, thumbnailsLength: thumbnails.length });\n // #endregion\n attachThumbnailListeners();\n updateClasses();\n renderAllSlidesToRoot();\n initThumbnailVirtualScroll();\n // #region debug-point D:rebuild\n __dbg('D', 'editor-script.ts:rebuildSlidesAndThumbnails', 'done', { current, slidesLength: slides.length });\n // #endregion\n }\n\n function refreshCurrentSlide() {\n if (typeof window.__lemonPPT_renderSlideToRoot === 'function') {\n renderCurrentSlideToRoot();\n } else {\n // \u515C\u5E95\uFF1A\u65E7\u7248\u9759\u6001 HTML \u66FF\u6362\n const wrapper = document.querySelector('.lp-slide-wrapper.active');\n if (!wrapper) return;\n const slide = goal.slides[selectedSlideIdx];\n if (!slide) return;\n if (typeof window.__lemonPPT_disposeECharts === 'function') {\n window.__lemonPPT_disposeECharts();\n }\n const html = window.__lemonPPT_renderSlideHtml(slide, { slideIdx: selectedSlideIdx, editable: true, theme: goal.theme });\n wrapper.innerHTML = html;\n initActiveSlideECharts();\n }\n }\n\n async function reloadEditor() {\n saveCurrentForReload();\n\n // \u4F18\u5148\u5C1D\u8BD5\u670D\u52A1\u5668\u7AEF\u91CD\u65B0\u6E32\u67D3\uFF08\u5F00\u53D1\u670D\u52A1\u5668\u6A21\u5F0F\uFF09\n try {\n const res = await fetch('/api/render-editor', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(goal),\n });\n if (!res.ok) throw new Error('\u91CD\u65B0\u6E32\u67D3\u5931\u8D25: ' + res.status);\n window.location.reload();\n return;\n } catch (err) {\n // \u9759\u6001\u6587\u4EF6\u6A21\u5F0F\u4E0B\uFF0C\u76F4\u63A5\u5728\u5F53\u524D DOM \u4E2D\u91CD\u5EFA slide \u5BB9\u5668\u548C\u7F29\u7565\u56FE\uFF0C\n // \u907F\u514D document.write \u4E2D\u65AD CSS/\u8D44\u6E90\u52A0\u8F7D\u5BFC\u81F4\u5E7B\u706F\u7247\u5185\u5BB9\u65E0\u6CD5\u6E32\u67D3\u3002\n if (typeof window.__lemonPPT_disposeECharts === 'function') {\n window.__lemonPPT_disposeECharts();\n }\n rebuildSlidesAndThumbnails();\n initActiveSlideECharts();\n }\n }\n\n // \u5143\u7D20\u9009\u4E2D\u4E0E\u53F3\u4FA7\u5C5E\u6027\u9762\u677F\n let selectedEl = null;\n let selectedSlideIdx = 0;\n const propertyContent = document.getElementById('lp-property-content');\n const layoutSchemas = window.__lemonPPT_layoutSchemas || {};\n\n const FIELD_LABELS = {\n kicker: '\u6807\u7B7E',\n title: '\u6807\u9898',\n subtitle: '\u526F\u6807\u9898',\n date: '\u65E5\u671F',\n quote: '\u5F15\u7528',\n author: '\u4F5C\u8005',\n role: '\u804C\u4F4D',\n company: '\u516C\u53F8',\n value: '\u6570\u503C',\n unit: '\u5355\u4F4D',\n label: '\u6307\u6807\u540D',\n description: '\u8BF4\u660E',\n change: '\u53D8\u5316',\n imageUrl: '\u56FE\u7247',\n url: '\u56FE\u7247',\n logoUrl: 'Logo',\n avatarUrl: '\u5934\u50CF',\n caption: '\u8BF4\u660E',\n q: '\u95EE\u9898',\n a: '\u56DE\u7B54',\n question: '\u95EE\u9898',\n answer: '\u56DE\u7B54',\n name: '\u540D\u79F0',\n price: '\u4EF7\u683C',\n period: '\u5468\u671F',\n cta: '\u6309\u94AE\u6587\u6848',\n type: '\u56FE\u8868\u7C7B\u578B',\n status: '\u72B6\u6001',\n bio: '\u7B80\u4ECB',\n items: '\u76EE\u5F55\u9879',\n points: '\u8981\u70B9',\n leftPoints: '\u5DE6\u4FA7\u8981\u70B9',\n rightPoints: '\u53F3\u4FA7\u8981\u70B9',\n features: '\u7279\u6027',\n steps: '\u6B65\u9AA4',\n stats: '\u7EDF\u8BA1\u6570\u636E',\n members: '\u6210\u5458',\n milestones: '\u91CC\u7A0B\u7891',\n phases: '\u9636\u6BB5',\n plans: '\u65B9\u6848',\n tiers: '\u5957\u9910',\n images: '\u56FE\u7247',\n partners: '\u5408\u4F5C\u4F19\u4F34',\n datasets: '\u6570\u636E\u96C6',\n metrics: '\u6307\u6807',\n cards: '\u5361\u7247',\n highlighted: '\u9AD8\u4EAE',\n columns: '\u4E3B\u9898\u5217',\n panels: '\u6307\u6807\u9762\u677F',\n left: '\u5DE6\u4FA7\u5185\u5BB9',\n right: '\u53F3\u4FA7\u5185\u5BB9',\n centerCard: '\u4E2D\u592E\u7ED3\u8BBA\u5361',\n comparisons: '\u5BF9\u6BD4\u9879',\n conclusion: '\u7ED3\u8BBA',\n chartType: '\u56FE\u8868\u7C7B\u578B',\n chartData: '\u56FE\u8868\u6570\u636E',\n footnote: '\u811A\u6CE8',\n source: '\u6765\u6E90',\n number: '\u5E8F\u53F7',\n context: '\u4E0A\u4E0B\u6587',\n showInsight: '\u91CD\u70B9\u5F3A\u8C03',\n insight: '\u6D1E\u5BDF\u9762\u677F',\n previous: '\u539F\u6570\u503C',\n current: '\u73B0\u6570\u503C',\n max: '\u6700\u5927\u503C',\n tone: '\u8272\u8C03',\n percent: '\u5360\u6BD4',\n color: '\u989C\u8272',\n rank: '\u6392\u540D',\n company: '\u516C\u53F8',\n track: '\u8D5B\u9053',\n amount: '\u91D1\u989D',\n highlightRow: '\u9AD8\u4EAE\u884C',\n highlightFirstColumn: '\u9AD8\u4EAE\u9996\u5217',\n leftTitle: '\u5DE6\u4FA7\u6807\u9898',\n rightTitle: '\u53F3\u4FA7\u6807\u9898',\n leftValue: '\u5DE6\u4FA7\u6570\u503C',\n rightValue: '\u53F3\u4FA7\u6570\u503C',\n leftLabel: '\u5DE6\u4FA7\u6807\u7B7E',\n rightLabel: '\u53F3\u4FA7\u6807\u7B7E',\n size: '\u5C3A\u5BF8',\n icon: '\u56FE\u6807',\n page: '\u9875\u7801',\n image: '\u56FE\u7247',\n avatar: '\u5934\u50CF',\n logo: 'Logo',\n };\n\n const LAYOUT_LABELS = {\n cover_v1: '\u5C01\u9762',\n cover_v2: '\u5C01\u9762\uFF08\u5C45\u4E2D\uFF09',\n cover_v3: '\u5C01\u9762\uFF08\u5168\u56FE\uFF09',\n table_of_contents_v1: '\u76EE\u5F55',\n table_of_contents_v2: '\u76EE\u5F55\uFF08\u5361\u7247\uFF09',\n content_v1: '\u5185\u5BB9\u5217\u8868',\n content_v2: '\u5185\u5BB9\u5BF9\u6BD4',\n content_v3: '\u5185\u5BB9\u6BB5\u843D',\n content_v4: '\u5185\u5BB9\u5361\u7247',\n chart_v1: '\u56FE\u8868',\n chart_v2: '\u591A\u7CFB\u5217\u56FE\u8868',\n process_v1: '\u6D41\u7A0B\u6B65\u9AA4',\n process_v2: '\u6D41\u7A0B\u65F6\u95F4\u7EBF',\n process_v3: '\u6D41\u7A0B\u5361\u7247',\n stats_v1: '\u6570\u636E\u6307\u6807',\n stats_v2: '\u6570\u636E\u6307\u6807\uFF08\u5361\u7247\uFF09',\n metric_v1: '\u6838\u5FC3\u6307\u6807',\n metric_v2: '\u6838\u5FC3\u6307\u6807\uFF08\u5927\u6570\u5B57\uFF09',\n metric_v3: '\u53CC\u6307\u6807\u5BF9\u6BD4',\n feature_v1: '\u7279\u6027\u5217\u8868',\n feature_v2: '\u7279\u6027\u7F51\u683C',\n feature_v3: '\u7279\u6027\u5361\u7247',\n team_v1: '\u56E2\u961F\u4ECB\u7ECD',\n team_v2: '\u56E2\u961F\u5361\u7247',\n timeline_v1: '\u65F6\u95F4\u7EBF',\n timeline_v2: '\u65F6\u95F4\u7EBF\uFF08\u6A2A\u5411\uFF09',\n timeline_v3: '\u65F6\u95F4\u7EBF\uFF08\u91CC\u7A0B\u7891\uFF09',\n roadmap_v1: '\u8DEF\u7EBF\u56FE',\n faq_v1: '\u95EE\u7B54',\n partners_v1: '\u5408\u4F5C\u4F19\u4F34',\n gallery_v1: '\u56FE\u7247\u96C6',\n gallery_v2: '\u56FE\u7247\u96C6\uFF08\u7F51\u683C\uFF09',\n pricing_v1: '\u5B9A\u4EF7',\n pricing_v2: '\u5B9A\u4EF7\u65B9\u6848',\n closing_v2: '\u7ED3\u675F\u9875',\n quote_v1: '\u5F15\u7528\u9875',\n testimonial_v1: '\u63A8\u8350\u8BED',\n metric_big: '\u5927\u6570\u5B57\u9875',\n metric_triptych: '\u4E09\u6307\u6807\u603B\u89C8',\n chart_donut: '\u73AF\u5F62\u56FE\u62C6\u89E3',\n chart_funnel: '\u6F0F\u6597\u56FE',\n table_data: '\u6570\u636E\u8868\u683C',\n case_study: '\u6848\u4F8B\u8BE6\u60C5',\n spotlight_grid: '\u4E3B\u9898\u805A\u7126\u7F51\u683C',\n diptych_contrast: '\u53CC\u8054\u5BF9\u6BD4',\n conclusion_v1: '\u7ED3\u8BBA\u9875',\n table_v1: '\u8868\u683C',\n ranking_v1: '\u6392\u884C\u699C',\n region_v1: '\u5730\u533A\u5206\u5E03',\n risk_v1: '\u98CE\u9669\u7814\u5224',\n outlook_v1: '\u6295\u8D44\u5C55\u671B',\n bento_v1: 'Bento \u7F51\u683C',\n gallery_v1: '\u56FE\u7247\u96C6',\n gantt_v1: '\u7518\u7279\u56FE',\n quadrant_v1: '\u56DB\u8C61\u9650',\n scorecard_v1: '\u8BC4\u5206\u5361',\n tags_v1: '\u6807\u7B7E\u4E91',\n filmstrip_v1: '\u80F6\u7247\u6761',\n swot_v1: 'SWOT \u5206\u6790',\n pest_v1: 'PEST \u5206\u6790',\n appendix_v1: '\u9644\u5F55',\n image_v1: '\u56FE\u7247\u9875',\n process_v1: '\u6D41\u7A0B\u6B65\u9AA4',\n faq_v1: '\u95EE\u7B54',\n partners_v1: '\u5408\u4F5C\u4F19\u4F34',\n pricing_v1: '\u5B9A\u4EF7',\n team_v1: '\u56E2\u961F\u4ECB\u7ECD',\n team_v2: '\u56E2\u961F\u5361\u7247',\n timeline_v1: '\u65F6\u95F4\u7EBF',\n roadmap_v1: '\u8DEF\u7EBF\u56FE',\n chapter_v1: '\u7AE0\u8282\u9875',\n chapter_v2: '\u7AE0\u8282\u9875\uFF08\u5168\u56FE\uFF09',\n chapter_v3: '\u7AE0\u8282\u9875\uFF08\u5361\u7247\uFF09',\n cover_v3: '\u5C01\u9762\uFF08\u5168\u56FE\uFF09',\n cover_v4: '\u5C01\u9762\uFF08\u6742\u5FD7\uFF09',\n chart_bar3d: '3D \u67F1\u72B6\u56FE',\n chart_gauge: '\u4EEA\u8868\u76D8',\n chart_graph: '\u5173\u7CFB\u56FE',\n chart_heatmap: '\u70ED\u529B\u56FE',\n chart_radar: '\u96F7\u8FBE\u56FE',\n chart_sankey: '\u6851\u57FA\u56FE',\n chart_sunburst: '\u65ED\u65E5\u56FE',\n chart_treemap: '\u77E9\u5F62\u6811\u56FE',\n chart_wordcloud: '\u8BCD\u4E91',\n content_v4: '\u5185\u5BB9\u5361\u7247',\n metrics_v1: '\u6307\u6807\u5899',\n delta_v1: '\u4ECA\u6614\u5BF9\u7167',\n comparison_v1: '\u5BF9\u6BD4\u5206\u6790',\n progress_v1: '\u8FDB\u5EA6\u6761',\n number_showcase_v1: '\u6570\u5B57\u79C0',\n testimonial_v1: '\u63A8\u8350\u8BED',\n closing_v1: '\u7ED3\u675F\u9875',\n };\n\n function getLayoutLabel(layoutId) {\n if (LAYOUT_LABELS[layoutId]) return LAYOUT_LABELS[layoutId];\n const shortId = String(layoutId).replace(/^themed+_/, '');\n return LAYOUT_LABELS[shortId] || layoutId;\n }\n\n function resolveLayoutSchema(slide) {\n if (layoutSchemas[slide.layout]) return layoutSchemas[slide.layout];\n const theme = goal.theme || 'theme01';\n const shortId = String(slide.layout).replace(/^themed+_/, '');\n return layoutSchemas[theme + '_' + shortId] || null;\n }\n\n function getFieldLabel(path) {\n const key = String(path).split('.').pop() || '';\n return FIELD_LABELS[key] || key;\n }\n\n function inferFieldType(path, value) {\n const key = String(path).split('.').pop() || '';\n if (/image|url|logo|avatar/i.test(key) && typeof value === 'string') return 'image';\n if (typeof value === 'boolean') return 'boolean';\n if (typeof value === 'number') return 'number';\n if (key === 'type' && ['bar', 'line', 'pie'].includes(String(value))) return 'select';\n if (key === 'status') return 'select';\n if (['description', 'bio', 'quote', 'answer', 'a', 'subtitle'].includes(key)) return 'textarea';\n return 'text';\n }\n\n function getSelectOptions(path) {\n const key = String(path).split('.').pop() || '';\n if (key === 'type') return [{ value: 'bar', label: '\u67F1\u72B6' }, { value: 'line', label: '\u6298\u7EBF' }, { value: 'pie', label: '\u997C\u56FE' }];\n if (key === 'status') return [{ value: '\u5DF2\u5B8C\u6210', label: '\u5DF2\u5B8C\u6210' }, { value: '\u8FDB\u884C\u4E2D', label: '\u8FDB\u884C\u4E2D' }, { value: '\u89C4\u5212\u4E2D', label: '\u89C4\u5212\u4E2D' }];\n return [];\n }\n\n function clearSelection() {\n if (selectedEl) {\n selectedEl.classList.remove('lp-selected');\n selectedEl = null;\n }\n selectedSlideIdx = current;\n renderSlidePanel();\n }\n\n function setField(path, value) {\n const slide = goal.slides[selectedSlideIdx];\n if (!slide) return;\n recordHistory();\n setProp(slide.props, path, value);\n // \u6253\u5F00\u4F9D\u8D56\u5F00\u5173\u65F6\uFF0C\u5982\u679C\u88AB\u63A7\u5B57\u6BB5\u4E3A\u7A7A\u4E14\u914D\u6709\u9ED8\u8BA4\u503C\uFF0C\u5219\u81EA\u52A8\u586B\u5145\u6D4B\u8BD5\u6570\u636E\n if (value === true) {\n const schema = resolveLayoutSchema(slide);\n if (schema && schema.fields) {\n schema.fields.forEach((field) => {\n if (field.visibleWhen && field.visibleWhen.key === path && field.visibleWhen.value === true && field.defaultValue !== undefined) {\n const current = getProp(slide.props, field.key);\n const isEmpty = current == null || (typeof current === 'object' && Object.keys(current).length === 0);\n if (isEmpty) {\n setProp(slide.props, field.key, deepClone(field.defaultValue));\n }\n }\n });\n }\n }\n syncDomFromGoal(path);\n autoSave();\n // \u5982\u679C\u8BE5\u5B57\u6BB5\u5728\u753B\u5E03\u4E0A\u6CA1\u6709\u5BF9\u5E94\u7684\u76F4\u63A5\u7F16\u8F91\u5143\u7D20\uFF08\u5982 SVG \u56FE\u8868\u6570\u636E\uFF09\uFF0C\u5237\u65B0\u6574\u9875\u5E7B\u706F\u7247\u3002\n // \u5BF9\u4E8E\u6807\u8BB0\u4E3A data-lp-chart-data \u7684\u56FE\u8868\u6570\u636E\u5B57\u6BB5\uFF0C\u5373\u4F7F\u753B\u5E03\u4E0A\u53EF\u76F4\u63A5\u7F16\u8F91\uFF0C\u4E5F\u9700\u8981\n // \u91CD\u65B0\u6E32\u67D3\u56FE\u8868\u4EE5\u540C\u6B65\u6570\u636E\u53D8\u5316\u3002\n if (!hasEditableElementForPath(path) || hasChartDataElementForPath(path)) {\n refreshCurrentSlide();\n }\n }\n\n function hasEditableElementForPath(path) {\n const selector = '[data-lp-editable=\"true\"][data-lp-slide-idx=\"' + selectedSlideIdx + '\"][data-lp-prop=\"' + path + '\"]';\n return document.querySelector(selector) !== null;\n }\n\n function hasChartDataElementForPath(path) {\n const selector = '[data-lp-editable=\"true\"][data-lp-chart-data=\"true\"][data-lp-slide-idx=\"' + selectedSlideIdx + '\"][data-lp-prop=\"' + path + '\"]';\n return document.querySelector(selector) !== null;\n }\n\n function createEl(tag, className, parent) {\n const el = document.createElement(tag);\n if (className) el.className = className;\n if (parent) parent.appendChild(el);\n return el;\n }\n\n function createTextField(label, value, onChange) {\n const wrap = createEl('div', 'lp-property-field');\n createEl('label', 'lp-property-label', wrap).textContent = label;\n const input = createEl('input', 'lp-property-input', wrap);\n input.type = 'text';\n input.value = value == null ? '' : String(value);\n input.addEventListener('input', () => onChange(input.value));\n return wrap;\n }\n\n function createTextareaField(label, value, onChange) {\n const wrap = createEl('div', 'lp-property-field');\n createEl('label', 'lp-property-label', wrap).textContent = label;\n const textarea = createEl('textarea', 'lp-property-textarea', wrap);\n textarea.value = value == null ? '' : String(value);\n textarea.addEventListener('input', () => onChange(textarea.value));\n return wrap;\n }\n\n function createNumberField(label, value, onChange) {\n const wrap = createEl('div', 'lp-property-field');\n createEl('label', 'lp-property-label', wrap).textContent = label;\n const input = createEl('input', 'lp-property-input', wrap);\n input.type = 'number';\n input.value = value == null ? '' : String(value);\n input.addEventListener('input', () => onChange(Number(input.value)));\n return wrap;\n }\n\n function renderSliderTicks(ruler, min, max) {\n const scale = createEl('div', 'lp-property-slider-scale', ruler);\n const span = max - min;\n if (span <= 0) return scale;\n // \u523B\u5EA6\u6B65\u957F\uFF1A\u8303\u56F4\u5C0F\u65F6\u6BCF\u4E2A\u6574\u6570\u4E00\u4E2A\u523B\u5EA6\uFF0C\u8303\u56F4\u5927\u65F6\u5747\u5300\u53D6 6~10 \u4E2A\n const desiredTicks = 8;\n const step = span <= desiredTicks ? 1 : Math.max(1, Math.round(span / desiredTicks));\n for (let i = min; i <= max; i += step) {\n const tick = createEl('div', 'lp-property-slider-tick', scale);\n tick.style.left = ((i - min) / span) * 100 + '%';\n const label = createEl('div', 'lp-property-slider-tick-label', tick);\n label.textContent = String(i);\n tick.addEventListener('click', () => {\n const input = ruler.querySelector('input[type=\"range\"]');\n if (input) {\n input.value = String(i);\n input.dispatchEvent(new Event('input', { bubbles: true }));\n input.dispatchEvent(new Event('change', { bubbles: true }));\n }\n });\n }\n return scale;\n }\n\n function createSliderField(field, value, onChange) {\n const wrap = createEl('div', 'lp-property-slider-field');\n const header = createEl('div', 'lp-property-section-header', wrap);\n createEl('div', 'lp-property-section-title', header).textContent = field.label;\n const countBadge = createEl('span', 'lp-property-slider-value', header);\n const initialValue = value == null ? (field.min || 0) : value;\n countBadge.textContent = String(initialValue);\n\n const min = typeof field.min === 'number' ? field.min : 0;\n const max = typeof field.max === 'number' ? field.max : 100;\n\n const sliderWrap = createEl('div', 'lp-property-slider-wrap', wrap);\n const ruler = createEl('div', 'lp-property-slider-ruler', sliderWrap);\n const range = document.createElement('input');\n range.type = 'range';\n range.className = 'lp-property-slider';\n range.min = String(min);\n range.max = String(max);\n range.value = String(initialValue);\n ruler.appendChild(range);\n renderSliderTicks(ruler, min, max);\n\n range.addEventListener('input', () => {\n const num = Number(range.value);\n countBadge.textContent = String(num);\n onChange(num);\n });\n\n return wrap;\n }\n\n function createToggleField(label, value, onChange) {\n const wrap = createEl('div', 'lp-property-field');\n const labelEl = createEl('label', 'lp-property-toggle', wrap);\n const text = createEl('span', '', labelEl);\n text.textContent = label;\n const input = createEl('input', '', labelEl);\n input.type = 'checkbox';\n input.checked = !!value;\n const track = createEl('div', 'lp-property-toggle-track', labelEl);\n createEl('div', 'lp-property-toggle-thumb', track);\n input.addEventListener('change', () => onChange(input.checked));\n return wrap;\n }\n\n function createSelectField(label, value, options, onChange) {\n const wrap = createEl('div', 'lp-property-field');\n createEl('label', 'lp-property-label', wrap).textContent = label;\n const segmented = createEl('div', 'lp-property-segmented', wrap);\n options.forEach((opt) => {\n const btn = createEl('button', '', segmented);\n btn.textContent = opt.label;\n btn.type = 'button';\n if (opt.value === value) btn.classList.add('active');\n btn.addEventListener('click', () => {\n Array.from(segmented.children).forEach((b) => b.classList.remove('active'));\n btn.classList.add('active');\n onChange(opt.value);\n });\n });\n return wrap;\n }\n\n function createImageHintField(label) {\n const wrap = createEl('div', 'lp-property-field');\n createEl('label', 'lp-property-label', wrap).textContent = label;\n const hint = createEl('div', 'lp-property-help', wrap);\n hint.textContent = '\u8BF7\u70B9\u51FB\u753B\u5E03\u4E2D\u7684\u56FE\u7247\u533A\u57DF\u76F4\u63A5\u4E0A\u4F20';\n return wrap;\n }\n\n function createColorField(label, value, onChange) {\n const wrap = createEl('div', 'lp-property-field');\n createEl('label', 'lp-property-label', wrap).textContent = label;\n const row = createEl('div', '', wrap);\n row.style.display = 'flex';\n row.style.alignItems = 'center';\n row.style.gap = '8px';\n const input = createEl('input', 'lp-property-input', row);\n input.type = 'text';\n input.value = value == null ? '' : String(value);\n input.style.flex = '1';\n input.addEventListener('input', () => onChange(input.value));\n const picker = createEl('input', '', row);\n picker.type = 'color';\n picker.value = value || '#000000';\n picker.style.width = '32px';\n picker.style.height = '32px';\n picker.style.border = 'none';\n picker.style.background = 'none';\n picker.style.cursor = 'pointer';\n picker.addEventListener('input', () => {\n input.value = picker.value;\n onChange(picker.value);\n });\n return wrap;\n }\n\n function createFieldControl(path, value) {\n const type = inferFieldType(path, value);\n const label = getFieldLabel(path);\n if (type === 'image') return createImageHintField(label);\n if (type === 'textarea') return createTextareaField(label, value, (v) => setField(path, v || undefined));\n if (type === 'number') return createNumberField(label, value, (v) => setField(path, v));\n if (type === 'boolean') return createToggleField(label, value, (v) => setField(path, v));\n if (type === 'select') return createSelectField(label, value, getSelectOptions(path), (v) => setField(path, v));\n return createTextField(label, value, (v) => setField(path, v || undefined));\n }\n\n function createSchemaFieldControl(field, value, path, skipCanvasEditable) {\n const label = field.label || field.key;\n if (skipCanvasEditable && (field.type === 'text' || field.type === 'textarea' || field.type === 'image')) {\n return null;\n }\n if (field.type === 'array') {\n const wrap = createEl('div', 'lp-property-nested-array');\n createSchemaArraySection(field, Array.isArray(value) ? value : [], wrap, path, skipCanvasEditable);\n return wrap;\n }\n if (field.type === 'image') return createImageHintField(label);\n if (field.type === 'textarea') return createTextareaField(label, value, (v) => setField(path, v || undefined));\n if (field.type === 'number') return createNumberField(label, value, (v) => setField(path, Number(v)));\n if (field.type === 'slider') {\n const effectiveValue = value === undefined && field.defaultValue !== undefined ? field.defaultValue : value;\n let sliderField = field;\n if (field.key === 'highlightRow') {\n const slideProps = goal.slides[selectedSlideIdx]?.props;\n const rows = Array.isArray(slideProps?.rows) ? slideProps.rows : [];\n sliderField = { ...field, max: Math.min(field.max ?? 11, Math.max(0, rows.length - 1)) };\n }\n return createSliderField(sliderField, effectiveValue, (v) => setField(path, v));\n }\n if (field.type === 'boolean') {\n const effectiveValue = value === undefined && field.defaultValue !== undefined ? field.defaultValue : value;\n return createToggleField(label, effectiveValue, (v) => {\n setField(path, v);\n renderSlidePanel();\n });\n }\n if (field.type === 'select') return createSelectField(label, value, field.options || [], (v) => setField(path, v));\n if (field.type === 'color') return createColorField(label, value, (v) => setField(path, v));\n return createTextField(label, value, (v) => setField(path, v || undefined));\n }\n\n function getSchemaEmptyItem(itemSchema) {\n if (!itemSchema || !itemSchema.length) return '\u793A\u4F8B\u9879';\n // itemSchema \u53EA\u6709\u5355\u4E00\u5360\u4F4D\u5B57\u6BB5\u65F6\uFF0C\u8868\u793A\u6570\u7EC4\u5143\u7D20\u662F\u539F\u59CB\u503C\uFF08\u5B57\u7B26\u4E32/\u6570\u5B57\u7B49\uFF09\n if (itemSchema.length === 1 && itemSchema[0].key === 'item') {\n const field = itemSchema[0];\n if (field.defaultValue !== undefined) return JSON.parse(JSON.stringify(field.defaultValue));\n if (field.type === 'boolean') return false;\n if (field.type === 'number') return 100;\n if (field.type === 'textarea') return '\u793A\u4F8B\u5185\u5BB9';\n return '\u793A\u4F8B\u9879';\n }\n const item = {};\n itemSchema.forEach((field) => {\n if (field.defaultValue !== undefined) {\n item[field.key] = JSON.parse(JSON.stringify(field.defaultValue));\n } else if (field.type === 'boolean') {\n item[field.key] = false;\n } else if (field.type === 'number') {\n item[field.key] = 100;\n } else if (field.type === 'textarea') {\n item[field.key] = '\u793A\u4F8B\u5185\u5BB9';\n } else if (field.type === 'array') {\n const count = typeof field.minItems === 'number' ? Math.max(1, field.minItems) : 1;\n const empty = [];\n for (let i = 0; i < count; i++) {\n empty.push(getSchemaEmptyItem(field.itemSchema));\n }\n item[field.key] = empty;\n } else if (field.type === 'image') {\n item[field.key] = '';\n } else if (field.type === 'text') {\n item[field.key] = '';\n } else if (field.type === 'select') {\n const firstOption = Array.isArray(field.options) && field.options.length > 0 ? field.options[0] : undefined;\n item[field.key] = firstOption && typeof firstOption === 'object' ? firstOption.value : '';\n } else {\n item[field.key] = '\u793A\u4F8B\u6587\u5B57';\n }\n });\n return item;\n }\n\n function createSchemaArraySection(field, array, parent, basePath, skipCanvasEditable) {\n const path = basePath || field.key;\n const section = createEl('div', 'lp-property-section', parent);\n\n // \u6761\u76EE\u6570\u7531\u7EC4\u4EF6\u6837\u5F0F\u51B3\u5B9A\uFF1AminItems / maxItems \u6765\u81EA\u7248\u5F0F Schema\n const minItems = typeof field.minItems === 'number' ? field.minItems : 1;\n // \u5F53 Schema \u672A\u58F0\u660E maxItems \u65F6\uFF0C\u4F7F\u7528\u4E00\u4E2A\u9ED8\u8BA4\u4E0A\u9650\uFF08\u800C\u4E0D\u662F\u5F53\u524D\u6570\u7EC4\u957F\u5EA6\uFF09\uFF0C\n // \u907F\u514D\u7528\u6237\u51CF\u5C11\u6761\u76EE\u540E slider \u7684 max \u540C\u6B65\u7F29\u5C0F\uFF0C\u5BFC\u81F4\u65E0\u6CD5\u6062\u590D\u6216\u7EE7\u7EED\u589E\u52A0\u3002\n const maxItems = typeof field.maxItems === 'number' ? field.maxItems : Math.max(array.length, minItems, 6);\n const currentCount = Math.max(minItems, Math.min(maxItems, array.length));\n\n // \u6807\u9898\u4E0E\u6570\u503C\u653E\u5728\u540C\u4E00\u884C\n const header = createEl('div', 'lp-property-section-header', section);\n createEl('div', 'lp-property-section-title', header).textContent = field.label;\n const countBadge = createEl('span', 'lp-property-slider-value', header);\n countBadge.textContent = String(currentCount);\n\n // \u6570\u91CF\u6ED1\u5757\uFF08\u6807\u5C3A\u6837\u5F0F\uFF09\n const sliderWrap = createEl('div', 'lp-property-slider-wrap', section);\n const ruler = createEl('div', 'lp-property-slider-ruler', sliderWrap);\n const range = document.createElement('input');\n range.type = 'range';\n range.className = 'lp-property-slider';\n range.min = String(minItems);\n range.max = String(maxItems);\n range.value = String(currentCount);\n ruler.appendChild(range);\n renderSliderTicks(ruler, minItems, maxItems);\n\n function setArrayLength(newLength) {\n const slide = goal.slides[selectedSlideIdx];\n if (!slide) return;\n let arr = getProp(slide.props, path);\n if (!Array.isArray(arr)) {\n arr = [];\n setProp(slide.props, path, arr);\n }\n if (newLength < arr.length) {\n arr.length = newLength;\n } else if (newLength > arr.length) {\n while (arr.length < newLength) {\n arr.push(getSchemaEmptyItem(field.itemSchema));\n }\n }\n }\n\n function updateSliderFill() {\n const min = Number(range.min);\n const max = Number(range.max);\n const pct = max === min ? 0 : ((Number(range.value) - min) / (max - min)) * 100;\n range.style.background = 'linear-gradient(90deg, rgba(91,155,213,1) 0%, rgba(139,92,246,1) ' + pct + '%, rgba(255,255,255,0.14) ' + pct + '%, rgba(255,255,255,0.14) 100%)';\n }\n updateSliderFill();\n\n range.addEventListener('input', () => {\n const newLength = Number(range.value);\n countBadge.textContent = String(newLength);\n setArrayLength(newLength);\n updateSliderFill();\n refreshCurrentSlide();\n });\n\n range.addEventListener('change', () => {\n recordHistory();\n autoSave();\n renderSlidePanel();\n });\n\n // \u70B9\u51FB\u6807\u5C3A\u5FEB\u901F\u8DF3\u8F6C\n ruler.addEventListener('click', (e) => {\n if (e.target === range || range.contains(e.target)) return;\n const rect = ruler.getBoundingClientRect();\n const padding = 9;\n const availableWidth = rect.width - padding * 2;\n const x = e.clientX - rect.left - padding;\n const ratio = Math.max(0, Math.min(1, x / availableWidth));\n const newLength = Math.round(minItems + ratio * (maxItems - minItems));\n if (newLength !== Number(range.value)) {\n range.value = String(newLength);\n countBadge.textContent = String(newLength);\n setArrayLength(newLength);\n updateSliderFill();\n refreshCurrentSlide();\n recordHistory();\n autoSave();\n renderSlidePanel();\n }\n });\n\n // \u663E\u793A\u5F53\u524D\u6761\u76EE\u7684\u53EF\u7F16\u8F91\u5B57\u6BB5\uFF08\u7B80\u5355\u6587\u5B57\u5B57\u6BB5\u4ECD\u5728\u753B\u5E03\u4E0A\u7F16\u8F91\uFF0C\u6B64\u5904\u8DF3\u8FC7\uFF09\n const isPrimitiveItem = field.itemSchema && field.itemSchema.length === 1 && field.itemSchema[0].key === 'item';\n const allSubFieldsAreCanvasEditable = skipCanvasEditable && (() => {\n if (!field.itemSchema) return true;\n if (isPrimitiveItem) {\n return ['text', 'textarea', 'image'].includes(field.itemSchema[0].type);\n }\n return field.itemSchema.every((subField) => ['text', 'textarea', 'image'].includes(subField.type));\n })();\n\n if (!allSubFieldsAreCanvasEditable) {\n const list = createEl('div', 'lp-property-array', section);\n array.forEach((item, index) => {\n const itemWrap = createEl('div', 'lp-property-array-item', list);\n\n const itemHeader = createEl('div', 'lp-property-array-item-header', itemWrap);\n const itemIndexLabel = createEl('span', '', itemHeader);\n itemIndexLabel.textContent = '#' + (index + 1);\n const deleteBtn = createEl('button', 'lp-property-btn-danger', itemHeader);\n deleteBtn.type = 'button';\n deleteBtn.title = '\u5220\u9664\u8BE5\u9879';\n deleteBtn.textContent = '\u00D7';\n deleteBtn.disabled = array.length <= minItems;\n deleteBtn.addEventListener('click', (e) => {\n e.stopPropagation();\n e.preventDefault();\n const slide = goal.slides[selectedSlideIdx];\n if (!slide) return;\n const arr = getProp(slide.props, path);\n if (!Array.isArray(arr)) return;\n arr.splice(index, 1);\n refreshCurrentSlide();\n recordHistory();\n autoSave();\n renderSlidePanel();\n });\n\n if (isPrimitiveItem) {\n const subField = field.itemSchema[0];\n const itemPath = path + '.' + index;\n const itemValue = item;\n const control = createSchemaFieldControl(subField, itemValue, itemPath, skipCanvasEditable);\n if (control) itemWrap.appendChild(control);\n } else if (field.itemSchema) {\n field.itemSchema.forEach((subField) => {\n if (skipCanvasEditable && (subField.type === 'text' || subField.type === 'textarea' || subField.type === 'image')) return;\n const itemPath = path + '.' + index + '.' + subField.key;\n const itemValue = typeof item === 'object' && item !== null ? item[subField.key] : undefined;\n const control = createSchemaFieldControl(subField, itemValue, itemPath, skipCanvasEditable);\n if (control) itemWrap.appendChild(control);\n });\n } else if (typeof item === 'string') {\n if (skipCanvasEditable) return;\n const textarea = createEl('textarea', 'lp-property-textarea', itemWrap);\n textarea.value = item;\n textarea.addEventListener('input', () => {\n setField(path + '.' + index, textarea.value);\n });\n }\n });\n }\n }\n\n function findSchemaField(schema, path) {\n if (!schema || !schema.fields || !path) return null;\n const parts = path.split('.');\n let fields = schema.fields;\n let field = null;\n for (let i = 0; i < parts.length; i++) {\n const key = parts[i];\n if (!fields) return null;\n if (/^d+$/.test(key)) {\n // \u6570\u7EC4\u7D22\u5F15\uFF0C\u8FDB\u5165\u4E0B\u4E00\u5C42 itemSchema\n if (field && field.itemSchema) {\n fields = field.itemSchema;\n }\n continue;\n }\n field = fields.find((f) => f.key === key) || null;\n if (!field) return null;\n if (i < parts.length - 1) {\n fields = field.type === 'array' ? field.itemSchema : (field.type === 'object' ? field.itemSchema : null);\n }\n }\n return field;\n }\n\n function renderSchemaFields(schema, props, parent, basePath, skipCanvasEditable) {\n if (!schema || !schema.fields) return;\n schema.fields.forEach((field) => {\n // \u7B80\u5355\u6587\u5B57\u5B57\u6BB5\u76F4\u63A5\u5728\u753B\u5E03\u4E0A\u7F16\u8F91\uFF0C\u4E0D\u5728\u53F3\u4FA7\u8FB9\u680F\u91CD\u590D\u6E32\u67D3\n if (field.inlineEditable) return;\n // \u975E\u56FE\u8868\u7EC4\u4EF6\u4E2D\uFF0C\u53EF\u5728\u753B\u5E03\u76F4\u63A5\u7F16\u8F91\u7684\u6587\u5B57\u3001\u56FE\u7247\u5B57\u6BB5\u4E0D\u518D\u5728\u53F3\u4FA7\u8FB9\u680F\u91CD\u590D\u63D0\u4F9B\n if (skipCanvasEditable && (field.type === 'text' || field.type === 'textarea' || field.type === 'image')) return;\n // \u4F9D\u8D56\u5F00\u5173\u7684\u5B57\u6BB5\uFF0C\u5F00\u5173\u5173\u95ED\u65F6\u6536\u8D77\uFF1B\u672A\u8BBE\u7F6E\u65F6\u6309\u5F00\u5173\u9ED8\u8BA4\u503C\u5224\u65AD\n if (field.visibleWhen) {\n const controller = findSchemaField(schema, field.visibleWhen.key);\n const rawValue = getProp(props, field.visibleWhen.key);\n const effectiveValue = rawValue === undefined && controller?.defaultValue !== undefined ? controller.defaultValue : rawValue;\n if (effectiveValue !== field.visibleWhen.value) return;\n }\n const path = basePath ? basePath + '.' + field.key : field.key;\n const value = getProp(props, path);\n if (field.type === 'array') {\n createSchemaArraySection(field, Array.isArray(value) ? value : [], parent, path, skipCanvasEditable);\n } else if (field.type === 'object') {\n const section = createEl('div', 'lp-property-section', parent);\n createEl('div', 'lp-property-section-title', section).textContent = field.label;\n if (field.itemSchema) {\n renderSchemaFields({ fields: field.itemSchema }, props, section, path, skipCanvasEditable);\n }\n } else if (field.type === 'slider') {\n const section = createEl('div', 'lp-property-section', parent);\n const effectiveValue = value === undefined && field.defaultValue !== undefined ? field.defaultValue : value;\n let sliderField = field;\n if (field.key === 'highlightRow') {\n const rows = Array.isArray(props?.rows) ? props.rows : [];\n sliderField = { ...field, max: Math.min(field.max ?? 11, Math.max(0, rows.length - 1)) };\n }\n section.appendChild(createSliderField(sliderField, effectiveValue, (v) => setField(path, v)));\n } else {\n const control = createSchemaFieldControl(field, value, path, skipCanvasEditable);\n if (control) parent.appendChild(control);\n }\n });\n }\n\n function renderSlideFields(props, parent, skipCanvasEditable) {\n Object.keys(props).forEach((key) => {\n if (key === '_style') return;\n const value = props[key];\n if (Array.isArray(value)) {\n // \u65E0 Schema \u65F6\u4EC5\u63D0\u4F9B\u7B80\u5355\u6587\u672C\u7F16\u8F91\uFF0C\u4E0D\u663E\u793A\u5E8F\u53F7\u523B\u5EA6\n const section = createEl('div', 'lp-property-section', parent);\n createEl('div', 'lp-property-section-title', section).textContent = getFieldLabel(key) + ' (' + value.length + ')';\n const list = createEl('div', 'lp-property-array', section);\n value.forEach((item, index) => {\n const itemWrap = createEl('div', 'lp-property-array-item', list);\n if (typeof item === 'string') {\n if (skipCanvasEditable) return;\n const textarea = createEl('textarea', 'lp-property-textarea', itemWrap);\n textarea.value = item;\n textarea.addEventListener('input', () => {\n setField(key + '.' + index, textarea.value);\n });\n } else if (typeof item === 'object' && item !== null) {\n Object.keys(item).forEach((subKey) => {\n const type = inferFieldType(key + '.' + index + '.' + subKey, item[subKey]);\n if (skipCanvasEditable && (type === 'text' || type === 'textarea' || type === 'image')) return;\n itemWrap.appendChild(createFieldControl(key + '.' + index + '.' + subKey, item[subKey]));\n });\n }\n if (!itemWrap.hasChildNodes()) {\n itemWrap.remove();\n }\n });\n if (!list.hasChildNodes()) {\n list.remove();\n }\n } else if (value !== null && typeof value === 'object') {\n // \u5D4C\u5957\u5BF9\u8C61\u76F4\u63A5\u5C55\u5F00\uFF08\u76EE\u524D\u8F83\u5C11\uFF09\n const section = createEl('div', 'lp-property-section', parent);\n createEl('div', 'lp-property-section-title', section).textContent = getFieldLabel(key);\n Object.keys(value).forEach((subKey) => {\n const type = inferFieldType(key + '.' + subKey, value[subKey]);\n if (skipCanvasEditable && (type === 'text' || type === 'textarea' || type === 'image')) return;\n section.appendChild(createFieldControl(key + '.' + subKey, value[subKey]));\n });\n } else {\n const type = inferFieldType(key, value);\n if (skipCanvasEditable && (type === 'text' || type === 'textarea' || type === 'image')) return;\n parent.appendChild(createFieldControl(key, value));\n }\n });\n }\n\n function renderSlidePanel() {\n if (!propertyContent) return;\n propertyContent.innerHTML = '';\n const slide = goal.slides[selectedSlideIdx];\n if (!slide) return;\n\n const info = createEl('div', 'lp-property-section', propertyContent);\n createEl('div', 'lp-property-section-title', info).textContent = '\u5E7B\u706F\u7247 ' + (selectedSlideIdx + 1);\n const layoutLabel = createEl('div', 'lp-property-help', info);\n layoutLabel.textContent = '\u7248\u5F0F\uFF1A' + getLayoutLabel(slide.layout);\n\n const transitionSection = createEl('div', 'lp-property-section', propertyContent);\n createEl('div', 'lp-property-section-title', transitionSection).textContent = '\u5207\u6362\u52A8\u753B';\n const transitionSelect = document.createElement('select');\n transitionSelect.className = 'lp-editor-select';\n transitionSelect.style.width = '100%';\n const currentTransition = slide.props.transition || 'none';\n TRANSITIONS.forEach((t) => {\n const opt = document.createElement('option');\n opt.value = t.key;\n opt.textContent = t.label;\n if (t.key === currentTransition) opt.selected = true;\n transitionSelect.appendChild(opt);\n });\n transitionSelect.addEventListener('change', () => {\n slide.props.transition = transitionSelect.value;\n const wrapper = document.querySelector('.lp-slide-wrapper[data-slide-index=\"' + selectedSlideIdx + '\"]');\n if (wrapper) wrapper.setAttribute('data-lp-transition', slide.props.transition);\n recordHistory();\n saveState();\n });\n transitionSection.appendChild(transitionSelect);\n\n if (selectedEl && slide.role === 'chart') {\n const prop = selectedEl.getAttribute('data-lp-prop');\n const schema = resolveLayoutSchema(slide);\n const schemaField = schema ? findSchemaField(schema, prop) : null;\n // \u7B80\u5355\u6587\u5B57\u5B57\u6BB5\u5DF2\u5728\u753B\u5E03\u4E0A\u76F4\u63A5\u7F16\u8F91\uFF0C\u4E0D\u5728\u53F3\u4FA7\u8FB9\u680F\u91CD\u590D\u663E\u793A\n if (!schemaField || !schemaField.inlineEditable) {\n const quickSection = createEl('div', 'lp-property-section', propertyContent);\n createEl('div', 'lp-property-section-title', quickSection).textContent = '\u5F53\u524D\u9009\u4E2D';\n const fieldValue = getProp(slide.props, prop);\n quickSection.appendChild(createFieldControl(prop, fieldValue));\n }\n }\n\n const fieldsSection = createEl('div', 'lp-property-section', propertyContent);\n createEl('div', 'lp-property-section-title', fieldsSection).textContent = '\u5185\u5BB9\u5C5E\u6027';\n const schema = resolveLayoutSchema(slide);\n const skipCanvasEditable = slide.role !== 'chart';\n if (schema && schema.fields) {\n renderSchemaFields(schema, slide.props, fieldsSection, undefined, skipCanvasEditable);\n } else {\n renderSlideFields(slide.props, fieldsSection, skipCanvasEditable);\n }\n }\n\n function selectEl(el) {\n if (selectedEl === el) return;\n if (selectedEl) selectedEl.classList.remove('lp-selected');\n selectedEl = el;\n selectedEl.classList.add('lp-selected');\n selectedSlideIdx = Number(el.getAttribute('data-lp-slide-idx')) || current;\n renderSlidePanel();\n // \u6EDA\u52A8\u5230\u5F53\u524D\u9009\u4E2D\u5B57\u6BB5\n const path = el.getAttribute('data-lp-prop');\n if (path && propertyContent) {\n const label = propertyContent.querySelector('.lp-property-label');\n // \u7B80\u5355\u9AD8\u4EAE\uFF1A\u6682\u65F6\u4E0D\u505A\u81EA\u52A8\u6EDA\u52A8\uFF0C\u907F\u514D\u590D\u6742\u5EA6\n }\n }\n\n function renderSlideToRootByIndex(index, wrapper) {\n if (typeof window.__lemonPPT_renderSlideToRoot !== 'function') return;\n const slide = goal.slides[index];\n if (!slide || !wrapper) {\n // #region debug-point C:render-missing\n __dbg('C', 'editor-script.ts:renderSlideToRootByIndex', 'missing slide or wrapper', { index, hasSlide: !!slide, hasWrapper: !!wrapper });\n // #endregion\n return;\n }\n // #region debug-point C:render-start\n __dbg('C', 'editor-script.ts:renderSlideToRootByIndex', 'rendering slide', { index, layout: slide.layout, wrapperChildren: wrapper.childNodes.length });\n // #endregion\n window.__lemonPPT_renderSlideToRoot(wrapper, slide, { slideIdx: index, editable: true, theme: goal.theme });\n initEditableElements(wrapper);\n }\n\n function initActiveSlideECharts() {\n const activeWrapper = document.querySelector('.lp-slide-wrapper.active');\n if (activeWrapper && typeof window.__lemonPPT_initECharts === 'function') {\n window.__lemonPPT_initECharts(goal.theme || 'theme01', activeWrapper);\n }\n }\n\n function disposeEChartsInWrapper(wrapper) {\n if (!wrapper) return;\n wrapper.querySelectorAll('[data-lp-echart-type]').forEach((container) => {\n const inst = container.__lpEChartInstance;\n if (inst && !inst.isDisposed()) {\n inst.dispose();\n }\n container.__lpEChartInstance = undefined;\n });\n }\n\n function renderCurrentSlideToRoot() {\n const wrapper = document.querySelector('.lp-slide-wrapper.active');\n if (!wrapper) return;\n renderSlideToRootByIndex(selectedSlideIdx, wrapper);\n // React 18 createRoot().render() \u662F\u5F02\u6B65\u7684\uFF0C\u5EF6\u8FDF\u5230\u4E0B\u4E00\u5E27\u518D\u521D\u59CB\u5316 ECharts\uFF0C\n // \u786E\u4FDD\u5360\u4F4D\u5BB9\u5668\u5DF2\u7ECF\u6302\u8F7D\u5230 DOM \u5E76\u6709\u53EF\u6D4B\u91CF\u7684\u5C3A\u5BF8\u3002\n requestAnimationFrame(() => {\n initActiveSlideECharts();\n });\n }\n\n // \u9884\u521B\u5EFA\u6240\u6709 slide \u7684 React root \u5E76\u6E32\u67D3\uFF0C\u4F7F\u5207\u9875\u548C\u975E active slide \u66F4\u65B0\u90FD\u65E0\u9700\u91CD\u65B0\u6784\u5EFA DOM\n function renderAllSlidesToRoot() {\n if (typeof window.__lemonPPT_renderSlideToRoot !== 'function') {\n // #region debug-point C:render-all-noop\n __dbg('C', 'editor-script.ts:renderAllSlidesToRoot', 'renderSlideToRoot not available', {});\n // #endregion\n return;\n }\n const wrappers = document.querySelectorAll('.lp-slide-wrapper');\n // #region debug-point C:render-all-start\n __dbg('C', 'editor-script.ts:renderAllSlidesToRoot', 'rendering all slides', { wrapperCount: wrappers.length, goalSlideCount: goal.slides.length });\n // #endregion\n wrappers.forEach((wrapper, index) => {\n renderSlideToRootByIndex(index, wrapper);\n });\n // \u7F16\u8F91\u5668\u573A\u666F\u4E0B\u53EA\u521D\u59CB\u5316\u5F53\u524D active slide \u7684 ECharts\uFF0C\u907F\u514D 75+ \u9875\u540C\u65F6\u6E32\u67D3\u5BFC\u81F4\u5361\u987F\u3002\n requestAnimationFrame(() => {\n initActiveSlideECharts();\n });\n }\n\n function selectSlide(index) {\n if (selectedEl) {\n selectedEl.classList.remove('lp-selected');\n selectedEl = null;\n }\n selectedSlideIdx = index;\n renderSlidePanel();\n }\n\n document.addEventListener('focusin', (e) => {\n const el = e.target.closest && e.target.closest('[data-lp-editable=\"true\"], [data-lp-editable-image=\"true\"]');\n if (el) selectEl(el);\n });\n\n document.addEventListener('click', (e) => {\n const editableEl = e.target.closest && e.target.closest('[data-lp-editable=\"true\"], [data-lp-editable-image=\"true\"]');\n if (editableEl) {\n selectEl(editableEl);\n return;\n }\n const wrapper = e.target.closest && e.target.closest('.lp-slide-wrapper');\n if (wrapper) {\n selectSlide(Number(wrapper.getAttribute('data-slide-index')));\n return;\n }\n if (e.target.closest && !e.target.closest('.lp-editor-right-panel')) {\n clearSelection();\n }\n });\n\n // \u6062\u590D\u4E0A\u6B21\u505C\u7559\u7684\u5E7B\u706F\u7247\uFF08\u7ED3\u6784\u53D8\u66F4\u540E\u91CD\u8F7D\u7528\uFF09\n const savedCurrent = localStorage.getItem('lemonppt:editor:currentSlide');\n if (savedCurrent) {\n const savedIndex = Number(savedCurrent);\n if (!Number.isNaN(savedIndex) && savedIndex >= 0 && savedIndex < slides.length) {\n goTo(savedIndex);\n }\n localStorage.removeItem('lemonppt:editor:currentSlide');\n }\n\n // \u9ED8\u8BA4\u9009\u4E2D\u5F53\u524D\u5E7B\u706F\u7247\n selectSlide(current);\n\n // \u82E5 localStorage \u4E2D\u5E7B\u706F\u7247\u6570\u91CF\u4E0E\u5F53\u524D DOM \u4E0D\u5339\u914D\uFF0C\u91CD\u5EFA DOM \u540E\u518D\u6E32\u67D3\n if (needsDomRebuild) {\n rebuildSlidesAndThumbnails();\n } else {\n // \u9884\u521B\u5EFA\u5E76\u6E32\u67D3\u6240\u6709 slide \u7684 React root\uFF0C\u4F7F\u5207\u9875\u4E0E\u540E\u7EED\u66F4\u65B0\u90FD\u8D70 React reconcile\n renderAllSlidesToRoot();\n }\n})();\n";
2
2
  //# sourceMappingURL=editor-script.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"editor-script.d.ts","sourceRoot":"","sources":["../src/editor-script.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,YAAY,ww9BA02BxB,CAAC"}
1
+ {"version":3,"file":"editor-script.d.ts","sourceRoot":"","sources":["../src/editor-script.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,YAAY,+4uJAwxGxB,CAAC"}