Files
AIcreat/app/static/app.js
2026-04-28 18:36:38 +08:00

828 lines
30 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const $ = (id) => document.getElementById(id);
function renderBodyPreview() {
const raw = ($("body") && $("body").value) || "";
const el = $("bodyPreview");
if (!el) return;
if (typeof marked !== "undefined" && marked.parse) {
el.innerHTML = marked.parse(raw, { breaks: true });
} else {
el.textContent = raw;
}
}
const statusEl = $("status");
const rewriteBtn = $("rewriteBtn");
const wechatBtn = $("wechatBtn");
const imBtn = $("imBtn");
const coverUploadBtn = $("coverUploadBtn");
const coverUrlUploadBtn = $("coverUrlUploadBtn");
const coverGenerateBtn = $("coverGenerateBtn");
const saveCoverImageModelBtn = $("saveCoverImageModelBtn");
const coverImageModelInput = $("coverImageModel");
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 clearDraftBtn = $("clearDraftBtn");
const targetBodyCharsInput = $("targetBodyChars");
const posterGenerateBtn = $("posterGenerateBtn");
const posterPreviewList = $("posterPreviewList");
const posterHint = $("posterHint");
const posterAutoInclude = $("posterAutoInclude");
const DRAFT_STORAGE_KEY = "aifagao:index:draft:v1";
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);
document.querySelectorAll(".target-char-chip").forEach((btn) => {
const n = Number(btn.getAttribute("data-target-chars") || 0);
btn.classList.toggle("is-active", Number.isFinite(val) && val === n);
});
}
function countText(v) {
return (v || "").trim().length;
}
/** 多选子项用顿号拼接,可选补充用分号接在末尾 */
function buildMultiPrompt(nameAttr, extraId) {
const boxes = document.querySelectorAll(`input[name="${nameAttr}"]:checked`);
const parts = Array.from(boxes).map((b) => (b.value || "").trim()).filter(Boolean);
const extraEl = extraId ? $(extraId) : null;
const extra = extraEl ? (extraEl.value || "").trim() : "";
let s = parts.join("、");
if (extra) s = s ? `${s}${extra}` : extra;
return s;
}
function updateMultiDropdownSummary(nameAttr, summaryId, emptyHint) {
const el = $(summaryId);
if (!el) return;
const parts = Array.from(document.querySelectorAll(`input[name="${nameAttr}"]:checked`)).map((b) =>
(b.value || "").trim(),
);
el.textContent = parts.length ? parts.join("、") : emptyHint;
}
function initMultiDropdowns() {
const pairs = [
{ name: "audienceChip", summary: "audienceSummary", empty: "点击展开,选择目标读者…" },
{ name: "toneChip", summary: "toneSummary", empty: "点击展开,选择语气风格…" },
];
pairs.forEach(({ name, summary, empty }) => {
updateMultiDropdownSummary(name, summary, empty);
document.querySelectorAll(`input[name="${name}"]`).forEach((cb) => {
cb.addEventListener("change", () => updateMultiDropdownSummary(name, summary, empty));
});
});
}
function updateCounters() {
$("sourceCount").textContent = `${countText($("sourceText").value)}`;
$("summaryCount").textContent = `${countText($("summary").value)}`;
$("bodyCount").textContent = `${countText($("body").value)}`;
renderBodyPreview();
}
function setLoading(button, loading, idleText, loadingText) {
if (!button) return;
button.disabled = loading;
button.textContent = loading ? loadingText : idleText;
}
function setStatus(msg, danger = false) {
statusEl.style.color = danger ? "#b42318" : "#0f5f3d";
statusEl.textContent = msg;
}
function saveDraftState() {
try {
const data = {
sourceText: ($("sourceText") && $("sourceText").value) || "",
titleHint: ($("titleHint") && $("titleHint").value) || "",
audienceExtra: ($("audienceExtra") && $("audienceExtra").value) || "",
toneExtra: ($("toneExtra") && $("toneExtra").value) || "",
avoidWords: ($("avoidWords") && $("avoidWords").value) || "",
keepPoints: ($("keepPoints") && $("keepPoints").value) || "",
targetBodyChars: ($("targetBodyChars") && $("targetBodyChars").value) || "500",
title: ($("title") && $("title").value) || "",
summary: ($("summary") && $("summary").value) || "",
body: ($("body") && $("body").value) || "",
thumbMediaId: ($("thumbMediaId") && $("thumbMediaId").value) || "",
coverStyleHint: ($("coverStyleHint") && $("coverStyleHint").value) || "",
coverImageModel: (coverImageModelInput && coverImageModelInput.value) || "",
coverAutoAfterRewrite: Boolean(coverAutoAfterRewrite && coverAutoAfterRewrite.checked),
posterAutoInclude: Boolean(posterAutoInclude && posterAutoInclude.checked),
audienceChipValues: Array.from(document.querySelectorAll('input[name="audienceChip"]:checked')).map((n) => n.value),
toneChipValues: Array.from(document.querySelectorAll('input[name="toneChip"]:checked')).map((n) => n.value),
};
window.localStorage.setItem(DRAFT_STORAGE_KEY, JSON.stringify(data));
} catch {
// ignore
}
}
function restoreDraftState() {
try {
const raw = window.localStorage.getItem(DRAFT_STORAGE_KEY);
if (!raw) return;
const data = JSON.parse(raw);
if (!data || typeof data !== "object") return;
const setVal = (id, val) => {
const el = $(id);
if (!el || typeof val !== "string") return;
el.value = val;
};
setVal("sourceText", data.sourceText || "");
setVal("titleHint", data.titleHint || "");
setVal("audienceExtra", data.audienceExtra || "");
setVal("toneExtra", data.toneExtra || "");
setVal("avoidWords", data.avoidWords || "");
setVal("keepPoints", data.keepPoints || "");
setVal("targetBodyChars", String(data.targetBodyChars || "500"));
setVal("title", data.title || "");
setVal("summary", data.summary || "");
setVal("body", data.body || "");
setVal("thumbMediaId", data.thumbMediaId || "");
setVal("coverStyleHint", data.coverStyleHint || "");
if (coverImageModelInput && typeof data.coverImageModel === "string" && data.coverImageModel.trim()) {
coverImageModelInput.value = data.coverImageModel;
}
if (coverAutoAfterRewrite) coverAutoAfterRewrite.checked = Boolean(data.coverAutoAfterRewrite);
if (posterAutoInclude) posterAutoInclude.checked = Boolean(data.posterAutoInclude);
const audienceSet = new Set(Array.isArray(data.audienceChipValues) ? data.audienceChipValues : []);
document.querySelectorAll('input[name="audienceChip"]').forEach((el) => {
el.checked = audienceSet.size ? audienceSet.has(el.value) : el.checked;
});
const toneSet = new Set(Array.isArray(data.toneChipValues) ? data.toneChipValues : []);
document.querySelectorAll('input[name="toneChip"]').forEach((el) => {
el.checked = toneSet.size ? toneSet.has(el.value) : el.checked;
});
} catch {
// ignore
}
}
function clearDraftState() {
try {
window.localStorage.removeItem(DRAFT_STORAGE_KEY);
} catch {
// ignore
}
const clearIds = [
"sourceText",
"titleHint",
"audienceExtra",
"toneExtra",
"avoidWords",
"keepPoints",
"title",
"summary",
"body",
"thumbMediaId",
"coverStyleHint",
"coverUrl",
];
clearIds.forEach((id) => {
const el = $(id);
if (!el) return;
el.value = "";
});
if ($("targetBodyChars")) $("targetBodyChars").value = "500";
if (coverAutoAfterRewrite) coverAutoAfterRewrite.checked = false;
if (posterAutoInclude) posterAutoInclude.checked = false;
if (coverImageModelInput) coverImageModelInput.value = "";
document.querySelectorAll('input[name="audienceChip"]').forEach((el) => {
el.checked = false;
});
document.querySelectorAll('input[name="toneChip"]').forEach((el) => {
el.checked = false;
});
if (coverPreviewWrap) coverPreviewWrap.hidden = true;
if (coverPreview) coverPreview.src = "";
if ($("coverFile")) $("coverFile").value = "";
posterState = { signature: "", bodyMarkdownWithPosters: "", posters: [] };
renderPosterPreview([]);
updateCounters();
syncTargetCharChips();
initMultiDropdowns();
initImageModelStatus();
if (posterHint) posterHint.textContent = "默认不生成海报,点击“生成段落海报”后再插入发布。";
setStatus("草稿已清除。");
}
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",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const data = await res.json();
if (!res.ok) {
const err = new Error(data.detail || "请求失败");
err.payload = data;
throw err;
}
data._requestId = res.headers.get("X-Request-ID") || "";
return data;
}
function handleUpgradeRequired(err) {
const data = (err && err.payload) || {};
const msg = (err && err.message) || data.detail || "";
if (!data.upgrade_required && !msg.includes("免费额度已用完") && !msg.includes("余额不足")) return false;
setStatus("免费额度已用完,请前往升级页充值或升级 VIP 用户。", true);
window.setTimeout(() => {
window.location.href = "/upgrade";
}, 800);
return true;
}
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,
image_model: (coverImageModelInput && coverImageModelInput.value.trim()) || "",
upload_to_wechat: true,
});
if (!data.ok) {
const err = new Error(data.detail || "海报生成失败");
err.payload = data;
throw err;
}
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(),
image_model: (coverImageModelInput && coverImageModelInput.value.trim()) || "",
upload_to_wechat: true,
});
if (!data.ok) {
const err = new Error(data.detail || "封面生成失败");
err.payload = data;
throw err;
}
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();
if (!data.ok || !data.logged_in) {
window.location.href = "/auth?next=/";
return null;
}
return data;
}
function renderWechatAccountSelect(me) {
const sel = $("wechatAccountSelect");
const hint = $("wechatAccountStatus");
if (!sel) return;
const list = Array.isArray(me.wechat_accounts) ? me.wechat_accounts : [];
const activeId =
me.active_wechat_account && me.active_wechat_account.id
? Number(me.active_wechat_account.id)
: 0;
sel.innerHTML = "";
if (!list.length) {
const opt = document.createElement("option");
opt.value = "";
opt.textContent = "未绑定公众号";
sel.appendChild(opt);
sel.disabled = true;
if (hint) hint.textContent = "请先绑定公众号";
return;
}
sel.disabled = false;
list.forEach((a) => {
const opt = document.createElement("option");
opt.value = String(a.id);
const name = (a.account_name || "未命名").trim();
const appid = (a.appid || "").trim();
opt.textContent = appid ? `${name} (${appid})` : name;
if ((activeId && Number(a.id) === activeId) || a.active) opt.selected = true;
sel.appendChild(opt);
});
}
function flashWechatAccountHint(msg, clearMs = 2600) {
const hint = $("wechatAccountStatus");
if (!hint) return;
hint.textContent = msg;
if (clearMs > 0) {
window.clearTimeout(flashWechatAccountHint._t);
flashWechatAccountHint._t = window.setTimeout(() => {
hint.textContent = "";
}, clearMs);
}
}
const wechatAccountSelect = $("wechatAccountSelect");
if (wechatAccountSelect) {
wechatAccountSelect.addEventListener("change", async () => {
const id = Number(wechatAccountSelect.value || 0);
if (!id) return;
try {
const out = await postJSON("/api/auth/wechat/switch", { account_id: id });
if (!out.ok) {
flashWechatAccountHint(out.detail || "切换失败", 4000);
const me = await fetchAuthMe();
if (me) renderWechatAccountSelect(me);
return;
}
const me = await fetchAuthMe();
if (me) renderWechatAccountSelect(me);
flashWechatAccountHint("已切换");
} catch (e) {
flashWechatAccountHint(e.message || "切换失败", 4000);
const me = await fetchAuthMe();
if (me) renderWechatAccountSelect(me);
}
});
}
async function initWechatAccountSwitch() {
const me = await fetchAuthMe();
if (me) renderWechatAccountSelect(me);
}
async function initImageModelStatus() {
try {
const me = await fetch("/api/auth/me").then((r) => r.json());
const active = me && me.active_ai_model ? me.active_ai_model : null;
const imageModel = active && active.image_model ? String(active.image_model).trim() : "";
if (coverImageModelInput) {
const current = (coverImageModelInput.value || "").trim();
if (!current) coverImageModelInput.value = imageModel || "wanx2.0-t2i-turbo";
}
} catch {
if (coverImageModelInput && !(coverImageModelInput.value || "").trim()) coverImageModelInput.value = "wanx2.0-t2i-turbo";
}
}
async function logoutAndGoAuth() {
try {
await postJSON("/api/auth/logout", {});
} catch {
// 忽略退出接口异常,直接跳转认证页
}
window.location.href = "/auth?next=/";
}
if (logoutBtn) {
logoutBtn.addEventListener("click", async () => {
setLoading(logoutBtn, true, "退出登录", "退出中...");
await logoutAndGoAuth();
});
}
if (clearDraftBtn) {
clearDraftBtn.addEventListener("click", () => {
clearDraftState();
});
}
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);
if (!targetBodyCharsInput || !Number.isFinite(n) || n < 1) return;
targetBodyCharsInput.value = String(n);
syncTargetCharChips();
});
});
if (targetBodyCharsInput) {
targetBodyCharsInput.addEventListener("input", syncTargetCharChips);
}
$("rewriteBtn").addEventListener("click", async () => {
const sourceText = $("sourceText").value.trim();
const targetBodyChars = Number(($("targetBodyChars") && $("targetBodyChars").value) || 500);
if (sourceText.length < 20) {
setStatus("原始内容太短,至少 20 个字符", true);
return;
}
if (!Number.isFinite(targetBodyChars) || targetBodyChars < 180 || targetBodyChars > 2200) {
setStatus("改写目标字数需在 180~2200 之间", true);
return;
}
setStatus("正在改写...");
setLoading(rewriteBtn, true, "改写并排版", "改写中...");
try {
const audience = buildMultiPrompt("audienceChip", "audienceExtra") || "公众号读者";
const tone = buildMultiPrompt("toneChip", "toneExtra") || "专业、可信、可读性强";
const data = await postJSON("/api/rewrite", {
source_text: sourceText,
title_hint: $("titleHint").value,
tone,
audience,
keep_points: $("keepPoints").value,
avoid_words: $("avoidWords").value,
target_body_chars: Math.round(targetBodyChars),
});
$("title").value = data.title || "";
$("summary").value = data.summary || "";
$("body").value = data.body_markdown || "";
updateCounters();
saveDraftState();
const tr = data.trace || {};
if (data.mode === "fallback") {
const note = (data.quality_notes || [])[0] || "当前为保底改写稿";
setStatus(`改写完成(保底模式):${note}`, true);
} else if (tr.quality_soft_accept) {
setStatus(`改写完成(有提示):${(data.quality_notes || []).join("") || "请检查正文"}`);
statusEl.style.color = "#9a3412";
} else {
setStatus("改写完成。");
}
if (posterHint) posterHint.textContent = "改写完成。默认不自动生成海报,可手动点击“生成段落海报”。";
if (coverAutoAfterRewrite && coverAutoAfterRewrite.checked) {
try {
setStatus("改写完成,正在按输出标题生成封面...");
await generateWechatCover({ silent: true });
setStatus("改写、封面与段落海报生成完成。");
} catch (coverErr) {
if (handleUpgradeRequired(coverErr)) return;
setStatus(`改写完成,封面未生成:${coverErr.message}`, true);
}
}
} catch (e) {
if (handleUpgradeRequired(e)) return;
setStatus(`改写失败: ${e.message}`, true);
} finally {
setLoading(rewriteBtn, false, "改写并排版", "改写中...");
}
});
$("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 && posterState.bodyMarkdownWithPosters) {
bodyForPublish = posterState.bodyMarkdownWithPosters;
} else {
setStatus("未检测到可用海报,本次仅发布文字;如需海报请先手动生成。", true);
}
}
const data = await postJSON("/api/publish/wechat", {
title: $("title").value,
summary: $("summary").value,
body_markdown: bodyForPublish,
thumb_media_id: $("thumbMediaId") ? $("thumbMediaId").value.trim() : "",
});
if (!data.ok) throw new Error(data.detail);
setStatus("公众号草稿发布成功");
} catch (e) {
setStatus(`公众号发布失败: ${e.message}`, true);
if ((e.message || "").includes("请先登录")) {
window.location.href = "/auth?next=/";
} else if ((e.message || "").includes("未绑定公众号")) {
window.location.href = "/settings";
}
} finally {
setLoading(wechatBtn, false, "发布到公众号草稿箱", "发布中...");
}
});
if (coverGenerateBtn) {
coverGenerateBtn.addEventListener("click", async () => {
try {
await generateWechatCover({ silent: false });
saveDraftState();
} catch (e) {
if (handleUpgradeRequired(e)) return;
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 (saveCoverImageModelBtn) {
saveCoverImageModelBtn.addEventListener("click", async () => {
const value = (coverImageModelInput && coverImageModelInput.value.trim()) || "";
if (!value) {
setStatus("请先填写文生图模型", true);
return;
}
setLoading(saveCoverImageModelBtn, true, "保存模型", "保存中...");
try {
const out = await postJSON("/api/auth/ai-models/image-model/update", { image_model: value });
if (!out.ok) {
setStatus(out.detail || "保存失败", true);
return;
}
setStatus("文生图模型已保存。");
saveDraftState();
} catch (e) {
setStatus(e.message || "保存失败", true);
} finally {
setLoading(saveCoverImageModelBtn, false, "保存模型", "保存中...");
}
});
}
if (coverUploadBtn) {
coverUploadBtn.addEventListener("click", async () => {
const fileInput = $("coverFile");
const hint = $("coverHint");
const file = fileInput && fileInput.files && fileInput.files[0];
if (!file) {
setStatus("请先选择封面图片再上传", true);
return;
}
if (hint) hint.textContent = "正在上传封面...";
setLoading(coverUploadBtn, true, "上传封面并绑定", "上传中...");
try {
const fd = new FormData();
fd.append("file", file);
const res = await fetch("/api/wechat/cover/upload", { method: "POST", body: fd });
const data = await res.json();
if (!res.ok || !data.ok) throw new Error(data.detail || "封面上传失败");
const mid = data.data && data.data.thumb_media_id ? data.data.thumb_media_id : "";
if ($("thumbMediaId")) $("thumbMediaId").value = mid;
if (hint) hint.textContent = `封面上传成功,已绑定 media_id${mid}`;
setStatus("封面上传成功,发布时将优先使用该封面。");
saveDraftState();
} catch (e) {
if (hint) hint.textContent = "封面上传失败,请看状态提示。";
setStatus(`封面上传失败: ${e.message}`, true);
if ((e.message || "").includes("请先登录")) {
window.location.href = "/auth?next=/";
} else if ((e.message || "").includes("未绑定公众号")) {
window.location.href = "/settings";
}
} finally {
setLoading(coverUploadBtn, false, "上传封面并绑定", "上传中...");
}
});
}
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 = "";
saveDraftState();
} 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 });
saveDraftState();
} catch (e) {
if (handleUpgradeRequired(e)) return;
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", "发送中...");
try {
const data = await postJSON("/api/publish/im", {
title: $("title").value,
body_markdown: $("body").value,
});
if (!data.ok) throw new Error(data.detail);
setStatus("IM 发送成功");
} catch (e) {
setStatus(`IM 发送失败: ${e.message}`, true);
} finally {
setLoading(imBtn, false, "发送到 IM", "发送中...");
}
});
["sourceText", "title", "summary", "body"].forEach((id) => {
$(id).addEventListener("input", updateCounters);
$(id).addEventListener("input", saveDraftState);
if (id !== "sourceText") $(id).addEventListener("input", markPosterStaleIfNeeded);
});
["titleHint", "audienceExtra", "toneExtra", "avoidWords", "keepPoints", "targetBodyChars", "thumbMediaId", "coverStyleHint"].forEach(
(id) => {
const el = $(id);
if (el) el.addEventListener("input", saveDraftState);
},
);
document.querySelectorAll('input[name="audienceChip"],input[name="toneChip"]').forEach((el) => {
el.addEventListener("change", saveDraftState);
});
if (coverAutoAfterRewrite) coverAutoAfterRewrite.addEventListener("change", saveDraftState);
if (posterAutoInclude) posterAutoInclude.addEventListener("change", saveDraftState);
if (coverImageModelInput) coverImageModelInput.addEventListener("input", saveDraftState);
restoreDraftState();
updateCounters();
initMultiDropdowns();
initWechatAccountSwitch();
syncTargetCharChips();
renderPosterPreview([]);
setCoverMode("manual");
initImageModelStatus();
window.addEventListener("beforeunload", saveDraftState);
window.addEventListener("load", () => setCoverMode("manual"));