// ========== 工具函数 ========== // HTML 转义函数 - 防止 XSS function escapeHtml(text) { if (typeof text !== 'string') return ''; const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // 剥离 Markdown 标记,获取纯文本 function stripMarkdown(text) { return text .replace(/^#{1,3}\s/, '') .replace(/\*\*(.*?)\*\*/g, '$1') .replace(/\*(.*?)\*/g, '$1') .replace(/__(.*?)__/g, '$1') .replace(/~~(.*?)~~/g, '$1') .replace(/\[color:#[a-fA-F0-9]{6}\](.*?)\[\/color\]/g, '$1') .replace(/^>\s/, '') .replace(/^[-*]\s/, '') .replace(/^\d+\.\s/, '') .replace(/`{3}[\s\S]*?`{3}/g, m => m.split('\n').join('')) .replace(/`(.*?)`/g, '$1'); } // 显示错误提示 function showError(message) { const existing = document.querySelector('.error-toast'); if (existing) existing.remove(); const toast = document.createElement('div'); toast.className = 'error-toast'; toast.textContent = message; toast.setAttribute('role', 'alert'); document.body.appendChild(toast); setTimeout(() => { toast.remove(); }, 5000); } // ========== DOM 元素缓存 ========== const DOM = {}; function cacheDOMElements() { const ids = [ 'inputText', 'fontSize', 'lineHeight', 'watermarkText', 'bgColor', 'bgTexture', 'stylePreset', 'aspectRatio', 'customWidth', 'customHeight', 'fontFamily', 'enableHeader', 'headerText', 'headerStyle', 'allowParagraphSplit', 'fontFile', 'previewContainer', 'downloadSection', 'pageCountBadge', 'draftTime', 'undoBtn', 'redoBtn', 'realtimePreviewBtn', 'markdownPreview', 'previewContent', 'emojiPicker', 'emojiToggleBtn', 'customSizeOptions', 'customFontUpload', 'headerOptions', 'headerStyleOptions', 'draftStatus' ]; ids.forEach(id => { DOM[id] = document.getElementById(id); }); } // ========== 全局变量 ========== let currentPages = []; let currentPageIndex = 0; let realtimePreviewEnabled = true; let realtimeDebounceTimer = null; const REALTIME_DEBOUNCE_MS = 300; let undoStack = []; let redoStack = []; const MAX_UNDO = 50; let draftTimer = null; let downloadCancelled = false; // ========== 实时预览系统 ========== function scheduleRealtimePreview() { if (!realtimePreviewEnabled) return; if (realtimeDebounceTimer) clearTimeout(realtimeDebounceTimer); realtimeDebounceTimer = setTimeout(() => { const inputText = DOM.inputText?.value || ''; if (inputText.trim()) { generatePages(true); } else { if (DOM.previewContainer) DOM.previewContainer.innerHTML = ''; if (DOM.downloadSection) DOM.downloadSection.style.display = 'none'; currentPages = []; } }, REALTIME_DEBOUNCE_MS); } function toggleRealtimePreview() { realtimePreviewEnabled = !realtimePreviewEnabled; const btn = DOM.realtimePreviewBtn; if (btn) { if (realtimePreviewEnabled) { btn.textContent = '🔄 实时预览: 开'; btn.style.background = '#2ed573'; scheduleRealtimePreview(); } else { btn.textContent = '🔄 实时预览: 关'; btn.style.background = '#666'; } } try { localStorage.setItem('xhs_realtime_preview', realtimePreviewEnabled ? '1' : '0'); } catch (e) { console.error('保存实时预览设置失败:', e); } } function initRealtimePreview() { try { const saved = localStorage.getItem('xhs_realtime_preview'); if (saved === '0') { realtimePreviewEnabled = false; const btn = DOM.realtimePreviewBtn; if (btn) { btn.textContent = '🔄 实时预览: 关'; btn.style.background = '#666'; } } } catch (e) { console.error('初始化实时预览设置失败:', e); } } // ========== 撤销/重做系统 ========== function pushUndoState() { const text = DOM.inputText?.value || ''; if (undoStack.length > 0 && undoStack[undoStack.length - 1] === text) return; undoStack.push(text); if (undoStack.length > MAX_UNDO) undoStack.shift(); redoStack = []; updateUndoRedoButtons(); } function undo() { if (undoStack.length <= 1) return; const current = undoStack.pop(); redoStack.push(current); const prev = undoStack[undoStack.length - 1]; if (DOM.inputText) DOM.inputText.value = prev; updateUndoRedoButtons(); updatePreview(); scheduleRealtimePreview(); } function redo() { if (redoStack.length === 0) return; const next = redoStack.pop(); undoStack.push(next); if (DOM.inputText) DOM.inputText.value = next; updateUndoRedoButtons(); updatePreview(); scheduleRealtimePreview(); } function updateUndoRedoButtons() { if (DOM.undoBtn) DOM.undoBtn.disabled = undoStack.length <= 1; if (DOM.redoBtn) DOM.redoBtn.disabled = redoStack.length === 0; } // ========== 草稿自动保存 ========== function saveDraft() { try { const data = { text: DOM.inputText?.value || '', fontSize: DOM.fontSize?.value || '13', lineHeight: DOM.lineHeight?.value || '1.8', watermarkText: DOM.watermarkText?.value || '', bgColor: DOM.bgColor?.value || 'white', bgTexture: DOM.bgTexture?.value || 'none', stylePreset: DOM.stylePreset?.value || 'custom', aspectRatio: DOM.aspectRatio?.value || '2:3', customWidth: DOM.customWidth?.value || '900', customHeight: DOM.customHeight?.value || '1600', fontFamily: DOM.fontFamily?.value || 'default', enableHeader: DOM.enableHeader?.checked || false, headerText: DOM.headerText?.value || '', headerStyle: DOM.headerStyle?.value || 'center', allowParagraphSplit: DOM.allowParagraphSplit?.checked || false, timestamp: Date.now() }; localStorage.setItem('xhs_draft', JSON.stringify(data)); const time = new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', second: '2-digit' }); const draftTimeEl = DOM.draftTime; if (draftTimeEl) { draftTimeEl.textContent = '已保存 ' + time; draftTimeEl.className = 'saved'; } } catch (e) { console.error('保存草稿失败:', e); showError('保存草稿失败,可能是存储空间不足'); } } function loadDraft() { try { const data = JSON.parse(localStorage.getItem('xhs_draft')); if (!data) { alert('没有找到已保存的草稿'); return; } if (DOM.inputText) DOM.inputText.value = data.text || ''; if (DOM.fontSize) DOM.fontSize.value = data.fontSize || '13'; if (DOM.lineHeight) DOM.lineHeight.value = data.lineHeight || '1.8'; if (DOM.watermarkText) DOM.watermarkText.value = data.watermarkText || ''; if (DOM.bgColor) DOM.bgColor.value = data.bgColor || 'white'; if (DOM.bgTexture) DOM.bgTexture.value = data.bgTexture || 'none'; if (DOM.stylePreset) DOM.stylePreset.value = data.stylePreset || 'custom'; if (DOM.aspectRatio) DOM.aspectRatio.value = data.aspectRatio || '2:3'; if (DOM.customWidth) DOM.customWidth.value = data.customWidth || '900'; if (DOM.customHeight) DOM.customHeight.value = data.customHeight || '1600'; if (DOM.fontFamily) DOM.fontFamily.value = data.fontFamily || 'default'; if (DOM.enableHeader) DOM.enableHeader.checked = data.enableHeader || false; if (DOM.headerText) DOM.headerText.value = data.headerText || ''; if (DOM.headerStyle) DOM.headerStyle.value = data.headerStyle || 'center'; if (DOM.allowParagraphSplit) DOM.allowParagraphSplit.checked = data.allowParagraphSplit || false; toggleHeaderOptions(); updatePreviewSize(); const time = new Date(data.timestamp).toLocaleString('zh-CN'); if (DOM.draftTime) DOM.draftTime.textContent = '已恢复 ' + time; undoStack = [data.text || '']; redoStack = []; updateUndoRedoButtons(); generatePages(); } catch (e) { console.error('加载草稿失败:', e); showError('加载草稿失败'); } } function autoSaveDraft() { if (draftTimer) clearTimeout(draftTimer); draftTimer = setTimeout(saveDraft, 30000); } function checkDraftOnLoad() { try { const data = JSON.parse(localStorage.getItem('xhs_draft')); if (data && data.timestamp && DOM.draftTime) { const time = new Date(data.timestamp).toLocaleString('zh-CN'); DOM.draftTime.textContent = '上次保存 ' + time; } } catch (e) { console.error('检查草稿失败:', e); } } // ========== 深色模式 ========== function toggleDarkMode() { document.body.classList.toggle('dark-mode'); const isDark = document.body.classList.contains('dark-mode'); try { localStorage.setItem('xhs_dark_mode', isDark ? '1' : '0'); } catch (e) { console.error('保存深色模式设置失败:', e); } const toggle = document.querySelector('.dark-mode-toggle'); if (toggle) { toggle.textContent = isDark ? '☀️' : '🌙'; toggle.setAttribute('aria-label', isDark ? '切换到浅色模式' : '切换到深色模式'); } } function initDarkMode() { try { if (localStorage.getItem('xhs_dark_mode') === '1') { document.body.classList.add('dark-mode'); const toggle = document.querySelector('.dark-mode-toggle'); if (toggle) { toggle.textContent = '☀️'; toggle.setAttribute('aria-label', '切换到浅色模式'); } } } catch (e) { console.error('初始化深色模式失败:', e); } } // ========== Emoji选择器 ========== const EMOJI_DATA = { '常用': ['😀','😃','😄','😁','😆','😅','🤣','😂','🙂','😊','😇','🥰','😍','🤩','😘','😗','😚','😙','🥲','😋','😛','😜','🤪','😝','🤑','🤗','🤭','🤫','🤔','🫡','🤐','🤨','😐','😑','😶','🫥','😏','😒','🙄','😬','🤥','😌','😔','😪','🤤','😴','😷','🤒','🤕','🤢','🤮','🥵','🥶','🥴','😵','🤯','🤠','🥳','🥸','😎','🤓','🧐'], '手势': ['👍','👎','👊','✊','🤛','🤜','👏','🙌','👐','🤲','🤝','🙏','✌️','🤞','🫰','🤟','🤘','🤙','👈','👉','👆','👇','☝️','🫵','👋','🤚','🖐️','✋','🖖','🫱','🫲','👌','🤌','🤏','💪','🦾','🦿','🖕'], '心形': ['❤️','🧡','💛','💚','💙','💜','🖤','🤍','🤎','💔','❣️','💕','💞','💓','💗','💖','💘','💝','💟','♥️'], '自然': ['🌸','🌺','🌻','🌹','🌷','💐','🌱','🌿','🍀','🍁','🍂','🍃','🌲','🌳','🌴','🌵','🌾','🍄','🪴','🎄','🔥','✨','⭐','🌟','💫','🌈','☀️','🌤️','⛅','🌥️','☁️','🌧️','❄️','🌊','💧'], '美食': ['🍎','🍊','🍋','🍌','🍉','🍇','🍓','🫐','🍈','🍒','🍑','🥭','🍍','🥥','🥑','🌽','🥕','🥒','🍅','🥦','🧄','🧅','🥔','🍠','☕','🍵','🧃','🥤','🍺','🍻','🥂','🍷','🧋','🧉','🥢','🥡','🍴','🍽️','🥣','🥘','🧆','🍕','🍔','🍟','🌭','🥪','🌮','🌯','🥙','🧇','🥞','🧈','🥓','🍳','🧊'], '物品': ['💡','📎','✏️','📝','📖','📚','🔖','💰','💎','🎁','🎀','🎉','🎊','🎈','🎯','🏆','🥇','🥈','🥉','🏅','🎖️','📋','📁','📂','📅','📆','🗓️','📇','📈','📉','📊','🔍','🔎','💻','🖥️','⌨️','📱','📲','📷','📹','🎥','🎬','📺','📻','🎙️','🎚️','🎛️','🧭','⏰','⏳','🔔','📣','📢','💬','💭','🗯️','♠️','♥️','♦️','♣️','🃏','🎲','🧩','♟️','🎭','🎨','🖌️','🖍️','✂️','📌'] }; function initEmojiPicker() { const picker = DOM.emojiPicker; if (!picker) return; let html = ''; for (const [category, emojis] of Object.entries(EMOJI_DATA)) { html += `
${escapeHtml(category)}
`; for (const emoji of emojis) { html += ``; } html += '
'; } picker.innerHTML = html; } function toggleEmojiPicker() { const picker = DOM.emojiPicker; const btn = DOM.emojiToggleBtn; if (!picker || !btn) return; const isVisible = picker.classList.toggle('show'); btn.setAttribute('aria-expanded', isVisible ? 'true' : 'false'); if (isVisible) { // 将焦点移到第一个 emoji const firstEmoji = picker.querySelector('.emoji-item'); if (firstEmoji) firstEmoji.focus(); } } function insertEmoji(emoji) { const textarea = DOM.inputText; if (!textarea) return; const start = textarea.selectionStart; const end = textarea.selectionEnd; pushUndoState(); textarea.value = textarea.value.substring(0, start) + emoji + textarea.value.substring(end); textarea.selectionStart = textarea.selectionEnd = start + emoji.length; textarea.focus(); const picker = DOM.emojiPicker; const btn = DOM.emojiToggleBtn; if (picker) picker.classList.remove('show'); if (btn) btn.setAttribute('aria-expanded', 'false'); updatePreview(); autoSaveDraft(); scheduleRealtimePreview(); } // 点击外部关闭Emoji选择器 document.addEventListener('click', function(e) { const picker = DOM.emojiPicker; const btn = DOM.emojiToggleBtn; if (!picker || !btn) return; if (!e.target.closest('.emoji-picker') && !e.target.closest('#emojiToggleBtn')) { picker.classList.remove('show'); btn.setAttribute('aria-expanded', 'false'); } }); // ========== 模板管理 ========== function getTemplates() { try { return JSON.parse(localStorage.getItem('xhs_templates') || '[]'); } catch (e) { return []; } } function saveTemplates(templates) { try { localStorage.setItem('xhs_templates', JSON.stringify(templates)); } catch (e) { console.error('保存模板失败:', e); showError('保存模板失败,可能是存储空间不足'); } } // 配置字段定义 const CONFIG_FIELDS = [ { id: 'fontSize', type: 'value', default: '13' }, { id: 'lineHeight', type: 'value', default: '1.8' }, { id: 'watermarkText', type: 'value', default: '' }, { id: 'bgColor', type: 'value', default: 'white' }, { id: 'bgTexture', type: 'value', default: 'none' }, { id: 'stylePreset', type: 'value', default: 'custom' }, { id: 'aspectRatio', type: 'value', default: '2:3' }, { id: 'fontFamily', type: 'value', default: 'default' }, { id: 'enableHeader', type: 'checked', default: false }, { id: 'headerText', type: 'value', default: '' }, { id: 'headerStyle', type: 'value', default: 'center' }, { id: 'allowParagraphSplit', type: 'checked', default: false } ]; function getCurrentConfig() { const config = {}; for (const field of CONFIG_FIELDS) { const el = DOM[field.id]; if (el) { config[field.id] = field.type === 'checked' ? el.checked : el.value; } else { config[field.id] = field.default; } } return config; } function applyConfig(config) { for (const field of CONFIG_FIELDS) { const el = DOM[field.id]; if (el) { if (field.type === 'checked') { el.checked = config[field.id] || field.default; } else { el.value = config[field.id] || field.default; } } } toggleHeaderOptions(); updatePreviewSize(); } // 模态框管理 let activeModal = null; let modalFocusTrap = null; function createModal(className, title) { // 关闭现有模态框 closeActiveModal(); const overlay = document.createElement('div'); overlay.className = `modal-overlay ${className}`; overlay.setAttribute('role', 'dialog'); overlay.setAttribute('aria-modal', 'true'); overlay.setAttribute('aria-labelledby', 'modal-title'); const content = document.createElement('div'); content.className = 'modal-content'; const closeBtn = document.createElement('button'); closeBtn.className = 'modal-close-btn'; closeBtn.textContent = '关闭'; closeBtn.setAttribute('aria-label', '关闭模态框'); closeBtn.onclick = closeActiveModal; const titleEl = document.createElement('h3'); titleEl.id = 'modal-title'; titleEl.style.marginBottom = '15px'; titleEl.textContent = title; content.appendChild(closeBtn); content.appendChild(titleEl); overlay.appendChild(content); document.body.appendChild(overlay); // 焦点陷阱 modalFocusTrap = document.createElement('div'); modalFocusTrap.className = 'modal-focus-trap'; modalFocusTrap.setAttribute('tabindex', '0'); modalFocusTrap.onfocus = () => { const focusable = overlay.querySelectorAll('button, input, select, textarea, [tabindex]:not(.modal-focus-trap)'); if (focusable.length > 0) { focusable[0].focus(); } }; overlay.appendChild(modalFocusTrap); // 保存之前聚焦的元素 activeModal = { overlay, previousActive: document.activeElement, close: closeActiveModal }; // 将焦点移到模态框 setTimeout(() => { const focusable = overlay.querySelectorAll('button, input, select, textarea'); if (focusable.length > 0) { focusable[0].focus(); } }, 0); // 点击外部关闭 overlay.onclick = (e) => { if (e.target === overlay) closeActiveModal(); }; // Escape 关闭 const escapeHandler = (e) => { if (e.key === 'Escape') { closeActiveModal(); document.removeEventListener('keydown', escapeHandler); } }; document.addEventListener('keydown', escapeHandler); activeModal.escapeHandler = escapeHandler; return { overlay, content, close: closeActiveModal }; } function closeActiveModal() { if (activeModal) { if (activeModal.escapeHandler) { document.removeEventListener('keydown', activeModal.escapeHandler); } activeModal.overlay.remove(); // 恢复焦点 if (activeModal.previousActive) { activeModal.previousActive.focus(); } activeModal = null; modalFocusTrap = null; } } function showTemplateManager() { const templates = getTemplates(); const modal = createModal('template-manager', '模板管理'); let templateListHTML = ''; if (templates.length === 0) { templateListHTML = '

暂无保存的模板

'; } else { templates.forEach((t, i) => { const time = new Date(t.timestamp).toLocaleDateString('zh-CN'); templateListHTML += `
${escapeHtml(t.name)}
${escapeHtml(t.config.bgColor)} / ${escapeHtml(t.config.bgTexture)} / ${escapeHtml(time)}
`; }); } const saveSection = document.createElement('div'); saveSection.style.cssText = 'display: flex; gap: 10px; margin-bottom: 20px;'; saveSection.innerHTML = ` `; const listSection = document.createElement('div'); listSection.innerHTML = `

