@musnows/scriverse 0.3.8 → 0.3.9
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/dist/app.js +117 -65
- package/dist/app.js.map +1 -1
- package/dist/cli-contract.js +4 -4
- package/dist/cli-contract.js.map +1 -1
- package/dist/cli-core.js +37 -6
- package/dist/cli-core.js.map +1 -1
- package/dist/database.js +41 -0
- package/dist/database.js.map +1 -1
- package/dist/public/app.js +257 -39
- package/dist/public/index.html +26 -3
- package/dist/public/styles.css +34 -0
- package/dist/store.js +417 -109
- package/dist/store.js.map +1 -1
- package/dist/user-auth.js +47 -13
- package/dist/user-auth.js.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/public/app.js
CHANGED
|
@@ -81,18 +81,36 @@ function analysisTaskStatusLabel(status) {
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
function canEditWork(work = state.work) {
|
|
84
|
+
return ["admin", "owner", "editor", "settings-editor"].includes(String(work?.accessRole));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function canEditProse(work = state.work) {
|
|
84
88
|
return ["admin", "owner", "editor"].includes(String(work?.accessRole));
|
|
85
89
|
}
|
|
86
90
|
|
|
91
|
+
function canManageWork(work = state.work) {
|
|
92
|
+
return ["admin", "owner"].includes(String(work?.accessRole));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function canEditModule(module, work = state.work) {
|
|
96
|
+
if (canEditProse(work)) return true;
|
|
97
|
+
return canEditWork(work) && ["settings", "characters", "races", "organizations", "timeline", "outlines", "relationships"].includes(module);
|
|
98
|
+
}
|
|
99
|
+
|
|
87
100
|
function applyWorkAccessMode() {
|
|
88
101
|
const viewOnly = Boolean(state.work) && !canEditWork();
|
|
102
|
+
const settingsOnly = String(state.work?.accessRole) === "settings-editor";
|
|
103
|
+
const proseReadOnly = Boolean(state.work) && !canEditProse();
|
|
89
104
|
$("#app").classList.toggle("view-only-mode", viewOnly);
|
|
105
|
+
$("#app").classList.toggle("settings-only-mode", settingsOnly);
|
|
106
|
+
$("#app").classList.toggle("prose-read-only-mode", proseReadOnly);
|
|
90
107
|
document.body.classList.toggle("work-viewer-mode", viewOnly);
|
|
91
|
-
|
|
92
|
-
$("#chapter-
|
|
93
|
-
$("#chapter-
|
|
94
|
-
$("#chapter-
|
|
95
|
-
|
|
108
|
+
document.body.classList.toggle("work-settings-editor-mode", settingsOnly);
|
|
109
|
+
$("#chapter-title").readOnly = proseReadOnly;
|
|
110
|
+
$("#chapter-content").readOnly = proseReadOnly;
|
|
111
|
+
$("#chapter-title").setAttribute("aria-readonly", String(proseReadOnly));
|
|
112
|
+
$("#chapter-content").setAttribute("aria-readonly", String(proseReadOnly));
|
|
113
|
+
if (proseReadOnly) {
|
|
96
114
|
cancelChapterAutoSave();
|
|
97
115
|
state.dirty = false;
|
|
98
116
|
}
|
|
@@ -1424,15 +1442,75 @@ applyTypographySettings(typographySettings);
|
|
|
1424
1442
|
applyColorTheme(currentColorTheme());
|
|
1425
1443
|
applyPanelLayout();
|
|
1426
1444
|
|
|
1445
|
+
function optimisticVersionForPath(path) {
|
|
1446
|
+
const normalizedPath = String(path).split("?")[0];
|
|
1447
|
+
const find = (items, id) => items.find((item) => String(item?.id ?? item?.chapterId ?? "") === id)?.versionNo;
|
|
1448
|
+
const workMatch = normalizedPath.match(/^\/api\/works\/([^/]+)(?:\/(?:cover|import|file-versions\/[^/]+\/restore))?$/u);
|
|
1449
|
+
if (workMatch) {
|
|
1450
|
+
const workId = decodeURIComponent(workMatch[1]);
|
|
1451
|
+
return state.works.find((item) => item.id === workId)?.versionNo ?? (state.work?.id === workId ? state.work.versionNo : undefined);
|
|
1452
|
+
}
|
|
1453
|
+
const resourceMatch = normalizedPath.match(/^\/api\/(volumes|chapters|settings|races|organizations|timeline-tracks|timeline|relationships|foreshadows|characters|character-sections)\/([^/]+)(?:\/(?:restore|move|split))?$/u);
|
|
1454
|
+
if (resourceMatch) {
|
|
1455
|
+
const resourceId = decodeURIComponent(resourceMatch[2]);
|
|
1456
|
+
const collection = {
|
|
1457
|
+
settings: state.settings,
|
|
1458
|
+
races: state.races,
|
|
1459
|
+
organizations: state.organizations,
|
|
1460
|
+
"timeline-tracks": state.timelineTracks,
|
|
1461
|
+
characters: state.characters
|
|
1462
|
+
}[resourceMatch[1]] ?? [];
|
|
1463
|
+
if (resourceMatch[1] === "chapters" && state.chapter?.id === resourceId) return state.chapter.versionNo;
|
|
1464
|
+
if (resourceMatch[1] === "volumes") return find(state.work?.volumes ?? [], resourceId);
|
|
1465
|
+
if (resourceMatch[1] === "character-sections") return find(characterEditorSections, resourceId);
|
|
1466
|
+
if (resourceMatch[1] === "characters" && characterEditorItem?.id === resourceId) return characterEditorItem.versionNo;
|
|
1467
|
+
return find(collection, resourceId);
|
|
1468
|
+
}
|
|
1469
|
+
const outlineMatch = normalizedPath.match(/^\/api\/chapters\/([^/]+)\/outline$/u);
|
|
1470
|
+
if (outlineMatch) return find(state.outlines ?? [], decodeURIComponent(outlineMatch[1]));
|
|
1471
|
+
const entityRestoreMatch = normalizedPath.match(/^\/api\/entity-versions\/(work|volume|setting|race|organization|timeline-track|timeline-event|relationship|chapter-outline|foreshadow)\/([^/]+)\/restore$/u);
|
|
1472
|
+
if (entityRestoreMatch) {
|
|
1473
|
+
const entityType = entityRestoreMatch[1];
|
|
1474
|
+
const entityId = decodeURIComponent(entityRestoreMatch[2]);
|
|
1475
|
+
if (entityType === "work") return state.works.find((item) => item.id === entityId)?.versionNo ?? (state.work?.id === entityId ? state.work.versionNo : undefined);
|
|
1476
|
+
if (entityType === "volume") return find(state.work?.volumes ?? [], entityId);
|
|
1477
|
+
if (entityType === "chapter-outline") return find(state.outlines ?? [], entityId);
|
|
1478
|
+
const collection = {
|
|
1479
|
+
setting: state.settings,
|
|
1480
|
+
race: state.races,
|
|
1481
|
+
organization: state.organizations,
|
|
1482
|
+
"timeline-track": state.timelineTracks
|
|
1483
|
+
}[entityType] ?? [];
|
|
1484
|
+
return find(collection, entityId);
|
|
1485
|
+
}
|
|
1486
|
+
return undefined;
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
function attachOptimisticVersion(path, method, body) {
|
|
1490
|
+
if (!["PATCH", "PUT", "DELETE", "POST"].includes(method)) return body;
|
|
1491
|
+
if (body instanceof FormData) {
|
|
1492
|
+
if (!body.has("expectedVersionNo")) {
|
|
1493
|
+
const versionNo = optimisticVersionForPath(path);
|
|
1494
|
+
if (Number.isInteger(versionNo) && versionNo > 0) body.append("expectedVersionNo", String(versionNo));
|
|
1495
|
+
}
|
|
1496
|
+
return body;
|
|
1497
|
+
}
|
|
1498
|
+
const currentBody = body && typeof body === "object" && !Array.isArray(body) ? body : {};
|
|
1499
|
+
if (currentBody.expectedVersionNo !== undefined) return currentBody;
|
|
1500
|
+
const versionNo = optimisticVersionForPath(path);
|
|
1501
|
+
return Number.isInteger(versionNo) && versionNo > 0 ? { ...currentBody, expectedVersionNo: versionNo } : body;
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1427
1504
|
async function api(path, options = {}) {
|
|
1428
1505
|
const method = String(options.method ?? "GET").toUpperCase();
|
|
1506
|
+
const body = attachOptimisticVersion(path, method, options.body);
|
|
1429
1507
|
const headers = { ...(options.headers ?? {}) };
|
|
1430
1508
|
if (state.csrfToken && !["GET", "HEAD", "OPTIONS"].includes(method)) headers["X-CSRF-Token"] = state.csrfToken;
|
|
1431
|
-
if (!(
|
|
1432
|
-
const response = await fetch(path,
|
|
1509
|
+
if (!(body instanceof FormData)) headers["Content-Type"] = "application/json";
|
|
1510
|
+
const response = await fetch(path, body instanceof FormData ? { ...options, body, headers } : {
|
|
1433
1511
|
...options,
|
|
1434
1512
|
headers,
|
|
1435
|
-
body:
|
|
1513
|
+
body: body && typeof body !== "string" ? JSON.stringify(body) : body
|
|
1436
1514
|
});
|
|
1437
1515
|
if (!response.ok) {
|
|
1438
1516
|
const payload = await response.json().catch(() => ({ error: { message: `请求失败:${response.status}` } }));
|
|
@@ -1616,7 +1694,7 @@ function cancelChapterAutoSave() {
|
|
|
1616
1694
|
}
|
|
1617
1695
|
|
|
1618
1696
|
function scheduleChapterAutoSave(delay = chapterAutoSaveDelay) {
|
|
1619
|
-
if (!state.chapter || !
|
|
1697
|
+
if (!state.chapter || !canEditProse()) return;
|
|
1620
1698
|
cancelChapterAutoSave();
|
|
1621
1699
|
setSaveState("等待自动保存", true);
|
|
1622
1700
|
chapterAutoSaveTimer = setTimeout(() => {
|
|
@@ -1626,7 +1704,7 @@ function scheduleChapterAutoSave(delay = chapterAutoSaveDelay) {
|
|
|
1626
1704
|
}
|
|
1627
1705
|
|
|
1628
1706
|
async function persistChapter({ automatic = false } = {}) {
|
|
1629
|
-
if (!
|
|
1707
|
+
if (!canEditProse()) return null;
|
|
1630
1708
|
if (!state.chapter) {
|
|
1631
1709
|
if (!automatic) toast("请先选择章节", "error");
|
|
1632
1710
|
return null;
|
|
@@ -1694,6 +1772,19 @@ function confirmDiscardChanges(message = "当前章节有未保存修改,继
|
|
|
1694
1772
|
return window.confirm(message);
|
|
1695
1773
|
}
|
|
1696
1774
|
|
|
1775
|
+
function chooseExistingWorkImportMode(file) {
|
|
1776
|
+
const dialog = $("#import-mode-dialog");
|
|
1777
|
+
$("#import-mode-file-summary").textContent = `文件:${file.name};当前作品:《${state.work.title}》`;
|
|
1778
|
+
$("#import-mode-unsaved-warning").classList.toggle("hidden", !state.dirty);
|
|
1779
|
+
dialog.returnValue = "cancel";
|
|
1780
|
+
dialog.showModal();
|
|
1781
|
+
return new Promise((resolve) => {
|
|
1782
|
+
dialog.addEventListener("close", () => {
|
|
1783
|
+
resolve(["append", "overwrite"].includes(dialog.returnValue) ? dialog.returnValue : null);
|
|
1784
|
+
}, { once: true });
|
|
1785
|
+
});
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1697
1788
|
function updateDocumentTitle(work = null) {
|
|
1698
1789
|
const workTitle = String(work?.title ?? "").trim();
|
|
1699
1790
|
document.title = workTitle ? `${workTitle} · 叙界` : platformDocumentTitle;
|
|
@@ -1886,8 +1977,8 @@ function renderMembers(members) {
|
|
|
1886
1977
|
const work = memberDialogWork ?? state.work;
|
|
1887
1978
|
const canManage = ["admin", "owner"].includes(String(work?.accessRole));
|
|
1888
1979
|
$("#members-list").innerHTML = members.map((member) => `<article class="access-row">
|
|
1889
|
-
<div class="access-person">${userAvatarHtml(member, "access-avatar")}<div class="access-person-copy"><strong>${esc(member.displayName)} · @${esc(member.username)}</strong><small>${member.role === "owner" ? "作品创建者" : member.role === "viewer" ? "查看者" : "
|
|
1890
|
-
${member.role === "owner" ? "<span>所有者</span>" : `<select data-member-role="${esc(member.userId)}" aria-label="${esc(member.displayName)}的作品权限" ${canManage ? "" : "disabled"}><option value="viewer" ${member.role === "viewer" ? "selected" : ""}>仅查看</option><option value="editor" ${member.role === "editor" ? "selected" : ""}
|
|
1980
|
+
<div class="access-person">${userAvatarHtml(member, "access-avatar")}<div class="access-person-copy"><strong>${esc(member.displayName)} · @${esc(member.username)}</strong><small>${member.role === "owner" ? "作品创建者" : member.role === "viewer" ? "查看者" : member.role === "settings-editor" ? "设定编辑" : "完整协作者"}${member.status === "disabled" ? " · 已停用" : ""}</small></div></div>
|
|
1981
|
+
${member.role === "owner" ? "<span>所有者</span>" : `<select data-member-role="${esc(member.userId)}" aria-label="${esc(member.displayName)}的作品权限" ${canManage ? "" : "disabled"}><option value="viewer" ${member.role === "viewer" ? "selected" : ""}>仅查看</option><option value="settings-editor" ${member.role === "settings-editor" ? "selected" : ""}>仅编辑设定</option><option value="editor" ${member.role === "editor" ? "selected" : ""}>编辑正文与设定</option></select>`}
|
|
1891
1982
|
${member.role === "owner" || !canManage ? "<span></span>" : `<button type="button" data-remove-member="${esc(member.userId)}">移除</button>`}
|
|
1892
1983
|
</article>`).join("");
|
|
1893
1984
|
bindUserAvatarFallbacks($("#members-list"));
|
|
@@ -2090,9 +2181,9 @@ function renderShelf() {
|
|
|
2090
2181
|
<span class="book-cover-fallback">${esc(Array.from(work.title)[0] ?? "书")}</span>
|
|
2091
2182
|
${work.coverUrl ? `<img src="${esc(work.coverUrl)}" alt="${esc(work.title)} 封面">` : ""}
|
|
2092
2183
|
</span>
|
|
2093
|
-
<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 === "editor" ? "
|
|
2184
|
+
<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 === "admin" ? "管理员访问" : "我的作品"}</em></span>
|
|
2094
2185
|
</button>
|
|
2095
|
-
${
|
|
2186
|
+
${canManageWork(work) ? `<button class="book-card-settings" type="button" data-edit-work="${esc(work.id)}" aria-label="作品设置" title="作品设置">设置</button>` : ""}
|
|
2096
2187
|
</article>`).join("")}
|
|
2097
2188
|
<button class="book-card book-add-card" id="book-add-card" type="button" aria-label="新建作品" data-testid="book-add-card"><span>+</span><strong>新建作品</strong><small>从零开始或导入 TXT / DOCX</small></button>`;
|
|
2098
2189
|
shelf.querySelectorAll("[data-open-work]").forEach((button) => button.addEventListener("click", () => selectWork(button.dataset.openWork)));
|
|
@@ -2177,7 +2268,7 @@ function renderTree() {
|
|
|
2177
2268
|
renderTree();
|
|
2178
2269
|
});
|
|
2179
2270
|
button.addEventListener("contextmenu", (event) => {
|
|
2180
|
-
if (!
|
|
2271
|
+
if (!canEditProse()) return;
|
|
2181
2272
|
event.preventDefault();
|
|
2182
2273
|
openVolumeDialog(state.work.volumes.find((volume) => volume.id === button.dataset.volumeToggle));
|
|
2183
2274
|
});
|
|
@@ -2185,7 +2276,7 @@ function renderTree() {
|
|
|
2185
2276
|
$("#novel-tree").querySelectorAll("[data-chapter-id]").forEach((button) => {
|
|
2186
2277
|
button.addEventListener("click", () => selectChapter(button.dataset.chapterId));
|
|
2187
2278
|
button.addEventListener("contextmenu", (event) => {
|
|
2188
|
-
if (!
|
|
2279
|
+
if (!canEditProse()) return;
|
|
2189
2280
|
event.preventDefault();
|
|
2190
2281
|
openChapterTypeMenu(button.dataset.chapterId, event.clientX, event.clientY);
|
|
2191
2282
|
});
|
|
@@ -2235,7 +2326,7 @@ async function selectChapter(chapterId) {
|
|
|
2235
2326
|
scheduleChapterLineNumbers();
|
|
2236
2327
|
$("#chapter-insight").classList.add("hidden");
|
|
2237
2328
|
updateChapterStats();
|
|
2238
|
-
if (!
|
|
2329
|
+
if (!canEditProse()) setSaveState(canEditWork() ? "正文只读" : "仅查看");
|
|
2239
2330
|
else if (spacingChanged) scheduleChapterAutoSave(120);
|
|
2240
2331
|
else setSaveState("已保存");
|
|
2241
2332
|
renderTree();
|
|
@@ -2293,7 +2384,7 @@ const moduleMeta = {
|
|
|
2293
2384
|
|
|
2294
2385
|
async function showModule(module) {
|
|
2295
2386
|
if (!state.work) return showWelcome();
|
|
2296
|
-
if (!
|
|
2387
|
+
if (!canEditProse() && ["tasks", "ai-settings"].includes(module)) module = "editor";
|
|
2297
2388
|
if (module !== "editor" && state.module === "editor" && !confirmDiscardChanges()) return;
|
|
2298
2389
|
if (module !== "editor" && state.module === "editor" && state.dirty) setSaveState("已放弃修改");
|
|
2299
2390
|
state.module = module;
|
|
@@ -2315,7 +2406,7 @@ async function showModule(module) {
|
|
|
2315
2406
|
$("#module-title").textContent = meta[1];
|
|
2316
2407
|
$("#module-description").textContent = meta[2];
|
|
2317
2408
|
$("#module-create-button").textContent = meta[3];
|
|
2318
|
-
$("#module-create-button").classList.toggle("hidden", module === "ai-settings" || !
|
|
2409
|
+
$("#module-create-button").classList.toggle("hidden", module === "ai-settings" || !canEditModule(module));
|
|
2319
2410
|
$("#module-content").innerHTML = '<div class="empty-state">正在载入……</div>';
|
|
2320
2411
|
try {
|
|
2321
2412
|
if (module === "settings") await renderSettings();
|
|
@@ -2404,6 +2495,36 @@ function bindEntityHistoryButtons(refresh) {
|
|
|
2404
2495
|
}));
|
|
2405
2496
|
}
|
|
2406
2497
|
|
|
2498
|
+
function openEntityMergeDialog({ typeLabel, source, candidates, endpoint, body, refresh, impact }) {
|
|
2499
|
+
const targetOptions = candidates
|
|
2500
|
+
.filter((candidate) => candidate.id !== source.id)
|
|
2501
|
+
.map((candidate) => [candidate.id, candidate.name]);
|
|
2502
|
+
openDialog(`合并${typeLabel}`,
|
|
2503
|
+
`<p class="merge-dialog-note">“${esc(source.name)}”将合并到所选档案,目标档案会保留。${esc(impact)}</p>` +
|
|
2504
|
+
field("targetId", `目标${typeLabel}`, "select", targetOptions[0]?.[0] ?? "", targetOptions),
|
|
2505
|
+
async (form) => {
|
|
2506
|
+
const target = candidates.find((candidate) => candidate.id === form.get("targetId"));
|
|
2507
|
+
if (!target) throw new Error(`请选择目标${typeLabel}`);
|
|
2508
|
+
await api(endpoint(source), { method: "POST", body: body(target) });
|
|
2509
|
+
await refresh();
|
|
2510
|
+
await loadAiReferences();
|
|
2511
|
+
toast(`已将“${source.name}”合并到“${target.name}”`);
|
|
2512
|
+
}, "人工资料管理", { submitLabel: "确认合并" });
|
|
2513
|
+
}
|
|
2514
|
+
|
|
2515
|
+
async function deleteManagedEntity({ typeLabel, item, endpoint, refresh, warning = "" }) {
|
|
2516
|
+
const detail = warning ? `\n${warning}` : "";
|
|
2517
|
+
if (!window.confirm(`确认删除${typeLabel}“${item.name}”吗?${detail}`)) return;
|
|
2518
|
+
try {
|
|
2519
|
+
await api(endpoint(item), { method: "DELETE" });
|
|
2520
|
+
await refresh();
|
|
2521
|
+
await loadAiReferences();
|
|
2522
|
+
toast(`已删除${typeLabel}“${item.name}”`);
|
|
2523
|
+
} catch (error) {
|
|
2524
|
+
toast(error.message, "error");
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2407
2528
|
async function renderSettings() {
|
|
2408
2529
|
const records = (await apiPage(`/api/works/${state.work.id}/settings`)).items;
|
|
2409
2530
|
state.settings = records;
|
|
@@ -2439,7 +2560,7 @@ async function renderCharacters() {
|
|
|
2439
2560
|
<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>
|
|
2440
2561
|
${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>`}
|
|
2441
2562
|
${item.profileSectionCount ? `<small class="character-section-count">${item.profileSectionCount} 个设定章节</small>` : ""}
|
|
2442
|
-
<div class="card-actions"><button data-edit-character="${esc(item.id)}">编辑</button
|
|
2563
|
+
<div class="card-actions"><button data-edit-character="${esc(item.id)}">编辑</button>${canEditWork() && state.characters.length > 1 ? `<button data-merge-character="${esc(item.id)}">合并</button>` : ""}${canEditWork() ? `<button class="danger-button" data-delete-character="${esc(item.id)}">删除</button>` : ""}</div></article>`;
|
|
2443
2564
|
}).join("")}</div>`
|
|
2444
2565
|
: emptyModule("还没有角色档案", "创建主要人物,并维护别名、身份、动机和当前状态。"));
|
|
2445
2566
|
$("#create-character-audit-task")?.addEventListener("click", async () => {
|
|
@@ -2464,6 +2585,34 @@ async function renderCharacters() {
|
|
|
2464
2585
|
});
|
|
2465
2586
|
});
|
|
2466
2587
|
$("#module-content").querySelectorAll("[data-edit-character]").forEach((button) => button.addEventListener("click", () => openCharacterEditor(state.characters.find((item) => item.id === button.dataset.editCharacter))));
|
|
2588
|
+
$("#module-content").querySelectorAll("[data-merge-character]").forEach((button) => button.addEventListener("click", () => {
|
|
2589
|
+
const source = state.characters.find((item) => item.id === button.dataset.mergeCharacter);
|
|
2590
|
+
if (!source) return;
|
|
2591
|
+
openEntityMergeDialog({
|
|
2592
|
+
typeLabel: "角色",
|
|
2593
|
+
source,
|
|
2594
|
+
candidates: state.characters,
|
|
2595
|
+
endpoint: (item) => `/api/characters/${encodeURIComponent(item.id)}/merge`,
|
|
2596
|
+
body: (target) => ({
|
|
2597
|
+
targetCharacterId: target.id,
|
|
2598
|
+
expectedTargetVersionNo: target.versionNo,
|
|
2599
|
+
expectedSourceVersionNo: source.versionNo
|
|
2600
|
+
}),
|
|
2601
|
+
refresh: renderCharacters,
|
|
2602
|
+
impact: "来源角色的别名、组织、档案章节、时间线与人物关系会迁移到目标角色。"
|
|
2603
|
+
});
|
|
2604
|
+
}));
|
|
2605
|
+
$("#module-content").querySelectorAll("[data-delete-character]").forEach((button) => button.addEventListener("click", () => {
|
|
2606
|
+
const item = state.characters.find((character) => character.id === button.dataset.deleteCharacter);
|
|
2607
|
+
if (!item) return;
|
|
2608
|
+
void deleteManagedEntity({
|
|
2609
|
+
typeLabel: "角色",
|
|
2610
|
+
item,
|
|
2611
|
+
endpoint: (character) => `/api/characters/${encodeURIComponent(character.id)}`,
|
|
2612
|
+
refresh: renderCharacters,
|
|
2613
|
+
warning: "相关人物关系会删除,时间线中的参与者引用会移除。"
|
|
2614
|
+
});
|
|
2615
|
+
}));
|
|
2467
2616
|
}
|
|
2468
2617
|
|
|
2469
2618
|
async function renderRaces() {
|
|
@@ -2479,13 +2628,37 @@ async function renderRaces() {
|
|
|
2479
2628
|
<p>${esc(item.description || "尚未填写种族简介")}</p>
|
|
2480
2629
|
<div class="race-settings">${item.effectiveSettings.length ? item.effectiveSettings.map((setting) => `<span class="pill${setting.inherited ? " inherited" : ""}" title="${esc(setting.inherited ? `继承自 ${setting.sourceRaceName}` : `定义于 ${setting.sourceRaceName}`)}">${esc(setting.value)}<small>${esc(setting.sourceRaceName)}</small></span>`).join("") : '<span class="pill">暂无共同设定</span>'}</div>
|
|
2481
2630
|
<p class="race-members">直接角色:${item.members.length ? item.members.map((member) => esc(member.name)).join("、") : "暂无绑定角色"}</p>
|
|
2482
|
-
<div class="card-actions"><button data-edit-race="${esc(item.id)}">编辑</button><button data-entity-history="race" data-entity-id="${esc(item.id)}" data-entity-title="${esc(item.name)}">版本历史</button
|
|
2631
|
+
<div class="card-actions"><button data-edit-race="${esc(item.id)}">编辑</button><button data-entity-history="race" data-entity-id="${esc(item.id)}" data-entity-title="${esc(item.name)}">版本历史</button>${canEditWork() && state.races.length > 1 ? `<button data-merge-race="${esc(item.id)}">合并</button>` : ""}${canEditWork() ? `<button class="danger-button" data-delete-race="${esc(item.id)}">删除</button>` : ""}</div>
|
|
2483
2632
|
</article>
|
|
2484
2633
|
${item.children.length ? `<div class="race-tree-children">${item.children.map(renderRaceNode).join("")}</div>` : ""}
|
|
2485
2634
|
</div>
|
|
2486
2635
|
</details>`;
|
|
2487
2636
|
$("#module-content").innerHTML = state.races.length ? `<section class="race-tree" aria-label="种族层级">${buildRaceForest(state.races).map(renderRaceNode).join("")}</section>` : emptyModule("还没有种族档案", "先创建种族及共同设定,之后角色编辑器才能选择该种族。");
|
|
2488
2637
|
$("#module-content").querySelectorAll("[data-edit-race]").forEach((button) => button.addEventListener("click", () => openRaceDialog(state.races.find((item) => item.id === button.dataset.editRace))));
|
|
2638
|
+
$("#module-content").querySelectorAll("[data-merge-race]").forEach((button) => button.addEventListener("click", () => {
|
|
2639
|
+
const source = state.races.find((item) => item.id === button.dataset.mergeRace);
|
|
2640
|
+
if (!source) return;
|
|
2641
|
+
openEntityMergeDialog({
|
|
2642
|
+
typeLabel: "种族",
|
|
2643
|
+
source,
|
|
2644
|
+
candidates: state.races,
|
|
2645
|
+
endpoint: (item) => `/api/races/${encodeURIComponent(item.id)}/merge`,
|
|
2646
|
+
body: (target) => ({ targetRaceId: target.id }),
|
|
2647
|
+
refresh: renderRaces,
|
|
2648
|
+
impact: "来源种族的角色、子种族、简介与共同设定会迁移到目标种族。"
|
|
2649
|
+
});
|
|
2650
|
+
}));
|
|
2651
|
+
$("#module-content").querySelectorAll("[data-delete-race]").forEach((button) => button.addEventListener("click", () => {
|
|
2652
|
+
const item = state.races.find((race) => race.id === button.dataset.deleteRace);
|
|
2653
|
+
if (!item) return;
|
|
2654
|
+
void deleteManagedEntity({
|
|
2655
|
+
typeLabel: "种族",
|
|
2656
|
+
item,
|
|
2657
|
+
endpoint: (race) => `/api/races/${encodeURIComponent(race.id)}`,
|
|
2658
|
+
refresh: renderRaces,
|
|
2659
|
+
warning: "已绑定角色将变为未指定种族;有子种族时需先迁移或合并。"
|
|
2660
|
+
});
|
|
2661
|
+
}));
|
|
2489
2662
|
bindEntityHistoryButtons(async () => { await renderRaces(); await loadAiReferences(); });
|
|
2490
2663
|
}
|
|
2491
2664
|
|
|
@@ -2499,9 +2672,33 @@ async function renderOrganizations() {
|
|
|
2499
2672
|
<h3>${esc(item.name)}</h3><p>${esc(item.description || "尚未填写组织简介")}</p>
|
|
2500
2673
|
<div class="organization-settings">${item.settings.map((setting) => `<span class="pill">${esc(setting)}</span>`).join("") || '<span class="pill">暂无组织设定</span>'}</div>
|
|
2501
2674
|
<p class="organization-members">成员:${item.members.length ? item.members.map((member) => esc(member.name)).join("、") : "暂无绑定角色"}</p>
|
|
2502
|
-
<div class="card-actions"><button data-edit-organization="${esc(item.id)}">编辑</button><button data-entity-history="organization" data-entity-id="${esc(item.id)}" data-entity-title="${esc(item.name)}">版本历史</button
|
|
2675
|
+
<div class="card-actions"><button data-edit-organization="${esc(item.id)}">编辑</button><button data-entity-history="organization" data-entity-id="${esc(item.id)}" data-entity-title="${esc(item.name)}">版本历史</button>${canEditWork() && state.organizations.length > 1 ? `<button data-merge-organization="${esc(item.id)}">合并</button>` : ""}${canEditWork() ? `<button class="danger-button" data-delete-organization="${esc(item.id)}">删除</button>` : ""}</div>
|
|
2503
2676
|
</article>`).join("")}</div>` : emptyModule("还没有组织", "创建国家、机构、阵营或团队,并维护组织设定与成员。");
|
|
2504
2677
|
$("#module-content").querySelectorAll("[data-edit-organization]").forEach((button) => button.addEventListener("click", () => openOrganizationDialog(state.organizations.find((item) => item.id === button.dataset.editOrganization))));
|
|
2678
|
+
$("#module-content").querySelectorAll("[data-merge-organization]").forEach((button) => button.addEventListener("click", () => {
|
|
2679
|
+
const source = state.organizations.find((item) => item.id === button.dataset.mergeOrganization);
|
|
2680
|
+
if (!source) return;
|
|
2681
|
+
openEntityMergeDialog({
|
|
2682
|
+
typeLabel: "组织",
|
|
2683
|
+
source,
|
|
2684
|
+
candidates: state.organizations,
|
|
2685
|
+
endpoint: (item) => `/api/organizations/${encodeURIComponent(item.id)}/merge`,
|
|
2686
|
+
body: (target) => ({ targetOrganizationId: target.id }),
|
|
2687
|
+
refresh: renderOrganizations,
|
|
2688
|
+
impact: "来源组织的成员、简介与组织设定会迁移到目标组织。"
|
|
2689
|
+
});
|
|
2690
|
+
}));
|
|
2691
|
+
$("#module-content").querySelectorAll("[data-delete-organization]").forEach((button) => button.addEventListener("click", () => {
|
|
2692
|
+
const item = state.organizations.find((organization) => organization.id === button.dataset.deleteOrganization);
|
|
2693
|
+
if (!item) return;
|
|
2694
|
+
void deleteManagedEntity({
|
|
2695
|
+
typeLabel: "组织",
|
|
2696
|
+
item,
|
|
2697
|
+
endpoint: (organization) => `/api/organizations/${encodeURIComponent(organization.id)}`,
|
|
2698
|
+
refresh: renderOrganizations,
|
|
2699
|
+
warning: "角色与该组织的成员关系会一并移除。"
|
|
2700
|
+
});
|
|
2701
|
+
}));
|
|
2505
2702
|
bindEntityHistoryButtons(async () => { await renderOrganizations(); await loadAiReferences(); });
|
|
2506
2703
|
}
|
|
2507
2704
|
|
|
@@ -2527,7 +2724,12 @@ async function renderTimeline() {
|
|
|
2527
2724
|
const eventIds = [...$("#module-content").querySelectorAll("[data-event-select]:checked")].map((input) => input.dataset.eventSelect);
|
|
2528
2725
|
if (eventIds.length < 2) return toast("请至少选择两个时间事件", "error");
|
|
2529
2726
|
openDialog("合并时间事件", field("name", "合并后的事件名称") + field("description", "合并说明(留空则拼接原说明)", "textarea"), async (form) => {
|
|
2530
|
-
await api(`/api/works/${state.work.id}/timeline/merge`, { method: "POST", body: {
|
|
2727
|
+
await api(`/api/works/${state.work.id}/timeline/merge`, { method: "POST", body: {
|
|
2728
|
+
eventIds,
|
|
2729
|
+
name: form.get("name"),
|
|
2730
|
+
description: form.get("description") || undefined,
|
|
2731
|
+
expectedVersionNos: Object.fromEntries(eventIds.map((eventId) => [eventId, Number(events.find((event) => event.id === eventId)?.versionNo)]))
|
|
2732
|
+
} });
|
|
2531
2733
|
await renderTimeline();
|
|
2532
2734
|
}, "保留参与者与证据");
|
|
2533
2735
|
});
|
|
@@ -3361,6 +3563,7 @@ function openWorkSettingsDialog(work) {
|
|
|
3361
3563
|
|
|
3362
3564
|
async function openChapterDialog() {
|
|
3363
3565
|
if (!state.work) return openWorkDialog();
|
|
3566
|
+
if (!canEditProse()) return toast("当前权限只能编辑设定资料,正文为只读", "error");
|
|
3364
3567
|
if (!state.work.volumes.length) {
|
|
3365
3568
|
await api(`/api/works/${state.work.id}/volumes`, { method: "POST", body: { title: "正文", kind: "main" } });
|
|
3366
3569
|
state.work = await api(`/api/works/${state.work.id}`);
|
|
@@ -3375,6 +3578,7 @@ async function openChapterDialog() {
|
|
|
3375
3578
|
|
|
3376
3579
|
function openVolumeDialog(item) {
|
|
3377
3580
|
if (!state.work) return openWorkDialog();
|
|
3581
|
+
if (!canEditProse()) return toast("当前权限只能编辑设定资料,不能修改分卷", "error");
|
|
3378
3582
|
const kindOptions = [["main", "正文卷"], ["prequel", "前传"], ["extra", "番外"], ["epilogue", "后记"], ["appendix", "附录"]];
|
|
3379
3583
|
openDialog(item ? "编辑分卷" : "新建分卷",
|
|
3380
3584
|
field("title", "分卷名称", "text", item?.title) +
|
|
@@ -3706,7 +3910,7 @@ async function showCharacterSectionVersions(sectionId) {
|
|
|
3706
3910
|
host.querySelectorAll("[data-character-section-restore]").forEach((button) => button.addEventListener("click", async () => {
|
|
3707
3911
|
button.disabled = true;
|
|
3708
3912
|
try {
|
|
3709
|
-
|
|
3913
|
+
await api(`/api/character-sections/${sectionId}/restore`, { method: "POST", body: { versionNo: Number(button.dataset.characterSectionRestore) } });
|
|
3710
3914
|
characterEditorSections = await api(`/api/characters/${characterEditorItem.id}/sections`);
|
|
3711
3915
|
renderCharacterMarkdownSections();
|
|
3712
3916
|
await Promise.all([renderCharacters(), loadAiReferences()]);
|
|
@@ -4033,7 +4237,7 @@ function openTimelineDialog(item, preferredTrackId = null) {
|
|
|
4033
4237
|
const trackOptions = [["", "未分组"], ...state.timelineTracks.map((track) => [track.id, track.name])];
|
|
4034
4238
|
openDialog(item ? "编辑大事件" : "新建大事件", field("trackId", "所属独立时间轴", "select", item?.trackId ?? preferredTrackId ?? "", trackOptions) + field("name", "事件名称", "text", item?.name) + field("timeLabel", "时间描述", "text", item?.timeLabel ?? "时间待定") + field("timeSort", "排序值(留空表示时间待定)", "number", item?.timeSort ?? "") + field("eventType", "事件类型", "text", item?.eventType ?? "other") + field("location", "地点", "text", item?.location) + field("description", "事件简述", "textarea", item?.description), async (form) => {
|
|
4035
4239
|
const rawSort = String(form.get("timeSort") ?? "").trim();
|
|
4036
|
-
const body = { trackId: form.get("trackId") || null, name: form.get("name"), timeLabel: form.get("timeLabel"), timeSort: rawSort ? Number(rawSort) : null, eventType: form.get("eventType"), location: form.get("location"), description: form.get("description"), status: item?.status ?? "confirmed" };
|
|
4240
|
+
const body = { trackId: form.get("trackId") || null, name: form.get("name"), timeLabel: form.get("timeLabel"), timeSort: rawSort ? Number(rawSort) : null, eventType: form.get("eventType"), location: form.get("location"), description: form.get("description"), status: item?.status ?? "confirmed", ...(item ? { expectedVersionNo: item.versionNo } : {}) };
|
|
4037
4241
|
await api(item ? `/api/timeline/${item.id}` : `/api/works/${state.work.id}/timeline`, { method: item ? "PATCH" : "POST", body });
|
|
4038
4242
|
await renderTimeline();
|
|
4039
4243
|
}, item ? "人工调整" : "作者确认事件");
|
|
@@ -4049,7 +4253,7 @@ function openOutlineDialog(item) {
|
|
|
4049
4253
|
field("status", "规划状态", "select", item.status ?? "draft", [["draft", "草稿"], ["ready", "可执行"], ["completed", "已完成"]]),
|
|
4050
4254
|
async (form) => {
|
|
4051
4255
|
await api(`/api/chapters/${item.chapterId}/outline`, { method: "PUT", body: {
|
|
4052
|
-
goal: form.get("goal"), conflict: form.get("conflict"), turningPoint: form.get("turningPoint"), notes: form.get("notes"), status: form.get("status")
|
|
4256
|
+
goal: form.get("goal"), conflict: form.get("conflict"), turningPoint: form.get("turningPoint"), notes: form.get("notes"), status: form.get("status"), expectedVersionNo: item.versionNo
|
|
4053
4257
|
} });
|
|
4054
4258
|
await renderOutlines();
|
|
4055
4259
|
toast("章节规划已保存");
|
|
@@ -4092,7 +4296,8 @@ function openForeshadowDialog(item) {
|
|
|
4092
4296
|
const occurrences = [...preservedOccurrences, ...editedOccurrences];
|
|
4093
4297
|
const body = {
|
|
4094
4298
|
title: form.get("title"), description: form.get("description"), importance: form.get("importance"), status: form.get("status"),
|
|
4095
|
-
plannedPayoffChapterId: form.get("payoffChapterId") || null, resolutionNote: form.get("resolutionNote"), occurrences
|
|
4299
|
+
plannedPayoffChapterId: form.get("payoffChapterId") || null, resolutionNote: form.get("resolutionNote"), occurrences,
|
|
4300
|
+
...(item ? { expectedVersionNo: item.versionNo } : {})
|
|
4096
4301
|
};
|
|
4097
4302
|
await api(item ? `/api/foreshadows/${item.id}` : `/api/works/${state.work.id}/foreshadows`, { method: item ? "PATCH" : "POST", body });
|
|
4098
4303
|
await renderOutlines();
|
|
@@ -4105,7 +4310,7 @@ function openTimelineSplitDialog(item) {
|
|
|
4105
4310
|
await api(`/api/timeline/${item.id}/split`, { method: "POST", body: { parts: [
|
|
4106
4311
|
{ name: form.get("firstName"), description: form.get("firstDescription") },
|
|
4107
4312
|
{ name: form.get("secondName"), description: form.get("secondDescription") }
|
|
4108
|
-
] } });
|
|
4313
|
+
], expectedVersionNo: item.versionNo } });
|
|
4109
4314
|
await renderTimeline();
|
|
4110
4315
|
}, "原证据同步保留");
|
|
4111
4316
|
}
|
|
@@ -4118,7 +4323,7 @@ async function openRelationshipDialog(item, options = {}) {
|
|
|
4118
4323
|
const defaultTo = characterOptions.find(([id]) => id !== defaultFrom)?.[0] ?? characterOptions[1][0];
|
|
4119
4324
|
openDialog(item ? "编辑人物关系" : "新建人物关系", field("from", "起点人物", "select", item?.fromCharacterId ?? defaultFrom, characterOptions) + field("to", "终点人物", "select", item?.toCharacterId ?? defaultTo, characterOptions) + field("category", "关系大类", "select", item?.category ?? "social", [["family", "亲属"], ["social", "社交"], ["emotional", "情感"], ["conflict", "冲突"], ["uncertain", "未确定"]]) + field("subtype", "关系子类", "text", item?.subtype) + field("keywords", "关系关键词", "keyword-chips", item?.keywords ?? []) + field("confidence", "置信度(0-1)", "number", item?.confidence ?? "1") + field("directed", "有方向性", "checkbox", item?.directed ?? false), async (form) => {
|
|
4120
4325
|
const keywords = uniqueRelationshipKeywords(form.getAll("keywords").map(String));
|
|
4121
|
-
await api(item ? `/api/relationships/${item.id}` : `/api/works/${state.work.id}/relationships`, { method: item ? "PATCH" : "POST", body: { fromCharacterId: form.get("from"), toCharacterId: form.get("to"), category: form.get("category"), subtype: form.get("subtype"), keywords, confidence: Number(form.get("confidence")), directed: form.get("directed") === "on", confirmationStatus: item?.confirmationStatus ?? "confirmed" } });
|
|
4326
|
+
await api(item ? `/api/relationships/${item.id}` : `/api/works/${state.work.id}/relationships`, { method: item ? "PATCH" : "POST", body: { fromCharacterId: form.get("from"), toCharacterId: form.get("to"), category: form.get("category"), subtype: form.get("subtype"), keywords, confidence: Number(form.get("confidence")), directed: form.get("directed") === "on", confirmationStatus: item?.confirmationStatus ?? "confirmed", ...(item ? { expectedVersionNo: item.versionNo } : {}) } });
|
|
4122
4327
|
await refreshRelationshipSurfaces(options.characterId ?? null);
|
|
4123
4328
|
}, item ? "关系档案" : "人工确认关系");
|
|
4124
4329
|
}
|
|
@@ -4421,7 +4626,7 @@ function appendSuggestion(suggestion, createdAt = null, messageId = null) {
|
|
|
4421
4626
|
async function showVersions() {
|
|
4422
4627
|
if (!state.chapter) return;
|
|
4423
4628
|
const versions = await api(`/api/chapters/${state.chapter.id}/versions`);
|
|
4424
|
-
$("#versions-list").innerHTML = versions.map((version) => `<div class="version-row"><div><b>v${version.versionNo}</b><small>${esc(version.source)} · ${esc(version.actor || "历史数据")}</small></div><p>${esc(version.content.slice(0, 300) || "空白章节")}</p
|
|
4629
|
+
$("#versions-list").innerHTML = versions.map((version) => `<div class="version-row"><div><b>v${version.versionNo}</b><small>${esc(version.source)} · ${esc(version.actor || "历史数据")}</small></div><p>${esc(version.content.slice(0, 300) || "空白章节")}</p>${canEditProse() ? `<button class="ghost-button" data-restore-version="${version.versionNo}">恢复</button>` : ""}</div>`).join("");
|
|
4425
4630
|
$("#versions-list").querySelectorAll("[data-restore-version]").forEach((button) => button.addEventListener("click", async () => {
|
|
4426
4631
|
if (!window.confirm(`将版本 v${button.dataset.restoreVersion} 恢复为一个新的保存版本?`)) return;
|
|
4427
4632
|
state.chapter = await api(`/api/chapters/${state.chapter.id}/restore`, { method: "POST", body: { versionNo: Number(button.dataset.restoreVersion) } });
|
|
@@ -4724,7 +4929,7 @@ $("#member-invite-form").addEventListener("submit", async (event) => {
|
|
|
4724
4929
|
const members = await api(`/api/works/${encodeURIComponent(work.id)}/members`, { method: "POST", body: { userId, role } });
|
|
4725
4930
|
renderMembers(members);
|
|
4726
4931
|
await fillMemberCandidates(members);
|
|
4727
|
-
toast(role === "viewer" ? "仅查看成员已邀请" : "
|
|
4932
|
+
toast(role === "viewer" ? "仅查看成员已邀请" : role === "settings-editor" ? "设定编辑已邀请" : "完整协作者已邀请");
|
|
4728
4933
|
} catch (error) { toast(error.message, "error"); }
|
|
4729
4934
|
});
|
|
4730
4935
|
$("#platform-new-provider").addEventListener("click", () => openProviderDialog());
|
|
@@ -4873,22 +5078,35 @@ $("#ai-mention-menu").addEventListener("click", (event) => {
|
|
|
4873
5078
|
if (button) selectAiMention(button);
|
|
4874
5079
|
});
|
|
4875
5080
|
$("#import-file").addEventListener("change", async (event) => {
|
|
4876
|
-
|
|
4877
|
-
if (!
|
|
5081
|
+
const file = event.target.files[0];
|
|
5082
|
+
if (!state.work || !file) return;
|
|
5083
|
+
if (!canEditProse()) {
|
|
5084
|
+
event.target.value = "";
|
|
5085
|
+
toast("当前权限只能编辑设定资料,不能导入正文", "error");
|
|
5086
|
+
return;
|
|
5087
|
+
}
|
|
5088
|
+
const mode = await chooseExistingWorkImportMode(file);
|
|
5089
|
+
if (!mode) {
|
|
4878
5090
|
event.target.value = "";
|
|
4879
5091
|
return;
|
|
4880
5092
|
}
|
|
5093
|
+
cancelChapterAutoSave();
|
|
4881
5094
|
const body = new FormData();
|
|
4882
|
-
body.append("file",
|
|
5095
|
+
body.append("file", file);
|
|
5096
|
+
body.append("mode", mode);
|
|
5097
|
+
body.append("expectedVersionNo", String(state.work.versionNo));
|
|
4883
5098
|
try {
|
|
4884
5099
|
const result = await api(`/api/works/${state.work.id}/import`, { method: "POST", body });
|
|
4885
|
-
setSaveState("
|
|
5100
|
+
setSaveState(mode === "append" ? "已追加" : "已覆盖");
|
|
4886
5101
|
state.work = result.tree;
|
|
4887
5102
|
renderTree();
|
|
4888
|
-
|
|
4889
|
-
|
|
4890
|
-
if (
|
|
4891
|
-
} catch (error) {
|
|
5103
|
+
const completion = mode === "append" ? "正文追加完成" : "正文覆盖完成";
|
|
5104
|
+
toast(result.warnings.length ? `${completion}:${result.warnings.join(";")}` : completion);
|
|
5105
|
+
if (result.firstImportedChapterId) await selectChapter(result.firstImportedChapterId);
|
|
5106
|
+
} catch (error) {
|
|
5107
|
+
toast(error.message, "error");
|
|
5108
|
+
if (state.dirty) scheduleChapterAutoSave();
|
|
5109
|
+
}
|
|
4892
5110
|
event.target.value = "";
|
|
4893
5111
|
});
|
|
4894
5112
|
$("#new-import-file").addEventListener("change", async (event) => {
|
package/dist/public/index.html
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
<script src="/theme-init.js?v=20260720-route-skeleton"></script>
|
|
10
10
|
<link rel="icon" href="/icon.svg?v=20260712" type="image/svg+xml">
|
|
11
11
|
<link rel="manifest" href="/site.webmanifest">
|
|
12
|
-
<link rel="stylesheet" href="/styles.css?v=20260722-
|
|
12
|
+
<link rel="stylesheet" href="/styles.css?v=20260722-settings-import-mode">
|
|
13
13
|
</head>
|
|
14
14
|
<body class="auth-pending">
|
|
15
15
|
<section id="auth-view" class="auth-view hidden" aria-labelledby="auth-title">
|
|
@@ -316,6 +316,29 @@
|
|
|
316
316
|
</form>
|
|
317
317
|
</dialog>
|
|
318
318
|
|
|
319
|
+
<dialog id="import-mode-dialog" class="dialog import-mode-dialog" aria-labelledby="import-mode-dialog-title" aria-describedby="import-mode-dialog-description" data-testid="import-mode-dialog">
|
|
320
|
+
<form method="dialog">
|
|
321
|
+
<div class="dialog-header">
|
|
322
|
+
<div><span class="eyebrow">已有项目</span><h2 id="import-mode-dialog-title">选择正文导入方式</h2></div>
|
|
323
|
+
<button class="dialog-close" value="cancel" aria-label="取消导入" type="submit">×</button>
|
|
324
|
+
</div>
|
|
325
|
+
<div class="import-mode-body">
|
|
326
|
+
<p id="import-mode-dialog-description">即将把所选文件导入当前作品。请选择追加还是覆盖正文。</p>
|
|
327
|
+
<p id="import-mode-file-summary" class="import-mode-file-summary"></p>
|
|
328
|
+
<p id="import-mode-unsaved-warning" class="import-mode-unsaved-warning hidden" role="alert">当前章节有未保存修改,继续导入会丢失这些本地修改。</p>
|
|
329
|
+
<div class="import-mode-options" aria-label="导入方式说明">
|
|
330
|
+
<section><strong>追加正文</strong><span>保留现有卷章,把新文件解析出的卷章添加到目录末尾。</span></section>
|
|
331
|
+
<section><strong>覆盖正文</strong><span>删除现有正文目录后写入新文件;导入前会保留可恢复的版本快照。</span></section>
|
|
332
|
+
</div>
|
|
333
|
+
</div>
|
|
334
|
+
<div class="dialog-actions">
|
|
335
|
+
<button class="ghost-button" value="cancel" type="submit">取消</button>
|
|
336
|
+
<button id="import-mode-append" class="ghost-button" value="append" type="submit">追加正文</button>
|
|
337
|
+
<button id="import-mode-overwrite" class="primary-button" value="overwrite" type="submit">覆盖正文</button>
|
|
338
|
+
</div>
|
|
339
|
+
</form>
|
|
340
|
+
</dialog>
|
|
341
|
+
|
|
319
342
|
<section id="entity-editor-view" class="entity-editor-view hidden" aria-label="全屏资料编辑">
|
|
320
343
|
<form id="setting-editor-form" class="entity-editor-page hidden" aria-labelledby="setting-editor-title">
|
|
321
344
|
<header class="entity-editor-header">
|
|
@@ -508,7 +531,7 @@
|
|
|
508
531
|
<dialog id="members-dialog" class="dialog wide-dialog" aria-labelledby="members-dialog-title">
|
|
509
532
|
<div class="dialog-header"><div><span id="members-dialog-eyebrow" class="eyebrow">作品权限</span><h2 id="members-dialog-title">可访问人</h2></div><button id="members-dialog-close" class="dialog-close" aria-label="关闭" type="button">×</button></div>
|
|
510
533
|
<div class="access-dialog-body">
|
|
511
|
-
<form id="member-invite-form" class="member-invite-form"><label>邀请已注册用户<select id="member-user-select" required></select></label><label>访问权限<select id="member-role-select" required><option value="viewer">仅查看</option><option value="editor"
|
|
534
|
+
<form id="member-invite-form" class="member-invite-form"><label>邀请已注册用户<select id="member-user-select" required></select></label><label>访问权限<select id="member-role-select" required><option value="viewer">仅查看</option><option value="settings-editor">仅编辑设定</option><option value="editor">编辑正文与设定</option></select></label><button class="primary-button" type="submit">发送邀请</button></form>
|
|
512
535
|
<div id="members-list" class="access-list"></div>
|
|
513
536
|
</div>
|
|
514
537
|
</dialog>
|
|
@@ -575,6 +598,6 @@
|
|
|
575
598
|
</dialog>
|
|
576
599
|
|
|
577
600
|
<div id="auth-loading" class="auth-loading" role="status" aria-label="正在载入工作台"></div>
|
|
578
|
-
<script type="module" src="/app.js?v=20260722-
|
|
601
|
+
<script type="module" src="/app.js?v=20260722-settings-import-version-lock"></script>
|
|
579
602
|
</body>
|
|
580
603
|
</html>
|