@musnows/scriverse 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +1 -1
- package/README.md +3 -1
- package/dist/app.js +13 -1
- package/dist/app.js.map +1 -1
- package/dist/database.js +36 -0
- package/dist/database.js.map +1 -1
- package/dist/public/app.js +317 -134
- package/dist/public/character-version.js +3 -1
- package/dist/public/index.html +38 -9
- package/dist/public/keyboard-shortcuts.d.ts +9 -0
- package/dist/public/keyboard-shortcuts.js +4 -0
- package/dist/public/relationship-graph.js +62 -2
- package/dist/public/styles.css +173 -48
- package/dist/public/work-permissions.js +1 -1
- package/dist/server-runtime.js +5 -1
- package/dist/server-runtime.js.map +1 -1
- package/dist/store.js +10 -7
- package/dist/store.js.map +1 -1
- package/dist/user-auth.js +75 -3
- package/dist/user-auth.js.map +1 -1
- package/dist/version.js +1 -1
- package/dist/work-permissions.js +1 -1
- package/package.json +1 -1
package/dist/public/app.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { buildRelationshipGraph, createGalaxyRenderer, renderRelationshipMindMap } from "/relationship-graph.js?v=
|
|
1
|
+
import { buildRelationshipGraph, createGalaxyRenderer, renderRelationshipMindMap } from "/relationship-graph.js?v=20260724-relationship-density";
|
|
2
2
|
import { collapseExcessBlankLines, formatDateTime, normalizeParagraphSpacing } from "/text-formatting.js?v=20260713-saved-at-seconds";
|
|
3
3
|
import { renderMarkdown } from "/markdown.js?v=20260722-inline-code";
|
|
4
4
|
import { buildAiReferenceScope, findAiMention, listAiMentionOptions } from "/ai-mentions.js?v=20260716-chapter-references";
|
|
@@ -19,8 +19,9 @@ import { splitRelationshipKeywordInput, splitRelationshipKeywords, uniqueRelatio
|
|
|
19
19
|
import { tokenizeVisibleSpaces } from "/whitespace-visualization.js?v=20260718-visible-whitespace";
|
|
20
20
|
import { buildRaceForest, eligibleRaceParents, racePathLabel } from "/race-hierarchy.js?v=20260721-race-hierarchy";
|
|
21
21
|
import { ANALYSIS_TYPES, analysisTypeDescription } from "/analysis-types.js?v=20260721-analysis-descriptions";
|
|
22
|
-
import { WORK_PERMISSION_MODULES, canReadPermissionModule, canReadUiModule, canWritePermissionModule, canWriteUiModule, emptyModulePermissions, firstReadableUiModule, normalizeModulePermissions, permissionSummary } from "/work-permissions.js?v=
|
|
22
|
+
import { WORK_PERMISSION_MODULES, canReadPermissionModule, canReadUiModule, canWritePermissionModule, canWriteUiModule, emptyModulePermissions, firstReadableUiModule, normalizeModulePermissions, permissionSummary } from "/work-permissions.js?v=20260724-outline-title";
|
|
23
23
|
import { MODULE_LAYOUT_STORAGE_KEY, LEGACY_SETTINGS_LAYOUT_STORAGE_KEY, normalizeModuleLayout, moduleLayoutLabel } from "/module-layout.js?v=20260723-module-layout-toggle";
|
|
24
|
+
import { isGlobalSearchShortcut } from "/keyboard-shortcuts.js?v=20260723-global-search";
|
|
24
25
|
|
|
25
26
|
const state = {
|
|
26
27
|
user: null,
|
|
@@ -52,6 +53,8 @@ const state = {
|
|
|
52
53
|
contextChapterId: null
|
|
53
54
|
};
|
|
54
55
|
|
|
56
|
+
let timelineMultiSelectEnabled = false;
|
|
57
|
+
|
|
55
58
|
const chapterTypes = ["正文", "设定", "作者的话", "其他"];
|
|
56
59
|
|
|
57
60
|
const taskTypeLabels = MODEL_PURPOSE_OPTIONS;
|
|
@@ -232,7 +235,7 @@ const workspaceOnboardingSteps = [
|
|
|
232
235
|
{ selector: "[data-new-chapter-volume]", eyebrow: "正文结构", title: "新建章节", description: "使用分卷和章节组织长篇正文,章节会自动保存并保留版本。", placement: "right" },
|
|
233
236
|
{ selector: "#versions-button", eyebrow: "版本安全", title: "查看章节版本", description: "每次保存都会生成可恢复版本,误改内容时可以随时回溯。", placement: "bottom" },
|
|
234
237
|
{ selector: "[data-module=\"characters\"]", eyebrow: "作品知识", title: "维护角色与世界资料", description: "角色、种族、组织、设定和时间线共同构成 AI 可引用的作品知识。", placement: "right" },
|
|
235
|
-
{ selector: "[data-module=\"outlines\"]", eyebrow: "创作规划", title: "
|
|
238
|
+
{ selector: "[data-module=\"outlines\"]", eyebrow: "创作规划", title: "跟踪大纲/伏笔", description: "记录剧情目标、冲突、转折和伏笔回收,避免长线遗漏。", placement: "right" },
|
|
236
239
|
{ selector: "[data-module=\"tasks\"]", eyebrow: "AI 分析中心", title: "从这里理解整部小说", description: "运行人物、关系、世界观、设定、事件和一致性分析,并查看每次分析的结果与进度。", placement: "right" },
|
|
237
240
|
{ selector: "#top-search-button", eyebrow: "全文检索", title: "搜索整部作品", description: "一次检索正文、角色、设定、种族与组织,快速定位创作依据。", placement: "bottom" },
|
|
238
241
|
{ selector: ".quick-actions button[data-task=\"continue\"]", eyebrow: "AI 快捷指令", title: "让创作助手基于正文工作", description: "总结、续写、剧情方向和冲突检查都以已保存内容为依据。", placement: "left" },
|
|
@@ -623,14 +626,25 @@ function syncChapterLineNumberScroll() {
|
|
|
623
626
|
}
|
|
624
627
|
}
|
|
625
628
|
|
|
629
|
+
function syncChapterWhitespaceControls() {
|
|
630
|
+
document.querySelectorAll("[data-toggle-whitespace]").forEach((button) => {
|
|
631
|
+
button.setAttribute("aria-pressed", String(chapterWhitespaceVisible));
|
|
632
|
+
button.textContent = chapterWhitespaceVisible ? "隐藏空白符" : "显示空白符";
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function toggleChapterWhitespaceVisibility() {
|
|
637
|
+
chapterWhitespaceVisible = !chapterWhitespaceVisible;
|
|
638
|
+
syncChapterWhitespaceControls();
|
|
639
|
+
scheduleChapterLineNumbers();
|
|
640
|
+
}
|
|
641
|
+
|
|
626
642
|
function renderChapterWhitespaceMarkers(input, style) {
|
|
627
643
|
const overlay = $("#chapter-whitespace-overlay");
|
|
628
644
|
const inner = $("#chapter-whitespace-inner");
|
|
629
|
-
|
|
630
|
-
if (!overlay || !inner
|
|
645
|
+
syncChapterWhitespaceControls();
|
|
646
|
+
if (!overlay || !inner) return;
|
|
631
647
|
overlay.classList.toggle("is-visible", chapterWhitespaceVisible);
|
|
632
|
-
button.setAttribute("aria-pressed", String(chapterWhitespaceVisible));
|
|
633
|
-
button.textContent = chapterWhitespaceVisible ? "隐藏空白符" : "显示空白符";
|
|
634
648
|
if (!chapterWhitespaceVisible) {
|
|
635
649
|
inner.replaceChildren();
|
|
636
650
|
return;
|
|
@@ -1504,6 +1518,7 @@ function renderTypographyPreview() {
|
|
|
1504
1518
|
function openAppearanceDialog() {
|
|
1505
1519
|
fillAppearanceForm(typographySettings);
|
|
1506
1520
|
renderTypographyPreview();
|
|
1521
|
+
syncChapterWhitespaceControls();
|
|
1507
1522
|
$("#appearance-dialog").showModal();
|
|
1508
1523
|
}
|
|
1509
1524
|
|
|
@@ -1609,6 +1624,23 @@ async function apiAllPages(path, limit = 100) {
|
|
|
1609
1624
|
}
|
|
1610
1625
|
}
|
|
1611
1626
|
|
|
1627
|
+
async function initializeProductFooters() {
|
|
1628
|
+
const year = String(new Date().getFullYear());
|
|
1629
|
+
document.querySelectorAll("[data-product-footer-year]").forEach((element) => { element.textContent = year; });
|
|
1630
|
+
try {
|
|
1631
|
+
const health = await api("/api/health");
|
|
1632
|
+
const version = String(health.version ?? "").trim();
|
|
1633
|
+
document.querySelectorAll("[data-product-footer-version]").forEach((element) => {
|
|
1634
|
+
element.textContent = version ? `v${version}` : "v—";
|
|
1635
|
+
});
|
|
1636
|
+
document.querySelectorAll("[data-product-footer-development]").forEach((element) => {
|
|
1637
|
+
element.classList.toggle("hidden", health.development !== true);
|
|
1638
|
+
});
|
|
1639
|
+
} catch {
|
|
1640
|
+
document.querySelectorAll("[data-product-footer-version]").forEach((element) => { element.textContent = "v—"; });
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1612
1644
|
function selectAuthMode(mode) {
|
|
1613
1645
|
const registerTab = $("#auth-register-tab");
|
|
1614
1646
|
const login = mode === "login" || registerTab.disabled;
|
|
@@ -1670,7 +1702,8 @@ function applyAuthenticatedUser(session) {
|
|
|
1670
1702
|
state.csrfToken = session.csrfToken;
|
|
1671
1703
|
$("#account-name").textContent = session.user.displayName;
|
|
1672
1704
|
renderUserAvatar($("#account-avatar"), session.user);
|
|
1673
|
-
$("#account-menu-name").textContent =
|
|
1705
|
+
$("#account-menu-display-name").textContent = session.user.displayName;
|
|
1706
|
+
$("#account-menu-username").textContent = `@${session.user.username}`;
|
|
1674
1707
|
$("#account-menu-role").textContent = session.user.role === "admin" ? "系统管理员" : "普通用户";
|
|
1675
1708
|
$("#auth-view").classList.add("hidden");
|
|
1676
1709
|
document.documentElement.classList.remove("login-route");
|
|
@@ -1934,7 +1967,8 @@ function restoredSettingsReturnContext(route) {
|
|
|
1934
1967
|
}
|
|
1935
1968
|
|
|
1936
1969
|
async function initializePage() {
|
|
1937
|
-
|
|
1970
|
+
const [authenticated] = await Promise.all([initializeAuthentication(), initializeProductFooters()]);
|
|
1971
|
+
if (!authenticated) {
|
|
1938
1972
|
restoringPageRoute = false;
|
|
1939
1973
|
return;
|
|
1940
1974
|
}
|
|
@@ -2209,6 +2243,11 @@ async function openSearchDialog() {
|
|
|
2209
2243
|
toast("请先打开一部作品", "error");
|
|
2210
2244
|
return;
|
|
2211
2245
|
}
|
|
2246
|
+
if ($("#search-dialog").open) {
|
|
2247
|
+
$("#search-query").focus();
|
|
2248
|
+
$("#search-query").select();
|
|
2249
|
+
return;
|
|
2250
|
+
}
|
|
2212
2251
|
$("#search-dialog .eyebrow").textContent = `当前作品 · 《${state.work.title}》`;
|
|
2213
2252
|
$("#search-query").value = "";
|
|
2214
2253
|
$("#search-results").innerHTML = '<p class="search-results-empty">输入关键词后开始检索。</p>';
|
|
@@ -2312,7 +2351,7 @@ async function returnFromSettings() {
|
|
|
2312
2351
|
$("#app").classList.remove("shelf-mode");
|
|
2313
2352
|
$("#shelf-view").classList.add("hidden");
|
|
2314
2353
|
updateDocumentTitle(state.work);
|
|
2315
|
-
$("#work-meta").textContent = `${state.work.title}${state.work.author ? ` · ${state.work.author}` : ""} · ${state.work.wordCount} 字`;
|
|
2354
|
+
$("#work-meta").textContent = `${state.work.title}${state.work.author ? ` · ${state.work.author}` : ""} · ${Number(state.work.wordCount ?? 0).toLocaleString("zh-CN")} 字`;
|
|
2316
2355
|
if (context.view === "module") return showModule(context.module);
|
|
2317
2356
|
if (context.view === "editor" && context.chapterId) return selectChapter(context.chapterId);
|
|
2318
2357
|
return showWelcome(true);
|
|
@@ -2346,7 +2385,7 @@ function renderShelf() {
|
|
|
2346
2385
|
<span class="book-cover-fallback">${esc(Array.from(work.title)[0] ?? "书")}</span>
|
|
2347
2386
|
${work.coverUrl ? `<img src="${esc(work.coverUrl)}" alt="${esc(work.title)} 封面">` : ""}
|
|
2348
2387
|
</span>
|
|
2349
|
-
<span class="book-info"><strong>${esc(work.title)}</strong><small>${esc(work.author || "未署名")} · ${work.chapterCount} 章 · ${work.wordCount} 字</small><span>${esc(work.description || "尚未填写作品简介")}</span><em class="book-access-badge">${work.accessRole === "viewer" ? "全部只读" : work.accessRole === "settings-editor" ? "设定协作" : work.accessRole === "editor" ? "全部可编辑" : work.accessRole === "custom" ? "自定义权限" : work.accessRole === "admin" ? "管理员访问" : "我的作品"}</em></span>
|
|
2388
|
+
<span class="book-info"><strong>${esc(work.title)}</strong><small>${esc(work.author || "未署名")} · ${work.chapterCount} 章 · ${Number(work.wordCount ?? 0).toLocaleString("zh-CN")} 字</small><span>${esc(work.description || "尚未填写作品简介")}</span><em class="book-access-badge">${work.accessRole === "viewer" ? "全部只读" : work.accessRole === "settings-editor" ? "设定协作" : work.accessRole === "editor" ? "全部可编辑" : work.accessRole === "custom" ? "自定义权限" : work.accessRole === "admin" ? "管理员访问" : "我的作品"}</em></span>
|
|
2350
2389
|
</button>
|
|
2351
2390
|
${canManageWork(work) ? `<button class="book-card-settings" type="button" data-edit-work="${esc(work.id)}" aria-label="作品设置" title="作品设置">设置</button>` : ""}
|
|
2352
2391
|
</article>`).join("")}
|
|
@@ -2410,7 +2449,7 @@ async function selectWork(workId, preferredChapterId = null) {
|
|
|
2410
2449
|
if (!canReadModule(state.module)) state.module = firstReadableUiModule(state.work) ?? "editor";
|
|
2411
2450
|
applyWorkAccessMode();
|
|
2412
2451
|
updateDocumentTitle(state.work);
|
|
2413
|
-
$("#work-meta").textContent = `${state.work.title}${state.work.author ? ` · ${state.work.author}` : ""} · ${state.work.wordCount} 字`;
|
|
2452
|
+
$("#work-meta").textContent = `${state.work.title}${state.work.author ? ` · ${state.work.author}` : ""} · ${Number(state.work.wordCount ?? 0).toLocaleString("zh-CN")} 字`;
|
|
2414
2453
|
$("#top-search-button").disabled = !canReadAggregateContent();
|
|
2415
2454
|
renderTree();
|
|
2416
2455
|
const chapters = state.work.volumes.flatMap((volume) => volume.chapters);
|
|
@@ -2561,7 +2600,7 @@ const moduleMeta = {
|
|
|
2561
2600
|
races: ["物种档案", "种族与共同设定", "先维护种族档案,再由角色选择引用;角色不能临时填写种族。", "新建种族"],
|
|
2562
2601
|
organizations: ["世界阵营", "组织与成员", "维护组织简介、设定清单,并将角色绑定到所属组织。", "新建组织"],
|
|
2563
2602
|
timeline: ["剧情脉络", "大事件时间轴", "候选事件经作者确认后,才进入正式时间线。", "新建事件"],
|
|
2564
|
-
outlines: ["创作规划", "
|
|
2603
|
+
outlines: ["创作规划", "大纲/伏笔", "为每章维护目标、冲突与转折,并持续提醒尚未回收的伏笔。", "新建伏笔"],
|
|
2565
2604
|
relationships: ["跨章证据", "人物关系", "记录关系方向、阶段、置信度与原文依据。", "新建关系"],
|
|
2566
2605
|
reviews: ["作者决策", "审核队列", "集中处理冲突、候选设定、低置信度关系和时间问题。", "新增审核项"],
|
|
2567
2606
|
tasks: ["AI 深度分析", "AI 分析中心", "对全书或指定章节运行人物关系、世界观、设定、事件与一致性分析。", "开始 AI 分析"],
|
|
@@ -2599,6 +2638,7 @@ async function showModule(module) {
|
|
|
2599
2638
|
$("#module-eyebrow").textContent = meta[0];
|
|
2600
2639
|
$("#module-title").textContent = meta[1];
|
|
2601
2640
|
$("#module-description").textContent = meta[2];
|
|
2641
|
+
$("#module-header-actions").querySelectorAll("[data-module-header-action]").forEach((action) => action.remove());
|
|
2602
2642
|
$("#module-create-button").textContent = meta[3];
|
|
2603
2643
|
$("#module-create-button").classList.toggle("hidden", module === "ai-settings" || !canEditModule(module));
|
|
2604
2644
|
$("#module-content").innerHTML = '<div class="empty-state">正在载入……</div>';
|
|
@@ -2689,30 +2729,82 @@ function bindEntityHistoryButtons(refresh) {
|
|
|
2689
2729
|
}));
|
|
2690
2730
|
}
|
|
2691
2731
|
|
|
2732
|
+
function pencilIconMarkup() {
|
|
2733
|
+
return '<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 20h9"></path><path d="m16.5 3.5 1.4-1.4a2.1 2.1 0 0 1 3 3L8 18l-4 1 1-4L16.5 3.5Z"></path></svg>';
|
|
2734
|
+
}
|
|
2735
|
+
|
|
2736
|
+
function recordCardEditButton(attribute, id, label) {
|
|
2737
|
+
return `<button class="record-card-edit" type="button" data-${attribute}="${esc(id)}" aria-label="编辑${esc(label)}" title="编辑">${pencilIconMarkup()}</button>`;
|
|
2738
|
+
}
|
|
2739
|
+
|
|
2740
|
+
function recordHistoryButton(type, id, title) {
|
|
2741
|
+
return `<button type="button" data-entity-history="${esc(type)}" data-entity-id="${esc(id)}" data-entity-title="${esc(title)}">版本历史</button>`;
|
|
2742
|
+
}
|
|
2743
|
+
|
|
2744
|
+
function entityDialogManagementHtml({ typeLabel, canMerge, canDelete }) {
|
|
2745
|
+
return `<section class="entity-dialog-management" aria-label="${esc(typeLabel)}档案操作">
|
|
2746
|
+
<div><strong>档案操作</strong><small>版本历史和高风险操作集中在编辑面板内。</small></div>
|
|
2747
|
+
<div class="entity-dialog-management-actions">
|
|
2748
|
+
<button class="ghost-button" type="button" data-dialog-entity-history>版本历史</button>
|
|
2749
|
+
${canMerge ? `<button class="ghost-button" type="button" data-dialog-entity-merge>合并</button>` : ""}
|
|
2750
|
+
${canDelete ? `<button class="danger-button" type="button" data-dialog-entity-delete>删除</button>` : ""}
|
|
2751
|
+
</div>
|
|
2752
|
+
</section>`;
|
|
2753
|
+
}
|
|
2754
|
+
|
|
2755
|
+
function bindManagedEntityDialogActions({ type, typeLabel, item, candidates, endpoint, body, impact, refresh, deleteEndpoint, warning }) {
|
|
2756
|
+
if (!item) return;
|
|
2757
|
+
const fields = $("#dialog-fields");
|
|
2758
|
+
fields.querySelector("[data-dialog-entity-history]")?.addEventListener("click", () => {
|
|
2759
|
+
$("#form-dialog").close();
|
|
2760
|
+
openEntityHistory(type, item.id, item.name, async () => { await refresh(); await loadAiReferences(); });
|
|
2761
|
+
});
|
|
2762
|
+
fields.querySelector("[data-dialog-entity-merge]")?.addEventListener("click", () => {
|
|
2763
|
+
$("#form-dialog").close();
|
|
2764
|
+
openEntityMergeDialog({ typeLabel, source: item, candidates, endpoint, body, refresh, impact });
|
|
2765
|
+
});
|
|
2766
|
+
fields.querySelector("[data-dialog-entity-delete]")?.addEventListener("click", () => {
|
|
2767
|
+
void deleteManagedEntity({
|
|
2768
|
+
typeLabel,
|
|
2769
|
+
item,
|
|
2770
|
+
endpoint: deleteEndpoint,
|
|
2771
|
+
refresh,
|
|
2772
|
+
warning,
|
|
2773
|
+
onDeleted: () => $("#form-dialog").close()
|
|
2774
|
+
});
|
|
2775
|
+
});
|
|
2776
|
+
}
|
|
2777
|
+
|
|
2692
2778
|
function openEntityMergeDialog({ typeLabel, source, candidates, endpoint, body, refresh, impact }) {
|
|
2693
2779
|
const targetOptions = candidates
|
|
2694
2780
|
.filter((candidate) => candidate.id !== source.id)
|
|
2695
2781
|
.map((candidate) => [candidate.id, candidate.name]);
|
|
2696
2782
|
openDialog(`合并${typeLabel}`,
|
|
2697
2783
|
`<p class="merge-dialog-note">“${esc(source.name)}”将合并到所选档案,目标档案会保留。${esc(impact)}</p>` +
|
|
2698
|
-
field("targetId", `目标${typeLabel}`, "select", targetOptions[0]?.[0] ?? "", targetOptions)
|
|
2784
|
+
field("targetId", `目标${typeLabel}`, "select", targetOptions[0]?.[0] ?? "", targetOptions) +
|
|
2785
|
+
`<label class="danger-confirm-field"><input name="mergeConfirm" type="checkbox" value="confirmed"><span><strong>我确认执行合并</strong><small>来源档案及关联资料将迁移到目标档案,合并后不可撤销。</small></span></label>`,
|
|
2699
2786
|
async (form) => {
|
|
2700
2787
|
const target = candidates.find((candidate) => candidate.id === form.get("targetId"));
|
|
2701
2788
|
if (!target) throw new Error(`请选择目标${typeLabel}`);
|
|
2789
|
+
if (form.get("mergeConfirm") !== "confirmed") throw new Error("请先完成合并二次确认");
|
|
2702
2790
|
await api(endpoint(source), { method: "POST", body: body(target) });
|
|
2703
2791
|
await refresh();
|
|
2704
2792
|
await loadAiReferences();
|
|
2705
2793
|
toast(`已将“${source.name}”合并到“${target.name}”`);
|
|
2706
2794
|
}, "人工资料管理", { submitLabel: "确认合并" });
|
|
2795
|
+
const confirmation = $("#dialog-fields").querySelector("[name='mergeConfirm']");
|
|
2796
|
+
const submit = $("#dialog-submit");
|
|
2797
|
+
submit.disabled = true;
|
|
2798
|
+
confirmation?.addEventListener("change", () => { submit.disabled = !confirmation.checked; });
|
|
2707
2799
|
}
|
|
2708
2800
|
|
|
2709
|
-
async function deleteManagedEntity({ typeLabel, item, endpoint, refresh, warning = "" }) {
|
|
2710
|
-
const
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
if (!(await confirmToast(message, { title: `删除${typeLabel}`, confirmLabel: "确认删除" }))) return;
|
|
2801
|
+
async function deleteManagedEntity({ typeLabel, item, endpoint, refresh, warning = "", onDeleted }) {
|
|
2802
|
+
const detail = warning ? `\n${warning}` : "";
|
|
2803
|
+
if (!await confirmToast(`确认删除${typeLabel}“${item.name}”吗?${detail}`, { title: `删除${typeLabel}`, confirmLabel: "继续删除" })) return;
|
|
2804
|
+
if (!await confirmToast(`删除${typeLabel}“${item.name}”后无法恢复。${detail}`, { title: "删除操作需要再次确认", confirmLabel: "确认删除" })) return;
|
|
2714
2805
|
try {
|
|
2715
2806
|
await api(endpoint(item), { method: "DELETE" });
|
|
2807
|
+
await onDeleted?.();
|
|
2716
2808
|
await refresh();
|
|
2717
2809
|
await loadAiReferences();
|
|
2718
2810
|
toast(`已删除${typeLabel}“${item.name}”`);
|
|
@@ -2746,7 +2838,7 @@ function moduleRowPreview(text, max = 180) {
|
|
|
2746
2838
|
}
|
|
2747
2839
|
|
|
2748
2840
|
function renderModuleLayoutToggle(layout, ariaLabel = "列表样式") {
|
|
2749
|
-
return `<div class="module-layout-toolbar">
|
|
2841
|
+
return `<div class="module-layout-toolbar" data-module-header-action="layout-toggle">
|
|
2750
2842
|
<div class="module-layout-toggle" role="group" aria-label="${esc(ariaLabel)}">
|
|
2751
2843
|
<button type="button" data-module-layout="cards" aria-pressed="${layout === "cards"}">卡片</button>
|
|
2752
2844
|
<button type="button" data-module-layout="rows" aria-pressed="${layout === "rows"}">列表</button>
|
|
@@ -2755,15 +2847,22 @@ function renderModuleLayoutToggle(layout, ariaLabel = "列表样式") {
|
|
|
2755
2847
|
</div>`;
|
|
2756
2848
|
}
|
|
2757
2849
|
|
|
2850
|
+
function mountModuleLayoutToggle(layout, ariaLabel) {
|
|
2851
|
+
$("#module-header-actions").querySelector('[data-module-header-action="layout-toggle"]')?.remove();
|
|
2852
|
+
$("#module-header-actions").insertAdjacentHTML("beforeend", renderModuleLayoutToggle(layout, ariaLabel));
|
|
2853
|
+
}
|
|
2854
|
+
|
|
2758
2855
|
function bindModuleLayoutToggle(refresh) {
|
|
2759
|
-
$("#module-
|
|
2856
|
+
$("#module-header-actions").querySelectorAll("[data-module-layout]").forEach((button) => button.addEventListener("click", async () => {
|
|
2760
2857
|
saveModuleLayout(button.dataset.moduleLayout);
|
|
2761
2858
|
await refresh();
|
|
2762
2859
|
}));
|
|
2763
2860
|
}
|
|
2764
2861
|
|
|
2765
2862
|
function settingRecordActions(item) {
|
|
2766
|
-
return
|
|
2863
|
+
return canEditModule("settings")
|
|
2864
|
+
? recordCardEditButton("edit-setting", item.id, `设定“${item.title}”`)
|
|
2865
|
+
: recordHistoryButton("setting", item.id, item.title);
|
|
2767
2866
|
}
|
|
2768
2867
|
|
|
2769
2868
|
function renderSettingCards(records) {
|
|
@@ -2787,15 +2886,11 @@ async function renderSettings() {
|
|
|
2787
2886
|
const records = (await apiPage(`/api/works/${state.work.id}/settings`)).items;
|
|
2788
2887
|
state.settings = records;
|
|
2789
2888
|
const layout = readModuleLayout();
|
|
2889
|
+
if (records.length) mountModuleLayoutToggle(layout, "设定列表样式");
|
|
2790
2890
|
$("#module-content").innerHTML = records.length
|
|
2791
|
-
? `${
|
|
2891
|
+
? `${layout === "rows" ? renderSettingRows(records) : renderSettingCards(records)}`
|
|
2792
2892
|
: emptyModule("还没有世界观设定", "新建规则、地点、组织、科技或创作约束。AI 提取的候选也会进入这里。");
|
|
2793
2893
|
bindModuleLayoutToggle(renderSettings);
|
|
2794
|
-
$("#module-content").querySelectorAll("[data-setting-status]").forEach((button) => button.addEventListener("click", async () => {
|
|
2795
|
-
await api(`/api/settings/${button.dataset.settingId}`, { method: "PATCH", body: { status: button.dataset.settingStatus, changeNote: button.dataset.settingStatus === "confirmed" ? "确认 AI 设定候选" : "弃用 AI 设定候选" } });
|
|
2796
|
-
await renderSettings();
|
|
2797
|
-
await loadAiReferences();
|
|
2798
|
-
}));
|
|
2799
2894
|
$("#module-content").querySelectorAll("[data-edit-setting]").forEach((button) => button.addEventListener("click", () => openSettingEditor(records.find((item) => item.id === button.dataset.editSetting))));
|
|
2800
2895
|
bindEntityHistoryButtons(async () => { await renderSettings(); await loadAiReferences(); });
|
|
2801
2896
|
}
|
|
@@ -2807,23 +2902,26 @@ async function renderCharacters() {
|
|
|
2807
2902
|
canReadModule("organizations") ? apiAllPages(`/api/works/${state.work.id}/organizations`) : Promise.resolve([])
|
|
2808
2903
|
]);
|
|
2809
2904
|
const layout = readModuleLayout();
|
|
2810
|
-
const characterActions = (item) =>
|
|
2905
|
+
const characterActions = (item) => recordCardEditButton("edit-character", item.id, `角色“${item.name}”`);
|
|
2811
2906
|
const characterCards = () => `<div class="card-grid">${state.characters.map((item) => {
|
|
2812
2907
|
const details = normalizeCharacterDetails(item.attributes?.details);
|
|
2813
2908
|
return `
|
|
2814
|
-
<article class="record-card character-card" data-open-character="${esc(item.id)}" role="button" tabindex="0" aria-label="查看角色 ${esc(item.name)}"
|
|
2815
|
-
<h3>${esc(item.name)}</h3
|
|
2816
|
-
${item.species ? `<div class="character-species"><b>种族</b><span class="pill">${esc(racePathLabel(item.race) || item.species)}</span></div>` : ""}
|
|
2909
|
+
<article class="record-card character-card has-card-edit" data-open-character="${esc(item.id)}" role="button" tabindex="0" aria-label="查看角色 ${esc(item.name)}">${recordCardEditButton("edit-character", item.id, `角色“${item.name}”`)}<small>${item.lockedFields.length ? `锁定 ${item.lockedFields.length} 项` : esc(item.visibility)}</small>
|
|
2910
|
+
<h3>${esc(item.name)}</h3>
|
|
2817
2911
|
${item.attributes?.identity ? `<p class="character-identity">${esc(item.attributes.identity)}</p>` : ""}
|
|
2912
|
+
${item.aliases.length ? `<div class="character-aliases">${item.aliases.map((alias) => `<span class="pill">${esc(alias)}</span>`).join("")}</div>` : ""}
|
|
2913
|
+
${item.code ? `<div class="character-code"><b>编号</b><span class="pill">${esc(item.code)}</span></div>` : ""}
|
|
2914
|
+
${item.species ? `<div class="character-species"><b>种族</b><span class="pill">${esc(racePathLabel(item.race) || item.species)}</span></div>` : ""}
|
|
2818
2915
|
${details.length ? `<dl class="character-detail-list">${details.slice(0, 4).map((detail) => `<div><dt>${esc(detail.label)}</dt><dd>${esc(detail.value)}</dd></div>`).join("")}</dl>` : ""}
|
|
2819
2916
|
<div class="organization-links"><b>所属组织</b>${(item.organizations ?? []).length ? item.organizations.map((organization) => `<span class="pill organization-pill">${esc(organization.name)}</span>`).join("") : '<span class="organization-empty">未加入组织</span>'}</div>
|
|
2820
2917
|
${item.profile?.summary ? `<p class="character-summary">${esc(item.profile.summary)}</p>` : `<p>${esc(Object.entries(item.currentState).map(([key, value]) => `${key}:${value}`).join("\n") || "尚未记录当前状态")}</p>`}
|
|
2821
2918
|
${item.profileSectionCount ? `<small class="character-section-count">${item.profileSectionCount} 个设定章节</small>` : ""}
|
|
2822
|
-
|
|
2919
|
+
</article>`;
|
|
2823
2920
|
}).join("")}</div>`;
|
|
2824
2921
|
const characterRows = () => `<div class="module-row-list">${state.characters.map((item) => {
|
|
2825
2922
|
const preview = moduleRowPreview(item.profile?.summary || item.attributes?.identity || Object.entries(item.currentState).map(([key, value]) => `${key}:${value}`).join(" ") || "尚未记录当前状态");
|
|
2826
2923
|
const meta = [
|
|
2924
|
+
item.code ? `编号 ${item.code}` : "",
|
|
2827
2925
|
item.species ? (racePathLabel(item.race) || item.species) : "",
|
|
2828
2926
|
...(item.aliases ?? []).slice(0, 3),
|
|
2829
2927
|
(item.organizations ?? []).length ? (item.organizations ?? []).map((organization) => organization.name).join("、") : ""
|
|
@@ -2838,8 +2936,9 @@ async function renderCharacters() {
|
|
|
2838
2936
|
</article>`;
|
|
2839
2937
|
}).join("")}</div>`;
|
|
2840
2938
|
const auditPanel = canEditModule("tasks") ? `<section class="character-audit-panel"><div><strong>角色身份确认</strong><small>让 AI 查询角色档案并搜索正文,找出可能被误建成两个档案的同一角色。AI 只提交审核建议,不会自动合并。</small></div><button id="create-character-audit-task" class="ghost-button" type="button" ${state.characters.length < 2 ? "disabled" : ""}>AI 角色查重</button></section>` : "";
|
|
2939
|
+
if (state.characters.length) mountModuleLayoutToggle(layout, "角色列表样式");
|
|
2841
2940
|
$("#module-content").innerHTML = auditPanel + (state.characters.length
|
|
2842
|
-
? `${
|
|
2941
|
+
? `${layout === "rows" ? characterRows() : characterCards()}`
|
|
2843
2942
|
: emptyModule("还没有角色档案", "创建主要人物,并维护别名、身份、动机和当前状态。"));
|
|
2844
2943
|
bindModuleLayoutToggle(renderCharacters);
|
|
2845
2944
|
$("#create-character-audit-task")?.addEventListener("click", async () => {
|
|
@@ -2864,34 +2963,6 @@ async function renderCharacters() {
|
|
|
2864
2963
|
});
|
|
2865
2964
|
});
|
|
2866
2965
|
$("#module-content").querySelectorAll("[data-edit-character]").forEach((button) => button.addEventListener("click", () => openCharacterEditor(state.characters.find((item) => item.id === button.dataset.editCharacter))));
|
|
2867
|
-
$("#module-content").querySelectorAll("[data-merge-character]").forEach((button) => button.addEventListener("click", () => {
|
|
2868
|
-
const source = state.characters.find((item) => item.id === button.dataset.mergeCharacter);
|
|
2869
|
-
if (!source) return;
|
|
2870
|
-
openEntityMergeDialog({
|
|
2871
|
-
typeLabel: "角色",
|
|
2872
|
-
source,
|
|
2873
|
-
candidates: state.characters,
|
|
2874
|
-
endpoint: (item) => `/api/characters/${encodeURIComponent(item.id)}/merge`,
|
|
2875
|
-
body: (target) => ({
|
|
2876
|
-
targetCharacterId: target.id,
|
|
2877
|
-
expectedTargetVersionNo: target.versionNo,
|
|
2878
|
-
expectedSourceVersionNo: source.versionNo
|
|
2879
|
-
}),
|
|
2880
|
-
refresh: renderCharacters,
|
|
2881
|
-
impact: "来源角色的别名、组织、档案章节、时间线与人物关系会迁移到目标角色。"
|
|
2882
|
-
});
|
|
2883
|
-
}));
|
|
2884
|
-
$("#module-content").querySelectorAll("[data-delete-character]").forEach((button) => button.addEventListener("click", () => {
|
|
2885
|
-
const item = state.characters.find((character) => character.id === button.dataset.deleteCharacter);
|
|
2886
|
-
if (!item) return;
|
|
2887
|
-
void deleteManagedEntity({
|
|
2888
|
-
typeLabel: "角色",
|
|
2889
|
-
item,
|
|
2890
|
-
endpoint: (character) => `/api/characters/${encodeURIComponent(character.id)}`,
|
|
2891
|
-
refresh: renderCharacters,
|
|
2892
|
-
warning: "相关人物关系会删除,时间线中的参与者引用会移除。"
|
|
2893
|
-
});
|
|
2894
|
-
}));
|
|
2895
2966
|
}
|
|
2896
2967
|
|
|
2897
2968
|
async function renderRaces() {
|
|
@@ -2900,16 +2971,22 @@ async function renderRaces() {
|
|
|
2900
2971
|
canReadModule("characters") ? apiAllPages(`/api/works/${state.work.id}/characters`) : Promise.resolve([])
|
|
2901
2972
|
]);
|
|
2902
2973
|
const layout = readModuleLayout();
|
|
2903
|
-
const
|
|
2974
|
+
const canEditRaces = canEditModule("races");
|
|
2975
|
+
const raceActions = (item) => canEditRaces
|
|
2976
|
+
? recordCardEditButton("edit-race", item.id, `种族“${item.name}”`)
|
|
2977
|
+
: recordHistoryButton("race", item.id, item.name);
|
|
2978
|
+
const raceCardActions = (item) => canEditRaces
|
|
2979
|
+
? raceActions(item)
|
|
2980
|
+
: `<div class="card-actions">${raceActions(item)}</div>`;
|
|
2904
2981
|
const renderRaceNode = (item) => `<details class="race-tree-node" open data-race-node="${esc(item.id)}">
|
|
2905
2982
|
<summary><span>${esc(item.name)}</span><small>${item.children.length} 个直接子种族</small></summary>
|
|
2906
2983
|
<div class="race-tree-branch">
|
|
2907
|
-
<article class="record-card race-card"><small>${item.memberIds.length} 位直接角色 · ${item.settings.length
|
|
2984
|
+
<article class="record-card race-card${canEditRaces ? " has-card-edit" : ""}"><small>${item.memberIds.length} 位直接角色 · ${item.settings.length} 条自身设定</small>
|
|
2908
2985
|
<div class="race-path" aria-label="种族路径">${esc(racePathLabel(item))}</div>
|
|
2909
2986
|
<p>${esc(item.description || "尚未填写种族简介")}</p>
|
|
2910
2987
|
<div class="race-settings">${item.effectiveSettings.length ? item.effectiveSettings.map((setting) => `<section class="knowledge-markdown-block${setting.inherited ? " inherited" : ""}"><div class="knowledge-markdown-block-heading"><h4>${esc(setting.title || "未命名章节")}</h4><small>${esc(setting.inherited ? `继承自 ${setting.sourceRaceName}` : `定义于 ${setting.sourceRaceName}`)}</small></div><div class="message-body">${renderMarkdown(setting.value) || '<p class="markdown-editor-empty">暂无内容</p>'}</div></section>`).join("") : '<span class="pill">暂无共同设定</span>'}</div>
|
|
2911
2988
|
<p class="race-members">直接角色:${item.members.length ? item.members.map((member) => esc(member.name)).join("、") : "暂无绑定角色"}</p>
|
|
2912
|
-
|
|
2989
|
+
${raceCardActions(item)}
|
|
2913
2990
|
</article>
|
|
2914
2991
|
${item.children.length ? `<div class="race-tree-children">${item.children.map(renderRaceNode).join("")}</div>` : ""}
|
|
2915
2992
|
</div>
|
|
@@ -2925,35 +3002,12 @@ async function renderRaces() {
|
|
|
2925
3002
|
<div class="card-actions">${raceActions(item)}</div>
|
|
2926
3003
|
</article>`;
|
|
2927
3004
|
}).join("")}</div>`;
|
|
3005
|
+
if (state.races.length) mountModuleLayoutToggle(layout, "种族列表样式");
|
|
2928
3006
|
$("#module-content").innerHTML = state.races.length
|
|
2929
|
-
? `${
|
|
3007
|
+
? `${layout === "rows" ? raceRows() : `<section class="race-tree" aria-label="种族层级">${buildRaceForest(state.races).map(renderRaceNode).join("")}</section>`}`
|
|
2930
3008
|
: emptyModule("还没有种族档案", "先创建种族及共同设定,之后角色编辑器才能选择该种族。");
|
|
2931
3009
|
bindModuleLayoutToggle(renderRaces);
|
|
2932
3010
|
$("#module-content").querySelectorAll("[data-edit-race]").forEach((button) => button.addEventListener("click", () => openRaceDialog(state.races.find((item) => item.id === button.dataset.editRace))));
|
|
2933
|
-
$("#module-content").querySelectorAll("[data-merge-race]").forEach((button) => button.addEventListener("click", () => {
|
|
2934
|
-
const source = state.races.find((item) => item.id === button.dataset.mergeRace);
|
|
2935
|
-
if (!source) return;
|
|
2936
|
-
openEntityMergeDialog({
|
|
2937
|
-
typeLabel: "种族",
|
|
2938
|
-
source,
|
|
2939
|
-
candidates: state.races,
|
|
2940
|
-
endpoint: (item) => `/api/races/${encodeURIComponent(item.id)}/merge`,
|
|
2941
|
-
body: (target) => ({ targetRaceId: target.id }),
|
|
2942
|
-
refresh: renderRaces,
|
|
2943
|
-
impact: "来源种族的角色、子种族、简介与共同设定会迁移到目标种族。"
|
|
2944
|
-
});
|
|
2945
|
-
}));
|
|
2946
|
-
$("#module-content").querySelectorAll("[data-delete-race]").forEach((button) => button.addEventListener("click", () => {
|
|
2947
|
-
const item = state.races.find((race) => race.id === button.dataset.deleteRace);
|
|
2948
|
-
if (!item) return;
|
|
2949
|
-
void deleteManagedEntity({
|
|
2950
|
-
typeLabel: "种族",
|
|
2951
|
-
item,
|
|
2952
|
-
endpoint: (race) => `/api/races/${encodeURIComponent(race.id)}`,
|
|
2953
|
-
refresh: renderRaces,
|
|
2954
|
-
warning: "已绑定角色将变为未指定种族;有子种族时需先迁移或合并。"
|
|
2955
|
-
});
|
|
2956
|
-
}));
|
|
2957
3011
|
bindEntityHistoryButtons(async () => { await renderRaces(); await loadAiReferences(); });
|
|
2958
3012
|
}
|
|
2959
3013
|
|
|
@@ -2963,13 +3017,19 @@ async function renderOrganizations() {
|
|
|
2963
3017
|
canReadModule("characters") ? apiAllPages(`/api/works/${state.work.id}/characters`) : Promise.resolve([])
|
|
2964
3018
|
]);
|
|
2965
3019
|
const layout = readModuleLayout();
|
|
2966
|
-
const
|
|
3020
|
+
const canEditOrganizations = canEditModule("organizations");
|
|
3021
|
+
const organizationActions = (item) => canEditOrganizations
|
|
3022
|
+
? recordCardEditButton("edit-organization", item.id, `组织“${item.name}”`)
|
|
3023
|
+
: recordHistoryButton("organization", item.id, item.name);
|
|
3024
|
+
const organizationCardActions = (item) => canEditOrganizations
|
|
3025
|
+
? organizationActions(item)
|
|
3026
|
+
: `<div class="card-actions">${organizationActions(item)}</div>`;
|
|
2967
3027
|
const organizationCards = () => `<div class="card-grid organization-grid">${state.organizations.map((item) => `
|
|
2968
|
-
<article class="record-card organization-card"><small>${item.memberIds.length} 位成员 · ${item.settings.length ? "已填写组织设定" : "暂无组织设定"}</small>
|
|
3028
|
+
<article class="record-card organization-card${canEditOrganizations ? " has-card-edit" : ""}"><small>${item.memberIds.length} 位成员 · ${item.settings.length ? "已填写组织设定" : "暂无组织设定"}</small>
|
|
2969
3029
|
<h3>${esc(item.name)}</h3><p>${esc(item.description || "尚未填写组织简介")}</p>
|
|
2970
3030
|
<div class="organization-settings">${item.settingsSections?.length ? item.settingsSections.map((section) => `<article class="knowledge-markdown-block"><div class="knowledge-markdown-block-heading"><h4>${esc(section.title || "未命名章节")}</h4></div><div class="message-body">${renderMarkdown(section.contentMarkdown) || '<p class="markdown-editor-empty">暂无内容</p>'}</div></article>`).join("") : '<span class="pill">暂无组织设定</span>'}</div>
|
|
2971
3031
|
<p class="organization-members">成员:${item.members.length ? item.members.map((member) => esc(member.name)).join("、") : "暂无绑定角色"}</p>
|
|
2972
|
-
|
|
3032
|
+
${organizationCardActions(item)}
|
|
2973
3033
|
</article>`).join("")}</div>`;
|
|
2974
3034
|
const organizationRows = () => `<div class="module-row-list">${state.organizations.map((item) => {
|
|
2975
3035
|
const preview = moduleRowPreview(item.description || "尚未填写组织简介");
|
|
@@ -2982,51 +3042,56 @@ async function renderOrganizations() {
|
|
|
2982
3042
|
<div class="card-actions">${organizationActions(item)}</div>
|
|
2983
3043
|
</article>`;
|
|
2984
3044
|
}).join("")}</div>`;
|
|
3045
|
+
if (state.organizations.length) mountModuleLayoutToggle(layout, "组织列表样式");
|
|
2985
3046
|
$("#module-content").innerHTML = state.organizations.length
|
|
2986
|
-
? `${
|
|
3047
|
+
? `${layout === "rows" ? organizationRows() : organizationCards()}`
|
|
2987
3048
|
: emptyModule("还没有组织", "创建国家、机构、阵营或团队,并维护组织设定与成员。");
|
|
2988
3049
|
bindModuleLayoutToggle(renderOrganizations);
|
|
2989
3050
|
$("#module-content").querySelectorAll("[data-edit-organization]").forEach((button) => button.addEventListener("click", () => openOrganizationDialog(state.organizations.find((item) => item.id === button.dataset.editOrganization))));
|
|
2990
|
-
$("#module-content").querySelectorAll("[data-merge-organization]").forEach((button) => button.addEventListener("click", () => {
|
|
2991
|
-
const source = state.organizations.find((item) => item.id === button.dataset.mergeOrganization);
|
|
2992
|
-
if (!source) return;
|
|
2993
|
-
openEntityMergeDialog({
|
|
2994
|
-
typeLabel: "组织",
|
|
2995
|
-
source,
|
|
2996
|
-
candidates: state.organizations,
|
|
2997
|
-
endpoint: (item) => `/api/organizations/${encodeURIComponent(item.id)}/merge`,
|
|
2998
|
-
body: (target) => ({ targetOrganizationId: target.id }),
|
|
2999
|
-
refresh: renderOrganizations,
|
|
3000
|
-
impact: "来源组织的成员、简介与组织设定会迁移到目标组织。"
|
|
3001
|
-
});
|
|
3002
|
-
}));
|
|
3003
|
-
$("#module-content").querySelectorAll("[data-delete-organization]").forEach((button) => button.addEventListener("click", () => {
|
|
3004
|
-
const item = state.organizations.find((organization) => organization.id === button.dataset.deleteOrganization);
|
|
3005
|
-
if (!item) return;
|
|
3006
|
-
void deleteManagedEntity({
|
|
3007
|
-
typeLabel: "组织",
|
|
3008
|
-
item,
|
|
3009
|
-
endpoint: (organization) => `/api/organizations/${encodeURIComponent(organization.id)}`,
|
|
3010
|
-
refresh: renderOrganizations,
|
|
3011
|
-
warning: "角色与该组织的成员关系会一并移除。"
|
|
3012
|
-
});
|
|
3013
|
-
}));
|
|
3014
3051
|
bindEntityHistoryButtons(async () => { await renderOrganizations(); await loadAiReferences(); });
|
|
3015
3052
|
}
|
|
3016
3053
|
|
|
3054
|
+
function updateTimelineMultiSelectControls() {
|
|
3055
|
+
const toggle = $("#timeline-multi-select-toggle");
|
|
3056
|
+
if (!toggle) return;
|
|
3057
|
+
const selectedCount = $("#module-content").querySelectorAll("[data-event-select]:checked").length;
|
|
3058
|
+
toggle.setAttribute("aria-pressed", String(timelineMultiSelectEnabled));
|
|
3059
|
+
toggle.textContent = timelineMultiSelectEnabled ? "退出多选" : "多选";
|
|
3060
|
+
$("#module-content").querySelectorAll("[data-event-select]").forEach((input) => {
|
|
3061
|
+
input.hidden = !timelineMultiSelectEnabled;
|
|
3062
|
+
if (!timelineMultiSelectEnabled) input.checked = false;
|
|
3063
|
+
});
|
|
3064
|
+
const merge = $("#merge-events");
|
|
3065
|
+
if (merge) {
|
|
3066
|
+
merge.hidden = !timelineMultiSelectEnabled;
|
|
3067
|
+
merge.disabled = !timelineMultiSelectEnabled || selectedCount < 2;
|
|
3068
|
+
}
|
|
3069
|
+
}
|
|
3070
|
+
|
|
3071
|
+
function setTimelineMultiSelectMode(enabled) {
|
|
3072
|
+
timelineMultiSelectEnabled = enabled;
|
|
3073
|
+
updateTimelineMultiSelectControls();
|
|
3074
|
+
}
|
|
3075
|
+
|
|
3017
3076
|
async function renderTimeline() {
|
|
3018
3077
|
const [events, tracks] = await Promise.all([
|
|
3019
3078
|
apiPage(`/api/works/${state.work.id}/timeline`).then((result) => result.items),
|
|
3020
3079
|
apiAllPages(`/api/works/${state.work.id}/timeline-tracks`)
|
|
3021
3080
|
]);
|
|
3081
|
+
timelineMultiSelectEnabled = false;
|
|
3082
|
+
$("#timeline-tools")?.remove();
|
|
3083
|
+
$("#module-header-actions").insertAdjacentHTML("beforeend", `<div id="timeline-tools" class="timeline-tools" data-module-header-action="timeline-tools" role="group" aria-label="时间轴操作"><button id="create-timeline-track" class="ghost-button" type="button">新建独立时间轴</button><button id="timeline-multi-select-toggle" class="ghost-button" type="button" aria-pressed="false">多选</button>${events.length > 1 ? '<button id="merge-events" class="ghost-button" type="button" hidden>合并所选事件</button>' : ""}</div>`);
|
|
3022
3084
|
state.timelineTracks = tracks;
|
|
3023
3085
|
const lanes = [...tracks, { id: "", name: "未分组时间轴", description: "尚未归入独立大事件的时间节点。", sortOrder: Number.MAX_SAFE_INTEGER }];
|
|
3024
|
-
const eventCard = (item) => `<article class="timeline-kanban-card"><div class="timeline-card-meta"><input type="checkbox" data-event-select="${esc(item.id)}" aria-label="选择 ${esc(item.name)}"><small>${esc(item.timeLabel)} · ${esc(item.status)}</small></div><h4>${esc(item.name)}</h4><p>${esc(item.description || "暂无说明")}</p>${item.location ? `<span>地点:${esc(item.location)}</span>` : ""}<div class="card-actions"><button data-edit-event="${esc(item.id)}">编辑与排序</button><button data-split-event="${esc(item.id)}">拆分</button><button data-entity-history="timeline-event" data-entity-id="${esc(item.id)}" data-entity-title="${esc(item.name)}">版本历史</button></div></article>`;
|
|
3025
|
-
$("#module-content").innerHTML = `<div class="timeline-
|
|
3086
|
+
const eventCard = (item) => `<article class="timeline-kanban-card"><div class="timeline-card-meta"><input type="checkbox" data-event-select="${esc(item.id)}" aria-label="选择 ${esc(item.name)}" hidden><small>${esc(item.timeLabel)} · ${esc(item.status)}</small></div><h4>${esc(item.name)}</h4><p>${esc(item.description || "暂无说明")}</p>${item.location ? `<span>地点:${esc(item.location)}</span>` : ""}<div class="card-actions"><button data-edit-event="${esc(item.id)}">编辑与排序</button><button data-split-event="${esc(item.id)}">拆分</button><button data-entity-history="timeline-event" data-entity-id="${esc(item.id)}" data-entity-title="${esc(item.name)}">版本历史</button></div></article>`;
|
|
3087
|
+
$("#module-content").innerHTML = `<div class="timeline-kanban" data-testid="timeline-kanban">${lanes.map((track) => {
|
|
3026
3088
|
const laneEvents = events.filter((item) => (item.trackId ?? "") === track.id);
|
|
3027
3089
|
return `<section class="timeline-lane" data-track-id="${esc(track.id)}"><header><div><small>${laneEvents.length} 个节点</small><h3>${esc(track.name)}</h3></div>${track.id ? `<div class="timeline-track-actions"><button class="timeline-track-menu" data-edit-timeline-track="${esc(track.id)}" type="button">编辑</button><button class="timeline-track-menu" data-entity-history="timeline-track" data-entity-id="${esc(track.id)}" data-entity-title="${esc(track.name)}" type="button">历史</button></div>` : ""}</header><p class="timeline-track-description">${esc(track.description || "暂无说明")}</p><div class="timeline-lane-events">${laneEvents.map(eventCard).join("") || '<div class="timeline-lane-empty">还没有时间节点</div>'}</div><button class="timeline-add-event" data-add-event-track="${esc(track.id)}" type="button">添加事件</button></section>`;
|
|
3028
3090
|
}).join("")}</div>`;
|
|
3029
3091
|
$("#create-timeline-track").addEventListener("click", () => openTimelineTrackDialog());
|
|
3092
|
+
$("#timeline-multi-select-toggle").addEventListener("click", () => setTimelineMultiSelectMode(!timelineMultiSelectEnabled));
|
|
3093
|
+
$("#module-content").querySelectorAll("[data-event-select]").forEach((input) => input.addEventListener("change", updateTimelineMultiSelectControls));
|
|
3094
|
+
setTimelineMultiSelectMode(false);
|
|
3030
3095
|
$("#module-content").querySelectorAll("[data-edit-timeline-track]").forEach((button) => button.addEventListener("click", () => openTimelineTrackDialog(tracks.find((track) => track.id === button.dataset.editTimelineTrack))));
|
|
3031
3096
|
$("#module-content").querySelectorAll("[data-add-event-track]").forEach((button) => button.addEventListener("click", () => openTimelineDialog(null, button.dataset.addEventTrack || null)));
|
|
3032
3097
|
$("#module-content").querySelectorAll("[data-edit-event]").forEach((button) => button.addEventListener("click", () => openTimelineDialog(events.find((item) => item.id === button.dataset.editEvent))));
|
|
@@ -3056,8 +3121,6 @@ async function renderOutlines() {
|
|
|
3056
3121
|
const layout = readModuleLayout();
|
|
3057
3122
|
const unresolved = foreshadows.filter((item) => item.unresolved);
|
|
3058
3123
|
const overdue = unresolved.filter((item) => item.overdue);
|
|
3059
|
-
const navButton = $("#module-nav [data-module=outlines] .nav-label");
|
|
3060
|
-
if (navButton) navButton.textContent = unresolved.length ? `大纲与伏笔 · ${unresolved.length}` : "大纲与伏笔";
|
|
3061
3124
|
const foreshadowActions = (item) => `<button data-edit-foreshadow="${esc(item.id)}">编辑伏笔</button><button data-entity-history="foreshadow" data-entity-id="${esc(item.id)}" data-entity-title="${esc(item.title)}">版本历史</button>`;
|
|
3062
3125
|
const foreshadowCards = () => `<div class="card-grid foreshadow-grid">${foreshadows.map((item) => `
|
|
3063
3126
|
<article class="record-card foreshadow-card ${item.overdue ? "is-overdue" : ""}">
|
|
@@ -3080,8 +3143,9 @@ async function renderOutlines() {
|
|
|
3080
3143
|
</article>`;
|
|
3081
3144
|
}).join("")}</div>`;
|
|
3082
3145
|
const foreshadowHtml = foreshadows.length
|
|
3083
|
-
? `${
|
|
3146
|
+
? `${layout === "rows" ? foreshadowRows() : foreshadowCards()}`
|
|
3084
3147
|
: emptyModule("还没有伏笔", "创建伏笔并关联埋设、提醒与回收章节,未回收项会持续显示。\n");
|
|
3148
|
+
if (foreshadows.length) mountModuleLayoutToggle(layout, "伏笔列表样式");
|
|
3085
3149
|
const outlineHtml = outlines.length ? `<div class="outline-list">${outlines.map((item) => `
|
|
3086
3150
|
<article class="outline-row ${item.status === "completed" ? "is-complete" : ""}">
|
|
3087
3151
|
<div><small>${esc(item.volumeTitle)} · ${esc(item.status)}</small><h3>${esc(item.chapterTitle)}</h3></div>
|
|
@@ -3108,7 +3172,7 @@ async function renderRelationships() {
|
|
|
3108
3172
|
state.relationshipGraph = graph;
|
|
3109
3173
|
$("#module-content").innerHTML = `<div id="relationship-map-host"></div>${relationships.length ? `<table class="table-list relationship-table"><thead><tr><th>人物</th><th>关系</th><th>关键词</th><th>证据</th><th>置信度</th><th>状态</th><th>操作</th></tr></thead><tbody>${relationships.map((item) => `
|
|
3110
3174
|
<tr><td>${esc(nameOf(item.fromCharacterId))} ${item.directed ? "→" : "—"} ${esc(nameOf(item.toCharacterId))}</td>
|
|
3111
|
-
<td>${esc(item.category)} / ${esc(item.subtype || "未细分")}</td><td>${(item.keywords ?? []).map((keyword) => `<span class="pill relationship-keyword">${esc(keyword)}</span>`).join("") || "—"}</td><td>${item.evidence.length}
|
|
3175
|
+
<td>${esc(item.category)} / ${esc(item.subtype || "未细分")}</td><td>${(item.keywords ?? []).map((keyword) => `<span class="pill relationship-keyword">${esc(keyword)}</span>`).join("") || "—"}</td><td>${item.evidence.length}</td><td>${Math.round(item.confidence * 100)}%</td><td>${esc(item.confirmationStatus)}</td><td class="relationship-actions"><button data-edit-relationship="${esc(item.id)}">编辑</button><button data-entity-history="relationship" data-entity-id="${esc(item.id)}" data-entity-title="${esc(`${nameOf(item.fromCharacterId)} / ${nameOf(item.toCharacterId)}`)}">历史</button></td></tr>`).join("")}</tbody></table>` : '<div class="relationship-empty-note">尚无关系边;孤立角色仍显示在力导向图谱中。可人工新建关系,或运行全书人物关系分析。</div>'}`;
|
|
3112
3176
|
const openGalaxy = () => {
|
|
3113
3177
|
state.galaxy?.destroy();
|
|
3114
3178
|
state.galaxy = createGalaxyRenderer($("#relationship-galaxy-dialog"), graph, { workId: state.work.id });
|
|
@@ -3172,8 +3236,9 @@ async function renderReviews() {
|
|
|
3172
3236
|
<div class="card-actions">${item.status === "pending" && canResolveReview ? `<button data-review-status="fixed" data-review-id="${esc(item.id)}">标为已修复</button><button data-review-status="ignored" data-review-id="${esc(item.id)}">忽略</button>` : ""}</div>
|
|
3173
3237
|
</article>`;
|
|
3174
3238
|
};
|
|
3239
|
+
if (reviews.length) mountModuleLayoutToggle(layout, "审核列表样式");
|
|
3175
3240
|
$("#module-content").innerHTML = reviews.length
|
|
3176
|
-
? `${
|
|
3241
|
+
? `${layout === "rows" ? `<div class="module-row-list">${reviews.map(reviewRow).join("")}</div>` : `<div class="card-grid">${reviews.map(reviewCard).join("")}</div>`}`
|
|
3177
3242
|
: emptyModule("没有待审核事项", "候选设定、冲突与低置信度结论会集中显示在这里。");
|
|
3178
3243
|
bindModuleLayoutToggle(renderReviews);
|
|
3179
3244
|
$("#module-content").querySelectorAll("[data-review-id]").forEach((button) => button.addEventListener("click", async () => {
|
|
@@ -3973,8 +4038,12 @@ function openWorkSettingsDialog(work) {
|
|
|
3973
4038
|
<div><strong id="import-history-settings-title">正文导入历史</strong><small>查看导入前快照并恢复被覆盖的分卷、章节标题和正文;大纲、伏笔等章节关联资料不在快照中。</small></div>
|
|
3974
4039
|
<button id="import-history-button" class="ghost-button" type="button" aria-controls="import-history-dialog" aria-haspopup="dialog" ${canOpenImportHistory ? "" : "disabled"}>${importHistoryAction}</button>
|
|
3975
4040
|
</section>`;
|
|
4041
|
+
const whitespaceField = isCurrentWork ? `<section class="work-access-field" aria-labelledby="whitespace-settings-title">
|
|
4042
|
+
<div><strong id="whitespace-settings-title">正文空白符</strong><small>在编辑器正文中显示或隐藏空格、全角空格和 Tab 的可视标记。</small></div>
|
|
4043
|
+
<button id="toggle-whitespace-settings" class="ghost-button" data-toggle-whitespace type="button" aria-pressed="${chapterWhitespaceVisible}" title="用点标记半角空格,用方框标记全角空格,用箭头标记 Tab">${chapterWhitespaceVisible ? "隐藏空白符" : "显示空白符"}</button>
|
|
4044
|
+
</section>` : "";
|
|
3976
4045
|
openDialog("作品信息",
|
|
3977
|
-
workCoverFieldHtml(work) + field("title", "作品名称", "text", work.title) + field("author", "作者", "text", work.author) + field("description", "简介", "textarea", work.description) + accessField + importHistoryField,
|
|
4046
|
+
workCoverFieldHtml(work) + field("title", "作品名称", "text", work.title) + field("author", "作者", "text", work.author) + field("description", "简介", "textarea", work.description) + whitespaceField + accessField + importHistoryField,
|
|
3978
4047
|
async (form) => {
|
|
3979
4048
|
await api(`/api/works/${work.id}`, { method: "PATCH", body: { title: form.get("title"), author: form.get("author"), description: form.get("description") } });
|
|
3980
4049
|
state.works = (await apiPage("/api/works")).items;
|
|
@@ -3986,12 +4055,13 @@ function openWorkSettingsDialog(work) {
|
|
|
3986
4055
|
state.work.description = String(form.get("description") ?? state.work.description);
|
|
3987
4056
|
if (updated?.coverUrl !== undefined) state.work.coverUrl = updated.coverUrl;
|
|
3988
4057
|
updateDocumentTitle(state.work);
|
|
3989
|
-
$("#work-meta").textContent = `${state.work.title}${state.work.author ? ` · ${state.work.author}` : ""} · ${state.work.wordCount} 字`;
|
|
4058
|
+
$("#work-meta").textContent = `${state.work.title}${state.work.author ? ` · ${state.work.author}` : ""} · ${Number(state.work.wordCount ?? 0).toLocaleString("zh-CN")} 字`;
|
|
3990
4059
|
}
|
|
3991
4060
|
renderShelf();
|
|
3992
4061
|
toast("作品信息已保存");
|
|
3993
4062
|
}, "作品设置");
|
|
3994
4063
|
bindWorkCoverControls(work);
|
|
4064
|
+
$("#toggle-whitespace-settings")?.addEventListener("click", toggleChapterWhitespaceVisibility);
|
|
3995
4065
|
$("#import-history-button")?.addEventListener("click", () => {
|
|
3996
4066
|
$("#form-dialog").close();
|
|
3997
4067
|
void openImportHistory();
|
|
@@ -4057,6 +4127,43 @@ function openSettingEditor(item = null) {
|
|
|
4057
4127
|
$("#setting-editor-form").querySelectorAll("input, textarea").forEach((control) => { control.readOnly = viewOnly; });
|
|
4058
4128
|
$("#setting-editor-form").querySelectorAll("select, input[type='checkbox']").forEach((control) => { control.disabled = viewOnly; });
|
|
4059
4129
|
$("#setting-editor-submit").classList.toggle("hidden", viewOnly);
|
|
4130
|
+
const management = $("#setting-editor-management");
|
|
4131
|
+
management.classList.toggle("hidden", !item || viewOnly);
|
|
4132
|
+
const statusButtons = [$("#setting-editor-confirm"), $("#setting-editor-deprecate")];
|
|
4133
|
+
$("#setting-editor-confirm").classList.toggle("hidden", item?.status !== "pending");
|
|
4134
|
+
$("#setting-editor-deprecate").classList.toggle("hidden", item?.status !== "pending");
|
|
4135
|
+
$("#setting-editor-history").onclick = async () => {
|
|
4136
|
+
if (!item) return;
|
|
4137
|
+
if (!(await closeEntityEditor())) return;
|
|
4138
|
+
openEntityHistory("setting", item.id, item.title, async () => { await renderSettings(); await loadAiReferences(); });
|
|
4139
|
+
};
|
|
4140
|
+
$("#setting-editor-delete").onclick = () => {
|
|
4141
|
+
if (!item) return;
|
|
4142
|
+
void deleteManagedEntity({
|
|
4143
|
+
typeLabel: "设定",
|
|
4144
|
+
item,
|
|
4145
|
+
endpoint: (setting) => `/api/settings/${encodeURIComponent(setting.id)}`,
|
|
4146
|
+
refresh: () => closeEntityEditor({ force: true }),
|
|
4147
|
+
warning: "这条设定及其版本历史会被删除。"
|
|
4148
|
+
});
|
|
4149
|
+
};
|
|
4150
|
+
statusButtons.forEach((button) => {
|
|
4151
|
+
button.onclick = async () => {
|
|
4152
|
+
if (!item) return;
|
|
4153
|
+
button.disabled = true;
|
|
4154
|
+
const status = button.id === "setting-editor-confirm" ? "confirmed" : "deprecated";
|
|
4155
|
+
try {
|
|
4156
|
+
await api(`/api/settings/${item.id}`, { method: "PATCH", body: { status, changeNote: status === "confirmed" ? "确认 AI 设定候选" : "弃用 AI 设定候选" } });
|
|
4157
|
+
await closeEntityEditor({ force: true });
|
|
4158
|
+
await loadAiReferences();
|
|
4159
|
+
toast(status === "confirmed" ? "设定候选已确认" : "设定候选已弃用");
|
|
4160
|
+
} catch (error) {
|
|
4161
|
+
toast(error.message, "error");
|
|
4162
|
+
} finally {
|
|
4163
|
+
button.disabled = false;
|
|
4164
|
+
}
|
|
4165
|
+
};
|
|
4166
|
+
});
|
|
4060
4167
|
$("#setting-editor-form").onsubmit = async (event) => {
|
|
4061
4168
|
event.preventDefault();
|
|
4062
4169
|
if (!canEditModule("settings")) return;
|
|
@@ -4278,7 +4385,7 @@ function createVditorUploadHandler(uploadAttachment, getEditor) {
|
|
|
4278
4385
|
};
|
|
4279
4386
|
}
|
|
4280
4387
|
|
|
4281
|
-
function createVditorEditor(host, value, { onInput = () => {}, uploadAttachment = uploadMarkdownAttachment, placeholder = "", readOnly = false } = {}) {
|
|
4388
|
+
function createVditorEditor(host, value, { onInput = () => {}, uploadAttachment = uploadMarkdownAttachment, placeholder = "", readOnly = false, width = "auto" } = {}) {
|
|
4282
4389
|
if (!window.Vditor) {
|
|
4283
4390
|
toast("Markdown 编辑器资源加载失败,请刷新页面后重试", "error");
|
|
4284
4391
|
return null;
|
|
@@ -4292,6 +4399,7 @@ function createVditorEditor(host, value, { onInput = () => {}, uploadAttachment
|
|
|
4292
4399
|
mode: "ir",
|
|
4293
4400
|
value: String(value ?? ""),
|
|
4294
4401
|
height: "100%",
|
|
4402
|
+
width,
|
|
4295
4403
|
minHeight: 260,
|
|
4296
4404
|
placeholder,
|
|
4297
4405
|
preview: { transform: transformVditorPreview },
|
|
@@ -4446,7 +4554,9 @@ async function openKnowledgeSectionEditor(index = null) {
|
|
|
4446
4554
|
const titleInput = $("#knowledge-section-title");
|
|
4447
4555
|
host.querySelectorAll("input, textarea").forEach((control) => control.addEventListener("input", () => { knowledgeSectionEditorDirty = true; }));
|
|
4448
4556
|
knowledgeSectionVditor = createVditorEditor($("#knowledge-section-markdown"), section?.contentMarkdown ?? "", {
|
|
4449
|
-
onInput: () => { knowledgeSectionEditorDirty = true; }
|
|
4557
|
+
onInput: () => { knowledgeSectionEditorDirty = true; },
|
|
4558
|
+
placeholder: "从这里开始写 Markdown 设定…",
|
|
4559
|
+
width: "100%"
|
|
4450
4560
|
});
|
|
4451
4561
|
host.querySelector("[data-knowledge-section-edit-close]").addEventListener("click", () => void closeKnowledgeSectionEditor());
|
|
4452
4562
|
host.querySelector("[data-knowledge-section-edit-cancel]").addEventListener("click", () => void closeKnowledgeSectionEditor());
|
|
@@ -4505,7 +4615,9 @@ async function openCharacterSectionEditor(section = null) {
|
|
|
4505
4615
|
host.querySelectorAll("input, textarea, select").forEach((control) => control.addEventListener("input", () => { characterSectionEditorDirty = true; }));
|
|
4506
4616
|
characterSectionVditor = createVditorEditor($("#character-section-markdown"), section?.contentMarkdown ?? "", {
|
|
4507
4617
|
uploadAttachment: uploadCharacterSectionAttachment,
|
|
4508
|
-
onInput: () => { characterSectionEditorDirty = true; }
|
|
4618
|
+
onInput: () => { characterSectionEditorDirty = true; },
|
|
4619
|
+
placeholder: "从这里开始写人物章节…",
|
|
4620
|
+
width: "100%"
|
|
4509
4621
|
});
|
|
4510
4622
|
host.querySelector("[data-character-section-edit-close]").addEventListener("click", () => void closeCharacterSectionEditor());
|
|
4511
4623
|
host.querySelector("[data-character-section-edit-cancel]").addEventListener("click", () => void closeCharacterSectionEditor());
|
|
@@ -4654,6 +4766,7 @@ function renderCharacterEditorFields(item) {
|
|
|
4654
4766
|
? field("firstChapterId", "首次登场章节", "select", item?.firstChapterId ?? "", chapterOptions)
|
|
4655
4767
|
: '<div class="character-editor-empty-field"><b>首次登场章节</b><span>当前账户没有正文读取权限,原有绑定不会被修改。</span></div>')),
|
|
4656
4768
|
characterEditorSection("profile", "人物档案", "记录人物定位、行为动力和便于创作时快速理解的简介。",
|
|
4769
|
+
field("code", "编号", "text", item?.code) +
|
|
4657
4770
|
field("identity", "身份与定位", "text", item?.attributes?.identity) +
|
|
4658
4771
|
field("motivation", "核心动机", "textarea", item?.profile?.motivation) +
|
|
4659
4772
|
field("summary", "人物简介", "textarea", item?.profile?.summary)),
|
|
@@ -4690,6 +4803,7 @@ function collectCharacterBody(form) {
|
|
|
4690
4803
|
delete profile.sections;
|
|
4691
4804
|
const body = {
|
|
4692
4805
|
name: String(form.get("name") ?? "").trim(),
|
|
4806
|
+
code: String(form.get("code") ?? "").trim(),
|
|
4693
4807
|
aliases: form.getAll("aliases").map((value) => String(value).trim()).filter(Boolean),
|
|
4694
4808
|
attributes: {
|
|
4695
4809
|
...(item?.attributes ?? {}),
|
|
@@ -4796,6 +4910,34 @@ async function openCharacterEditor(item = null) {
|
|
|
4796
4910
|
$("#character-editor-submit").textContent = item ? "保存新版本" : "创建人物档案";
|
|
4797
4911
|
$("#character-history-button").disabled = !item;
|
|
4798
4912
|
$("#character-history-button").title = item ? "查看、比较和回滚历史版本" : "创建人物档案后即可查看版本历史";
|
|
4913
|
+
const characterMergeButton = $("#character-merge-button");
|
|
4914
|
+
const characterDeleteButton = $("#character-delete-button");
|
|
4915
|
+
const canManageCharacter = Boolean(item && canEditModule("characters"));
|
|
4916
|
+
characterMergeButton.classList.toggle("hidden", !canManageCharacter || state.characters.length < 2);
|
|
4917
|
+
characterDeleteButton.classList.toggle("hidden", !canManageCharacter);
|
|
4918
|
+
characterMergeButton.onclick = async () => {
|
|
4919
|
+
if (!item) return;
|
|
4920
|
+
await closeEntityEditor({ force: true });
|
|
4921
|
+
openEntityMergeDialog({
|
|
4922
|
+
typeLabel: "角色",
|
|
4923
|
+
source: item,
|
|
4924
|
+
candidates: state.characters,
|
|
4925
|
+
endpoint: (character) => `/api/characters/${encodeURIComponent(character.id)}/merge`,
|
|
4926
|
+
body: (target) => ({ targetCharacterId: target.id, expectedTargetVersionNo: target.versionNo, expectedSourceVersionNo: item.versionNo }),
|
|
4927
|
+
refresh: renderCharacters,
|
|
4928
|
+
impact: "来源角色的别名、组织、档案章节、时间线与人物关系会迁移到目标角色。"
|
|
4929
|
+
});
|
|
4930
|
+
};
|
|
4931
|
+
characterDeleteButton.onclick = () => {
|
|
4932
|
+
if (!item) return;
|
|
4933
|
+
void deleteManagedEntity({
|
|
4934
|
+
typeLabel: "角色",
|
|
4935
|
+
item,
|
|
4936
|
+
endpoint: (character) => `/api/characters/${encodeURIComponent(character.id)}`,
|
|
4937
|
+
refresh: () => closeEntityEditor({ force: true }),
|
|
4938
|
+
warning: "相关人物关系会删除,时间线中的参与者引用会移除。"
|
|
4939
|
+
});
|
|
4940
|
+
};
|
|
4799
4941
|
setCharacterHistoryVisible(false);
|
|
4800
4942
|
renderCharacterEditorFields(item);
|
|
4801
4943
|
const viewOnly = !canEditModule("characters");
|
|
@@ -4900,6 +5042,43 @@ async function openKnowledgeEditor(kind, item) {
|
|
|
4900
5042
|
$("#knowledge-editor-header-note").textContent = isRace ? "层级、共同设定与角色归属" : "简介、组织设定与成员归属";
|
|
4901
5043
|
$("#knowledge-editor-footer-note").textContent = `保存${label}档案后返回${isRace ? "种族" : "组织"}列表。`;
|
|
4902
5044
|
$("#knowledge-editor-submit").textContent = item ? `保存${label}档案` : `创建${label}档案`;
|
|
5045
|
+
const historyButton = $("#knowledge-editor-history");
|
|
5046
|
+
const mergeButton = $("#knowledge-editor-merge");
|
|
5047
|
+
const deleteButton = $("#knowledge-editor-delete");
|
|
5048
|
+
const refresh = isRace ? renderRaces : renderOrganizations;
|
|
5049
|
+
const candidates = isRace ? state.races : state.organizations;
|
|
5050
|
+
const typeLabel = label;
|
|
5051
|
+
historyButton.classList.toggle("hidden", !item);
|
|
5052
|
+
mergeButton.classList.toggle("hidden", !item || !canEditModule(module) || candidates.length < 2);
|
|
5053
|
+
deleteButton.classList.toggle("hidden", !item || !canEditModule(module));
|
|
5054
|
+
historyButton.onclick = async () => {
|
|
5055
|
+
if (!item) return;
|
|
5056
|
+
if (!(await closeEntityEditor())) return;
|
|
5057
|
+
openEntityHistory(kind, item.id, item.name, async () => { await refresh(); await loadAiReferences(); });
|
|
5058
|
+
};
|
|
5059
|
+
mergeButton.onclick = async () => {
|
|
5060
|
+
if (!item) return;
|
|
5061
|
+
await closeEntityEditor({ force: true });
|
|
5062
|
+
openEntityMergeDialog({
|
|
5063
|
+
typeLabel,
|
|
5064
|
+
source: item,
|
|
5065
|
+
candidates,
|
|
5066
|
+
endpoint: (source) => `/api/${module}/${encodeURIComponent(source.id)}/merge`,
|
|
5067
|
+
body: (target) => isRace ? { targetRaceId: target.id } : { targetOrganizationId: target.id },
|
|
5068
|
+
refresh,
|
|
5069
|
+
impact: isRace ? "来源种族的角色、子种族、简介与共同设定会迁移到目标种族。" : "来源组织的成员、简介与组织设定会迁移到目标组织。"
|
|
5070
|
+
});
|
|
5071
|
+
};
|
|
5072
|
+
deleteButton.onclick = () => {
|
|
5073
|
+
if (!item) return;
|
|
5074
|
+
void deleteManagedEntity({
|
|
5075
|
+
typeLabel,
|
|
5076
|
+
item,
|
|
5077
|
+
endpoint: (source) => `/api/${module}/${encodeURIComponent(source.id)}`,
|
|
5078
|
+
refresh: () => closeEntityEditor({ force: true }),
|
|
5079
|
+
warning: isRace ? "已绑定角色将变为未指定种族;有子种族时需先迁移或合并。" : "角色与该组织的成员关系会一并移除。"
|
|
5080
|
+
});
|
|
5081
|
+
};
|
|
4903
5082
|
renderKnowledgeEditorFields(kind, item, memberOptions, parentOptions);
|
|
4904
5083
|
const viewOnly = !canEditModule(module);
|
|
4905
5084
|
if (viewOnly) {
|
|
@@ -5867,6 +6046,7 @@ function cleanupExpandedRelationshipMap() {
|
|
|
5867
6046
|
$("#relationship-map-close").addEventListener("click", () => $("#relationship-map-dialog").close());
|
|
5868
6047
|
$("#relationship-map-dialog").addEventListener("close", cleanupExpandedRelationshipMap);
|
|
5869
6048
|
$("#appearance-button").addEventListener("click", openAppearanceDialog);
|
|
6049
|
+
$("#toggle-whitespace-appearance").addEventListener("click", toggleChapterWhitespaceVisibility);
|
|
5870
6050
|
$("#theme-toggle").addEventListener("click", () => {
|
|
5871
6051
|
const theme = nextTheme(currentColorTheme());
|
|
5872
6052
|
const persisted = saveColorTheme(theme);
|
|
@@ -5898,10 +6078,6 @@ $("#chapter-content").addEventListener("input", (event) => {
|
|
|
5898
6078
|
});
|
|
5899
6079
|
$("#chapter-content").addEventListener("select", scheduleAiContextUsage);
|
|
5900
6080
|
$("#chapter-content").addEventListener("scroll", syncChapterLineNumberScroll);
|
|
5901
|
-
$("#toggle-whitespace-button").addEventListener("click", () => {
|
|
5902
|
-
chapterWhitespaceVisible = !chapterWhitespaceVisible;
|
|
5903
|
-
scheduleChapterLineNumbers();
|
|
5904
|
-
});
|
|
5905
6081
|
$("#chapter-line-numbers-inner").addEventListener("pointerdown", (event) => {
|
|
5906
6082
|
const row = event.target.closest(".chapter-line-number");
|
|
5907
6083
|
if (!row || event.button !== 0) return;
|
|
@@ -6103,6 +6279,13 @@ document.addEventListener("keydown", (event) => {
|
|
|
6103
6279
|
hideAiMentionMenu();
|
|
6104
6280
|
}
|
|
6105
6281
|
});
|
|
6282
|
+
document.addEventListener("keydown", (event) => {
|
|
6283
|
+
if (!isGlobalSearchShortcut(event) || !state.work) return;
|
|
6284
|
+
event.preventDefault();
|
|
6285
|
+
event.stopPropagation();
|
|
6286
|
+
if (event.repeat) return;
|
|
6287
|
+
openSearchDialog().catch((error) => toast(error.message, "error"));
|
|
6288
|
+
}, { capture: true });
|
|
6106
6289
|
$("#ai-send").addEventListener("click", sendAi);
|
|
6107
6290
|
$("#ai-new-conversation").addEventListener("click", async () => {
|
|
6108
6291
|
const button = $("#ai-new-conversation");
|