/** * YCat Editor - 核心功能模块 * 提取自 editor.html,优化性能和可维护性 */ // ==================== 全局对象兼容层 ==================== // 真实 OAuth 平台:前端中文 canonical 名 -> 后端客户端英文 key // 与 editor.html 的 OAUTH_KEYS 保持一致,避免加载顺序问题 if (typeof OAUTH_KEYS === 'undefined') { var OAUTH_KEYS = { '微信公众号': 'wechat', '知乎': 'zhihu', '小红书': 'xiaohongshu', 'B站': 'bilibili' }; } // 确保 AUTH 和 API 在全局可用 (function() { // AUTH fallback if (typeof AUTH === 'undefined') { window.AUTH = { isLoggedIn: function() { return !!localStorage.getItem('ycat_token'); }, getToken: function() { return localStorage.getItem('ycat_token') || ''; }, getUser: function() { try { return JSON.parse(localStorage.getItem('ycat_user') || 'null'); } catch { return null; } }, getAuthHeader: function() { const t = (localStorage.getItem('ycat_token') || ''); return t ? 'Bearer ' + t : ''; }, currentUser: function() { return this.getUser(); }, requireAuth: function() { return true; }, logout: function() { localStorage.removeItem('ycat_token'); localStorage.removeItem('ycat_user'); }, getAuthHeader: function() { return 'Bearer ' + (localStorage.getItem('ycat_token') || ''); }, refreshUser: function() { return Promise.resolve(null); } }; } else { window.AUTH = AUTH; } // API fallback - 仅在 window.API 不存在时设置 if (typeof window.API === 'undefined') { window.API = { createArticle: function() { return Promise.resolve({ article: { id: 1 } }); }, updateArticle: function() { return Promise.resolve({}); }, convertMarkdown: function() { return Promise.resolve({}); }, generateTitles: function() { return Promise.resolve({ titles: [] }); }, generateSummary: function() { return Promise.resolve({}); }, analyzeContent: function() { return Promise.resolve({}); }, seoSuggestions: function() { return Promise.resolve({}); }, polishContent: function() { return Promise.resolve({ content: '' }); }, expandContent: function() { return Promise.resolve({ content: '' }); }, condenseContent: function() { return Promise.resolve({ content: '' }); }, suggestPostingTime: function() { return Promise.resolve({}); }, htmlToMarkdown: function() { return Promise.resolve({}); }, importFromUrl: function() { return Promise.resolve({}); }, createScheduledPublish: function(data) { return (typeof window !== 'undefined' && window.API && window.API.createScheduledPublish) ? window.API.createScheduledPublish(data) : Promise.resolve({}); } }; } })(); // ==================== 编辑器核心 ==================== let editor = null; let markdownPreviewEnabled = false; let markdownPreviewTimer = null; let currentMarkdownText = ''; let editorInitialized = false; // 初始化 wangEditor // 安全的 Toast 包装:捕获 Toast 内部可能的异常,避免打断主流程 function safeToast(type, msg){ try{ if(typeof Toast !== 'undefined' && Toast[type]){ Toast[type](msg); } }catch(e){} } function initEditor() { if (editorInitialized) return; editorInitialized = true; const { createEditor, createToolbar } = window.wangEditor; editor = createEditor({ selector: '#editor-container', html: localStorage.getItem('ycat_editor_content') || '


', config: { placeholder: '开始创作...', onChange(editor) { // 自动保存到 localStorage const html = editor.getHtml(); try { localStorage.setItem('ycat_editor_content', html); } catch(e) { console.warn('localStorage 写入失败(配额满?)', e); } // 更新字数统计 updateWordCount(); // 触发自动保存 triggerAutoSave(); // Markdown 实时预览同步 if (markdownPreviewEnabled) { debounceMarkdownPreview(); } }, MENU_CONF: { uploadImage: { async customUpload(file, insertFn) { // 转换为 base64 const reader = new FileReader(); reader.onload = (e) => { insertFn(e.target.result); }; reader.readAsDataURL(file); } } } } }); window.editor = editor; // 暴露给 editor-outline.js(大纲跳转/实时同步) createToolbar({ editor, selector: '#toolbar-container', config: { excludeKeys: ['group-video', 'fullScreen'] } }); // 恢复 Markdown 预览状态 if (localStorage.getItem('ycat_markdown_preview') === '1') { toggleMarkdownPreview(true); } } // ==================== Markdown 实时预览 ==================== function debounceMarkdownPreview() { if (markdownPreviewTimer) { clearTimeout(markdownPreviewTimer); } markdownPreviewTimer = setTimeout(() => { updateMarkdownPreview(); }, 800); } async function updateMarkdownPreview() { if (!markdownPreviewEnabled) return; const content = getEditorContent(); const statusEl = document.getElementById('markdownPreviewStatus'); if (statusEl) { statusEl.textContent = '正在同步...'; statusEl.classList.add('syncing'); } try { // 优先使用后端 API 转换,降级到本地转换 let markdown = ''; if (typeof API !== 'undefined' && API.convertMarkdown) { const res = await API.convertMarkdown({ html: content, direction: 'html2md' }); if (res.ok && res.result) { markdown = res.result; } else { markdown = localHtmlToMarkdown(content); } } else { markdown = localHtmlToMarkdown(content); } currentMarkdownText = markdown; renderMarkdownPreview(markdown); if (statusEl) { statusEl.textContent = '已同步 ' + new Date().toLocaleTimeString(); statusEl.classList.remove('syncing'); } } catch (err) { console.error('Markdown 预览生成失败:', err); const fallback = localHtmlToMarkdown(content); currentMarkdownText = fallback; renderMarkdownPreview(fallback); if (statusEl) { statusEl.textContent = '本地同步'; statusEl.classList.remove('syncing'); } } } function localHtmlToMarkdown(html) { let md = html || ''; // 移除 style 属性 md = md.replace(/\s+style="[^"]*"/gi, ''); // 标题 md = md.replace(/]*>([\s\S]*?)<\/h1>/gi, '# $1\n\n'); md = md.replace(/]*>([\s\S]*?)<\/h2>/gi, '## $1\n\n'); md = md.replace(/]*>([\s\S]*?)<\/h3>/gi, '### $1\n\n'); md = md.replace(/]*>([\s\S]*?)<\/h4>/gi, '#### $1\n\n'); md = md.replace(/]*>([\s\S]*?)<\/h5>/gi, '##### $1\n\n'); md = md.replace(/]*>([\s\S]*?)<\/h6>/gi, '###### $1\n\n'); // 代码块 md = md.replace(/]*>]*>([\s\S]*?)<\/code><\/pre>/gi, (match, code) => { const decoded = code.replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&').replace(/"/g, '"'); return '```\n' + decoded + '\n```\n\n'; }); // 行内代码 md = md.replace(/]*>([\s\S]*?)<\/code>/gi, '`$1`'); // 粗体+斜体 md = md.replace(/<(strong|b)><(em|i)>([\s\S]*?)<\/(em|i)><\/(strong|b)>/gi, '***$3***'); md = md.replace(/<(em|i)><(strong|b)>([\s\S]*?)<\/(strong|b)><\/(em|i)>/gi, '***$3***'); md = md.replace(/<(strong|b)>([\s\S]*?)<\/(strong|b)>/gi, '**$2**'); md = md.replace(/<(em|i)>([\s\S]*?)<\/(em|i)>/gi, '*$2*'); md = md.replace(/<(del|s|strike)>([\s\S]*?)<\/(del|s|strike)>/gi, '~~$2~~'); // 图片和链接 md = md.replace(/]*src="([^"]*)"[^>]*alt="([^"]*)"[^>]*\/?>/gi, '![$2]($1)'); md = md.replace(/]*src="([^"]*)"[^>]*\/?>/gi, '![]($1)'); md = md.replace(/]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi, '[$2]($1)'); // 引用 md = md.replace(/]*>([\s\S]*?)<\/blockquote>/gi, (match, content) => { return content.trim().split('\n').map(l => '> ' + l.trim()).join('\n') + '\n\n'; }); // 列表 md = md.replace(/]*>([\s\S]*?)<\/ul>/gi, '$1\n'); md = md.replace(/]*>([\s\S]*?)<\/ol>/gi, '$1\n'); md = md.replace(/]*>([\s\S]*?)<\/li>/gi, '- $1\n'); // 水平线 md = md.replace(/]*\/?>/gi, '---\n\n'); // 段落和换行 md = md.replace(/]*>([\s\S]*?)<\/p>/gi, '$1\n\n'); md = md.replace(//gi, '\n'); // 清理剩余 HTML 标签和实体 md = md.replace(/<[^>]+>/g, ''); md = md.replace(/ /g, ' '); md = md.replace(/&/g, '&'); md = md.replace(/</g, '<'); md = md.replace(/>/g, '>'); md = md.replace(/"/g, '"'); // 清理空行 md = md.replace(/\n{3,}/g, '\n\n'); return md.trim(); } function markdownToHtml(md) { if (!md) return ''; const lines = md.replace(/\r\n?/g, '\n').split('\n'); let html = ''; let i = 0; let para = []; const flush = () => { if (para.length) { html += '

