Files
novel-app/static/js/pages/novel-detail.js
2026-08-17 17:11:30 +08:00

181 lines
7.9 KiB
JavaScript

/* novel detail page: 章节 / 角色 / 伏笔 三 tab 管理. */
import { escapeHtml, fetchJson, postJson, showError } from "../utils.js";
import { statusBadge } from "../components/status-badge.js";
const NOVEL_ID = window.NOVEL_ID;
const alertsEl = document.getElementById("ndAlerts");
/* ---------------- header ---------------- */
async function loadHeader() {
const el = document.getElementById("ndHeader");
try {
const n = await fetchJson("/api/novels/" + NOVEL_ID);
const s = n.stats || {};
const pct = n.target_words > 0
? Math.min(100, Math.round(((n.current_words || 0) / n.target_words) * 100)) : 0;
el.innerHTML = ''
+ '<div class="card-body">'
+ ' <h2 class="card-title">' + escapeHtml(n.title) + '</h2>'
+ ' <p class="card-text">'
+ ' <span class="badge text-bg-primary">' + escapeHtml(n.genre || "未分类") + '</span> '
+ statusBadge(n.status)
+ ' <span class="text-muted ms-2">' + escapeHtml(n.style || "") + '</span>'
+ ' </p>'
+ ' <div class="progress mb-2"><div class="progress-bar" style="width:' + pct + '%">' + pct + '%</div></div>'
+ ' <p class="card-text small text-muted">'
+ ' 章节 ' + (s.chapter_count || 0) + ' (完成 ' + (s.done_chapters || 0) + ') · '
+ ' 总字数 ' + (s.total_words || 0) + ' · '
+ ' 角色 ' + (s.character_count || 0) + ' · '
+ ' 未回收伏笔 ' + (s.open_foreshadowing || 0)
+ ' </p>'
+ '</div>';
} catch (err) {
el.innerHTML = '<div class="card-body"><div class="alert alert-danger mb-0">'
+ escapeHtml(err.message) + "</div></div>";
}
}
/* ---------------- chapters ---------------- */
async function loadChapters() {
const el = document.getElementById("ndChapterList");
try {
const chapters = await fetchJson("/api/novels/" + NOVEL_ID + "/chapters");
if (!chapters.length) {
el.innerHTML = '<span class="text-muted">还没有章节, 用上方表单添加。</span>';
return;
}
const byVolume = {};
chapters.forEach((c) => {
const v = c.volume || 1;
(byVolume[v] = byVolume[v] || []).push(c);
});
el.innerHTML = Object.keys(byVolume).sort((a, b) => a - b).map((v) => {
const rows = byVolume[v].map((c) => ''
+ '<tr>'
+ '<td>第' + c.chapter_number + '章</td>'
+ '<td>' + escapeHtml(c.title || "") + '</td>'
+ '<td>' + statusBadge(c.status) + '</td>'
+ '<td>' + (c.word_count || 0) + '</td>'
+ '<td><a class="btn btn-outline-primary btn-sm" href="/novel/' + NOVEL_ID
+ '/chapter/' + c.id + '">' + (c.content ? "续写/查看" : "写") + "</a></td>"
+ "</tr>").join("");
return ''
+ '<h5 class="mt-3">第' + v + '卷</h5>'
+ '<table class="table table-sm table-hover">'
+ '<thead><tr><th>章节</th><th>标题</th><th>状态</th><th>字数</th><th></th></tr></thead>'
+ "<tbody>" + rows + "</tbody></table>";
}).join("");
} catch (err) {
el.innerHTML = '<div class="alert alert-danger">' + escapeHtml(err.message) + "</div>";
}
}
document.getElementById("ndAddChapterForm").addEventListener("submit", async (e) => {
e.preventDefault();
try {
await postJson("/api/novels/" + NOVEL_ID + "/chapters", {
volume: parseInt(document.getElementById("ndChVolume").value, 10),
chapter_number: parseInt(document.getElementById("ndChNumber").value, 10),
title: document.getElementById("ndChTitle").value.trim(),
outline: document.getElementById("ndChOutline").value.trim(),
});
document.getElementById("ndChTitle").value = "";
document.getElementById("ndChOutline").value = "";
loadChapters();
} catch (err) {
showError(alertsEl, "添加章节失败: " + err.message);
}
});
/* ---------------- characters ---------------- */
async function loadCharacters() {
const el = document.getElementById("ndCharacterList");
try {
const chars = await fetchJson("/api/novels/" + NOVEL_ID + "/characters");
if (!chars.length) {
el.innerHTML = '<span class="text-muted">还没有角色。</span>';
return;
}
el.innerHTML = '<table class="table table-sm"><thead><tr>'
+ "<th>名字</th><th>定位</th><th>状态</th><th>描述</th><th></th>"
+ "</tr></thead><tbody>"
+ chars.map((c) => ''
+ "<tr><td>" + escapeHtml(c.name) + "</td>"
+ "<td>" + escapeHtml(c.role || "") + "</td>"
+ "<td>" + statusBadge(c.status) + "</td>"
+ '<td class="small">' + escapeHtml((c.description || "").slice(0, 120)) + "</td>"
+ '<td><button type="button" class="btn btn-outline-secondary btn-sm nd-edit-char" data-id="'
+ c.id + '" data-name="' + escapeHtml(c.name) + '">编辑</button></td></tr>'
).join("") + "</tbody></table>";
el.querySelectorAll(".nd-edit-char").forEach((btn) => {
btn.addEventListener("click", async () => {
const desc = prompt("编辑角色描述 (" + btn.dataset.name + "):");
if (desc === null) return;
try {
await postJson("/api/characters/" + btn.dataset.id, { description: desc });
loadCharacters();
} catch (err) {
showError(alertsEl, "编辑失败: " + err.message);
}
});
});
} catch (err) {
el.innerHTML = '<div class="alert alert-danger">' + escapeHtml(err.message) + "</div>";
}
}
/* ---------------- foreshadowing ---------------- */
async function loadForeshadowing() {
const el = document.getElementById("ndForeshadowingList");
try {
const list = await fetchJson("/api/novels/" + NOVEL_ID + "/foreshadowing");
if (!list.length) {
el.innerHTML = '<span class="text-muted">还没有伏笔。</span>';
return;
}
el.innerHTML = '<table class="table table-sm"><thead><tr>'
+ "<th>描述</th><th>埋设</th><th>回收</th><th>状态</th><th></th>"
+ "</tr></thead><tbody>"
+ list.map((f) => ''
+ '<tr><td class="small">' + escapeHtml((f.description || "").slice(0, 150)) + "</td>"
+ "<td>" + (f.planted_chapter ? "第" + f.planted_chapter + "章" : "-") + "</td>"
+ "<td>" + (f.resolved_chapter ? "第" + f.resolved_chapter + "章" : "-") + "</td>"
+ "<td>" + statusBadge(f.status) + "</td>"
+ "<td>" + (f.status !== "resolved"
? '<button type="button" class="btn btn-outline-success btn-sm nd-resolve-fs" data-id="'
+ f.id + '">标记回收</button>' : "")
+ "</td></tr>"
).join("") + "</tbody></table>";
el.querySelectorAll(".nd-resolve-fs").forEach((btn) => {
btn.addEventListener("click", async () => {
const ch = prompt("回收于第几章?");
if (ch === null) return;
try {
await postJson("/api/foreshadowing/" + btn.dataset.id, {
status: "resolved", resolved_chapter: parseInt(ch, 10) || null,
});
loadForeshadowing();
} catch (err) {
showError(alertsEl, "操作失败: " + err.message);
}
});
});
} catch (err) {
el.innerHTML = '<div class="alert alert-danger">' + escapeHtml(err.message) + "</div>";
}
}
/* ---------------- init ---------------- */
loadHeader();
loadChapters();
loadCharacters();
loadForeshadowing();