// ========== 工具函数 ==========
// 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('')}${tag}>`;
});
// 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 += `| ${parseMarkdown(cell)} | `; });
html += '
';
rows.forEach(row => {
html += '';
row.forEach(cell => { html += `| ${parseMarkdown(cell)} | `; });
html += '
';
});
html += '
';
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);
});