已保存的模板

${templateListHTML}`; modal.content.appendChild(saveSection); modal.content.appendChild(listSection); } function saveAsTemplate() { const nameInput = document.getElementById('templateName'); const name = nameInput ? nameInput.value.trim() : ''; if (!name) { showError('请输入模板名称'); return; } const templates = getTemplates(); templates.push({ name: name, config: getCurrentConfig(), timestamp: Date.now() }); saveTemplates(templates); closeActiveModal(); showTemplateManager(); } function applyTemplate(index) { const templates = getTemplates(); if (index >= 0 && index < templates.length) { applyConfig(templates[index].config); closeActiveModal(); const inputText = DOM.inputText?.value || ''; if (inputText.trim()) generatePages(); } } function deleteTemplate(index) { const templates = getTemplates(); if (index >= 0 && index < templates.length) { templates.splice(index, 1); saveTemplates(templates); closeActiveModal(); showTemplateManager(); } } // ========== 自动排版 ========== function showAutoLayoutPanel() { const inputText = DOM.inputText?.value || ''; if (!inputText.trim()) { showError('请先输入要排版的文本'); return; } const modal = createModal('auto-layout-panel', '✨ 自动排版'); const desc = document.createElement('p'); desc.style.cssText = 'font-size: 13px; color: var(--text-muted); margin-bottom: 15px;'; desc.textContent = '选择排版风格,自动优化文本格式和页面配置'; modal.content.appendChild(desc); const options = [ { key: 'lifestyle', icon: '🌸', title: '生活方式', desc: '温暖粉色系,适合日常分享、美食、穿搭' }, { key: 'knowledge', icon: '📚', title: '知识干货', desc: '简约笔记风,适合教程、攻略、干货分享' }, { key: 'emotion', icon: '💭', title: '情感语录', desc: '复古纸张风,适合情感表达、金句分享' }, { key: 'travel', icon: '🌍', title: '旅行风景', desc: '清新蓝绿系,适合旅行日记、风景记录' }, { key: 'minimal', icon: '✨', title: '极简黑白', desc: '纯净白底,适合专业内容、品牌展示' }, { key: 'auto', icon: '🤖', title: '智能识别', desc: '根据文本内容自动判断最佳排版风格' } ]; options.forEach(opt => { const div = document.createElement('div'); div.className = 'auto-layout-option'; div.setAttribute('tabindex', '0'); div.setAttribute('role', 'button'); div.innerHTML = `
${opt.icon}

