This commit is contained in:
Daniel
2026-04-28 11:50:55 +08:00
parent 1bbabc2a78
commit 2724e69b4f
20 changed files with 3881 additions and 554 deletions

View File

@@ -16,8 +16,51 @@ const rewriteBtn = $("rewriteBtn");
const wechatBtn = $("wechatBtn");
const imBtn = $("imBtn");
const coverUploadBtn = $("coverUploadBtn");
const coverUrlUploadBtn = $("coverUrlUploadBtn");
const coverGenerateBtn = $("coverGenerateBtn");
const coverModeManualBtn = $("coverModeManualBtn");
const coverModeAiBtn = $("coverModeAiBtn");
const coverManualSection = $("coverManualSection");
const coverAiSection = $("coverAiSection");
const coverAutoAfterRewrite = $("coverAutoAfterRewrite");
const coverPreview = $("coverPreview");
const coverPreviewWrap = $("coverPreviewWrap");
const logoutBtn = $("logoutBtn");
const targetBodyCharsInput = $("targetBodyChars");
const posterGenerateBtn = $("posterGenerateBtn");
const posterPreviewList = $("posterPreviewList");
const posterHint = $("posterHint");
const posterAutoInclude = $("posterAutoInclude");
let posterState = {
signature: "",
bodyMarkdownWithPosters: "",
posters: [],
};
let coverMode = "manual";
function setCoverMode(mode) {
coverMode = mode === "ai" ? "ai" : "manual";
if (coverModeManualBtn) coverModeManualBtn.classList.toggle("is-active", coverMode === "manual");
if (coverModeAiBtn) coverModeAiBtn.classList.toggle("is-active", coverMode === "ai");
if (coverManualSection) {
const hideManual = coverMode !== "manual";
coverManualSection.hidden = hideManual;
coverManualSection.style.display = hideManual ? "none" : "";
}
if (coverAiSection) {
const hideAi = coverMode !== "ai";
coverAiSection.hidden = hideAi;
coverAiSection.style.display = hideAi ? "none" : "";
}
const hint = $("coverHint");
if (hint) {
hint.textContent =
coverMode === "manual"
? "当前为手动上传模式,可切换到 AI 自动生成。"
: "当前为 AI 生成模式,也可切换回手动上传。";
}
}
function syncTargetCharChips() {
const val = Number((targetBodyCharsInput && targetBodyCharsInput.value) || 0);
@@ -82,6 +125,68 @@ function setStatus(msg, danger = false) {
statusEl.textContent = msg;
}
function buildPosterSignature() {
const title = ($("title") && $("title").value.trim()) || "";
const summary = ($("summary") && $("summary").value.trim()) || "";
const body = ($("body") && $("body").value.trim()) || "";
return `${title}\n||\n${summary}\n||\n${body}`;
}
function renderPosterPreview(posters) {
if (!posterPreviewList) return;
posterPreviewList.innerHTML = "";
const list = Array.isArray(posters) ? posters : [];
if (!list.length) {
const empty = document.createElement("p");
empty.className = "muted small";
empty.textContent = "暂无段落海报预览。";
posterPreviewList.appendChild(empty);
return;
}
list.forEach((item) => {
const card = document.createElement("article");
card.className = "poster-card";
const img = document.createElement("img");
img.className = "poster-thumb";
img.alt = `段落 ${Number(item.paragraph_index || 0) + 1} 海报`;
img.src = item.preview_data_url || "";
card.appendChild(img);
const meta = document.createElement("div");
meta.className = "poster-meta";
const top = document.createElement("div");
top.className = "poster-topline";
top.textContent = `段落 ${Number(item.paragraph_index || 0) + 1} · ${item.note || "ai"}`;
meta.appendChild(top);
const excerpt = document.createElement("p");
excerpt.className = "poster-excerpt";
excerpt.textContent = item.paragraph_excerpt || "";
meta.appendChild(excerpt);
if (item.wechat_url) {
const link = document.createElement("a");
link.className = "poster-link";
link.href = item.wechat_url;
link.target = "_blank";
link.rel = "noreferrer noopener";
link.textContent = "微信素材 URL";
meta.appendChild(link);
}
card.appendChild(meta);
posterPreviewList.appendChild(card);
});
}
function markPosterStaleIfNeeded() {
if (!posterState.signature || !posterHint) return;
if (posterState.signature !== buildPosterSignature()) {
posterHint.textContent = "正文已修改,发布前会自动重建段落海报。";
}
}
async function postJSON(url, body) {
const res = await fetch(url, {
method: "POST",
@@ -94,6 +199,78 @@ async function postJSON(url, body) {
return data;
}
async function generatePosterMaterials({ silent = false } = {}) {
const bodyMarkdown = (($("body") && $("body").value) || "").trim();
if (bodyMarkdown.length < 20) {
throw new Error("正文太短,暂无法生成段落海报");
}
if (!silent) setStatus("正在生成段落海报...");
if (posterHint) posterHint.textContent = "正在生成并上传段落海报...";
setLoading(posterGenerateBtn, true, "生成段落海报", "生成中...");
try {
const data = await postJSON("/api/material/posters/generate", {
title: $("title").value,
summary: $("summary").value,
body_markdown: $("body").value,
upload_to_wechat: true,
});
if (!data.ok) throw new Error(data.detail || "海报生成失败");
posterState = {
signature: buildPosterSignature(),
bodyMarkdownWithPosters: data.body_markdown_with_posters || $("body").value,
posters: Array.isArray(data.posters) ? data.posters : [],
};
renderPosterPreview(posterState.posters);
const warnText = Array.isArray(data.warnings) && data.warnings.length ? `(提示:${data.warnings.join("")}` : "";
if (posterHint) posterHint.textContent = `${data.detail || "海报生成完成"}${warnText}`;
if (!silent) setStatus(`${data.detail || "海报生成完成"}${warnText}`);
return data;
} finally {
setLoading(posterGenerateBtn, false, "生成段落海报", "生成中...");
}
}
function coverTitleForGeneration() {
const generatedTitle = (($("title") && $("title").value) || "").trim();
const titleHint = (($("titleHint") && $("titleHint").value) || "").trim();
return generatedTitle || titleHint;
}
async function generateWechatCover({ silent = false } = {}) {
const title = coverTitleForGeneration();
if (!title) {
throw new Error("请先填写标题提示,或先改写生成标题");
}
if (!silent) setStatus("正在按标题生成公众号封面...");
const hint = $("coverHint");
if (hint) hint.textContent = "正在生成 900×383 公众号封面并上传...";
setLoading(coverGenerateBtn, true, "按标题生成封面", "生成中...");
try {
const data = await postJSON("/api/wechat/cover/generate", {
title,
summary: (($("summary") && $("summary").value) || "").trim(),
style_hint: (($("coverStyleHint") && $("coverStyleHint").value) || "").trim(),
upload_to_wechat: true,
});
if (!data.ok) throw new Error(data.detail || "封面生成失败");
const mid = data.thumb_media_id || "";
if (mid && $("thumbMediaId")) $("thumbMediaId").value = mid;
if (data.preview_data_url && coverPreview && coverPreviewWrap) {
coverPreview.src = data.preview_data_url;
coverPreviewWrap.hidden = false;
}
const warnText = Array.isArray(data.warnings) && data.warnings.length ? `(提示:${data.warnings.join("")}` : "";
const detail = data.detail || "封面生成完成";
if (hint) hint.textContent = `${detail}${warnText}`;
if (!silent) setStatus(`${detail}${warnText}`);
return data;
} finally {
setLoading(coverGenerateBtn, false, "按标题生成封面", "生成中...");
}
}
async function fetchAuthMe() {
const res = await fetch("/api/auth/me");
const data = await res.json();
@@ -117,10 +294,10 @@ function renderWechatAccountSelect(me) {
if (!list.length) {
const opt = document.createElement("option");
opt.value = "";
opt.textContent = "暂无公众号";
opt.textContent = "未绑定公众号";
sel.appendChild(opt);
sel.disabled = true;
if (hint) hint.textContent = "请先在「公众号设置」绑定";
if (hint) hint.textContent = "请先绑定公众号";
return;
}
sel.disabled = false;
@@ -192,6 +369,13 @@ if (logoutBtn) {
});
}
if (coverModeManualBtn) {
coverModeManualBtn.addEventListener("click", () => setCoverMode("manual"));
}
if (coverModeAiBtn) {
coverModeAiBtn.addEventListener("click", () => setCoverMode("ai"));
}
document.querySelectorAll(".target-char-chip").forEach((btn) => {
btn.addEventListener("click", () => {
const n = Number(btn.getAttribute("data-target-chars") || 0);
@@ -245,6 +429,22 @@ $("rewriteBtn").addEventListener("click", async () => {
} else {
setStatus("改写完成。");
}
try {
setStatus("改写完成,正在生成段落海报...");
await generatePosterMaterials({ silent: true });
setStatus("改写与段落海报生成完成。");
} catch (posterErr) {
setStatus(`改写完成,段落海报未生成:${posterErr.message}`, true);
}
if (coverAutoAfterRewrite && coverAutoAfterRewrite.checked) {
try {
setStatus("改写完成,正在按输出标题生成封面...");
await generateWechatCover({ silent: true });
setStatus("改写、封面与段落海报生成完成。");
} catch (coverErr) {
setStatus(`改写完成,封面未生成:${coverErr.message}`, true);
}
}
} catch (e) {
setStatus(`改写失败: ${e.message}`, true);
} finally {
@@ -256,10 +456,25 @@ $("wechatBtn").addEventListener("click", async () => {
setStatus("正在发布到公众号草稿箱...");
setLoading(wechatBtn, true, "发布到公众号草稿箱", "发布中...");
try {
let bodyForPublish = $("body").value;
const autoInclude = Boolean(posterAutoInclude && posterAutoInclude.checked);
if (autoInclude) {
const stale = posterState.signature !== buildPosterSignature() || !posterState.bodyMarkdownWithPosters;
if (stale) {
try {
await generatePosterMaterials({ silent: true });
} catch (posterErr) {
setStatus(`海报生成失败,本次仅发布文字:${posterErr.message}`, true);
}
}
if (posterState.bodyMarkdownWithPosters) {
bodyForPublish = posterState.bodyMarkdownWithPosters;
}
}
const data = await postJSON("/api/publish/wechat", {
title: $("title").value,
summary: $("summary").value,
body_markdown: $("body").value,
body_markdown: bodyForPublish,
thumb_media_id: $("thumbMediaId") ? $("thumbMediaId").value.trim() : "",
});
if (!data.ok) throw new Error(data.detail);
@@ -276,6 +491,23 @@ $("wechatBtn").addEventListener("click", async () => {
}
});
if (coverGenerateBtn) {
coverGenerateBtn.addEventListener("click", async () => {
try {
await generateWechatCover({ silent: false });
} catch (e) {
const hint = $("coverHint");
if (hint) hint.textContent = "AI 封面生成失败,请检查标题、模型或公众号配置。";
setStatus(`封面生成失败: ${e.message}`, true);
if ((e.message || "").includes("请先登录")) {
window.location.href = "/auth?next=/";
} else if ((e.message || "").includes("未绑定公众号")) {
window.location.href = "/settings";
}
}
});
}
if (coverUploadBtn) {
coverUploadBtn.addEventListener("click", async () => {
const fileInput = $("coverFile");
@@ -311,6 +543,54 @@ if (coverUploadBtn) {
});
}
if (coverUrlUploadBtn) {
coverUrlUploadBtn.addEventListener("click", async () => {
const hint = $("coverHint");
const imageUrl = (($("coverUrl") && $("coverUrl").value) || "").trim();
if (!imageUrl) {
setStatus("请先粘贴图片 URL", true);
return;
}
if (hint) hint.textContent = "正在下载并上传 URL 图片...";
setLoading(coverUrlUploadBtn, true, "URL 上传并绑定", "上传中...");
try {
const data = await postJSON("/api/wechat/cover/upload-by-url", { image_url: imageUrl });
if (!data.ok) throw new Error(data.detail || "URL 封面上传失败");
const mid = data.data && data.data.thumb_media_id ? data.data.thumb_media_id : "";
if ($("thumbMediaId")) $("thumbMediaId").value = mid;
if (hint) hint.textContent = `URL 封面上传成功,已绑定 media_id${mid}`;
setStatus("URL 封面上传成功,发布时将优先使用该封面。");
if ($("coverUrl")) $("coverUrl").value = "";
} catch (e) {
if (hint) hint.textContent = "URL 封面上传失败,请看状态提示。";
setStatus(`URL 封面上传失败: ${e.message}`, true);
if ((e.message || "").includes("请先登录")) {
window.location.href = "/auth?next=/";
} else if ((e.message || "").includes("未绑定公众号")) {
window.location.href = "/settings";
}
} finally {
setLoading(coverUrlUploadBtn, false, "URL 上传并绑定", "上传中...");
}
});
}
if (posterGenerateBtn) {
posterGenerateBtn.addEventListener("click", async () => {
try {
await generatePosterMaterials({ silent: false });
} catch (e) {
setStatus(`海报生成失败: ${e.message}`, true);
if (posterHint) posterHint.textContent = "海报生成失败,请检查配置后重试。";
if ((e.message || "").includes("请先登录")) {
window.location.href = "/auth?next=/";
} else if ((e.message || "").includes("未绑定公众号")) {
window.location.href = "/settings";
}
}
});
}
$("imBtn").addEventListener("click", async () => {
setStatus("正在发送到 IM...");
setLoading(imBtn, true, "发送到 IM", "发送中...");
@@ -328,11 +608,15 @@ $("imBtn").addEventListener("click", async () => {
}
});
["sourceText", "summary", "body"].forEach((id) => {
["sourceText", "title", "summary", "body"].forEach((id) => {
$(id).addEventListener("input", updateCounters);
if (id !== "sourceText") $(id).addEventListener("input", markPosterStaleIfNeeded);
});
updateCounters();
initMultiDropdowns();
initWechatAccountSwitch();
syncTargetCharChips();
renderPosterPreview([]);
setCoverMode("manual");
window.addEventListener("load", () => setCoverMode("manual"));