/** * 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') || '
]*>]*>([\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, '');
md = md.replace(/
]*src="([^"]*)"[^>]*\/?>/gi, '');
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, '
');
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);
})();