${escapeHtml(opt.title)}

${escapeHtml(opt.desc)}

`; div.onclick = () => autoLayout(opt.key); div.onkeydown = (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); autoLayout(opt.key); } }; modal.content.appendChild(div); }); } const STYLE_CONFIGS = { lifestyle: { fontSize: '14', lineHeight: '2.0', bgColor: 'light-pink', bgTexture: 'memo', fontFamily: 'default' }, knowledge: { fontSize: '13', lineHeight: '1.8', bgColor: 'light-yellow', bgTexture: 'lines', fontFamily: 'default' }, emotion: { fontSize: '14', lineHeight: '2.0', bgColor: 'cream', bgTexture: 'paper', fontFamily: 'default' }, travel: { fontSize: '13', lineHeight: '1.8', bgColor: 'light-blue', bgTexture: 'dots', fontFamily: 'default' }, minimal: { fontSize: '13', lineHeight: '1.8', bgColor: 'white', bgTexture: 'none', fontFamily: 'default' } }; function autoLayout(style) { const textarea = DOM.inputText; if (!textarea) return; let text = textarea.value; text = autoFormatText(text, style); pushUndoState(); textarea.value = text; let config = STYLE_CONFIGS[style]; if (style === 'auto') { config = detectTextStyle(text); } if (config) { if (DOM.fontSize) DOM.fontSize.value = config.fontSize; if (DOM.lineHeight) DOM.lineHeight.value = config.lineHeight; if (DOM.bgColor) DOM.bgColor.value = config.bgColor; if (DOM.bgTexture) DOM.bgTexture.value = config.bgTexture; if (DOM.fontFamily) DOM.fontFamily.value = config.fontFamily; if (DOM.stylePreset) DOM.stylePreset.value = 'custom'; } closeActiveModal(); generatePages(); saveDraft(); } function detectTextStyle(text) { const keywords = { lifestyle: ['美食','做饭','穿搭','护肤','化妆','好物','推荐','种草','拔草','分享','日常','生活','厨房','食谱','OOTD','穿搭','美妆','口红','粉底','面膜'], knowledge: ['教程','攻略','方法','步骤','技巧','干货','学习','笔记','总结','指南','入门','进阶','原理','分析','对比','测评','评测','清单','checklist'], emotion: ['感悟','心情','语录','金句','治愈','温暖','想念','回忆','成长','人生','岁月','时光','幸福','孤独','坚强','勇敢','希望','梦想'], travel: ['旅行','旅游','景点','风景','出发','打卡','攻略','民宿','酒店','机票','签证','拍照','出片','路线','行程','自驾','徒步','海岛','古镇'] }; let scores = { lifestyle: 0, knowledge: 0, emotion: 0, travel: 0 }; const lowerText = text.toLowerCase(); for (const [style, words] of Object.entries(keywords)) { for (const word of words) { if (lowerText.includes(word.toLowerCase())) { scores[style] += 1; } } } const lines = text.split('\n'); const hasManyLists = lines.filter(l => /^[-*]\s|^\d+\.\s/.test(l.trim())).length > 3; const hasManyHeadings = lines.filter(l => /^#{1,3}\s/.test(l)).length > 2; const avgLineLen = text.length / lines.length; if (hasManyLists || hasManyHeadings) scores.knowledge += 3; if (avgLineLen < 20) scores.emotion += 2; // 修复:检测全角和半角感叹号 if (text.includes('!') || text.includes('!') || text.includes('✨') || text.includes('🔥')) scores.lifestyle += 2; const maxStyle = Object.entries(scores).sort((a, b) => b[1] - a[1])[0][0]; return STYLE_CONFIGS[maxStyle] || STYLE_CONFIGS.knowledge; } function autoFormatText(text, style) { let lines = text.split('\n'); let result = []; let inCodeBlock = false; for (let i = 0; i < lines.length; i++) { let line = lines[i]; if (line.trim().startsWith('```')) { inCodeBlock = !inCodeBlock; result.push(line); continue; } if (inCodeBlock) { result.push(line); continue; } if (/^#{1,3}\s/.test(line) || /^[-*]\s/.test(line) || /^\d+\.\s/.test(line) || /^>\s/.test(line) || /^---/.test(line) || line.trim() === '') { result.push(line); continue; } if (/^[一二三四五六七八九十]+[、.]/.test(line.trim()) && line.trim().length < 30) { line = '## ' + line.trim().replace(/^[一二三四五六七八九十]+[、.]\s*/, ''); result.push(line); continue; } if (/^第[一二三四五六七八九十\d]+[部分章节]/.test(line.trim()) && line.trim().length < 30) { line = '## ' + line.trim(); result.push(line); continue; } if (/^[①②③④⑤⑥⑦⑧⑨⑩]/.test(line.trim()) && line.trim().length < 30) { line = '### ' + line.trim(); result.push(line); continue; } result.push(line); } let finalLines = []; let emptyCount = 0; for (const line of result) { if (line.trim() === '') { emptyCount++; if (emptyCount <= 2) finalLines.push(line); } else { emptyCount = 0; finalLines.push(line); } } let formatted = []; for (let i = 0; i < finalLines.length; i++) { const line = finalLines[i]; const isHeading = /^#{1,3}\s/.test(line); if (isHeading && formatted.length > 0 && formatted[formatted.length - 1].trim() !== '') { formatted.push(''); } formatted.push(line); if (isHeading && i + 1 < finalLines.length && finalLines[i + 1].trim() !== '') { formatted.push(''); } } return formatted.join('\n'); } // ========== 代码块插入 ========== function insertCodeBlock() { insertAtCursor('```\n代码内容\n```', 4, 8); } // ========== 通用插入函数 ========== function insertAtCursor(text, selectStartOffset, selectEndOffset) { const textarea = DOM.inputText; if (!textarea) return; const start = textarea.selectionStart; const end = textarea.selectionEnd; const selectedText = textarea.value.substring(start, end); pushUndoState(); let replacement; if (selectedText) { replacement = text.replace('代码内容', selectedText).replace('请输入文字', selectedText); } else { replacement = text; } textarea.value = textarea.value.substring(0, start) + replacement + textarea.value.substring(end); textarea.selectionStart = start + selectStartOffset; textarea.selectionEnd = start + (selectEndOffset || selectStartOffset + 4); textarea.focus(); updatePreview(); autoSaveDraft(); scheduleRealtimePreview(); } // ========== 分页算法 ========== function splitTextIntoPages(text) { try { const fontSize = parseInt(DOM.fontSize?.value) || 13; const lineHeight = parseFloat(DOM.lineHeight?.value) || 1.8; const enableHeader = DOM.enableHeader?.checked || false; const aspectRatio = DOM.aspectRatio?.value || '2:3'; const allowParagraphSplit = DOM.allowParagraphSplit?.checked || false; const dimensions = getAspectRatioDimensions(aspectRatio); const pageHeight = dimensions.preview.height; const pageWidth = dimensions.preview.width; const padding = { top: 15, bottom: 20, left: 20, right: 20 }; const headerHeight = enableHeader ? 30 : 0; const footerHeight = 30; const availableHeight = pageHeight - padding.top - padding.bottom - headerHeight - footerHeight; const availableWidth = pageWidth - padding.left - padding.right; const lineHeightPx = fontSize * lineHeight; const maxLinesPerPage = Math.max(1, Math.floor(availableHeight / lineHeightPx) - 1); const charsPerLine = Math.floor(availableWidth / (fontSize * 0.9)); const rawLines = text.split('\n'); const paragraphs = []; let currentCodeBlock = null; for (let i = 0; i < rawLines.length; i++) { const line = rawLines[i]; if (line.trim().startsWith('```')) { if (currentCodeBlock !== null) { currentCodeBlock.lines.push(line); paragraphs.push(currentCodeBlock); currentCodeBlock = null; } else { currentCodeBlock = { text: line, lines: [line], isCodeBlock: true }; } continue; } if (currentCodeBlock !== null) { currentCodeBlock.lines.push(line); currentCodeBlock.text += '\n' + line; continue; } paragraphs.push({ text: line, lines: [line], isCodeBlock: false }); } if (currentCodeBlock !== null) { paragraphs.push(currentCodeBlock); } const processedParagraphs = paragraphs.map(para => { if (para.isCodeBlock) { const codeLines = para.lines.length + 1; return { ...para, lineCount: codeLines, isEmptyLine: false }; } const text = para.text; if (text.trim() === '') { return { ...para, lineCount: 1, isEmptyLine: true }; } const plainText = stripMarkdown(text); const headingMatch = text.match(/^(#{1,3})\s/); let lineMultiplier = 1; if (headingMatch) { lineMultiplier = headingMatch[1] === '#' ? 1.5 : headingMatch[1] === '##' ? 1.3 : 1.1; } if (/^>\s/.test(text)) lineMultiplier = 1.1; const linesNeeded = Math.max(1, Math.ceil(plainText.length / charsPerLine) * lineMultiplier); return { ...para, lineCount: Math.ceil(linesNeeded), isEmptyLine: false }; }); const pages = []; let currentPage = []; let currentPageLines = 0; for (let i = 0; i < processedParagraphs.length; i++) { const paragraph = processedParagraphs[i]; if (currentPageLines + paragraph.lineCount > maxLinesPerPage && currentPage.length > 0) { if (allowParagraphSplit && !paragraph.isCodeBlock && !paragraph.isEmptyLine && paragraph.lineCount > 1) { const remainingLines = maxLinesPerPage - currentPageLines; if (remainingLines >= 2) { const plainText = stripMarkdown(paragraph.text); const charsForRemaining = remainingLines * charsPerLine; const firstPart = paragraph.text.substring(0, Math.min(charsForRemaining, paragraph.text.length)); const secondPart = paragraph.text.substring(Math.min(charsForRemaining, paragraph.text.length)); if (secondPart.trim()) { currentPage.push(firstPart); pages.push(currentPage); const remainingPara = { ...paragraph, text: ' ' + secondPart.trim(), lineCount: Math.max(1, Math.ceil(secondPart.trim().length / charsPerLine)) }; currentPage = [remainingPara.text]; currentPageLines = remainingPara.lineCount; continue; } } } pages.push(currentPage); currentPage = [paragraph.text]; currentPageLines = paragraph.lineCount; } else { currentPage.push(paragraph.text); currentPageLines += paragraph.lineCount; } } if (currentPage.length > 0) { pages.push(currentPage); } if (pages.length === 0) { pages.push(['']); } return pages; } catch (error) { console.error('分页错误:', error); return [['']]; } } // ========== Markdown解析(安全版) ========== function parseMarkdown(text) { // 1. 处理代码块 let codeBlockMap = {}; let codeIdx = 0; text = text.replace(/```([\s\S]*?)```/g, function(match, code) { const key = `__CODE_BLOCK_${codeIdx}__`; codeBlockMap[key] = code.trim(); codeIdx++; return key; }); // 2. 处理行内代码(安全转义) text = text.replace(/`([^`]+)`/g, function(match, code) { return '' + escapeHtml(code) + ''; }); // 3. 处理表格 let tableMap = {}; let tableIdx = 0; text = text.replace(/((?:^\s*\|.*\|.*(?:\n|$))+)/gm, function(tableBlock) { const lines = tableBlock.trim().split('\n'); if (lines.length < 2) return tableBlock; if (!/^\s*\|?\s*(:?-+:?\s*\|)+\s*$/.test(lines[1])) return tableBlock; const key = `__TABLE_BLOCK_${tableIdx}__`; tableMap[key] = tableBlock; tableIdx++; return key; }); // 4. 处理分割线 text = text.replace(/^---+$/gm, '
'); // 5. 处理引用块(安全转义) text = text.replace(/^>\s*(.*$)/gim, function(match, content) { return '
' + escapeHtml(content) + '
'; }); // 6. 处理标题(安全转义) text = text .replace(/^### (.*$)/gim, function(match, content) { return '