' + inlineMd(para.join(' ')) + '

\n'; para = []; } }; while (i < lines.length) { const line = lines[i]; if (/^```/.test(line)) { flush(); const code = []; i++; while (i < lines.length && !/^```/.test(lines[i])) { code.push(lines[i]); i++; } i++; html += '
' + escapeHtml(code.join('\n')) + '
\n'; continue; } const h = line.match(/^(#{1,6})\s+(.*)$/); if (h) { flush(); const lv = h[1].length; html += `${inlineMd(h[2])}\n`; i++; continue; } if (/^(\s*[-*_]){3,}\s*$/.test(line)) { flush(); html += '
\n'; i++; continue; } if (/^>\s?/.test(line)) { flush(); const q = []; while (i < lines.length && /^>\s?/.test(lines[i])) { q.push(lines[i].replace(/^>\s?/, '')); i++; } html += '
' + inlineMd(q.join(' ')) + '
\n'; continue; } if (/^\s*([-*+])\s+/.test(line) || /^\s*\d+\.\s+/.test(line)) { flush(); let list = ''; const ordered = /^\s*\d+\.\s+/.test(line); while (i < lines.length && (/^\s*([-*+])\s+/.test(lines[i]) || /^\s*\d+\.\s+/.test(lines[i]))) { const item = lines[i].replace(/^\s*(?:[-*+]|\d+\.)\s+/, ''); list += '
  • ' + inlineMd(item) + '
  • \n'; i++; } html += (ordered ? '
      ' : '
        ') + list + (ordered ? '
    ' : '') + '\n'; continue; } if (line.trim() === '') { flush(); i++; continue; } para.push(line); i++; } flush(); return html; } function inlineMd(text) { let t = escapeHtml(text); t = t.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '$1'); t = t.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1'); t = t.replace(/`([^`]+)`/g, '$1'); t = t.replace(/\*\*([^*]+)\*\*/g, '$1'); t = t.replace(/(^|[^*])\*([^*]+)\*(?!\*)/g, '$1$2'); t = t.replace(/~~([^~]+)~~/g, '$1'); return t; } function renderMarkdownPreview(markdown) { const previewContent = document.getElementById('markdown-preview-content'); if (!previewContent) return; if (!markdown || markdown === '


    ') { previewContent.innerHTML = '

    在编辑器中输入文字,即可实时生成 Markdown 预览。

    '; return; } // 转义 HTML 后添加语法高亮 let html = escapeHtml(markdown); // 代码块(需先处理,避免内部被误解析) html = html.replace(/^(```[\s\S]*?```)$/gm, (match) => { return '' + match + ''; }); // 标题 html = html.replace(/^(#{1,6}\s+)(.+)$/gm, '$1$2'); // 粗体 html = html.replace(/(\*\*[\s\S]*?\*\*)/g, '$1'); // 斜体(避免误匹配粗体) html = html.replace(/(?*$1*'); // 行内代码 html = html.replace(/(`[^`]+`)/g, '$1'); // 图片 html = html.replace(/(!\[[^\]]*\]\([^)]+\))/g, '$1'); // 链接 html = html.replace(/(\[[^\]]+\]\([^)]+\))/g, '$1'); // 引用 html = html.replace(/^(>\s+.*)$/gm, '$1'); // 列表 html = html.replace(/^([-*+]\s+.+)$/gm, '$1'); html = html.replace(/^(\d+\.\s+.+)$/gm, '$1'); // 水平线 html = html.replace(/^(-{3,})$/gm, '$1'); previewContent.innerHTML = '
    ' + markdownToHtml(markdown) + '
    '; } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } function toggleMarkdownPreview(forceState) { const app = document.getElementById('editor-app'); const articleEditor = document.getElementById('article-editor'); const previewContainer = document.getElementById('markdown-preview-container'); const toggleBtn = document.getElementById('markdownPreviewToggle'); if (typeof forceState === 'boolean') { markdownPreviewEnabled = forceState; } else { markdownPreviewEnabled = !markdownPreviewEnabled; } localStorage.setItem('ycat_markdown_preview', markdownPreviewEnabled ? '1' : '0'); // 分屏模式:预览开启时编辑区与预览同屏并排,编辑器始终可编辑 if (app) app.classList.toggle('preview-on', markdownPreviewEnabled); if (articleEditor) { const isVideo = document.getElementById('videoUploadArea') && document.getElementById('videoUploadArea').style.display !== 'none'; // 预览开启不再隐藏编辑器(分屏模式始终可编辑),仅视频模式隐藏 articleEditor.style.display = isVideo ? 'none' : ''; } if (previewContainer) { previewContainer.style.display = markdownPreviewEnabled ? 'flex' : 'none'; } if (toggleBtn) { toggleBtn.classList.toggle('active', markdownPreviewEnabled); } if (markdownPreviewEnabled) { updateMarkdownPreview(); } } function copyMarkdownPreview() { if (!currentMarkdownText) { safeToast.warning('没有可复制的内容'); return; } navigator.clipboard.writeText(currentMarkdownText).then(() => { safeToast.success('Markdown 已复制到剪贴板'); }).catch(() => { safeToast.error('复制失败'); }); } function downloadMarkdownPreview() { const rawTitle = document.getElementById('articleTitle')?.value?.trim() || '未命名'; // 清洗文件名:去除文件系统非法字符,空格转下划线 const title = rawTitle.replace(/[\\/:*?"<>|]+/g, '').replace(/\s+/g, '_') || '未命名'; if (!currentMarkdownText) { safeToast.warning('没有可下载的内容'); return; } const blob = new Blob([currentMarkdownText], { type: 'text/markdown' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `${title}.md`; a.click(); URL.revokeObjectURL(url); safeToast.success('Markdown 下载已触发'); } // 键盘快捷键:Ctrl+M 切换 Markdown 预览 document.addEventListener('keydown', (e) => { if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'm') { e.preventDefault(); toggleMarkdownPreview(); } }); // 获取编辑器内容 function getEditorContent() { return editor ? editor.getHtml() : ''; } // 设置编辑器内容 function setEditorContent(html) { if (editor) { editor.setHtml(html); } } // ==================== 字数统计 ==================== function updateWordCount() { const content = getEditorContent(); const text = stripHtml(content).replace(/\s+/g, ''); const wordCount = text.length; const countEl = document.getElementById('wordCount'); if (countEl) countEl.textContent = wordCount.toLocaleString(); updateFloatingSeoScore(); } // ==================== SEO 实时评分 ==================== let seoDebounceTimer = null; const SEO_COLORS = { pass: '#10b981', warn: '#eab308', fail: '#ef4444', empty: '#6b7280' }; async function runSEOScore() { const btn = document.getElementById('seoRefreshBtn'); if (btn) { btn.textContent = '分析中…'; btn.disabled = true; } try { const title = (document.getElementById('articleTitle') || document.querySelector('[id=title]') || { value: '' }).value || ''; const content = getEditorContent(); const tagEls = document.querySelectorAll('.tag-item'); const keywords = Array.from(tagEls).map(el => el.textContent.replace(/×$/, '').trim()).filter(Boolean); const resp = await fetch('/api/seo-score/analyze', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': AUTH.getAuthHeader() }, body: JSON.stringify({ title, content, keywords }) }); const j = await resp.json(); if (!j.success) return; // 更新圆环 const circumference = 226.2; const pct = j.percentage / 100; const fill = document.getElementById('seoRingFill'); if (fill) { fill.setAttribute('stroke-dashoffset', circumference * (1 - pct)); fill.setAttribute('stroke', j.gradeColor || '#ef4444'); } const gradeEl = document.getElementById('seoGrade'); const pctEl = document.getElementById('seoPct'); if (gradeEl) gradeEl.textContent = j.grade || '-'; if (gradeEl) gradeEl.style.color = j.gradeColor || '#6b7280'; if (pctEl) pctEl.textContent = (j.percentage || 0) + '%'; // 更新四维评分 updateSEODim('Title', j.breakdown.title, 'seoTitle'); updateSEODim('Content', j.breakdown.content, 'seoContent'); updateSEODim('Keywords', j.breakdown.keywords, 'seoKeywords'); updateSEODim('Structure', j.breakdown.structure, 'seoStructure'); // 更新建议 const tipsEl = document.getElementById('seoTips'); if (tipsEl) { tipsEl.innerHTML = ''; const PRIORITY_COLORS = { high: '#ef4444', medium: '#f97316', low: '#6b7280' }; (j.tips || []).slice(0, 3).forEach(tip => { const badge = document.createElement('div'); badge.style.cssText = 'font-size:11px; color:var(--text-2); display:flex; align-items:flex-start; gap:5px; padding:5px 8px; background:var(--bg); border-radius:6px;'; badge.innerHTML = `●${tip.text}`; tipsEl.appendChild(badge); }); } } catch (err) { console.warn('SEO评分失败:', err); } finally { if (btn) { btn.textContent = '刷新'; btn.disabled = false; } } } function updateSEODim(label, data, prefix) { if (!data) return; const scoreEl = document.getElementById(prefix + 'Score'); const barEl = document.getElementById(prefix + 'Bar'); if (scoreEl) scoreEl.textContent = data.score + '/' + data.max; if (barEl) { const pct = (data.score / data.max) * 100; barEl.style.width = pct + '%'; barEl.style.background = SEO_COLORS[data.status] || '#6b7280'; } } // 内容变化时防抖触发 SEO 评分 function scheduleSEOScore() { if (seoDebounceTimer) clearTimeout(seoDebounceTimer); seoDebounceTimer = setTimeout(runSEOScore, 2000); } // 在 updateWordCount 中顺便触发 SEO const _origUpdateWordCount = updateWordCount; updateWordCount = function() { _origUpdateWordCount.apply(this, arguments); scheduleSEOScore(); }; // 编辑器变化时也触发 if (typeof editor !== 'undefined' && editor && editor.onChange) { editor.onChange(() => scheduleSEOScore()); } // 标题变化时立即触发 const titleInput = document.getElementById('articleTitle') || document.querySelector('[id=title]'); if (titleInput) { titleInput.addEventListener('input', () => { if (seoDebounceTimer) clearTimeout(seoDebounceTimer); seoDebounceTimer = setTimeout(runSEOScore, 1500); }); } function stripHtml(html) { if (!html) return ''; return String(html).replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim(); } function calculateReadTime(content) { const text = stripHtml(content); const chineseChars = (text.match(/[\u4e00-\u9fa5]/g) || []).length; const otherWords = text.replace(/[\u4e00-\u9fa5]/g, '').split(/\s+/).filter(w => w.length > 0).length; const minutes = Math.ceil((chineseChars / 400) + (otherWords / 250)); return Math.max(1, minutes); } // ==================== 自动保存 ==================== let autoSaveTimer = null; let lastSaveTime = null; // 版本历史 / 自动快照功能已移除(2026-07-20) function triggerAutoSave() { // 清除之前的定时器 if (autoSaveTimer) { clearTimeout(autoSaveTimer); } // 3秒后保存 autoSaveTimer = setTimeout(async () => { await saveDraft(); }, 3000); } async function saveDraft() { // 拼装完整文章 payload(封面 / 标签 / 分类 / 摘要),保存与发布共用 function buildArticlePayload() { const title = (document.getElementById('articleTitle')?.value || '').trim(); const content = getEditorContent(); const cover = coverDataUrl || ''; const tagsStr = Array.isArray(tags) ? tags.join(',') : ''; const categoryEl = document.getElementById('categorySelect'); const categoryVal = categoryEl ? categoryEl.value : ''; const summaryEl = document.getElementById('articleSummary'); const summaryVal = summaryEl ? summaryEl.value.trim() : ''; return { title: title || '无标题', content, summary: summaryVal || content.replace(/<[^>]*>/g, '').slice(0, 200), tags: tagsStr, category: categoryVal, cover: cover, platformCategories: (window._platformCategories || {}) }; } // 暴露给内联脚本的 publishArticle 复用,避免重复构造丢字段 if (typeof window !== 'undefined') window.buildArticlePayload = buildArticlePayload; const title = document.getElementById('articleTitle')?.value?.trim(); const content = getEditorContent(); if (!title && !content) { return; } try { // 检查是否有已存在的文章 ID const articleId = localStorage.getItem('ycat_current_article_id'); const payload = buildArticlePayload(); if (articleId) { await API.updateArticle(parseInt(articleId), payload); } else { const res = await API.createArticle(payload); if (res.article?.id) { localStorage.setItem('ycat_current_article_id', res.article.id); } } lastSaveTime = new Date(); showAutoSaveIndicator(); } catch (err) { console.error('自动保存失败:', err); } } function showAutoSaveIndicator() { // 轻量反馈:Toast 告知已保存(若可用) try { if (typeof Toast !== 'undefined' && Toast.success) safeToast.success('已保存'); } catch(e) {} // 更新底部 action bar 保存态(页面真实存在的元素) const status = document.getElementById('saveStatus'); const statusText = document.getElementById('saveStatusText'); if (status) { status.classList.add('saved'); if (statusText) statusText.textContent = lastSaveTime ? ('已保存 ' + lastSaveTime.toLocaleTimeString()) : '已保存'; } // 同步顶部保存态(#topSaveState 内部含 .ed-dot) const top = document.getElementById('topSaveState'); if (top) { top.classList.add('saved'); top.innerHTML = '' + (lastSaveTime ? ('已保存 ' + lastSaveTime.toLocaleTimeString()) : '已保存'); } const col = document.getElementById('colSaveState'); if (col) col.textContent = lastSaveTime ? ('已保存 ' + lastSaveTime.toLocaleTimeString()) : '已保存'; } // ==================== 标签管理 ==================== let tags = []; function handleTag(e) { if (e.key === 'Enter' && e.target.value.trim()) { e.preventDefault(); const tag = e.target.value.trim(); if (!tags.includes(tag) && tags.length < 5) { tags.push(tag); renderTags(); updateFloatingSeoScore(); } e.target.value = ''; } } function renderTags() { const container = document.getElementById('tagsContainer'); if (!container) return; container.innerHTML = tags.map(tag => ` ${tag} × `).join(''); } function removeTag(tag) { tags = tags.filter(t => t !== tag); renderTags(); updateFloatingSeoScore(); } // ==================== 封面图上传 ==================== let coverDataUrl = null; let coverOriginal = null; let coverDeleteArmed = false; let coverDeleteTimer = null; // 各比例裁切值(宽/高) const COVER_RATIO_VALUES = { '1:1': 1, '16:9': 16/9, '3:4': 3/4, '9:16': 9/16, '4:3': 4/3, '2.35:1': 2.35 }; // 比例缩略图对应的 DOM id(必须与 editor.html 中的 id 一致) const COVER_RATIO_THUMB = { '1:1': 'coverSquare', '16:9': 'coverWide', '3:4': 'coverVertical', '9:16': 'coverStory', '4:3': 'coverStandard', '2.35:1': 'coverCinema' }; function handleCoverFile(event) { const file = event.target.files[0]; if (!file) return; // 验证文件类型 if (!file.type.startsWith('image/')) { safeToast.error('请选择图片文件'); return; } // 验证文件大小 (最大 5MB) if (file.size > 5 * 1024 * 1024) { safeToast.error('图片大小不能超过 5MB'); return; } const _ld = document.getElementById('coverLoading'); if (_ld) _ld.style.display = 'flex'; coverDeleteArmed = false; const _db = document.getElementById('coverDeleteBtn'); if (_db) { _db.textContent = '删除'; _db.classList.remove('danger'); } const reader = new FileReader(); reader.onload = (e) => { coverOriginal = e.target.result; coverDataUrl = e.target.result; showCoverPreview(coverDataUrl, file.name); const _ld = document.getElementById('coverLoading'); if (_ld) _ld.style.display = 'none'; }; reader.onerror = () => { const _ld = document.getElementById('coverLoading'); if (_ld) _ld.style.display = 'none'; safeToast.error('图片读取失败'); }; reader.readAsDataURL(file); } function showCoverPreview(dataUrl, fileName) { const preview = document.getElementById('coverPreview'); const uploadBtn = document.getElementById('coverUploadBtn'); const previewImg = document.getElementById('coverPreviewImg'); const grid = document.getElementById('coverRatioGrid'); if (preview && uploadBtn && previewImg) { previewImg.src = dataUrl; preview.style.display = 'block'; uploadBtn.style.display = 'none'; // 上传后才显示比例选择网格,删除后由 deleteCover 隐藏 if (grid) grid.style.display = 'grid'; // 基于原始封面生成各比例缩略图 generateRatioThumbnails(coverOriginal || dataUrl); // 显示文件名与像素尺寸 const info = document.getElementById('coverFileInfo'); if (info) { const render = (w, h) => { info.textContent = ''; if (fileName) { const n = document.createElement('span'); n.className = 'fi-name'; n.textContent = fileName; info.appendChild(n); } if (w) { const d = document.createElement('span'); d.textContent = w + '×' + h; info.appendChild(d); } info.style.display = (fileName || w) ? 'flex' : 'none'; }; render(0, 0); const _im = new Image(); _im.onload = () => render(_im.naturalWidth, _im.naturalHeight); _im.src = dataUrl; } // 复位删除确认按钮 coverDeleteArmed = false; const _db = document.getElementById('coverDeleteBtn'); if (_db) { _db.textContent = '删除'; _db.classList.remove('danger'); } } } function cropCoverToRatio(dataUrl, value) { return new Promise((resolve) => { const img = new Image(); img.onload = () => { const imgRatio = img.width / img.height; let sx, sy, sw, sh; if (imgRatio > value) { sh = img.height; sw = sh * value; sx = (img.width - sw) / 2; sy = 0; } else { sw = img.width; sh = sw / value; sx = 0; sy = (img.height - sh) / 2; } const canvas = document.createElement('canvas'); canvas.width = Math.max(1, Math.round(sw)); canvas.height = Math.max(1, Math.round(sh)); canvas.getContext('2d').drawImage(img, sx, sy, sw, sh, 0, 0, canvas.width, canvas.height); resolve(canvas.toDataURL('image/jpeg', 0.9)); }; img.onerror = () => resolve(dataUrl); img.src = dataUrl; }); } // 基于原始封面生成各比例缩略图(避免重复裁切失真) function generateRatioThumbnails(dataUrl) { if (!dataUrl) return; Object.entries(COVER_RATIO_VALUES).forEach(([ratio, value]) => { const thumbId = COVER_RATIO_THUMB[ratio]; const thumbEl = document.getElementById(thumbId); if (!thumbEl) return; cropCoverToRatio(dataUrl, value).then((cropped) => { thumbEl.src = cropped; }); }); } function deleteCover() { coverDataUrl = null; coverOriginal = null; const preview = document.getElementById('coverPreview'); const uploadBtn = document.getElementById('coverUploadBtn'); const grid = document.getElementById('coverRatioGrid'); const _badge = document.getElementById('coverRatioBadge'); if (_badge) _badge.style.display = 'none'; if (preview && uploadBtn) { preview.style.display = 'none'; uploadBtn.style.display = 'block'; } if (grid) grid.style.display = 'none'; } // ==================== 裁剪功能 ==================== let cropData = null; let selectedRatio = '1:1'; function cropCover() { if (!coverDataUrl) return; const modal = document.getElementById('cropModal'); const cropImage = document.getElementById('cropImage'); if (modal && cropImage) { cropImage.src = coverDataUrl; modal.style.display = 'flex'; initCropFrame(); } } function initCropFrame() { const container = document.querySelector('.crop-container'); const frame = document.querySelector('.crop-frame'); if (!container || !frame) return; // 根据选择的比例设置裁剪框 const ratios = { '1:1': 1, '16:9': 16/9, '3:4': 3/4, '9:16': 9/16, '4:3': 4/3 }; const ratio = ratios[selectedRatio] || 1; // 初始位置和大小 let frameWidth = 300; let frameHeight = frameWidth / ratio; frame.style.width = frameWidth + 'px'; frame.style.height = frameHeight + 'px'; frame.style.left = '50px'; frame.style.top = '50px'; // TODO: 实现拖拽和调整大小 } function selectCoverRatio(element) { if (!coverOriginal) return; // 移除其他选中状态 document.querySelectorAll('.cover-ratio-item').forEach(item => { item.classList.remove('active'); }); // 添加当前选中状态 element.classList.add('active'); selectedRatio = element.dataset.ratio; // 卡片标题同步显示“已应用”比例徽标,给用户明确反馈 const _badge = document.getElementById('coverRatioBadge'); if (_badge) { _badge.textContent = selectedRatio + ' · 已应用'; _badge.style.display = 'inline-block'; } const value = COVER_RATIO_VALUES[selectedRatio] || 1; // 按所选比例中心裁切,并更新主预览与保存用封面 cropCoverToRatio(coverOriginal, value).then((cropped) => { coverDataUrl = cropped; const previewImg = document.getElementById('coverPreviewImg'); if (previewImg) previewImg.src = cropped; }); } function applyCrop() { // TODO: 实现实际裁剪逻辑 safeToast.success('裁剪成功'); closeCropModal(); } function closeCropModal() { const modal = document.getElementById('cropModal'); if (modal) { modal.style.display = 'none'; } } // ==================== 发布功能 ==================== async function publishContent() { const title = document.getElementById('articleTitle')?.value?.trim(); const content = getEditorContent(); if (!title) { safeToast.warning('请输入标题'); return; } if (!content || content === '


    ') { safeToast.warning('请输入内容'); return; } try { // 先保存文章 const articleId = localStorage.getItem('ycat_current_article_id'); if (articleId) { await API.updateArticle(parseInt(articleId), { title, content, summary: content.replace(/<[^>]*>/g, '').slice(0, 200) }); } else { const res = await API.createArticle({ title, content, summary: content.replace(/<[^>]*>/g, '').slice(0, 200), tags: tags.join(','), category: '' }); if (res.article?.id) { localStorage.setItem('ycat_current_article_id', res.article.id); } } // 跳转到分发页面 window.location.href = 'distribute.html'; } catch (err) { safeToast.error('发布失败'); console.error(err); } } // ==================== 定时发布 ==================== let pendingSchedule = null; // ---- 定时发布状态持久化(刷新后不丢失)---- const SCHEDULE_KEY = 'ycat_pending_schedule_v1'; function currentArticleId(){ try { return localStorage.getItem('ycat_current_article_id') || 'new'; } catch(e){ return 'new'; } } function persistSchedule(){ try { if (pendingSchedule) { pendingSchedule.articleId = currentArticleId(); localStorage.setItem(SCHEDULE_KEY, JSON.stringify(pendingSchedule)); } else { localStorage.removeItem(SCHEDULE_KEY); } } catch(e) {} } function restoreSchedule(){ try { const raw = localStorage.getItem(SCHEDULE_KEY); if (!raw) return; const s = JSON.parse(raw); if (!s || !s.iso) { localStorage.removeItem(SCHEDULE_KEY); return; } if (new Date(s.iso) <= new Date()) { localStorage.removeItem(SCHEDULE_KEY); return; } if (s.articleId && s.articleId !== currentArticleId()) { localStorage.removeItem(SCHEDULE_KEY); return; } delete s.articleId; pendingSchedule = s; applyScheduledUI(); } catch(e) { try { localStorage.removeItem(SCHEDULE_KEY); } catch(_){} } } function formatScheduleLabel(iso){ const d = new Date(iso); if (isNaN(d.getTime())) return iso; const p = n => String(n).padStart(2,'0'); return p(d.getMonth()+1)+'-'+p(d.getDate())+' '+p(d.getHours())+':'+p(d.getMinutes()); } function applyScheduledUI(){ const b=document.getElementById('publishBtn'); const lbl=document.getElementById('publishBtnLabel'); const t=document.getElementById('schedToggle'); const tlabel=document.getElementById('schedLabel'); if(pendingSchedule){ if(b) b.classList.add('is-scheduled'); if(lbl) lbl.textContent='已设置定时发布'; if(t){ t.classList.add('is-clear'); t.title='清除定时发布'; } if(tlabel) tlabel.textContent='清除'; } else { if(b) b.classList.remove('is-scheduled'); if(lbl) lbl.textContent='发布'; if(t){ t.classList.remove('is-clear'); t.title='定时发布'; } if(tlabel) tlabel.textContent='定时'; } } async function onPublishAction(){ // 主按钮语义:保存并分发(无论是否已设置定时) // 已定时时点击 = 立即发布并脱离定时态(发布即消费掉定时) // 2026-08-03 重构:直接调 doDirectPublish,不再跳 distribute.html if(pendingSchedule){ pendingSchedule = null; applyScheduledUI(); persistSchedule(); } const id = await saveArticleOnly(); if(!id){ safeToast.warning('请先填写标题和内容'); return; } const els = getSelectedPlatformEls(); if(!els.length){ window.location.href = 'distribute.html'; return; } runDistribution(els); } function onSchedToggle(){ if(pendingSchedule){ clearSchedule(); } else { openScheduleModal(); } } function clearSchedule(){ pendingSchedule=null; applyScheduledUI(); persistSchedule(); if(typeof safeToast!=='undefined') safeToast.info('已取消定时发布'); } function publishArticle(){ return publishContent(); } function toLocalInputValue(d){ const p = n => String(n).padStart(2,'0'); return d.getFullYear()+'-'+p(d.getMonth()+1)+'-'+p(d.getDate())+'T'+p(d.getHours())+':'+p(d.getMinutes()); } function openScheduleModal() { const modal = document.getElementById('scheduleModal'); if (!modal) return; modal.style.display = 'flex'; // 默认时间:1 小时后(编辑已有定时则回填)。datetime-local 为本地时间,必须按本地时区格式化 const dt = document.getElementById('scheduleDatetime'); if (dt) { if (pendingSchedule && pendingSchedule.iso) dt.value = pendingSchedule.iso; else dt.value = toLocalInputValue(new Date(Date.now() + 3600000)); } } function closeScheduleModal() { const modal = document.getElementById('scheduleModal'); if (modal) { modal.style.display = 'none'; } } async function submitScheduledPublish() { const scheduledAt = document.getElementById('scheduleDatetime')?.value; if (!scheduledAt) { safeToast.warning('请选择发布时间'); return; } if (new Date(scheduledAt) <= new Date()) { safeToast.warning('发布时间必须晚于当前时间'); return; } // 平台:优先用右侧已勾选,未勾选则默认全部渠道(不再卡死) const allItems = document.querySelectorAll('#platformPickerGrid .platform-item'); const sel = Array.from(allItems).filter(function(it){ return it.classList.contains('selected'); }); const items = sel.length ? sel : Array.from(allItems); if (items.length === 0) { safeToast.warning('请先在右侧选择发布平台'); return; } const platforms = items.map(function(it) { var cn = it.getAttribute('data-platform') || (it.querySelector('.platform-name') || {}).textContent || it.textContent.trim(); cn = cn.trim(); // 真实 OAuth 平台需用后端英文 key 提交,token 才能匹配 return (typeof OAUTH_KEYS !== 'undefined' && OAUTH_KEYS[cn]) ? OAUTH_KEYS[cn] : cn; }); // 先落前端状态,保证按钮立即变为「已设置定时发布」 pendingSchedule = { iso: scheduledAt, platforms: platforms }; applyScheduledUI(); persistSchedule(); closeScheduleModal(); safeToast.success('已设置定时发布(' + platforms.length + ' 个平台)' + (sel.length ? '' : ',未勾选渠道默认全部')); // 后台提交(mock / 失败都不影响前端已保存的状态) try { const title = document.getElementById('articleTitle')?.value?.trim(); const content = getEditorContent(); const articleId = localStorage.getItem('ycat_current_article_id'); let aid = articleId; if (!aid) { const res = await API.createArticle({ title: title || '无标题', content, summary: (content || '').replace(/<[^>]*>/g, '').slice(0, 200) }); if (res && res.article && res.article.id) { aid = res.article.id; localStorage.setItem('ycat_current_article_id', aid); } } if (aid) { await API.createScheduledPublish({ articleId: parseInt(aid), platforms: platforms.map(function(p){ return (typeof OAUTH_KEYS !== 'undefined' && OAUTH_KEYS[p]) ? OAUTH_KEYS[p] : p; }), scheduledAt: new Date(scheduledAt).toISOString() }); } } catch (err) { console.error('定时发布后台提交失败(前端状态已保存)', err); } } // ==================== 分发状态(定时发布后实时看各平台)==================== // 草稿仅本次会话有效:关闭网站立即清除,不长期保存 const DRAFT_KEY = 'ycat_drafts_session_v1'; let draftStore = {}; // platform -> { state, at } function snapshotDrafts(){ try { sessionStorage.setItem(DRAFT_KEY, JSON.stringify(draftStore)); } catch(e){} } function clearAllDrafts(){ draftStore = {}; try { sessionStorage.removeItem(DRAFT_KEY); } catch(e){} } window.addEventListener('pagehide', clearAllDrafts); window.addEventListener('beforeunload', clearAllDrafts); const DIST_STATES = { queued: { label:'排队中', cls:'ds-queued' }, draft: { label:'已存草稿', cls:'ds-draft' }, publishing: { label:'发布中', cls:'ds-pub' }, published: { label:'已发布', cls:'ds-done' }, failed: { label:'失败', cls:'ds-fail' } }; function getSelectedPlatformEls(){ const grid = document.getElementById('platformPickerGrid'); if(!grid) return []; const sel = Array.from(grid.querySelectorAll('.platform-item.selected')); return sel.length ? sel : Array.from(grid.querySelectorAll('.platform-item')); } function cloneDistLogo(el){ const logo = el.querySelector('.platform-logo'); if(!logo) return null; const clone = logo.cloneNode(true); clone.style.width = '38px'; clone.style.height = '38px'; clone.style.margin = '0'; clone.style.borderRadius = '11px'; const svg = clone.querySelector('svg'); if(svg){ svg.style.width = '24px'; svg.style.height = '24px'; } const wm = clone.querySelector('.wm'); if(wm){ wm.style.fontSize = '10px'; } return clone; } function setDistRowState(row, state, stageText){ const st = DIST_STATES[state] || DIST_STATES.queued; const badge = row.querySelector('.dist-badge'); badge.className = 'dist-badge ' + st.cls; badge.innerHTML = '' + st.label; if(stageText){ row.querySelector('.dist-stage').textContent = stageText; } } // ── 直接发布(真 API,无假弹窗)────────────────────────────────────────────── async function doDirectPublish(platformEls){ // 收集平台名(与 distribute.html 保持一致:用中文名) const platforms = platformEls.map(function(el){ return (el.querySelector('.platform-name') || {}).textContent || el.textContent.trim(); }).filter(function(n){ return n && n.trim(); }); if(!platforms.length){ safeToast.warning('请先选择至少一个发布平台'); return; } // ── 真实分发状态弹窗(实时反馈每个平台的进度)───────────────────────── const list = document.getElementById('distList'); const prog = document.getElementById('distProgress'); const doneBtn = document.getElementById('distDoneBtn'); if(list) list.innerHTML = ''; if(prog) prog.textContent = '0 / ' + platforms.length; if(doneBtn) doneBtn.style.display = 'none'; openDistributeModal(); const results = []; let doneCount = 0; const rowMap = {}; // name → DOM row // 构建每行的 DOM(等所有行建完再统一 open,避免闪烁) platformEls.forEach(function(el){ const name = (el.querySelector('.platform-name') || {}).textContent || el.textContent.trim(); const row = document.createElement('div'); row.className = 'dist-row'; const logo = cloneDistLogo(el); const nameEl = document.createElement('div'); nameEl.className = 'dist-name'; nameEl.textContent = name; const stage = document.createElement('div'); stage.className = 'dist-stage'; stage.textContent = '等待中'; const badge = document.createElement('span'); badge.className = 'dist-badge ds-queued'; badge.innerHTML = '排队中'; const right = document.createElement('div'); right.style.textAlign = 'right'; right.appendChild(stage); right.appendChild(badge); if(logo) row.appendChild(logo); row.appendChild(nameEl); row.appendChild(right); if(list) list.appendChild(row); rowMap[name] = row; }); // 逐个平台调用真实 API(可并行,UI 实时更新每个结果) const publishPromises = platforms.map(async function(pName){ try { setDistRowState(rowMap[pName], 'publishing', '正在发布到「' + pName + '」…'); const res = await API.post('/api/publish', { articleId: localStorage.getItem('ycat_current_article_id') || null, platforms: [pName] }); const ok = res && res.ok; const msg = ok ? '✅ 已发布到「' + pName + '」' : '❌ 失败「' + pName + '」' + (res && res.error ? ': ' + res.error : ''); setDistRowState(rowMap[pName], ok ? 'published' : 'failed', msg); results.push({ platform: pName, status: ok ? 'success' : 'failed', error: res && res.error }); } catch(e) { setDistRowState(rowMap[pName], 'failed', '❌ 「' + pName + '」网络错误'); results.push({ platform: pName, status: 'failed', error: String(e) }); } doneCount++; if(prog) prog.textContent = doneCount + ' / ' + platforms.length; if(doneCount >= platforms.length && doneBtn) { doneBtn.style.display = ''; // 汇总提示 const okCount = results.filter(function(r){ return r.status === 'success'; }).length; if(okCount === results.length){ if(prog) prog.textContent = '全部成功 ' + okCount + ' / ' + platforms.length; } else { if(prog) prog.textContent = '部分成功 ' + okCount + ' / ' + platforms.length; } } }); await Promise.all(publishPromises); } // ── 兼容旧调用(保留函数签名)───────────────────────────────────────────── function runDistribution(platformEls){ doDirectPublish(platformEls); } function openDistributeModal(){ const m = document.getElementById('distributeModal'); if(m) m.style.display = 'flex'; } function closeDistributeModal(){ const m = document.getElementById('distributeModal'); if(m) m.style.display = 'none'; } function distToResult(){ window.location.href = 'articles.html'; } async function saveArticleOnly(){ const title = document.getElementById('articleTitle')?.value?.trim(); const content = getEditorContent(); if(!title || !content || content === '


    '){ return null; } const articleId = localStorage.getItem('ycat_current_article_id'); try { if(articleId){ await API.updateArticle(parseInt(articleId), { title, content, summary: content.replace(/<[^>]*>/g, '').slice(0,200) }); } else { const res = await API.createArticle({ title, content, summary: content.replace(/<[^>]*>/g, '').slice(0,200), tags: tags.join(','), category:'' }); if(res.article && res.article.id) localStorage.setItem('ycat_current_article_id', res.article.id); } return localStorage.getItem('ycat_current_article_id'); } catch(e){ return null; } } function checkScheduledFire(){ if(!pendingSchedule || !pendingSchedule.iso) return; if(new Date(pendingSchedule.iso) <= new Date()){ const plats = pendingSchedule.platforms || []; const grid = document.getElementById('platformPickerGrid'); let els = []; if(grid && plats.length){ els = plats.map(function(nm){ return Array.from(grid.querySelectorAll('.platform-item')).find(function(x){ return ((x.querySelector('.platform-name') || {}).textContent || '') === nm; }); }).filter(Boolean); } if(!els.length) els = getSelectedPlatformEls(); pendingSchedule = null; applyScheduledUI(); persistSchedule(); if(els.length){ doDirectPublish(els); } else { window.location.href = 'distribute.html'; } } } function saveAsTemplate() { const title = document.getElementById('articleTitle')?.value?.trim(); const content = getEditorContent(); if (!title || !content) { safeToast.warning('请先输入标题和内容'); return; } const templateName = prompt('模板名称:', title); if (!templateName) return; API.createTemplate({ name: templateName, category: '我的模板', structure: { title, content, tags: tags.join(',') } }).then(() => { safeToast.success('模板保存成功'); }).catch(err => { safeToast.error('保存失败'); console.error(err); }); } // ==================== 导入导出 ==================== function showImportUrlModal() { const modal = document.getElementById('importUrlModal'); if (modal) { modal.style.display = 'flex'; } } function showMarkdownImportModal() { const modal = document.getElementById('markdownImportModal'); if (modal) { modal.style.display = 'flex'; const ta = document.getElementById('mdImportText'); if (ta) ta.value = ''; const fi = document.getElementById('mdImportFile'); if (fi) fi.value = ''; setTimeout(() => { if (ta) ta.focus(); }, 50); } } function closeMarkdownImportModal() { const modal = document.getElementById('markdownImportModal'); if (modal) modal.style.display = 'none'; } function readMdFile(event) { const file = event.target.files && event.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (e) => { const ta = document.getElementById('mdImportText'); if (ta) ta.value = e.target.result; }; reader.readAsText(file); } function confirmMdImport() { const ta = document.getElementById('mdImportText'); const md = ta ? ta.value.trim() : ''; if (!md) { safeToast.warning('请粘贴或选择 Markdown 内容'); return; } const html = markdownToHtml(md); if (window.editor && typeof window.editor.setHtml === 'function') { window.editor.setHtml(html); try { localStorage.setItem('ycat_editor_content', html); } catch (e) {} if (typeof updateWordCount === 'function') updateWordCount(); if (typeof updateMarkdownPreview === 'function' && markdownPreviewEnabled) updateMarkdownPreview(); safeToast.success('Markdown 已导入编辑器'); closeMarkdownImportModal(); } else { safeToast.error('编辑器未就绪,请稍候重试'); } } async function importFromUrl() { const url = document.getElementById('importUrl')?.value?.trim(); if (!url) { safeToast.warning('请输入URL'); return; } try { const res = await API.importFromUrl({ url }); if (res.success) { // 填充标题和内容 const titleInput = document.getElementById('articleTitle'); if (titleInput) { titleInput.value = res.title || '导入的文章'; } setEditorContent(res.content || ''); safeToast.success('导入成功'); closeImportUrlModal(); } } catch (err) { safeToast.error('导入失败'); console.error(err); } } function closeImportUrlModal() { const modal = document.getElementById('importUrlModal'); if (modal) { modal.style.display = 'none'; } } // ==================== AI 智能助手 ==================== function getArticleTitle() { return document.getElementById('articleTitle')?.value || ''; } function showAiLoading(show) { const el = document.getElementById('aiLoading'); if (el) el.style.display = show ? 'inline' : 'none'; } function showAiResult(html) { const panel = document.getElementById('aiResultPanel'); if (panel) { panel.innerHTML = html; panel.style.display = 'block'; } } function hideAiResult() { const panel = document.getElementById('aiResultPanel'); if (panel) { panel.style.display = 'none'; panel.innerHTML = ''; } } // AI 标题优化 async function aiGenerateTitles() { const content = getEditorContent(); const title = getArticleTitle(); if (!content || content.length < 10) { showAiResult('
    请先输入文章内容
    '); return; } showAiLoading(true); try { const res = await API.generateTitles({ title, content, count: 5 }); if (res.ok && res.titles) { const categoryLabels = { original: '原标题', howto: '教程型', listicle: '清单型', hook: '钩子型', social: '社交爆款', professional: '专业型' }; let html = '
    💡 点击标题即可使用
    '; res.titles.forEach(t => { html += `
    ${categoryLabels[t.category] || t.category}
    ${t.title}
    `; }); showAiResult(html); } else { showAiResult('
    生成失败:' + (res.error || '未知错误') + '
    '); } } catch (err) { showAiResult('
    网络错误
    '); } showAiLoading(false); } function applyAiTitle(title) { document.getElementById('articleTitle').value = title; showAiResult('
    ✅ 标题已更新
    '); setTimeout(hideAiResult, 1500); } // AI 智能摘要 async function aiGenerateSummary() { const content = getEditorContent(); if (!content || content.length < 10) { showAiResult('
    请先输入文章内容
    '); return; } showAiLoading(true); try { const res = await API.generateSummary({ content, maxLength: 200 }); if (res.ok && res.summary) { const summary = res.summary; showAiResult(`
    📝 智能摘要
    ${summary}
    `); window._aiLastSummary = summary; } else { showAiResult('
    提取失败
    '); } } catch (err) { showAiResult('
    网络错误
    '); } showAiLoading(false); } function applyAiSummary() { if (window._aiLastSummary) { const el = document.getElementById('articleSummary') || document.getElementById('article-summary'); if (el) el.value = window._aiLastSummary; showAiResult('
    ✅ 摘要已填入
    '); setTimeout(hideAiResult, 1500); } } // AI 内容分析 async function aiAnalyzeContent() { const content = getEditorContent(); if (!content || content.length < 10) { showAiResult('
    请先输入文章内容
    '); return; } showAiLoading(true); try { const res = await API.analyzeContent({ content }); if (res.ok) { let html = '
    📊 内容分析
    '; html += `
    字数
    ${res.charCount}
    句子数
    ${res.sentenceCount}
    `; html += '
    '; (res.suggestions || []).forEach(s => { const icons = { warning: '⚠️', info: 'ℹ️', success: '✅' }; html += `
    ${icons[s.type] || '•'}${s.message}
    `; }); html += '
    '; if (res.keywords && res.keywords.length > 0) { html += '
    关键词
    '; res.keywords.forEach(k => { html += `${k}`; }); html += '
    '; } showAiResult(html); } else { showAiResult('
    分析失败
    '); } } catch (err) { showAiResult('
    网络错误
    '); } showAiLoading(false); } // AI SEO 建议 async function aiSeoSuggest() { const content = getEditorContent(); const title = getArticleTitle(); if (!content || content.length < 10) { showAiResult('
    请先输入文章内容
    '); return; } showAiLoading(true); try { const res = await API.seoSuggestions({ title, content }); if (res.ok) { let html = '
    🔍 SEO 建议
    '; if (res.titleOptimization) html += `
    📝${res.titleOptimization}
    `; if (res.primary && res.primary.length > 0) { html += '
    主关键词
    '; res.primary.forEach(k => { html += `${k}`; }); html += '
    '; } if (res.secondary && res.secondary.length > 0) { html += '
    次要关键词
    '; res.secondary.forEach(k => { html += `${k}`; }); html += '
    '; } if (res.longTail && res.longTail.length > 0) { html += '
    长尾关键词
    '; res.longTail.forEach(k => { html += `${k}`; }); html += '
    '; } if (res.metaDescription) { html += `
    Meta 描述建议
    ${res.metaDescription}
    `; } showAiResult(html); } else { showAiResult('
    分析失败
    '); } } catch (err) { showAiResult('
    网络错误
    '); } showAiLoading(false); } // AI 内容润色 async function aiPolishContent() { const content = getEditorContent(); if (!content || content.length < 10) { showAiResult('
    请先输入文章内容
    '); return; } showAiLoading(true); try { const res = await API.polishContent({ content, style: 'formal' }); if (res.ok) { let html = '
    ✏️ 内容润色(正式风格)
    '; html += `
    字符变化:${res.charDiff > 0 ? '+' : ''}${res.charDiff}字
    `; html += `
    ${res.polished}
    `; html += `
    `; showAiResult(html); window._aiPolishResult = res.polished; } else { showAiResult('
    润色失败
    '); } } catch (err) { showAiResult('
    网络错误
    '); } showAiLoading(false); } async function aiPolishContentAlt(style) { const content = getEditorContent(); showAiLoading(true); try { const res = await API.polishContent({ content, style }); if (res.ok) { window._aiPolishResult = res.polished; const styleNames = { casual: '🗣️ 口语风格', formal: '✏️ 正式风格', marketing: '📣 营销风格' }; let html = `
    ${styleNames[style] || style} 润色结果
    `; html += `
    ${res.polished}
    `; html += ``; showAiResult(html); } } catch (err) { showAiResult('
    网络错误
    '); } showAiLoading(false); } function applyPolishResult() { if (window._aiPolishResult) { if (window.editor) window.editor.setContent(window._aiPolishResult); showAiResult('
    ✅ 内容已替换为润色版本
    '); setTimeout(hideAiResult, 2000); } } // AI 内容扩写 async function aiExpandContent() { const content = getEditorContent(); if (!content || content.length < 10) { showAiResult('
    请先输入文章内容
    '); return; } showAiLoading(true); try { const res = await API.expandContent({ content, intensity: 'medium' }); if (res.ok) { let html = '
    📖 内容扩写(中度扩展)
    '; html += `
    原文
    ${res.originalCharCount}字
    扩写后
    ${res.expandedCharCount}字
    增长率
    +${res.growthRate}%
    `; html += `
    ${res.expanded}
    `; html += `
    `; showAiResult(html); window._aiExpandResult = res.expanded; } else { showAiResult('
    扩写失败
    '); } } catch (err) { showAiResult('
    网络错误
    '); } showAiLoading(false); } async function aiExpandAlt(intensity) { const content = getEditorContent(); showAiLoading(true); try { const res = await API.expandContent({ content, intensity }); if (res.ok) { window._aiExpandResult = res.expanded; const label = { light: '轻度', medium: '中度', heavy: '深度' }; let html = `
    📖 内容扩写(${label[intensity] || intensity}扩展 +${res.growthRate}%)
    `; html += `
    ${res.expanded}
    `; html += ``; showAiResult(html); } } catch (err) { showAiResult('
    网络错误
    '); } showAiLoading(false); } function applyExpandResult() { if (window._aiExpandResult) { if (window.editor) window.editor.setContent(window._aiExpandResult); showAiResult('
    ✅ 内容已替换为扩写版本
    '); setTimeout(hideAiResult, 2000); } } // AI 内容缩写 async function aiCondenseContent() { const content = getEditorContent(); if (!content || content.length < 20) { showAiResult('
    请先输入文章内容
    '); return; } showAiLoading(true); try { const res = await API.condenseContent({ content, targetRatio: 0.5 }); if (res.ok) { let html = '
    🔽 内容缩写(精简50%)
    '; html += `
    原句数
    ${res.originalSentences}
    精简后
    ${res.condensedSentences}
    `; html += `
    ${res.condensed}
    `; html += `
    `; showAiResult(html); window._aiCondenseResult = res.condensed; } else { showAiResult('
    缩写失败
    '); } } catch (err) { showAiResult('
    网络错误
    '); } showAiLoading(false); } async function aiCondenseAlt(ratio) { const content = getEditorContent(); showAiLoading(true); try { const res = await API.condenseContent({ content, targetRatio: ratio }); if (res.ok) { window._aiCondenseResult = res.condensed; const pct = Math.round((1 - res.condensedSentences / res.originalSentences) * 100); let html = `
    🔽 内容缩写(精简${pct}%)
    `; html += `
    ${res.condensed}
    `; html += ``; showAiResult(html); } } catch (err) { showAiResult('
    网络错误
    '); } showAiLoading(false); } function applyCondenseResult() { if (window._aiCondenseResult) { if (window.editor) window.editor.setContent(window._aiCondenseResult); showAiResult('
    ✅ 内容已替换为缩写版本
    '); setTimeout(hideAiResult, 2000); } } // AI 发布时机建议 async function aiSuggestPostTime() { const content = getEditorContent(); const title = getArticleTitle(); showAiLoading(true); try { const res = await API.suggestPostingTime({ content, platform: null }); if (res.ok) { let html = '
    ⏰ 各平台最佳发布时间
    '; html += `
    📌 内容类型:${res.contentType}
    `; const platforms = { wechat: { name: '公众号', icon: '💰' }, zhihu: { name: '知乎', icon: '💬' }, xiaohongshu: { name: '小红书', icon: '📕' }, douyin: { name: '抖音', icon: '🎵' }, bilibili: { name: 'B站', icon: '📺' }, weibo: { name: '微博', icon: '🌐' } }; (res.recommendations || []).forEach(r => { const p = platforms[r.platform] || { name: r.platform, icon: '📌' }; html += `
    ${p.icon} ${p.name} ${r.bestTime}
    `; if (r.reason) html += `
    ${r.reason}
    `; }); showAiResult(html); } else { showAiResult('
    获取失败
    '); } } catch (err) { showAiResult('
    网络错误
    '); } showAiLoading(false); } // ==================== 导入/导出 ==================== async function exportAsMarkdown() { const title = document.getElementById('articleTitle')?.value?.trim() || '未命名'; const content = getEditorContent(); try { const res = await API.htmlToMarkdown({ html: content }); if (res.success) { // 创建下载链接 const blob = new Blob([res.markdown], { type: 'text/markdown' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `${title}.md`; a.click(); URL.revokeObjectURL(url); safeToast.success('导出成功'); } } catch (err) { safeToast.error('导出失败'); console.error(err); } } // ==================== 初始化 ==================== document.addEventListener('DOMContentLoaded', () => { // 初始化编辑器 initEditor(); // 恢复定时发布状态(刷新后不丢失) restoreSchedule(); // 到了定时时间则自动触发分发弹窗 checkScheduledFire(); setInterval(checkScheduledFire, 20000); // 初始化字数统计 updateWordCount(); // 迁移旧版本遗留的 .tag-item 孤立标签为正式标签,并刷新 SEO 评分 (function migrateLegacyTags() { const tc = document.getElementById('tagContainer'); if (!tc) return; const legacy = Array.from(tc.querySelectorAll(':scope > .tag-item')); let changed = false; legacy.forEach(el => { const t = (el.textContent || '').trim(); if (t && !tags.includes(t) && tags.length < 5) { tags.push(t); changed = true; } el.remove(); }); if (changed) { renderTags(); } updateFloatingSeoScore(); })(); // 恢复之前的草稿 const savedTitle = localStorage.getItem('ycat_editor_title'); if (savedTitle) { const titleInput = document.getElementById('articleTitle'); if (titleInput) { titleInput.value = savedTitle; } } // 监听标题变化 const titleInput = document.getElementById('articleTitle'); if (titleInput) { titleInput.addEventListener('input', (e) => { localStorage.setItem('ycat_editor_title', e.target.value); triggerAutoSave(); }); } // 页面离开前保存 window.addEventListener('beforeunload', () => { const title = document.getElementById('articleTitle')?.value; const content = getEditorContent(); localStorage.setItem('ycat_editor_title', title || ''); localStorage.setItem('ycat_editor_content', content); }); }); // 版本历史 / 手动快照:已于 2026-07-20 移除(UI 与自动快照一并停用) // ==================== 模板加载 ==================== // Load template data if coming from templates page (function() { const urlParams = new URLSearchParams(window.location.search); if (urlParams.get('template') === 'true') { const templateData = localStorage.getItem('template_data'); if (templateData) { try { const template = JSON.parse(templateData); // Wait for editor to initialize const checkEditor = setInterval(() => { const titleInput = document.getElementById('articleTitle'); if (titleInput) { clearInterval(checkEditor); if (template.title) titleInput.value = template.title; if (template.content && window.editor) { window.editor.setHtml(template.content); } const summaryInput = document.getElementById('article-summary'); if (summaryInput && template.summary) summaryInput.value = template.summary; const categoryInput = document.getElementById('article-category'); if (categoryInput && template.category) categoryInput.value = template.category; if (template.tags && template.tags.length > 0) { const tagsInput = document.getElementById('article-tags'); if (tagsInput) tagsInput.value = template.tags.join(', '); } showToast('已应用模板', 'success'); localStorage.removeItem('template_data'); } }, 100); // Timeout after 5 seconds setTimeout(() => clearInterval(checkEditor), 5000); } catch (e) { console.error('Failed to load template:', e); } } } })(); // ==================== 专注模式 ==================== let focusModeEnabled = false; function toggleFocusMode() { focusModeEnabled = !focusModeEnabled; if (focusModeEnabled) { enterFocusMode(); } else { exitFocusMode(); } } function enterFocusMode() { // 添加专注模式样式类 const app = document.getElementById('editor-app'); if (app) app.classList.add('focus-mode'); // 更新按钮状态 const btn = document.getElementById('focusModeToggle'); if (btn) { btn.classList.add('active'); const span = btn.querySelector('span:last-child'); if (span) span.textContent = '退出专注'; } // 显示退出提示 showFocusModeHint(); // 保存偏好设置 localStorage.setItem('ycat_focus_mode', 'true'); // 全屏请求(如果支持) if (document.documentElement.requestFullscreen) { document.documentElement.requestFullscreen().catch(err => { console.log('Fullscreen request failed:', err); }); } // 触发窗口大小变化事件,让编辑器自适应 window.dispatchEvent(new Event('resize')); console.log('✨ 进入专注模式'); } function exitFocusMode() { // 移除专注模式样式类 const app = document.getElementById('editor-app'); if (app) app.classList.remove('focus-mode'); // 更新按钮状态 const btn = document.getElementById('focusModeToggle'); if (btn) { btn.classList.remove('active'); const span = btn.querySelector('span:last-child'); if (span) span.textContent = '专注模式'; } // 隐藏退出提示 hideFocusModeHint(); // 保存偏好设置 localStorage.setItem('ycat_focus_mode', 'false'); // 退出全屏(如果当前是全屏) if (document.fullscreenElement) { document.exitFullscreen().catch(err => { console.log('Exit fullscreen failed:', err); }); } // 触发窗口大小变化事件,让编辑器自适应 window.dispatchEvent(new Event('resize')); console.log('✨ 退出专注模式'); } function showFocusModeHint() { // 创建提示元素 let hint = document.getElementById('focusModeHint'); if (!hint) { hint = document.createElement('div'); hint.id = 'focusModeHint'; hint.className = 'focus-mode-exit-hint'; hint.innerHTML = '按 Esc 或 F11 退出专注模式'; document.body.appendChild(hint); } // 显示提示 setTimeout(() => { hint.classList.add('show'); }, 100); // 3秒后自动隐藏 setTimeout(() => { hideFocusModeHint(); }, 3000); } function hideFocusModeHint() { const hint = document.getElementById('focusModeHint'); if (hint) { hint.classList.remove('show'); } } // 键盘快捷键支持 document.addEventListener('keydown', (e) => { // F11 切换专注模式 if (e.key === 'F11') { e.preventDefault(); toggleFocusMode(); } // Esc 退出专注模式 if (e.key === 'Escape' && focusModeEnabled) { e.preventDefault(); exitFocusMode(); } }); // 页面加载时恢复专注模式设置 document.addEventListener('DOMContentLoaded', () => { const savedFocusMode = localStorage.getItem('ycat_focus_mode'); if (savedFocusMode === 'true') { // 延迟进入专注模式,确保编辑器已初始化 setTimeout(() => { enterFocusMode(); }, 500); } }); // ==================== 悬浮 SEO 轻量评分(本地估算 1-100) ==================== function updateFloatingSeoScore() { const fab = document.getElementById('seoFab'); const scoreEl = document.getElementById('seoScore'); const tipEl = document.getElementById('seoFabTip'); if (!fab || !scoreEl) return; const content = getEditorContent(); const text = stripHtml(content).replace(/\s+/g, ''); const wc = text.length; if (wc === 0) { fab.style.display = 'none'; return; } fab.style.display = 'flex'; const title = (document.getElementById('articleTitle') || {}).value || ''; const summary = (document.getElementById('articleSummary') || {}).value || ''; const tagCount = document.querySelectorAll('#tagContainer .tag, #tagContainer .tag-item').length; const imgCount = (content.match(/= 600 }, { label: '标题长度', got: title.trim().length >= 4 ? 15 : 0, max: 15, ok: title.trim().length >= 4 }, { label: '内容摘要', got: summary.trim().length >= 10 ? 10 : 0, max: 10, ok: summary.trim().length >= 10 }, { label: '标签设置', got: tagCount > 0 ? 10 : 0, max: 10, ok: tagCount > 0 }, { label: '配图', got: imgCount > 0 ? 10 : 0, max: 10, ok: imgCount > 0 }, { label: '段落数', got: paraCount >= 3 ? 15 : 0, max: 15, ok: paraCount >= 3 } ]; let score = 0; dims.forEach(function (d) { score += d.got; }); score = Math.max(1, Math.min(100, score)); scoreEl.textContent = score; scoreEl.style.color = score >= 80 ? '#10b981' : (score >= 60 ? '#eab308' : '#ef4444'); if (tipEl) { const colorOf = function (r) { return r >= 0.8 ? '#34d399' : (r >= 0.5 ? '#fbbf24' : '#f87171'); }; const rows = dims.map(function (d) { const ratio = d.max ? d.got / d.max : 0; const pct = Math.round(ratio * 100); return '
    ' + d.label + '' + '' + '' + d.got + '/' + d.max + '
    '; }).join(''); const grade = score >= 80 ? '优秀' : (score >= 60 ? '良好' : '待优化'); tipEl.innerHTML = '

    SEO 评分明细' + grade + '

    ' + rows + '
    本地估算 · 满分 100,越高越利于收录
    '; } } ['articleTitle', 'articleSummary'].forEach(function (id) { const el = document.getElementById(id); if (el) el.addEventListener('input', updateFloatingSeoScore); }); updateFloatingSeoScore(); function updateSchedulePlatformCount() { const grid = document.getElementById('schedulePlatformGrid'); const label = document.getElementById('schedulePlatformCount'); if (!grid || !label) return; const n = grid.querySelectorAll('.platform-item.selected').length; label.textContent = '已选 ' + n; } // ==================== 封面图 UX 优化 ==================== // 拖拽上传(封面区与预览区均可接收) function initCoverDnd() { const zone = document.getElementById('coverUploadBtn'); const preview = document.getElementById('coverPreview'); [zone, preview].forEach(z => { if (!z || z.dataset.dndBound) return; z.dataset.dndBound = '1'; const add = () => z.classList.add('drag-over'); const remove = (e) => { if (!e || e.target === z || !z.contains(e.relatedTarget)) z.classList.remove('drag-over'); }; ['dragenter', 'dragover'].forEach(ev => z.addEventListener(ev, e => { e.preventDefault(); e.stopPropagation(); add(); })); ['dragleave', 'dragend'].forEach(ev => z.addEventListener(ev, e => { e.preventDefault(); e.stopPropagation(); remove(e); })); z.addEventListener('drop', e => { e.preventDefault(); e.stopPropagation(); z.classList.remove('drag-over'); const f = e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files[0]; if (!f) return; const inp = document.getElementById('coverFileInput'); if (!inp) return; try { const dt = new DataTransfer(); dt.items.add(f); inp.files = dt.files; handleCoverFile({ target: inp }); } catch (err) { safeToast.error('当前浏览器不支持拖拽,请点击选择'); } }); }); } // 删除二次确认(防止误删) function confirmDeleteCover(btn) { if (!coverDeleteArmed) { coverDeleteArmed = true; if (btn) { btn.textContent = '确认删除?'; btn.classList.add('danger'); } clearTimeout(coverDeleteTimer); coverDeleteTimer = setTimeout(() => { coverDeleteArmed = false; if (btn) { btn.textContent = '删除'; btn.classList.remove('danger'); } }, 2600); return; } clearTimeout(coverDeleteTimer); coverDeleteArmed = false; if (btn) { btn.textContent = '删除'; btn.classList.remove('danger'); } deleteCover(); } // 封面大图预览(灯箱) function openCoverLightbox(e) { if (e && e.stopPropagation) e.stopPropagation(); const img = document.getElementById('coverPreviewImg'); const box = document.getElementById('coverLightbox'); const big = document.getElementById('coverLightboxImg'); if (!box || !big) return; if (img && img.getAttribute('src')) { big.src = img.src; box.classList.add('open'); } else if (typeof safeToast !== 'undefined') { safeToast.info('尚未上传封面'); } } function closeCoverLightbox() { const box = document.getElementById('coverLightbox'); if (box) box.classList.remove('open'); } // 初始化拖拽 (function () { function ready(fn) { if (document.readyState !== 'loading') fn(); else document.addEventListener('DOMContentLoaded', fn); } ready(initCoverDnd); })();