@musnows/scriverse 0.5.7 → 0.5.8

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.
@@ -2768,6 +2768,7 @@ const workAuditActionLabels = {
2768
2768
  "chapter.saved": "保存章节",
2769
2769
  "chapter.moved": "移动章节",
2770
2770
  "chapter.deleted": "删除章节",
2771
+ "chapter.purged": "彻底删除章节",
2771
2772
  "chapter.restored": "恢复章节",
2772
2773
  "work.imported": "导入正文"
2773
2774
  };
@@ -6354,7 +6355,7 @@ function openWorkSettingsDialog(work) {
6354
6355
  <button id="work-export-button" class="ghost-button" type="button">下载 ZIP</button>
6355
6356
  </section>`;
6356
6357
  const recycleBinField = isCurrentWork ? `<section class="work-access-field" aria-labelledby="chapter-recycle-bin-settings-title">
6357
- <div><strong id="chapter-recycle-bin-settings-title">章节回收站</strong><small>查看并恢复已软删除的章节,正文、版本和关联资料不会在删除时清理。</small></div>
6358
+ <div><strong id="chapter-recycle-bin-settings-title">章节回收站</strong><small>恢复已软删除的章节,或彻底删除正文、版本和关联资料。</small></div>
6358
6359
  <button id="chapter-recycle-bin-button" class="ghost-button" type="button" aria-controls="chapter-recycle-bin-dialog" aria-haspopup="dialog" ${canEditProse() ? "" : "disabled"}>打开回收站</button>
6359
6360
  </section>` : "";
6360
6361
  const whitespaceField = isCurrentWork ? `<section class="work-access-field" aria-labelledby="whitespace-settings-title">
@@ -6417,7 +6418,7 @@ function openVolumeDialog(item) {
6417
6418
  if (!canEditProse()) return toast("当前权限只能编辑设定资料,不能修改分卷", "error");
6418
6419
  const kindOptions = [["main", "正文卷"], ["prequel", "前传"], ["extra", "番外"], ["epilogue", "后记"], ["appendix", "附录"]];
6419
6420
  const management = item ? `<section class="entity-dialog-management" aria-label="分卷操作">
6420
- <div><strong>分卷操作</strong><small>仅空分卷可以删除;卷内章节及回收站章节需先移动到其他分卷。</small></div>
6421
+ <div><strong>分卷操作</strong><small>仅空分卷可以删除;回收站章节需先彻底删除,或恢复后移动到其他分卷。</small></div>
6421
6422
  <div class="entity-dialog-management-actions"><button class="danger-button" type="button" data-dialog-volume-delete>删除分卷</button></div>
6422
6423
  </section>` : "";
6423
6424
  openDialog(item ? "编辑分卷" : "新建分卷",
@@ -8458,7 +8459,10 @@ function renderChapterRecycleBin(chapters) {
8458
8459
  <time>${esc(formatDateTime(chapter.deletedAt))} · ${esc(chapter.actor)}</time>
8459
8460
  <p>${esc(chapter.contentPreview || "空白章节")}</p>
8460
8461
  <small>${Number(chapter.wordCount).toLocaleString("zh-CN")} 字 · 删除版本 v${Number(chapter.versionNo)}</small>
8461
- <button type="button" data-restore-deleted-chapter="${esc(chapter.id)}">恢复章节</button>
8462
+ <footer class="entity-version-actions">
8463
+ <button type="button" data-restore-deleted-chapter="${esc(chapter.id)}">恢复章节</button>
8464
+ <button class="danger-button" type="button" data-purge-deleted-chapter="${esc(chapter.id)}">彻底删除</button>
8465
+ </footer>
8462
8466
  </article>`).join("");
8463
8467
  host.querySelectorAll("[data-restore-deleted-chapter]").forEach((button) => button.addEventListener("click", async () => {
8464
8468
  const chapter = chapters.find((item) => item.id === button.dataset.restoreDeletedChapter);
@@ -8490,6 +8494,36 @@ function renderChapterRecycleBin(chapters) {
8490
8494
  toast(error.message, "error");
8491
8495
  }
8492
8496
  }));
8497
+ host.querySelectorAll("[data-purge-deleted-chapter]").forEach((button) => button.addEventListener("click", async () => {
8498
+ const chapter = chapters.find((item) => item.id === button.dataset.purgeDeletedChapter);
8499
+ if (!chapter || !state.work) return;
8500
+ const dialog = $("#chapter-recycle-bin-dialog");
8501
+ dialog.close();
8502
+ const confirmed = await confirmToast(`彻底删除章节“${chapter.title}”吗?正文、版本和关联资料将无法恢复。`, {
8503
+ title: "彻底删除章节",
8504
+ confirmLabel: "确认彻底删除"
8505
+ });
8506
+ if (!confirmed) {
8507
+ dialog.showModal();
8508
+ return;
8509
+ }
8510
+ button.disabled = true;
8511
+ try {
8512
+ await api(`/api/chapters/${encodeURIComponent(chapter.id)}/permanent`, {
8513
+ method: "DELETE",
8514
+ body: { expectedVersionNo: chapter.versionNo }
8515
+ });
8516
+ state.work = await api(`/api/works/${encodeURIComponent(state.work.id)}`);
8517
+ renderTree();
8518
+ await loadChapterRecycleBin();
8519
+ dialog.showModal();
8520
+ toast(`已彻底删除章节“${chapter.title}”`);
8521
+ } catch (error) {
8522
+ button.disabled = false;
8523
+ dialog.showModal();
8524
+ toast(error.message, "error");
8525
+ }
8526
+ }));
8493
8527
  }
8494
8528
 
8495
8529
  async function loadChapterRecycleBin() {
@@ -10,7 +10,7 @@
10
10
  <link rel="icon" href="/icon.svg?v=20260712" type="image/svg+xml">
11
11
  <link rel="manifest" href="/site.webmanifest">
12
12
  <link rel="stylesheet" href="/vendor/vditor/dist/index.css?v=3.11.2">
13
- <link rel="stylesheet" href="/styles.css?v=20260728-writing-daily-line-v2">
13
+ <link rel="stylesheet" href="/styles.css?v=20260728-shelf-cover-scale-v1">
14
14
  </head>
15
15
  <body class="auth-pending">
16
16
  <section id="auth-view" class="auth-view hidden" aria-labelledby="auth-title">
@@ -653,7 +653,7 @@
653
653
  <button id="chapter-recycle-bin-close" class="dialog-close" aria-label="关闭章节回收站" type="button">×</button>
654
654
  </div>
655
655
  <div class="import-history-body">
656
- <p id="chapter-recycle-bin-description" class="import-history-note">软删除的章节会保留正文、版本和关联资料。恢复后章节将重新出现在原分卷中。</p>
656
+ <p id="chapter-recycle-bin-description" class="import-history-note">软删除的章节会保留正文、版本和关联资料。可以恢复到原分卷,也可以彻底删除;彻底删除后无法恢复。</p>
657
657
  <div id="chapter-recycle-bin-list" class="entity-history-list" aria-live="polite"></div>
658
658
  </div>
659
659
  </dialog>
@@ -863,6 +863,6 @@
863
863
  <div id="auth-loading" class="auth-loading" role="status" aria-label="正在载入工作台"></div>
864
864
  <script id="vditorIconScript" src="/vendor/vditor/dist/js/icons/ant.js?v=3.11.2"></script>
865
865
  <script src="/vendor/vditor/dist/index.min.js?v=3.11.2"></script>
866
- <script type="module" src="/app.js?v=20260728-galaxy-edge-stars-v3"></script>
866
+ <script type="module" src="/app.js?v=20260728-chapter-purge-v1"></script>
867
867
  </body>
868
868
  </html>
@@ -987,7 +987,7 @@ body.work-viewer-mode .character-editor-actions { display: none !important; }
987
987
  .book-shelf { display: grid; grid-template-columns: repeat(auto-fill, minmax(205px, 1fr)); gap: clamp(22px, 3vw, 38px); width: 100%; min-width: 0; max-width: 1400px; margin: 0 auto; }
988
988
  .book-card { position: relative; min-width: 0; border: 0; background: transparent; text-align: left; }
989
989
  .book-open { width: 100%; padding: 0; border: 0; background: transparent; text-align: left; }
990
- .book-cover { position: relative; display: grid; place-items: center; aspect-ratio: 3 / 4.25; overflow: hidden; border-radius: 8px 13px 13px 8px; background: linear-gradient(145deg, #a96350, #612d27); color: rgba(255,255,255,.9); box-shadow: 0 20px 34px rgba(62,39,29,.18), inset 9px 0 0 rgba(0,0,0,.08); transition: transform .2s ease, box-shadow .2s ease; }
990
+ .book-cover { position: relative; display: grid; place-items: center; width: 90%; margin-inline: auto; aspect-ratio: 3 / 4.25; overflow: hidden; border-radius: 8px 13px 13px 8px; background: linear-gradient(145deg, #a96350, #612d27); color: rgba(255,255,255,.9); box-shadow: 0 20px 34px rgba(62,39,29,.18), inset 9px 0 0 rgba(0,0,0,.08); transition: transform .2s ease, box-shadow .2s ease; }
991
991
  .book-cover::after { content: ""; position: absolute; inset: 0 auto 0 9px; width: 1px; background: rgba(255,255,255,.2); }
992
992
  .book-open:hover .book-cover, .book-open:focus-visible .book-cover { transform: translateY(-6px) rotateY(-2deg); box-shadow: 0 28px 44px rgba(62,39,29,.24), inset 9px 0 0 rgba(0,0,0,.08); }
993
993
  .book-cover-fallback { font-size: clamp(46px, 6vw, 74px); font-weight: 700; opacity: .75; }
@@ -2668,6 +2668,8 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
2668
2668
  .entity-version-card > button:not(:disabled):hover { border-color: var(--accent); color: var(--accent-dark); }
2669
2669
  .entity-version-card > button.is-confirming { border-color: var(--accent); background: var(--accent); color: #fff; }
2670
2670
  .entity-version-card > button:disabled { cursor: default; opacity: .68; }
2671
+ .entity-version-actions { display: flex; justify-self: end; gap: 7px; }
2672
+ .entity-version-actions button { min-height: 30px; padding: 6px 9px; font-size: 9px; }
2671
2673
  .entity-history-empty { margin: 0; padding: 28px 12px; color: var(--muted); font-size: 11px; text-align: center; }
2672
2674
  .relationship-table .relationship-actions { white-space: nowrap; vertical-align: middle; }
2673
2675
  .relationship-table .relationship-actions button { display: inline-block; min-height: 30px; padding: 6px 10px; border: 1px solid var(--line); border-radius: 4px; background: var(--surface); color: var(--muted); font: inherit; font-size: 10px; line-height: 1.2; vertical-align: middle; appearance: none; transition: border-color .14s ease, background .14s ease, color .14s ease; }
package/dist/store.js CHANGED
@@ -1027,7 +1027,7 @@ export class Store {
1027
1027
  throw new AppError(409, "VOLUME_NOT_EMPTY", "卷内仍有章节,需先移动或删除章节");
1028
1028
  }
1029
1029
  if (numberValue(counts ?? {}, "deleted_count") > 0) {
1030
- throw new AppError(409, "VOLUME_HAS_DELETED_CHAPTERS", "分卷回收站中仍有章节,请先恢复并移动这些章节后再删除分卷");
1030
+ throw new AppError(409, "VOLUME_HAS_DELETED_CHAPTERS", "分卷回收站中仍有章节,请先彻底删除或恢复并移动这些章节");
1031
1031
  }
1032
1032
  this.recordEntityVersion("volume", volumeId, "delete", null, "删除分卷");
1033
1033
  this.db.run("DELETE FROM volumes WHERE id = ?", volumeId);
@@ -1620,6 +1620,35 @@ export class Store {
1620
1620
  this.audit(String(chapter.workId), "chapter.deleted", "chapter", chapterId, { versionNo });
1621
1621
  });
1622
1622
  }
1623
+ permanentlyDeleteChapter(chapterId, expectedVersionNo) {
1624
+ const chapter = this.db.get("SELECT * FROM chapters WHERE id = ?", chapterId);
1625
+ if (!chapter)
1626
+ throw notFound("章节");
1627
+ if (!chapter.deleted_at)
1628
+ throw new AppError(409, "CHAPTER_NOT_IN_RECYCLE_BIN", "仅回收站中的章节可以彻底删除");
1629
+ this.assertExpectedRevision("chapter", chapterId, expectedVersionNo, "章节", numberValue(chapter, "version_no"));
1630
+ const workId = requiredString(chapter, "work_id");
1631
+ const timestamp = now();
1632
+ this.db.transaction(() => {
1633
+ const locked = this.db.get("SELECT * FROM chapters WHERE id = ?", chapterId);
1634
+ if (!locked)
1635
+ throw notFound("章节");
1636
+ if (!locked.deleted_at)
1637
+ throw new AppError(409, "CHAPTER_NOT_IN_RECYCLE_BIN", "仅回收站中的章节可以彻底删除");
1638
+ this.assertExpectedRevision("chapter", chapterId, expectedVersionNo, "章节", numberValue(locked, "version_no"));
1639
+ this.db.run("DELETE FROM chapter_versions WHERE chapter_id = ?", chapterId);
1640
+ this.db.run("DELETE FROM entity_versions WHERE entity_type = 'chapter-outline' AND entity_id = ?", chapterId);
1641
+ this.db.run("DELETE FROM attachment_references WHERE entity_type = 'chapter' AND entity_id = ?", chapterId);
1642
+ this.db.run("DELETE FROM chapters WHERE id = ?", chapterId);
1643
+ this.db.run("UPDATE works SET updated_at = ? WHERE id = ?", timestamp, workId);
1644
+ this.audit(workId, "chapter.purged", "chapter", chapterId, {
1645
+ title: requiredString(locked, "title"),
1646
+ volumeId: requiredString(locked, "volume_id"),
1647
+ versionNo: numberValue(locked, "version_no"),
1648
+ recoverable: false
1649
+ });
1650
+ });
1651
+ }
1623
1652
  insertChapter(workId, volumeId, title, content, sortOrder, source, sourceRef, chapterType = "正文") {
1624
1653
  const chapterId = id("chapter");
1625
1654
  const timestamp = now();