' + escapeHtml(content) + '

'; }) .replace(/^## (.*$)/gim, function(match, content) { return '

' + escapeHtml(content) + '

'; }) .replace(/^# (.*$)/gim, function(match, content) { return '

' + escapeHtml(content) + '

'; }); // 7. 处理列表(安全转义) text = text.replace(/^[-*]\s+(.*$)/gim, function(match, content) { return '
  • ' + escapeHtml(content) + '
  • '; }); text = text.replace(/^\d+\.\s+(.*$)/gim, function(match, content) { return '
  • ' + escapeHtml(content) + '
  • '; }); // 合并连续的li为ul/ol text = text.replace(/((?:]*>.*?<\/li>(?:\n|$))+)/g, function(listBlock) { const items = listBlock.trim().split('\n').filter(l => l.trim()); const isOrdered = items.length > 0 && /list-style: decimal/.test(items[0]); const tag = isOrdered ? 'ol' : 'ul'; return `<${tag} class="md-list" style="margin: 0.3em 0; padding-left: 1.5em;">${items.join('')}`; }); // 8. 处理行内格式(安全转义) text = text .replace(/\*\*(.*?)\*\*/g, function(match, content) { return '' + escapeHtml(content) + ''; }) .replace(/\*(.*?)\*/g, function(match, content) { return '' + escapeHtml(content) + ''; }) .replace(/__(.*?)__/g, function(match, content) { return '' + escapeHtml(content) + ''; }) .replace(/~~(.*?)~~/g, function(match, content) { return '' + escapeHtml(content) + ''; }) .replace(/\[color:(#[a-fA-F0-9]{6})\](.*?)\[\/color\]/g, function(match, color, content) { return '' + escapeHtml(content) + ''; }); // 9. 替换回表格HTML Object.keys(tableMap).forEach(key => { text = text.replace(key, renderMarkdownTable(tableMap[key].split('\n'))); }); // 10. 替换回代码块HTML Object.keys(codeBlockMap).forEach(key => { const code = codeBlockMap[key].replace(//g, '>'); text = text.replace(key, `
    ${code}
    `); }); return text; } function renderMarkdownTable(lines) { if (lines.length < 2) return lines.join('
    '); const headerCells = lines[0].split('|').slice(1, -1).map(cell => cell.trim()); const rows = lines.slice(2).map(row => row.split('|').slice(1, -1).map(cell => cell.trim())); let html = ''; headerCells.forEach(cell => { html += ``; }); html += ''; rows.forEach(row => { html += ''; row.forEach(cell => { html += ``; }); html += ''; }); html += '
    ${parseMarkdown(cell)}
    ${parseMarkdown(cell)}
    '; return html; } // ========== 格式化文本 ========== function formatTextWithParagraphs(lines) { return lines.map((line, index) => { if (line.trim() === '') { return '

     

    '; } else { const isHeading = /^#{1,3}\s/.test(line); const isHr = /^---+$/.test(line.trim()); const isQuote = /^>\s/.test(line); const isList = /^[-*]\s/.test(line) || /^\d+\.\s/.test(line); const isCodeBlock = line.trim().startsWith('```'); if (isHeading || isHr || isQuote || isList) { return parseMarkdown(line); } else { const marginBottom = index === lines.length - 1 ? '0' : '0.3em'; return `

    ${parseMarkdown(line)}

    `; } } }).join(''); } // ========== 背景与纹理 ========== function getBackgroundColor(colorOption) { switch (colorOption) { case 'cream': return '#FFF8E7'; case 'light-pink': return '#FDF2F8'; case 'light-blue': return '#EFF6FF'; case 'light-green': return '#F0FDF4'; case 'light-yellow': return '#FEFCE8'; case 'light-purple': return '#FAF5FF'; case 'light-gray': return '#F9FAFB'; case 'gradient1': return 'linear-gradient(135deg, #FFE0EC 0%, #FFF5E0 100%)'; case 'gradient2': return 'linear-gradient(135deg, #E0F0FF 0%, #F0E0FF 100%)'; case 'gradient3': return 'linear-gradient(135deg, #F5E0FF 0%, #E0E8FF 100%)'; default: return 'white'; } } function getTextureOverlay(textureOption) { switch (textureOption) { case 'paper': return 'repeating-linear-gradient(123deg, transparent, transparent 2px, rgba(0,0,0,0.008) 2px, rgba(0,0,0,0.008) 3px), repeating-linear-gradient(27deg, transparent, transparent 1px, rgba(0,0,0,0.005) 1px, rgba(0,0,0,0.005) 2px), radial-gradient(circle at 20% 50%, rgba(0,0,0,0.01) 0%, transparent 50%), radial-gradient(circle at 80% 30%, rgba(0,0,0,0.01) 0%, transparent 50%)'; case 'dots': return 'radial-gradient(circle at 10px 10px, rgba(0,0,0,0.1) 1px, transparent 0), radial-gradient(circle at 10px 10px, rgba(0,0,0,0.1) 1px, transparent 0)'; case 'lines': return 'repeating-linear-gradient(0deg, transparent, transparent 23px, rgba(0,0,0,0.1) 24px)'; case 'grid': return 'repeating-linear-gradient(0deg, transparent, transparent 19px, rgba(0,0,0,0.05) 20px), repeating-linear-gradient(90deg, transparent, transparent 19px, rgba(0,0,0,0.05) 20px)'; case 'notebook': return 'repeating-linear-gradient(0deg, transparent, transparent 23px, rgba(0,0,0,0.1) 24px), linear-gradient(90deg, #ff69b4 80px, #ff69b4 82px, transparent 82px)'; case 'memo': return 'linear-gradient(45deg, rgba(255,255,255,0.1) 25%, transparent 25%, transparent 75%, rgba(255,255,255,0.1) 75%), linear-gradient(-45deg, rgba(255,255,255,0.1) 25%, transparent 25%, transparent 75%, rgba(255,255,255,0.1) 75%)'; default: return null; } } function applyBackground(element, bgColor, bgTexture) { const baseColor = getBackgroundColor(bgColor); element.style.background = baseColor; if (bgTexture !== 'none') { const textureOverlay = getTextureOverlay(bgTexture); if (textureOverlay) { element.style.background = textureOverlay + ', ' + baseColor; if (bgTexture === 'dots') { element.style.backgroundSize = '20px 20px, 20px 20px'; } else if (bgTexture === 'memo') { element.style.backgroundSize = '10px 10px, 10px 10px'; } } } } function applyPreset() { const preset = DOM.stylePreset?.value; if (!preset) return; const bgColorSelect = DOM.bgColor; const bgTextureSelect = DOM.bgTexture; if (!bgColorSelect || !bgTextureSelect) return; switch (preset) { case 'classic-memo': bgColorSelect.value = 'light-yellow'; bgTextureSelect.value = 'lines'; break; case 'modern-note': bgColorSelect.value = 'white'; bgTextureSelect.value = 'dots'; break; case 'vintage-paper': bgColorSelect.value = 'cream'; bgTextureSelect.value = 'paper'; break; case 'minimal-clean': bgColorSelect.value = 'white'; bgTextureSelect.value = 'none'; break; case 'warm-journal': bgColorSelect.value = 'light-pink'; bgTextureSelect.value = 'memo'; break; } } // ========== 图片比例 ========== function getAspectRatioDimensions(ratio) { const previewBase = 300; const downloadBase = 900; switch (ratio) { case '1:1': return { preview: { width: previewBase, height: previewBase }, download: { width: downloadBase, height: downloadBase } }; case '3:4': return { preview: { width: previewBase, height: Math.round(previewBase * 4 / 3) }, download: { width: downloadBase, height: Math.round(downloadBase * 4 / 3) } }; case '4:3': return { preview: { width: previewBase, height: Math.round(previewBase * 3 / 4) }, download: { width: downloadBase, height: Math.round(downloadBase * 3 / 4) } }; case '16:9': return { preview: { width: previewBase, height: Math.round(previewBase * 9 / 16) }, download: { width: downloadBase, height: Math.round(downloadBase * 9 / 16) } }; case '9:16': return { preview: { width: previewBase, height: Math.round(previewBase * 16 / 9) }, download: { width: downloadBase, height: Math.round(downloadBase * 16 / 9) } }; case '2:3': return { preview: { width: previewBase, height: Math.round(previewBase * 3 / 2) }, download: { width: downloadBase, height: Math.round(downloadBase * 3 / 2) } }; case 'custom': const customWidth = parseInt(DOM.customWidth?.value) || 900; const customHeight = parseInt(DOM.customHeight?.value) || 1600; const scale = previewBase / customWidth; return { preview: { width: previewBase, height: Math.round(customHeight * scale) }, download: { width: customWidth, height: customHeight } }; default: return { preview: { width: previewBase, height: Math.round(previewBase * 3 / 2) }, download: { width: downloadBase, height: Math.round(downloadBase * 3 / 2) } }; } } function updatePreviewSize() { const aspectRatio = DOM.aspectRatio?.value; if (DOM.customSizeOptions) { DOM.customSizeOptions.style.display = aspectRatio === 'custom' ? 'block' : 'none'; } } // ========== 字体管理 ========== function getFontFamilyCSS(fontFamily) { if (fontFamily === 'default') { return '-apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif'; } else if (fontFamily !== 'custom') { return `"${fontFamily}", -apple-system, BlinkMacSystemFont, sans-serif`; } return null; } function updateFontDisplay() { const fontFamily = DOM.fontFamily?.value; if (DOM.customFontUpload) { DOM.customFontUpload.style.display = fontFamily === 'custom' ? 'block' : 'none'; } } function loadCustomFont() { const fileInput = DOM.fontFile; if (!fileInput) return; const file = fileInput.files[0]; if (!file) { showError('请选择字体文件'); return; } const reader = new FileReader(); reader.onload = function(e) { try { const fontData = e.target.result; const fontFamily = 'CustomFont_' + Date.now(); const style = document.createElement('style'); style.textContent = `@font-face { font-family: "${fontFamily}"; src: url(${fontData}); }`; document.head.appendChild(style); // 应用到现有内容 const pageContentElements = document.querySelectorAll('.page-content'); pageContentElements.forEach(element => { element.style.fontFamily = `"${fontFamily}", sans-serif`; }); alert('字体加载成功!'); } catch (err) { showError('字体加载失败: ' + err.message); } }; reader.onerror = function() { showError('读取字体文件失败'); }; reader.readAsDataURL(file); } function showFontDownloadInfo() { const modal = createModal('font-info-modal', '免费中文字体资源'); const content = document.createElement('div'); content.style.cssText = 'max-width: 500px; text-align: left;'; content.innerHTML = `

    免费中文字体资源推荐:


    1. 思源字体系列:
    • 思源黑体 (Source Han Sans)
    • 思源宋体 (Source Han Serif)
    • 下载地址:GitHub 搜索 "adobe-fonts"

    2. 站酷字体系列:
    • 站酷高端黑、站酷快乐体、站酷文艺体
    • 下载地址:站酷网 (zcool.com.cn)

    3. 方正字库免费字体:
    • 方正黑体、方正楷体、方正仿宋
    • 下载地址:方正字库官网

    4. 文泉驿字体:
    • 文泉驿微米黑、文泉驿正黑
    • 下载地址:wenq.org

    使用说明:
    1. 下载字体文件(.ttf, .otf格式)
    2. 在"字体选择"中选择"上传自定义字体"
    3. 点击"选择文件"上传字体
    4. 点击"加载字体"即可使用
    `; modal.content.appendChild(content); } function toggleHeaderOptions() { const enableHeader = DOM.enableHeader?.checked || false; if (DOM.headerOptions) DOM.headerOptions.style.display = enableHeader ? 'block' : 'none'; if (DOM.headerStyleOptions) DOM.headerStyleOptions.style.display = enableHeader ? 'block' : 'none'; scheduleRealtimePreview(); } // ========== 生成页面 ========== function generatePages(silent) { try { const inputText = DOM.inputText?.value || ''; if (!inputText || !inputText.trim()) { if (!silent) showError('请输入要排版的文本'); return; } const fontSize = DOM.fontSize?.value || '13'; const lineHeight = DOM.lineHeight?.value || '1.8'; const watermarkText = DOM.watermarkText?.value || ''; const bgColor = DOM.bgColor?.value || 'white'; const bgTexture = DOM.bgTexture?.value || 'none'; const fontFamily = DOM.fontFamily?.value || 'default'; const enableHeader = DOM.enableHeader?.checked || false; const headerText = DOM.headerText?.value || ''; const headerStyle = DOM.headerStyle?.value || 'center'; currentPages = splitTextIntoPages(inputText); const previewContainer = DOM.previewContainer; if (!previewContainer) return; previewContainer.innerHTML = ''; const aspectRatio = DOM.aspectRatio?.value || '2:3'; const dimensions = getAspectRatioDimensions(aspectRatio); const fontCSS = getFontFamilyCSS(fontFamily); currentPages.forEach((pageTextArray, index) => { const pageDiv = document.createElement('div'); pageDiv.className = 'page-preview'; pageDiv.setAttribute('data-page-index', index); pageDiv.style.width = dimensions.preview.width + 'px'; pageDiv.style.height = dimensions.preview.height + 'px'; pageDiv.style.position = 'relative'; pageDiv.style.overflow = 'hidden'; applyBackground(pageDiv, bgColor, bgTexture); const contentWrapper = document.createElement('div'); contentWrapper.style.cssText = `position: absolute; top: ${enableHeader && headerText.trim() ? '70px' : '30px'}; left: 25px; right: 25px; bottom: 55px; overflow: hidden;`; if (enableHeader && headerText.trim()) { const headerDiv = document.createElement('div'); headerDiv.className = 'page-header'; headerDiv.style.cssText = `position: absolute; top: 0; left: 0; right: 0; padding: 15px 25px; font-size: ${Math.max(parseInt(fontSize) - 2, 12)}px; font-weight: bold; color: var(--page-text-secondary); text-align: ${headerStyle}; border-bottom: 1px solid var(--page-border); background: rgba(255, 255, 255, 0.5);`; if (fontCSS) headerDiv.style.fontFamily = fontCSS; headerDiv.textContent = headerText; pageDiv.appendChild(headerDiv); } const contentDiv = document.createElement('div'); contentDiv.className = 'page-content'; contentDiv.style.cssText = `font-size: ${fontSize}px; line-height: ${lineHeight}; color: var(--page-text); word-wrap: break-word; word-break: break-all;`; if (fontCSS) contentDiv.style.fontFamily = fontCSS; contentDiv.innerHTML = formatTextWithParagraphs(pageTextArray); contentWrapper.appendChild(contentDiv); pageDiv.appendChild(contentWrapper); const pageNumber = document.createElement('div'); pageNumber.className = 'page-number'; pageNumber.style.cssText = 'position: absolute; bottom: 15px; right: 20px; color: var(--page-text-muted); font-size: 12px;'; pageNumber.textContent = `${index + 1}/${currentPages.length}`; pageDiv.appendChild(pageNumber); if (watermarkText) { const watermark = document.createElement('div'); watermark.className = 'watermark'; watermark.style.cssText = 'position: absolute; bottom: 15px; left: 20px; color: var(--page-watermark); font-size: 12px;'; watermark.textContent = watermarkText; pageDiv.appendChild(watermark); } pageDiv.onclick = () => { currentPageIndex = index; document.querySelectorAll('.page-preview').forEach(p => p.style.border = '1px solid var(--page-border)'); pageDiv.style.border = '2px solid var(--brand-color)'; }; previewContainer.appendChild(pageDiv); }); if (currentPages.length > 0) { if (DOM.downloadSection) DOM.downloadSection.style.display = 'block'; const badge = DOM.pageCountBadge; if (badge) { badge.style.display = 'inline-flex'; badge.textContent = currentPages.length + ' 页'; } } } catch (error) { console.error('生成页面错误:', error); showError('生成页面时出现错误,请检查您的输入'); } } // ========== 下载功能 ========== function downloadPageByHtml2Canvas(pageIndex) { return new Promise((resolve, reject) => { const pageElement = document.querySelector(`[data-page-index="${pageIndex}"]`); if (!pageElement) { reject(new Error('页面元素不存在')); return; } html2canvas(pageElement, { scale: 4, useCORS: true }).then(canvas => { try { const url = canvas.toDataURL('image/png'); const a = document.createElement('a'); a.href = url; a.download = `小红书排版_第${pageIndex + 1}页.png`; document.body.appendChild(a); a.click(); document.body.removeChild(a); resolve(); } catch (err) { reject(err); } }).catch(err => { console.error(`下载第 ${pageIndex + 1} 页失败:`, err); reject(err); }); }); } function downloadCurrentPage() { downloadPageByHtml2Canvas(currentPageIndex).catch(err => { showError('下载失败: ' + err.message); }); } function downloadAllPages() { if (!confirm(`确定要下载全部 ${currentPages.length} 页吗?`)) return; downloadCancelled = false; // 创建进度条 const progressOverlay = document.createElement('div'); progressOverlay.style.cssText = 'position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 10000; display: flex; justify-content: center; align-items: center;'; const progressDiv = document.createElement('div'); progressDiv.className = 'download-progress'; progressDiv.innerHTML = `

    正在下载...

    0 / ${currentPages.length}
    `; progressOverlay.appendChild(progressDiv); document.body.appendChild(progressOverlay); document.getElementById('downloadCancelBtn').onclick = () => { downloadCancelled = true; progressOverlay.remove(); showError('下载已取消'); }; let completed = 0; const errors = []; async function downloadNext(index) { if (downloadCancelled) return; if (index >= currentPages.length) { progressOverlay.remove(); if (errors.length > 0) { showError(`${errors.length} 页下载失败,请重试`); } else { alert('全部下载完成!'); } return; } try { await downloadPageByHtml2Canvas(index); completed++; const fill = document.getElementById('downloadProgressFill'); const text = document.getElementById('downloadProgressText'); if (fill) fill.style.width = `${(completed / currentPages.length) * 100}%`; if (text) text.textContent = `${completed} / ${currentPages.length}`; } catch (err) { errors.push({ page: index + 1, error: err.message }); } // 延迟下载下一页,避免浏览器阻塞 setTimeout(() => downloadNext(index + 1), 500); } downloadNext(0); } // ========== 清空 ========== function clearAll() { if (!confirm('确定要清空所有内容吗?')) return; if (DOM.inputText) DOM.inputText.value = ''; if (DOM.previewContainer) DOM.previewContainer.innerHTML = ''; if (DOM.downloadSection) DOM.downloadSection.style.display = 'none'; currentPages = []; currentPageIndex = 0; undoStack = ['']; redoStack = []; updateUndoRedoButtons(); } // ========== 编辑器工具栏 ========== function insertHeading(level) { const textarea = DOM.inputText; if (!textarea) return; const start = textarea.selectionStart; const end = textarea.selectionEnd; const selectedText = textarea.value.substring(start, end); pushUndoState(); const lines = textarea.value.split('\n'); let currentLineIndex = 0; let currentPos = 0; for (let i = 0; i < lines.length; i++) { if (currentPos <= start && start <= currentPos + lines[i].length) { currentLineIndex = i; break; } currentPos += lines[i].length + 1; } if (selectedText) { const replacement = level + ' ' + selectedText; textarea.value = textarea.value.substring(0, start) + replacement + textarea.value.substring(end); textarea.selectionStart = start; textarea.selectionEnd = start + replacement.length; } else { const currentLine = lines[currentLineIndex]; const lineStart = currentPos; const lineEnd = lineStart + currentLine.length; if (currentLine.match(/^#{1,3}\s/)) { const newLine = level + ' ' + currentLine.replace(/^#{1,3}\s/, ''); textarea.value = textarea.value.substring(0, lineStart) + newLine + textarea.value.substring(lineEnd); textarea.selectionStart = lineStart; textarea.selectionEnd = lineStart + newLine.length; } else { const newLine = level + ' ' + currentLine; textarea.value = textarea.value.substring(0, lineStart) + newLine + textarea.value.substring(lineEnd); textarea.selectionStart = lineStart + level.length + 1; textarea.selectionEnd = lineStart + level.length + 1; } } textarea.focus(); updatePreview(); autoSaveDraft(); scheduleRealtimePreview(); } function insertMarkdown(startTag, endTag) { const textarea = DOM.inputText; if (!textarea) return; const start = textarea.selectionStart; const end = textarea.selectionEnd; const selectedText = textarea.value.substring(start, end); pushUndoState(); if (selectedText) { const replacement = startTag + selectedText + endTag; textarea.value = textarea.value.substring(0, start) + replacement + textarea.value.substring(end); textarea.selectionStart = start; textarea.selectionEnd = start + replacement.length; } else { const replacement = startTag + '请输入文字' + endTag; textarea.value = textarea.value.substring(0, start) + replacement + textarea.value.substring(end); textarea.selectionStart = start + startTag.length; textarea.selectionEnd = start + startTag.length + 4; } textarea.focus(); updatePreview(); autoSaveDraft(); scheduleRealtimePreview(); } function insertColor(color) { const textarea = DOM.inputText; if (!textarea) return; const start = textarea.selectionStart; const end = textarea.selectionEnd; const selectedText = textarea.value.substring(start, end); pushUndoState(); const startTag = `[color:${color}]`; const endTag = '[/color]'; if (selectedText) { const replacement = startTag + selectedText + endTag; textarea.value = textarea.value.substring(0, start) + replacement + textarea.value.substring(end); textarea.selectionStart = start; textarea.selectionEnd = start + replacement.length; } else { const replacement = startTag + '请输入文字' + endTag; textarea.value = textarea.value.substring(0, start) + replacement + textarea.value.substring(end); textarea.selectionStart = start + startTag.length; textarea.selectionEnd = start + startTag.length + 4; } textarea.focus(); updatePreview(); autoSaveDraft(); scheduleRealtimePreview(); } // ========== 预览 ========== function togglePreview() { const preview = DOM.markdownPreview; if (!preview) return; if (preview.style.display === 'none') { preview.style.display = 'block'; updatePreview(); } else { preview.style.display = 'none'; } } function updatePreview() { const inputText = DOM.inputText?.value || ''; const previewContent = DOM.previewContent; const preview = DOM.markdownPreview; if (previewContent && preview && preview.style.display !== 'none') { previewContent.innerHTML = parseMarkdown(inputText.replace(/\n/g, '
    ')); } } // ========== 快捷键系统 ========== document.addEventListener('keydown', function(e) { const textarea = DOM.inputText; if (!textarea) return; if (document.activeElement !== textarea) return; if (e.ctrlKey || e.metaKey) { switch (e.key.toLowerCase()) { case 'b': e.preventDefault(); insertMarkdown('**', '**'); break; case 'i': e.preventDefault(); insertMarkdown('*', '*'); break; case 'u': e.preventDefault(); insertMarkdown('__', '__'); break; case 'z': if (e.shiftKey) { e.preventDefault(); redo(); } else { e.preventDefault(); undo(); } break; case 'y': e.preventDefault(); redo(); break; case 's': e.preventDefault(); saveDraft(); break; } } }); // ========== 初始化 ========== document.addEventListener('DOMContentLoaded', function() { cacheDOMElements(); initDarkMode(); initEmojiPicker(); initRealtimePreview(); checkDraftOnLoad(); // 设置示例文本 const inputText = DOM.inputText; if (inputText) { inputText.value = `# 小红书长文排版工具使用指南 这是一个**示例文本**。小红书是一个生活方式分享平台,用户可以通过图片、视频和文字记录生活点滴,发现美好事物。 ## 平台特色 在这个平台上,你可以找到各种*生活灵感*。从[color:#ff4757]美食制作[/color]到旅行攻略,从护肤心得到穿搭技巧,每个人都能在这里找到自己感兴趣的内容。 ### 创作者分享 创作者们用心分享着自己的经验和见解。他们通过__精美的图片__和详细的文字,帮助其他用户解决生活中的各种问题。 > 分享是一种美德,让生活更美好。 这种分享精神让小红书成为了一个充满正能量的社区。 ## 长文排版挑战 对于长文创作者来说,如何将大段文字优雅地呈现在小红书上是一个~~挑战~~**机会**。 ### 解决方案 这个工具就是为了解决这个问题而设计的。它会自动分页确保内容不会覆盖水印和页码。 --- ## 使用技巧 - 使用标题按钮快速添加标题格式 - 支持 **加粗**、*斜体*、__下划线__、~~删除线~~ - 支持颜色标记:[color:#ff4757]红色文字[/color] - 支持引用块、列表、分割线、代码块 - 自动分页,智能排版 - 快捷键:Ctrl+B 加粗、Ctrl+I 斜体、Ctrl+U 下划线 ### 代码示例 \`\`\` console.log("Hello 小红书!"); \`\`\` > ✨ 试试"自动排版"功能,一键优化你的内容!`; undoStack = [inputText.value]; } // 事件监听器 if (inputText) { inputText.addEventListener('input', function() { autoSaveDraft(); scheduleRealtimePreview(); }); inputText.addEventListener('change', function() { pushUndoState(); }); } const fontSelect = DOM.fontFamily; if (fontSelect) { fontSelect.addEventListener('change', function() { updateFontDisplay(); scheduleRealtimePreview(); }); } const fontSizeInput = DOM.fontSize; if (fontSizeInput) { fontSizeInput.addEventListener('input', scheduleRealtimePreview); fontSizeInput.addEventListener('change', scheduleRealtimePreview); } const lineHeightInput = DOM.lineHeight; if (lineHeightInput) { lineHeightInput.addEventListener('input', scheduleRealtimePreview); lineHeightInput.addEventListener('change', scheduleRealtimePreview); } // 所有配置项变动时触发实时预览 const configIds = ['watermarkText', 'bgColor', 'bgTexture', 'enableHeader', 'headerText', 'headerStyle', 'allowParagraphSplit', 'customWidth', 'customHeight']; configIds.forEach(id => { const el = DOM[id]; if (el) { el.addEventListener('change', scheduleRealtimePreview); el.addEventListener('input', scheduleRealtimePreview); } }); // aspectRatio 需要先更新预览尺寸再触发实时预览 const aspectRatioSelect = DOM.aspectRatio; if (aspectRatioSelect) { aspectRatioSelect.addEventListener('change', function() { updatePreviewSize(); scheduleRealtimePreview(); }); } // 风格预设需要先applyPreset再触发预览 const stylePreset = DOM.stylePreset; if (stylePreset) { stylePreset.addEventListener('change', function() { applyPreset(); scheduleRealtimePreview(); }); } // 延迟自动生成预览 setTimeout(() => { try { generatePages(); } catch (e) { console.error('自动生成预览失败:', e); } }, 500); });