@musnows/scriverse 0.5.6 → 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.
- package/dist/app.js +30 -3
- package/dist/app.js.map +1 -1
- package/dist/database.js +14 -0
- package/dist/database.js.map +1 -1
- package/dist/public/app.js +127 -18
- package/dist/public/index.html +14 -6
- package/dist/public/relationship-graph.js +80 -22
- package/dist/public/styles.css +42 -16
- package/dist/store.js +30 -1
- package/dist/store.js.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
package/dist/public/index.html
CHANGED
|
@@ -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=
|
|
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">
|
|
@@ -140,7 +140,7 @@
|
|
|
140
140
|
<span>作品目录</span>
|
|
141
141
|
<div class="panel-heading-actions">
|
|
142
142
|
<span id="chapter-count">0 章</span>
|
|
143
|
-
<button id="chapter-batch-button" class="chapter-batch-button" type="button"
|
|
143
|
+
<button id="chapter-batch-button" class="chapter-batch-button" type="button" aria-label="批量管理章节" title="批量管理章节"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m3.5 6 1.5 1.5L7.5 5"/><path d="M10 6h10"/><path d="m3.5 12 1.5 1.5 2.5-2.5"/><path d="M10 12h10"/><path d="m3.5 18 1.5 1.5 2.5-2.5"/><path d="M10 18h10"/></svg></button>
|
|
144
144
|
<button id="new-volume-button" class="add-button" type="button" aria-label="新建分卷" title="新建分卷">+</button>
|
|
145
145
|
</div>
|
|
146
146
|
</div>
|
|
@@ -197,7 +197,7 @@
|
|
|
197
197
|
<button id="writing-progress-button" class="settings-hub-card" type="button"><span class="settings-card-mark">字</span><strong>写作目标</strong><small>每日目标、总字数目标与近 30 天趋势</small></button>
|
|
198
198
|
<button id="work-audit-button" class="settings-hub-card" type="button"><span class="settings-card-mark">录</span><strong>操作记录</strong><small>查看作品修改、操作者、对象与时间</small></button>
|
|
199
199
|
<button id="appearance-button" class="settings-hub-card" type="button" data-settings-action="appearance"><span class="settings-card-mark">Aa</span><strong>显示设置</strong><small>中文字体、等宽英文字体、字号与行距</small></button>
|
|
200
|
-
<button id="export-button" class="settings-hub-card" type="button" data-settings-action="export"><span class="settings-card-mark">
|
|
200
|
+
<button id="export-button" class="settings-hub-card" type="button" data-settings-action="export"><span class="settings-card-mark">ZIP</span><strong>导出正文</strong><small>以 ZIP 下载 Markdown 正文,不包含角色和设定资料</small></button>
|
|
201
201
|
</div>
|
|
202
202
|
<p id="settings-work-note" class="settings-work-note"></p>
|
|
203
203
|
<footer class="product-footer settings-product-footer" data-product-footer aria-label="叙界项目信息">
|
|
@@ -400,6 +400,14 @@
|
|
|
400
400
|
<button id="chapter-batch-clear" class="ghost-button" type="button">清空</button>
|
|
401
401
|
<output id="chapter-batch-count">已选择 0 章</output>
|
|
402
402
|
</div>
|
|
403
|
+
<div class="chapter-batch-search">
|
|
404
|
+
<label for="chapter-batch-search">搜索章节名称</label>
|
|
405
|
+
<div class="chapter-batch-search-field">
|
|
406
|
+
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="11" cy="11" r="6.5"/><path d="m16 16 4 4"/></svg>
|
|
407
|
+
<input id="chapter-batch-search" type="search" placeholder="输入章节名称" autocomplete="off" aria-describedby="chapter-batch-search-count">
|
|
408
|
+
<output id="chapter-batch-search-count">0 章</output>
|
|
409
|
+
</div>
|
|
410
|
+
</div>
|
|
403
411
|
<div id="chapter-batch-list" class="chapter-batch-list"></div>
|
|
404
412
|
<div class="chapter-batch-controls">
|
|
405
413
|
<label>批量操作<select id="chapter-batch-action"><option value="move">移动到分卷</option><option value="setType">修改章节类型</option><option value="exclude">排除 AI 分析</option><option value="include">纳入 AI 分析</option><option value="delete">软删除章节</option></select></label>
|
|
@@ -645,7 +653,7 @@
|
|
|
645
653
|
<button id="chapter-recycle-bin-close" class="dialog-close" aria-label="关闭章节回收站" type="button">×</button>
|
|
646
654
|
</div>
|
|
647
655
|
<div class="import-history-body">
|
|
648
|
-
<p id="chapter-recycle-bin-description" class="import-history-note"
|
|
656
|
+
<p id="chapter-recycle-bin-description" class="import-history-note">软删除的章节会保留正文、版本和关联资料。可以恢复到原分卷,也可以彻底删除;彻底删除后无法恢复。</p>
|
|
649
657
|
<div id="chapter-recycle-bin-list" class="entity-history-list" aria-live="polite"></div>
|
|
650
658
|
</div>
|
|
651
659
|
</dialog>
|
|
@@ -668,7 +676,7 @@
|
|
|
668
676
|
<div class="dialog-header"><div><span class="eyebrow">创作节奏</span><h2 id="writing-progress-title">写作目标与字数趋势</h2><p class="dialog-header-meta">趋势根据正文版本历史重建,软删除章节不会继续计入。</p></div><button id="writing-progress-close" class="dialog-close" aria-label="关闭写作目标" type="button">×</button></div>
|
|
669
677
|
<div class="writing-progress-body">
|
|
670
678
|
<div class="writing-progress-stats"><article><small>当前总字数</small><strong id="writing-current-words">0</strong></article><article><small>今日新增</small><strong id="writing-today-words">0</strong></article><article><small>每日目标完成</small><strong id="writing-daily-completion">0%</strong></article><article><small>总目标完成</small><strong id="writing-total-completion">0%</strong></article></div>
|
|
671
|
-
<div id="writing-trend-chart" class="writing-trend-chart" role="
|
|
679
|
+
<div id="writing-trend-chart" class="writing-trend-chart" role="group" aria-label="近 30 天作品字数趋势"></div>
|
|
672
680
|
<div class="writing-goal-fields"><label>每日目标字数<input id="writing-daily-goal" type="number" min="0" max="1000000" required></label><label>总字数目标<input id="writing-total-goal" type="number" min="0" max="100000000" required></label><label>计划完成日期<input id="writing-deadline" type="date"></label></div>
|
|
673
681
|
</div>
|
|
674
682
|
<div class="dialog-actions"><button id="writing-progress-refresh" class="ghost-button" type="button">刷新趋势</button><button id="writing-goal-save" class="primary-button" type="submit">保存目标</button></div>
|
|
@@ -855,6 +863,6 @@
|
|
|
855
863
|
<div id="auth-loading" class="auth-loading" role="status" aria-label="正在载入工作台"></div>
|
|
856
864
|
<script id="vditorIconScript" src="/vendor/vditor/dist/js/icons/ant.js?v=3.11.2"></script>
|
|
857
865
|
<script src="/vendor/vditor/dist/index.min.js?v=3.11.2"></script>
|
|
858
|
-
<script type="module" src="/app.js?v=
|
|
866
|
+
<script type="module" src="/app.js?v=20260728-chapter-purge-v1"></script>
|
|
859
867
|
</body>
|
|
860
868
|
</html>
|
|
@@ -6,7 +6,7 @@ const RELATION_STYLE = Object.freeze({
|
|
|
6
6
|
uncertain: { label: "未确定", color: "#9aa5b5" }
|
|
7
7
|
});
|
|
8
8
|
|
|
9
|
-
/** Obsidian Graph View
|
|
9
|
+
/** Obsidian Graph View 风格:为浅色和深色背景提供可辨识的低饱和度均衡配色 */
|
|
10
10
|
export const OBSIDIAN_NODE_PALETTE = Object.freeze([
|
|
11
11
|
Object.freeze({ key: "blue", color: "#3f6f99", darkColor: "#7fb7e3", glow: "rgba(63,111,153,.42)", darkGlow: "rgba(127,183,227,.56)" }),
|
|
12
12
|
Object.freeze({ key: "indigo", color: "#5b5fa3", darkColor: "#9ea6ed", glow: "rgba(91,95,163,.42)", darkGlow: "rgba(158,166,237,.56)" }),
|
|
@@ -25,7 +25,13 @@ export const OBSIDIAN_NODE_PALETTE = Object.freeze([
|
|
|
25
25
|
Object.freeze({ key: "steel", color: "#576f86", darkColor: "#98afc5", glow: "rgba(87,111,134,.42)", darkGlow: "rgba(152,175,197,.56)" }),
|
|
26
26
|
Object.freeze({ key: "slate", color: "#626b78", darkColor: "#aab3c0", glow: "rgba(98,107,120,.42)", darkGlow: "rgba(170,179,192,.56)" }),
|
|
27
27
|
Object.freeze({ key: "sand", color: "#806e58", darkColor: "#c5ad91", glow: "rgba(128,110,88,.42)", darkGlow: "rgba(197,173,145,.56)" }),
|
|
28
|
-
Object.freeze({ key: "taupe", color: "#75636d", darkColor: "#b9a0ad", glow: "rgba(117,99,109,.42)", darkGlow: "rgba(185,160,173,.56)" })
|
|
28
|
+
Object.freeze({ key: "taupe", color: "#75636d", darkColor: "#b9a0ad", glow: "rgba(117,99,109,.42)", darkGlow: "rgba(185,160,173,.56)" }),
|
|
29
|
+
Object.freeze({ key: "gold", color: "#8b702b", darkColor: "#dfbd63", glow: "rgba(139,112,43,.42)", darkGlow: "rgba(223,189,99,.56)" }),
|
|
30
|
+
Object.freeze({ key: "lime", color: "#5e7a35", darkColor: "#a4cd74", glow: "rgba(94,122,53,.42)", darkGlow: "rgba(164,205,116,.56)" }),
|
|
31
|
+
Object.freeze({ key: "mint", color: "#397d59", darkColor: "#78ce99", glow: "rgba(57,125,89,.42)", darkGlow: "rgba(120,206,153,.56)" }),
|
|
32
|
+
Object.freeze({ key: "turquoise", color: "#287d78", darkColor: "#69cec8", glow: "rgba(40,125,120,.42)", darkGlow: "rgba(105,206,200,.56)" }),
|
|
33
|
+
Object.freeze({ key: "azure", color: "#326ea3", darkColor: "#76b7ee", glow: "rgba(50,110,163,.42)", darkGlow: "rgba(118,183,238,.56)" }),
|
|
34
|
+
Object.freeze({ key: "peach", color: "#a35d50", darkColor: "#eda197", glow: "rgba(163,93,80,.42)", darkGlow: "rgba(237,161,151,.56)" })
|
|
29
35
|
]);
|
|
30
36
|
|
|
31
37
|
const clamp = (value, minimum, maximum) => Math.min(maximum, Math.max(minimum, value));
|
|
@@ -87,6 +93,8 @@ const GALAXY_CELESTIAL_TYPES = Object.freeze({
|
|
|
87
93
|
outer: Object.freeze(["rocky", "ocean", "ice", "volcanic", "dwarf", "ringed"])
|
|
88
94
|
});
|
|
89
95
|
export const GALAXY_ROTATION_RADIANS_PER_MS = 0.000012;
|
|
96
|
+
export const GALAXY_BASE_STAR_COUNT = 7200;
|
|
97
|
+
export const GALAXY_EDGE_STAR_BOOST_RATIO = 1.1 * 1.1 - 1;
|
|
90
98
|
export const GALAXY_LAYOUT_CONFIG = Object.freeze({
|
|
91
99
|
minimumRadius: 220,
|
|
92
100
|
radialSpan: 830,
|
|
@@ -220,15 +228,9 @@ export function resolveRelationshipNodeGroup(node) {
|
|
|
220
228
|
return { type: "default", key: "default", label: "未分组" };
|
|
221
229
|
}
|
|
222
230
|
|
|
223
|
-
|
|
224
|
-
const group = resolveRelationshipNodeGroup(node);
|
|
231
|
+
function createObsidianNodeAppearance(node, maxDegree, group, paletteIndex) {
|
|
225
232
|
const degree = Math.max(0, Number(node?.degree) || 0);
|
|
226
233
|
const normalizedDegree = clamp(degree / Math.max(1, Number(maxDegree) || 1), 0, 1);
|
|
227
|
-
// 未分组角色没有可共享的分组语义,按角色稳定标识散列,避免资料不完整时大片同色。
|
|
228
|
-
const colorKey = group.key === "default"
|
|
229
|
-
? `node:${String(node?.id ?? node?.name ?? "default")}`
|
|
230
|
-
: group.key;
|
|
231
|
-
const paletteIndex = hashString(colorKey) % OBSIDIAN_NODE_PALETTE.length;
|
|
232
234
|
const palette = OBSIDIAN_NODE_PALETTE[paletteIndex];
|
|
233
235
|
return {
|
|
234
236
|
group,
|
|
@@ -242,6 +244,52 @@ export function getObsidianNodeAppearance(node, maxDegree = 1) {
|
|
|
242
244
|
};
|
|
243
245
|
}
|
|
244
246
|
|
|
247
|
+
function getObsidianNodeColorSeed(node, group = resolveRelationshipNodeGroup(node)) {
|
|
248
|
+
return mixHash(hashString([
|
|
249
|
+
String(node?.id ?? ""),
|
|
250
|
+
String(node?.name ?? ""),
|
|
251
|
+
group.key,
|
|
252
|
+
String(node?.species ?? ""),
|
|
253
|
+
String(node?.identity ?? "")
|
|
254
|
+
].join("|")));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function getObsidianNodeAppearance(node, maxDegree = 1) {
|
|
258
|
+
const group = resolveRelationshipNodeGroup(node);
|
|
259
|
+
const paletteIndex = getObsidianNodeColorSeed(node, group) % OBSIDIAN_NODE_PALETTE.length;
|
|
260
|
+
return createObsidianNodeAppearance(node, maxDegree, group, paletteIndex);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function assignBalancedObsidianNodeAppearances(nodes, maxDegree = 1) {
|
|
264
|
+
// 高连接节点优先占用尚未使用的颜色,再按稳定散列打散同一轮的色槽顺序。
|
|
265
|
+
const orderedNodes = [...(Array.isArray(nodes) ? nodes : [])].sort((left, right) => (
|
|
266
|
+
Number(right?.degree ?? 0) - Number(left?.degree ?? 0)
|
|
267
|
+
|| Number(right?.weightedDegree ?? 0) - Number(left?.weightedDegree ?? 0)
|
|
268
|
+
|| String(left?.name ?? "").localeCompare(String(right?.name ?? ""), "zh-CN")
|
|
269
|
+
|| String(left?.id ?? "").localeCompare(String(right?.id ?? ""))
|
|
270
|
+
));
|
|
271
|
+
const paletteUsage = Array.from({ length: OBSIDIAN_NODE_PALETTE.length }, () => 0);
|
|
272
|
+
const appearances = new Map();
|
|
273
|
+
for (const node of orderedNodes) {
|
|
274
|
+
const group = resolveRelationshipNodeGroup(node);
|
|
275
|
+
const seed = getObsidianNodeColorSeed(node, group);
|
|
276
|
+
const minimumUsage = Math.min(...paletteUsage);
|
|
277
|
+
let paletteIndex = 0;
|
|
278
|
+
let bestScore = -1;
|
|
279
|
+
for (let index = 0; index < paletteUsage.length; index += 1) {
|
|
280
|
+
if (paletteUsage[index] !== minimumUsage) continue;
|
|
281
|
+
const score = mixHash(seed ^ Math.imul(index + 1, 0x9e3779b1));
|
|
282
|
+
if (score > bestScore) {
|
|
283
|
+
bestScore = score;
|
|
284
|
+
paletteIndex = index;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
paletteUsage[paletteIndex] += 1;
|
|
288
|
+
appearances.set(String(node?.id ?? node?.name ?? appearances.size), createObsidianNodeAppearance(node, maxDegree, group, paletteIndex));
|
|
289
|
+
}
|
|
290
|
+
return appearances;
|
|
291
|
+
}
|
|
292
|
+
|
|
245
293
|
function hashString(value) {
|
|
246
294
|
let hash = 2166136261;
|
|
247
295
|
for (let index = 0; index < value.length; index += 1) {
|
|
@@ -341,7 +389,10 @@ export function buildRelationshipGraph(characters, relationships) {
|
|
|
341
389
|
const maxDegree = Math.max(1, ...nodes.map((node) => node.degree));
|
|
342
390
|
for (const node of nodes) {
|
|
343
391
|
node.importance = node.weightedDegree + Math.sqrt(node.degree) * 0.8;
|
|
344
|
-
|
|
392
|
+
}
|
|
393
|
+
const nodeAppearances = assignBalancedObsidianNodeAppearances(nodes, maxDegree);
|
|
394
|
+
for (const node of nodes) {
|
|
395
|
+
const appearance = nodeAppearances.get(node.id) ?? getObsidianNodeAppearance(node, maxDegree);
|
|
345
396
|
node.color = appearance.color;
|
|
346
397
|
node.darkColor = appearance.darkColor;
|
|
347
398
|
node.glow = appearance.glow;
|
|
@@ -854,7 +905,7 @@ export function renderRelationshipMindMap(container, graph, options = {}) {
|
|
|
854
905
|
|
|
855
906
|
const focusBadge = document.createElement("div");
|
|
856
907
|
focusBadge.className = "relationship-network-focus";
|
|
857
|
-
focusBadge.innerHTML = "<strong>人物关系图谱</strong><span>力导向布局 ·
|
|
908
|
+
focusBadge.innerHTML = "<strong>人物关系图谱</strong><span>力导向布局 · 稳定均衡配色</span>";
|
|
858
909
|
const focusText = focusBadge.querySelector("span");
|
|
859
910
|
const help = document.createElement("div");
|
|
860
911
|
help.className = "relationship-network-help";
|
|
@@ -1212,7 +1263,7 @@ export function renderRelationshipMindMap(container, graph, options = {}) {
|
|
|
1212
1263
|
edgeElements.forEach((item) => {
|
|
1213
1264
|
item.path.classList.remove("is-highlighted", "is-dimmed");
|
|
1214
1265
|
});
|
|
1215
|
-
focusText.textContent = "力导向布局 ·
|
|
1266
|
+
focusText.textContent = "力导向布局 · 稳定均衡配色";
|
|
1216
1267
|
edgeDetail.classList.add("hidden");
|
|
1217
1268
|
edgeDetail.replaceChildren();
|
|
1218
1269
|
};
|
|
@@ -1629,26 +1680,32 @@ export function layoutGalaxy(graph, seed) {
|
|
|
1629
1680
|
return { nodes, byId };
|
|
1630
1681
|
}
|
|
1631
1682
|
|
|
1632
|
-
export function createGalaxyStarfield(seed, count
|
|
1683
|
+
export function createGalaxyStarfield(seed, count) {
|
|
1633
1684
|
const random = seededRandom(hashString(seed));
|
|
1634
1685
|
const stars = [];
|
|
1635
1686
|
const armCount = 4;
|
|
1636
|
-
|
|
1687
|
+
const baseCount = count === undefined ? GALAXY_BASE_STAR_COUNT : Math.max(0, Math.floor(Number(count) || 0));
|
|
1688
|
+
const edgeBoostCount = count === undefined ? Math.round(baseCount * GALAXY_EDGE_STAR_BOOST_RATIO) : 0;
|
|
1689
|
+
const totalCount = baseCount + edgeBoostCount;
|
|
1690
|
+
for (let index = 0; index < totalCount; index += 1) {
|
|
1691
|
+
const isEdgeBoost = index >= baseCount;
|
|
1637
1692
|
const population = random();
|
|
1638
|
-
const isCore = population < 0.62;
|
|
1639
|
-
const isHalo = !isCore && population > 0.9;
|
|
1640
|
-
const radius =
|
|
1693
|
+
const isCore = !isEdgeBoost && population < 0.62;
|
|
1694
|
+
const isHalo = !isEdgeBoost && !isCore && population > 0.9;
|
|
1695
|
+
const radius = isEdgeBoost
|
|
1696
|
+
? 900 + Math.pow(random(), 0.82) * 900
|
|
1697
|
+
: isCore
|
|
1641
1698
|
? 32 + Math.pow(random(), 1.72) * 720
|
|
1642
1699
|
: 160 + Math.pow(random(), 0.68) * 1510;
|
|
1643
1700
|
const arm = index % armCount;
|
|
1644
1701
|
const armAngle = arm / armCount * Math.PI * 2;
|
|
1645
1702
|
const angle = isHalo
|
|
1646
1703
|
? random() * Math.PI * 2
|
|
1647
|
-
: armAngle + radius * 0.0065 + (random() - 0.5) * (isCore ? 0.72 : 0.42 + radius / 1100);
|
|
1648
|
-
const thickness = isHalo ? 70 + radius * 0.2 : (isCore ? 8 + radius * 0.045 : 22 + radius * 0.105);
|
|
1704
|
+
: armAngle + radius * 0.0065 + (random() - 0.5) * (isEdgeBoost ? 0.36 + radius / 1700 : isCore ? 0.72 : 0.42 + radius / 1100);
|
|
1705
|
+
const thickness = isHalo ? 70 + radius * 0.2 : (isEdgeBoost ? 26 + radius * 0.11 : isCore ? 8 + radius * 0.045 : 22 + radius * 0.105);
|
|
1649
1706
|
const temperature = random();
|
|
1650
|
-
const x = Math.cos(angle) * radius + (random() - 0.5) * (isCore ? 42 : 78);
|
|
1651
|
-
const z = Math.sin(angle) * radius + (random() - 0.5) * (isCore ? 42 : 78);
|
|
1707
|
+
const x = Math.cos(angle) * radius + (random() - 0.5) * (isCore ? 42 : isEdgeBoost ? 68 : 78);
|
|
1708
|
+
const z = Math.sin(angle) * radius + (random() - 0.5) * (isCore ? 42 : isEdgeBoost ? 68 : 78);
|
|
1652
1709
|
stars.push({
|
|
1653
1710
|
x,
|
|
1654
1711
|
y: (random() + random() + random() - 1.5) * thickness,
|
|
@@ -1659,7 +1716,8 @@ export function createGalaxyStarfield(seed, count = 7200) {
|
|
|
1659
1716
|
vz: 0,
|
|
1660
1717
|
size: isCore && random() > 0.94 ? 1.25 + random() * 1.25 : 0.38 + random() * 0.82,
|
|
1661
1718
|
brightness: isCore ? 0.3 + random() * 0.7 : 0.16 + random() * 0.66,
|
|
1662
|
-
color: temperature < 0.2 ? "255,218,176" : temperature > 0.78 ? "174,211,255" : "226,237,255"
|
|
1719
|
+
color: temperature < 0.2 ? "255,218,176" : temperature > 0.78 ? "174,211,255" : "226,237,255",
|
|
1720
|
+
region: isEdgeBoost ? "edge-arm" : isCore ? "core" : isHalo ? "halo" : "arm"
|
|
1663
1721
|
});
|
|
1664
1722
|
}
|
|
1665
1723
|
return stars;
|
package/dist/public/styles.css
CHANGED
|
@@ -547,28 +547,39 @@ body.is-panel-resizing { cursor: col-resize; user-select: none; }
|
|
|
547
547
|
.settings-parent-button { white-space: nowrap; }
|
|
548
548
|
.settings-hub-header { align-items: center; }
|
|
549
549
|
.settings-hub-header h1 { font-size: clamp(26px, 3vw, 36px); line-height: 1.15; letter-spacing: -.02em; }
|
|
550
|
-
.settings-hub-grid { display: grid; grid-template-columns: repeat(
|
|
550
|
+
.settings-hub-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px; width: 100%; max-width: 1000px; margin: 0 auto; }
|
|
551
551
|
.settings-hub-card { display: grid; grid-template-columns: 48px minmax(0, 1fr); grid-template-rows: auto auto; gap: 4px 14px; align-items: center; min-height: 128px; padding: 22px; border: 1px solid var(--line); border-radius: 7px; background: var(--surface-soft); text-align: left; }
|
|
552
552
|
.settings-hub-card:hover, .settings-hub-card:focus-visible { border-color: var(--accent); background: var(--paper-deep); box-shadow: var(--shadow); transform: translateY(-2px); }
|
|
553
553
|
.settings-hub-card:disabled { cursor: not-allowed; opacity: .42; box-shadow: none; transform: none; }
|
|
554
554
|
.settings-card-mark { display: grid; grid-row: 1 / 3; place-items: center; width: 48px; height: 48px; border: 1px solid var(--line); border-radius: 50%; color: var(--accent-dark); background: var(--surface); font-size: 14px; font-weight: 700; }
|
|
555
555
|
.settings-hub-card strong { align-self: end; font-size: 16px; }
|
|
556
556
|
.settings-hub-card small { align-self: start; color: var(--muted); font-size: 10px; line-height: 1.5; }
|
|
557
|
-
.writing-progress-dialog { width: min(900px, 94vw); }
|
|
557
|
+
.dialog.writing-progress-dialog { width: min(900px, 94vw); }
|
|
558
558
|
.writing-progress-dialog form { display: grid; }
|
|
559
|
-
.writing-progress-body { display: grid; gap: 18px; padding: 20px 24px; }
|
|
559
|
+
.writing-progress-body { display: grid; gap: 18px; max-height: calc(88vh - 150px); padding: 20px 24px; overflow-y: auto; }
|
|
560
560
|
.writing-progress-stats { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; }
|
|
561
|
-
.writing-progress-stats article { display: grid; gap:
|
|
561
|
+
.writing-progress-stats article { display: grid; align-content: center; gap: 7px; min-height: 78px; padding: 13px 15px; border: 1px solid var(--line); border-radius: 6px; background: var(--surface-soft); }
|
|
562
562
|
.writing-progress-stats small { color: var(--muted); font-size: 9px; }
|
|
563
|
-
.writing-progress-stats strong { font: 20px/1.2 var(--font-
|
|
564
|
-
.writing-trend-chart { display: grid; grid-template-columns: repeat(30, minmax(4px, 1fr)); align-items: end; gap: 3px; height:
|
|
565
|
-
.writing-trend-bar { display: grid; grid-template-rows: 1fr
|
|
566
|
-
.writing-trend-bar
|
|
563
|
+
.writing-progress-stats strong { font: 20px/1.2 var(--font-latin), monospace; }
|
|
564
|
+
.writing-trend-chart { position: relative; display: grid; grid-template-columns: repeat(30, minmax(4px, 1fr)); align-items: end; gap: 3px; height: 210px; margin-top: 14px; padding: 14px 10px 10px; border: 1px solid var(--line); border-radius: 6px; background: linear-gradient(to top, var(--panel) 0 22px, transparent 22px), linear-gradient(to top, var(--line) 1px, transparent 1px) 0 0 / 100% 25%; }
|
|
565
|
+
.writing-trend-bar { display: grid; grid-template-rows: minmax(0, 1fr) 22px; align-items: end; height: 100%; min-width: 0; cursor: default; }
|
|
566
|
+
.writing-trend-bar:focus-visible { outline: none; }
|
|
567
|
+
.writing-trend-bar i { display: block; justify-self: center; width: clamp(4px, 68%, 14px); height: var(--bar-height); min-height: 3px; border-radius: 4px 4px 1px 1px; background: color-mix(in srgb, var(--accent) 62%, var(--surface)); transition: background-color .14s ease, box-shadow .14s ease, transform .14s ease; }
|
|
567
568
|
.writing-trend-bar.is-today i { background: var(--accent); }
|
|
568
|
-
.writing-trend-bar
|
|
569
|
+
.writing-trend-bar:hover i, .writing-trend-bar:focus-visible i { background: var(--accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 20%, transparent); transform: translateY(-2px); }
|
|
570
|
+
.writing-trend-bar small { align-self: center; justify-self: center; overflow: visible; color: var(--muted); font: 7px/22px var(--font-latin), monospace; white-space: nowrap; }
|
|
571
|
+
.writing-trend-daily-line { position: absolute; z-index: 1; top: 14px; right: 10px; bottom: 32px; left: 10px; width: calc(100% - 20px); height: calc(100% - 46px); overflow: visible; pointer-events: none; }
|
|
572
|
+
.writing-trend-daily-line polyline { fill: none; stroke: var(--green); stroke-width: 3; vector-effect: non-scaling-stroke; }
|
|
573
|
+
.writing-trend-daily-points { position: absolute; z-index: 2; top: 14px; right: 10px; bottom: 32px; left: 10px; pointer-events: none; }
|
|
574
|
+
.writing-trend-daily-points i { position: absolute; top: var(--point-y); left: var(--point-x); box-sizing: border-box; width: 7px; height: 7px; border: 2px solid var(--green); border-radius: 50%; background: var(--panel); transform: translate(-50%, -50%); }
|
|
575
|
+
.writing-trend-legend { position: absolute; z-index: 2; top: -27px; right: 0; display: flex; align-items: center; gap: 5px; padding: 3px 0; color: var(--muted); font: 8px/1.4 var(--font-latin), monospace; pointer-events: none; }
|
|
576
|
+
.writing-trend-legend i { width: 10px; height: 5px; border-radius: 2px 2px 0 0; background: color-mix(in srgb, var(--accent) 62%, var(--surface)); }
|
|
577
|
+
.writing-trend-legend b { width: 12px; height: 0; margin-left: 3px; border-top: 2px solid var(--green); }
|
|
578
|
+
.writing-trend-tooltip { position: absolute; z-index: 3; width: 250px; max-width: calc(100% - 16px); padding: 7px 9px; border: 1px solid color-mix(in srgb, var(--ink) 24%, transparent); border-radius: 5px; background: color-mix(in srgb, var(--ink) 94%, transparent); box-shadow: 0 8px 22px rgba(0,0,0,.22); color: var(--panel); font: 10px/1.45 var(--font-latin), monospace; overflow-wrap: anywhere; text-align: center; pointer-events: none; transform: translate(-50%, -100%); }
|
|
579
|
+
.writing-trend-tooltip[hidden] { display: none; }
|
|
569
580
|
.writing-goal-fields { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; }
|
|
570
|
-
.writing-goal-fields label { display: grid; gap:
|
|
571
|
-
.writing-goal-fields input { width: 100%; }
|
|
581
|
+
.writing-goal-fields label { display: grid; gap: 7px; color: var(--muted); font-size: 10px; font-weight: 650; }
|
|
582
|
+
.writing-goal-fields input { width: 100%; min-height: 38px; padding: 8px 10px; background: var(--surface); font-size: 12px; font-weight: 500; }
|
|
572
583
|
@media (max-width: 680px) { .writing-progress-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); } .writing-goal-fields { grid-template-columns: 1fr; } }
|
|
573
584
|
.work-audit-dialog { width: min(860px, 94vw); }
|
|
574
585
|
.work-audit-list { display: grid; max-height: 62vh; overflow: auto; }
|
|
@@ -976,7 +987,7 @@ body.work-viewer-mode .character-editor-actions { display: none !important; }
|
|
|
976
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; }
|
|
977
988
|
.book-card { position: relative; min-width: 0; border: 0; background: transparent; text-align: left; }
|
|
978
989
|
.book-open { width: 100%; padding: 0; border: 0; background: transparent; text-align: left; }
|
|
979
|
-
.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; }
|
|
980
991
|
.book-cover::after { content: ""; position: absolute; inset: 0 auto 0 9px; width: 1px; background: rgba(255,255,255,.2); }
|
|
981
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); }
|
|
982
993
|
.book-cover-fallback { font-size: clamp(46px, 6vw, 74px); font-weight: 700; opacity: .75; }
|
|
@@ -1080,7 +1091,9 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
|
|
|
1080
1091
|
#module-more-button { grid-column: 1 / -1; color: var(--accent-dark); text-align: center; }
|
|
1081
1092
|
.panel-heading { display: flex; align-items: center; gap: 8px; padding: 15px 0 9px 7px; color: var(--muted); font-size: 11px; letter-spacing: .08em; text-transform: uppercase; }
|
|
1082
1093
|
.panel-heading-actions { display: flex; align-items: center; gap: 7px; margin-left: auto; }
|
|
1083
|
-
.chapter-batch-button {
|
|
1094
|
+
.chapter-batch-button { display: grid; flex: none; place-items: center; width: 24px; height: 24px; padding: 0; border: 1px solid var(--line); border-radius: 4px; background: transparent; color: var(--muted); }
|
|
1095
|
+
.chapter-batch-button svg { width: 14px; height: 14px; fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; }
|
|
1096
|
+
.chapter-batch-button:hover, .chapter-batch-button:focus-visible { border-color: var(--accent); color: var(--accent-dark); outline: none; }
|
|
1084
1097
|
.novel-tree { font-size: 13px; }
|
|
1085
1098
|
.empty-copy { color: var(--muted); padding: 14px 7px; line-height: 1.5; }
|
|
1086
1099
|
.volume-node { margin-bottom: 8px; }
|
|
@@ -1108,14 +1121,25 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
|
|
|
1108
1121
|
.chapter-batch-dialog form { display: grid; gap: 14px; }
|
|
1109
1122
|
.chapter-batch-toolbar { display: flex; align-items: center; gap: 8px; margin: 0 24px; }
|
|
1110
1123
|
.chapter-batch-toolbar output { margin-left: auto; color: var(--muted); font-size: 11px; }
|
|
1124
|
+
.chapter-batch-search { display: grid; gap: 6px; margin: 0 24px; }
|
|
1125
|
+
.chapter-batch-search > label { color: var(--muted); font-size: 10px; font-weight: 650; }
|
|
1126
|
+
.chapter-batch-search-field { position: relative; display: flex; align-items: center; }
|
|
1127
|
+
.chapter-batch-search-field svg { position: absolute; left: 11px; width: 15px; height: 15px; fill: none; stroke: var(--muted); stroke-width: 1.7; stroke-linecap: round; pointer-events: none; }
|
|
1128
|
+
.chapter-batch-search-field input { width: 100%; min-height: 38px; padding: 8px 68px 8px 34px; background: var(--surface); font-size: 12px; }
|
|
1129
|
+
.chapter-batch-search-field input::-webkit-search-cancel-button { cursor: pointer; }
|
|
1130
|
+
.chapter-batch-search-field output { position: absolute; right: 11px; color: var(--muted); font-size: 10px; font-variant-numeric: tabular-nums; pointer-events: none; }
|
|
1111
1131
|
.chapter-batch-list { display: grid; max-height: min(44vh, 420px); margin: 0 24px; overflow: auto; border: 1px solid var(--line); border-radius: 5px; }
|
|
1112
1132
|
.chapter-batch-item { display: grid; grid-template-columns: 18px minmax(0, 1fr); align-items: center; gap: 9px; padding: 9px 11px; border-bottom: 1px solid var(--line); }
|
|
1113
1133
|
.chapter-batch-item:last-child { border-bottom: 0; }
|
|
1114
1134
|
.chapter-batch-item span { display: grid; min-width: 0; gap: 2px; }
|
|
1115
1135
|
.chapter-batch-item strong { overflow: hidden; font-size: 12px; text-overflow: ellipsis; white-space: nowrap; }
|
|
1116
1136
|
.chapter-batch-item small { color: var(--muted); font-size: 9px; }
|
|
1117
|
-
.chapter-batch-controls { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap:
|
|
1118
|
-
.chapter-batch-controls label { display: grid; gap:
|
|
1137
|
+
.chapter-batch-controls { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; margin: 0 24px; padding: 14px; border: 1px solid var(--line); border-radius: 6px; background: var(--surface-soft); }
|
|
1138
|
+
.chapter-batch-controls label { display: grid; min-width: 0; gap: 7px; color: var(--muted); font-size: 11px; font-weight: 650; line-height: 1.35; }
|
|
1139
|
+
.chapter-batch-controls select { width: 100%; min-width: 0; min-height: 40px; appearance: none; -webkit-appearance: none; padding: 8px 40px 8px 12px; border-color: color-mix(in srgb, var(--line) 82%, var(--ink)); border-radius: 5px; background-color: var(--surface); background-image: linear-gradient(45deg, transparent 50%, var(--muted) 50%), linear-gradient(135deg, var(--muted) 50%, transparent 50%); background-position: right 20px center, right 15px center; background-repeat: no-repeat; background-size: 5px 5px, 5px 5px; color: var(--ink); font-size: 12px; font-weight: 500; line-height: 1.4; cursor: pointer; transition: border-color .15s ease, box-shadow .15s ease, background-color .15s ease; }
|
|
1140
|
+
.chapter-batch-controls select:hover:not(:disabled) { border-color: color-mix(in srgb, var(--accent) 58%, var(--line)); }
|
|
1141
|
+
.chapter-batch-controls select:focus-visible { border-color: var(--accent); outline: none; box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 18%, transparent); }
|
|
1142
|
+
.chapter-batch-controls select:disabled { cursor: not-allowed; opacity: .52; }
|
|
1119
1143
|
@media (max-width: 620px) { .chapter-batch-controls { grid-template-columns: 1fr; } }
|
|
1120
1144
|
|
|
1121
1145
|
.ghost-button, .primary-button { border-radius: 4px; padding: 8px 13px; font-size: 12px; }
|
|
@@ -2644,6 +2668,8 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
|
|
|
2644
2668
|
.entity-version-card > button:not(:disabled):hover { border-color: var(--accent); color: var(--accent-dark); }
|
|
2645
2669
|
.entity-version-card > button.is-confirming { border-color: var(--accent); background: var(--accent); color: #fff; }
|
|
2646
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; }
|
|
2647
2673
|
.entity-history-empty { margin: 0; padding: 28px 12px; color: var(--muted); font-size: 11px; text-align: center; }
|
|
2648
2674
|
.relationship-table .relationship-actions { white-space: nowrap; vertical-align: middle; }
|
|
2649
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; }
|
|
@@ -2684,7 +2710,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
|
|
|
2684
2710
|
.task-auto-run-progress { grid-column: 1; grid-row: auto; justify-self: stretch; width: 100%; }
|
|
2685
2711
|
.task-auto-run-progress-ring { display: none; }
|
|
2686
2712
|
.task-auto-run-progress-bar-layout { display: grid; gap: 6px; width: 100%; }
|
|
2687
|
-
.settings-hub-grid { grid-template-columns: 1fr; }
|
|
2713
|
+
.settings-hub-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
2688
2714
|
.usage-stat-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
2689
2715
|
.settings-layout-toolbar, .module-layout-toolbar { justify-content: stretch; }
|
|
2690
2716
|
.settings-layout-toggle, .module-layout-toggle { width: 100%; }
|
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();
|