dsh-maoying 0.1.11 → 0.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +169 -0
- package/README.md +17 -2
- package/bin/maoying.mjs +95 -50
- package/docs/creator-panel.md +33 -7
- package/docs/design.md +10 -7
- package/docs/spec.md +7 -4
- package/index.mjs +242 -88
- package/lib/base.css +156 -25
- package/lib/client.js +688 -232
- package/lib/epub.mjs +37 -6
- package/lib/novel-parse.mjs +171 -46
- package/lib/panel-core.mjs +116 -10
- package/lib/version.mjs +122 -36
- package/lib/yaml-loader.mjs +10 -2
- package/package.json +7 -2
- package/presets/maoying/agent.cordis.v2.yml +6 -1
- package/presets/maoying/agent.cordis.yml +5 -2
- package/presets/maoying/restrict.mjs +5 -6
- package/skills/maoying-review-checklist/SKILL.md +1 -1
- package/templates/novel/README.md +3 -3
- package/templates/novel/foreshadows.yml +1 -0
- package/templates/novel/timeline.yml +1 -1
package/lib/client.js
CHANGED
|
@@ -32,7 +32,7 @@ window.__ModuleLoader__.load({
|
|
|
32
32
|
|
|
33
33
|
var React = require("react");
|
|
34
34
|
var h = React.createElement;
|
|
35
|
-
var useState = React.useState, useEffect = React.useEffect, useCallback = React.useCallback, useRef = React.useRef;
|
|
35
|
+
var useState = React.useState, useEffect = React.useEffect, useCallback = React.useCallback, useRef = React.useRef, useMemo = React.useMemo;
|
|
36
36
|
// 复制一份导出面再补属性:不往共享模块对象上写东西(避免污染宿主里
|
|
37
37
|
// 其他 ui-primitives 消费者);后续片段看到的就是这份副本。
|
|
38
38
|
var P = Object.assign({}, require("@deepseek-ai/dsh-client-ui-primitives"));
|
|
@@ -55,19 +55,15 @@ window.__ModuleLoader__.load({
|
|
|
55
55
|
var API_TIMEOUT_MS = 60000;
|
|
56
56
|
|
|
57
57
|
var STATUS_FLOW = ["pending", "writing", "review", "accepted"];
|
|
58
|
-
var STATUS_LABEL = { pending: "待写", writing: "写作中", review: "审校中", accepted: "已定稿" };
|
|
59
58
|
var FS_ORDER = ["planted", "progressing", "resolved", "abandoned"];
|
|
60
59
|
var FS_FLOW = { planted: "progressing", progressing: "resolved" };
|
|
61
|
-
var FS_LABEL = { planted: "埋设", progressing: "推进中", resolved: "已回收", abandoned: "已弃用" };
|
|
62
|
-
var PRI_LABEL = { high: "高", normal: "中", low: "低" };
|
|
63
60
|
var PHASES = ["idea", "setting", "outline", "writing", "review", "consistency", "export"];
|
|
64
|
-
|
|
65
|
-
var
|
|
66
|
-
var CLUE_KIND_LABEL = { clue: "线索", red_herring: "红鲱鱼", inference: "推理", question: "疑问" };
|
|
61
|
+
// snake→camel 键表:clue.* 词典键用小驼峰,数据里的 kind 是 snake_case。
|
|
62
|
+
var CLUE_KIND_KEYS = { clue: "clue", red_herring: "redHerring", inference: "inference", question: "question" };
|
|
67
63
|
var TABS = [
|
|
68
|
-
["overview", "
|
|
69
|
-
["foreshadows", "
|
|
70
|
-
["world", "
|
|
64
|
+
["overview", "tabs.overview"], ["chapters", "tabs.chapters"], ["characters", "tabs.characters"],
|
|
65
|
+
["foreshadows", "tabs.foreshadows"], ["relations", "tabs.relations"], ["timeline", "tabs.timeline"],
|
|
66
|
+
["world", "tabs.world"], ["index", "tabs.index"], ["settings", "tabs.settings"],
|
|
71
67
|
];
|
|
72
68
|
|
|
73
69
|
// 角色徽章四色(role 是自由文本):按关键词归 tone,命中即返。
|
|
@@ -85,16 +81,16 @@ window.__ModuleLoader__.load({
|
|
|
85
81
|
}
|
|
86
82
|
function statusBadge(status) {
|
|
87
83
|
var s = STATUS_FLOW.indexOf(status) !== -1 ? status : "pending";
|
|
88
|
-
return h("span", { className: "myt-badge", "data-status": s },
|
|
84
|
+
return h("span", { className: "myt-badge", "data-status": s }, tt("status." + s) + (status && s !== status ? tf("status.rawSuffix", { raw: status }) : ""));
|
|
89
85
|
}
|
|
90
86
|
function phasePill(phase) {
|
|
91
87
|
var p = PHASES.indexOf(phase) !== -1 ? phase : null;
|
|
92
|
-
if (!p) return h("span", { className: "myt-phase", "data-phase": "idea", title: phase || "" }, phase ? String(phase) : "
|
|
93
|
-
return h("span", { className: "myt-phase", "data-phase": p },
|
|
88
|
+
if (!p) return h("span", { className: "myt-phase", "data-phase": "idea", title: phase || "" }, phase ? String(phase) : tt("phase.unset"));
|
|
89
|
+
return h("span", { className: "myt-phase", "data-phase": p }, tt("phase." + p));
|
|
94
90
|
}
|
|
95
91
|
function priDot(pri) {
|
|
96
92
|
var p = pri === "high" || pri === "low" ? pri : "normal";
|
|
97
|
-
return h("span", { className: "myt-priDot", "data-pri": p, title: "
|
|
93
|
+
return h("span", { className: "myt-priDot", "data-pri": p, title: tf("common.priorityTitle", { pri: tt("pri." + p) }), "aria-label": tf("common.priorityAria", { pri: tt("pri." + p) }) });
|
|
98
94
|
}
|
|
99
95
|
|
|
100
96
|
// ── 格式化小工具 ─────────────────────────────────────────────────────
|
|
@@ -114,7 +110,7 @@ window.__ModuleLoader__.load({
|
|
|
114
110
|
// 章节「卷-章」定位:volume 缺省只显示第 N 章。
|
|
115
111
|
function chWhere(ch) {
|
|
116
112
|
if (!ch) return "";
|
|
117
|
-
var num = typeof ch.num === "number" ? "
|
|
113
|
+
var num = typeof ch.num === "number" ? tf("common.chapterNum", { n: ch.num }) : "";
|
|
118
114
|
return ch.volume ? String(ch.volume) + " · " + num : num;
|
|
119
115
|
}
|
|
120
116
|
function chFullTitle(ch) {
|
|
@@ -178,11 +174,11 @@ window.__ModuleLoader__.load({
|
|
|
178
174
|
var d = null;
|
|
179
175
|
try { d = raw ? JSON.parse(raw) : null; } catch (err) { d = null; }
|
|
180
176
|
if (!r.ok) throw new Error((d && d.error) || ("HTTP " + r.status));
|
|
181
|
-
if (!d) throw new Error("
|
|
177
|
+
if (!d) throw new Error(tf("api.notJson", { code: r.status }));
|
|
182
178
|
return d;
|
|
183
179
|
});
|
|
184
180
|
}).catch(function (err) {
|
|
185
|
-
if (ctrl && err && err.name === "AbortError") throw new Error("
|
|
181
|
+
if (ctrl && err && err.name === "AbortError") throw new Error(tf("api.timeout", { s: API_TIMEOUT_MS / 1000 }));
|
|
186
182
|
throw err;
|
|
187
183
|
}).finally(function () {
|
|
188
184
|
if (timer !== null) clearTimeout(timer);
|
|
@@ -196,7 +192,7 @@ window.__ModuleLoader__.load({
|
|
|
196
192
|
qs = usp.toString();
|
|
197
193
|
}
|
|
198
194
|
return maoyingApi(path + (qs ? (path.indexOf("?") === -1 ? "?" : "&") + qs : "")).catch(function (err) {
|
|
199
|
-
toast.err("
|
|
195
|
+
toast.err(tf("toast.prefix", { msg: err.message }));
|
|
200
196
|
throw err;
|
|
201
197
|
});
|
|
202
198
|
}
|
|
@@ -205,17 +201,17 @@ window.__ModuleLoader__.load({
|
|
|
205
201
|
toast.ok(apiPostOkText(path, d));
|
|
206
202
|
return d;
|
|
207
203
|
}, function (err) {
|
|
208
|
-
toast.err("
|
|
204
|
+
toast.err(tf("toast.prefix", { msg: err.message }));
|
|
209
205
|
throw err;
|
|
210
206
|
});
|
|
211
207
|
}
|
|
212
208
|
// 写操作成功提示按路由分流(server 返回 chapters 计数时带上)。
|
|
213
209
|
function apiPostOkText(path, d) {
|
|
214
|
-
if (path === "rebuild-index") return "
|
|
215
|
-
if (path === "chapter-status") return "
|
|
216
|
-
if (path === "foreshadow-status") return "
|
|
217
|
-
if (path === "phase") return "
|
|
218
|
-
return "
|
|
210
|
+
if (path === "rebuild-index") return tf("toast.indexRebuilt", { n: fmtNum(d && d.chapters) });
|
|
211
|
+
if (path === "chapter-status") return tt("toast.chapterStatus");
|
|
212
|
+
if (path === "foreshadow-status") return tt("toast.foreshadowStatus");
|
|
213
|
+
if (path === "phase") return tf("toast.phaseAdvanced", { phase: d && d.phase && PHASES.indexOf(d.phase) !== -1 ? tt("phase." + d.phase) : (d && d.phase) || "—" });
|
|
214
|
+
return tt("toast.success");
|
|
219
215
|
}
|
|
220
216
|
|
|
221
217
|
// ── Toast 控制器(模块级单例;ok 4s / warn 6s 自动消失,err 手动关)────
|
|
@@ -267,7 +263,7 @@ window.__ModuleLoader__.load({
|
|
|
267
263
|
},
|
|
268
264
|
h("div", { className: "myt-toastText" }, it.text),
|
|
269
265
|
h("button", {
|
|
270
|
-
type: "button", className: "myt-toastClose", "aria-label": "
|
|
266
|
+
type: "button", className: "myt-toastClose", "aria-label": tt("common.closeTip"),
|
|
271
267
|
onClick: function () { toast.dismiss(it.id); },
|
|
272
268
|
}, h("span", { className: "myt-closeGlyph", "aria-hidden": "true" }, "×")));
|
|
273
269
|
}));
|
|
@@ -446,14 +442,14 @@ window.__ModuleLoader__.load({
|
|
|
446
442
|
|
|
447
443
|
// ── 引导卡(无 novel 工作区 / 未初始化):maoying init 提示 + 复制 ──────
|
|
448
444
|
function GuideCard(props) {
|
|
449
|
-
var title = props.title || "
|
|
450
|
-
var desc = props.desc || "
|
|
445
|
+
var title = props.title || tt("guide.defaultTitle");
|
|
446
|
+
var desc = props.desc || tt("guide.defaultDesc");
|
|
451
447
|
var _b = useState(false), busy = _b[0], setBusy = _b[1];
|
|
452
448
|
function doCopy() {
|
|
453
449
|
setBusy(true);
|
|
454
450
|
copyText("maoying init").then(function (ok) {
|
|
455
|
-
if (ok) toast.ok("
|
|
456
|
-
else toast.warn("
|
|
451
|
+
if (ok) toast.ok(tt("guide.copied"));
|
|
452
|
+
else toast.warn(tt("guide.copyFailed"));
|
|
457
453
|
}).finally(function () { setBusy(false); });
|
|
458
454
|
}
|
|
459
455
|
return h("div", { className: "myt-card myt-guide" },
|
|
@@ -461,7 +457,7 @@ window.__ModuleLoader__.load({
|
|
|
461
457
|
h("p", { className: "myt-guideText" }, desc),
|
|
462
458
|
h("div", { className: "myt-guideCode" },
|
|
463
459
|
h("code", { className: "myt-guideCodeText" }, "maoying init"),
|
|
464
|
-
h("button", { type: "button", className: "myt-btn myt-copyBtn", disabled: busy, onClick: doCopy }, busy ? "
|
|
460
|
+
h("button", { type: "button", className: "myt-btn myt-copyBtn", disabled: busy, onClick: doCopy }, busy ? tt("guide.copying") : tt("guide.copyCmd"))));
|
|
465
461
|
}
|
|
466
462
|
|
|
467
463
|
// ── 骨架 / 错误 / 空态小件 ──────────────────────────────────────────────
|
|
@@ -474,9 +470,41 @@ window.__ModuleLoader__.load({
|
|
|
474
470
|
return h("div", { className: "myt-strip myt-stripError" },
|
|
475
471
|
h("span", { className: "myt-stripIcon" }, ic("IconWarningOutlineRegular", 14)),
|
|
476
472
|
h("span", { className: "myt-stripText" }, props.text),
|
|
477
|
-
props.onRetry ? h("button", { type: "button", className: "myt-btn", style: { marginLeft: "auto" }, onClick: props.onRetry }, "
|
|
473
|
+
props.onRetry ? h("button", { type: "button", className: "myt-btn", style: { marginLeft: "auto" }, onClick: props.onRetry }, tt("common.retry")) : null);
|
|
474
|
+
}
|
|
475
|
+
function Empty(props) { return h("div", { className: "myt-empty" }, props.text || tt("common.empty")); }
|
|
476
|
+
|
|
477
|
+
// tab 内容错误边界:任何 tab / 阅读页渲染抛错都降级为卡内错误态(错误摘要
|
|
478
|
+
// + 重试按钮),不再把整个面板炸成白屏。错误边界只有 class 组件能做
|
|
479
|
+
// (getDerivedStateFromError / componentDidCatch),此处破例用 class。
|
|
480
|
+
class TabErrorBoundary extends React.Component {
|
|
481
|
+
constructor(props) {
|
|
482
|
+
super(props);
|
|
483
|
+
this.state = { err: null };
|
|
484
|
+
this.retry = this.retry.bind(this);
|
|
485
|
+
}
|
|
486
|
+
static getDerivedStateFromError(err) {
|
|
487
|
+
return { err: err }; // 渲染抛错 → 下一次 render 走降级分支
|
|
488
|
+
}
|
|
489
|
+
componentDidCatch(err) {
|
|
490
|
+
// 只记控制台:卡内已有降级 UI,toast 通道留给请求错误,不重复打扰。
|
|
491
|
+
if (typeof console !== "undefined" && console.warn) console.warn("maoying: 分区渲染出错:", err);
|
|
492
|
+
}
|
|
493
|
+
retry() {
|
|
494
|
+
this.setState({ err: null });
|
|
495
|
+
if (typeof this.props.onRetry === "function") this.props.onRetry(); // 顺手重拉快照,坏数据一并刷掉
|
|
496
|
+
}
|
|
497
|
+
render() {
|
|
498
|
+
var err = this.state.err;
|
|
499
|
+
if (!err) return this.props.children;
|
|
500
|
+
var msg = String((err && err.message) || err || tt("common.unknownErr"));
|
|
501
|
+
return h("div", { className: "myt-card" },
|
|
502
|
+
h("div", { className: "myt-cardHead" }, h("h3", { className: "myt-cardTitle" }, tt("errBoundary.title"))),
|
|
503
|
+
h("div", { className: "myt-muted", style: { overflowWrap: "anywhere" } }, tf("errBoundary.summary", { msg: truncate(msg, 300) })),
|
|
504
|
+
h("div", { className: "myt-heroMeta" },
|
|
505
|
+
h("button", { type: "button", className: "myt-btn", onClick: this.retry }, tt("common.retry"))));
|
|
506
|
+
}
|
|
478
507
|
}
|
|
479
|
-
function Empty(props) { return h("div", { className: "myt-empty" }, props.text || "暂无数据"); }
|
|
480
508
|
|
|
481
509
|
// ── 页面骨架:顶栏(工作区下拉 + 刷新 + 关闭)+ tab 条 + 内容区 ─────────
|
|
482
510
|
// 数据流:workspaces 一次拉取;state 跟随选中工作区;写操作 POST 成功后
|
|
@@ -490,7 +518,28 @@ window.__ModuleLoader__.load({
|
|
|
490
518
|
var _ld = useState(true), loading = _ld[0], setLoading = _ld[1];
|
|
491
519
|
var _se = useState(""), stateErr = _se[0], setStateErr = _se[1];
|
|
492
520
|
var _tb = useState("overview"), tab = _tb[0], setTab = _tb[1];
|
|
493
|
-
|
|
521
|
+
// 章节阅读页(整页视图,替代原右侧抽屉):null = 列表态。
|
|
522
|
+
var _rd = useState(null), readerFile = _rd[0], setReaderFile = _rd[1];
|
|
523
|
+
// 系统设置:字号档位持久化 + 面板整体缩放(portal 节点读同一 CSS 变量)。
|
|
524
|
+
var _sm = useState(false), showSettings = _sm[0], setShowSettings = _sm[1];
|
|
525
|
+
var _fs = useState(function () {
|
|
526
|
+
var stored = lsGet(FONT_SCALE_KEY) || "1";
|
|
527
|
+
return FONT_SCALES.some(function (o) { return o[0] === stored; }) ? stored : "1"; // 旧档位(1.8/2)回落 100%
|
|
528
|
+
}), fontScale = _fs[0], setFontScale = _fs[1];
|
|
529
|
+
var _lg = useState(function () {
|
|
530
|
+
var stored = lsGet(LANG_KEY) || "zh";
|
|
531
|
+
return UI_LANGS.some(function (o) { return o[0] === stored; }) ? stored : "zh";
|
|
532
|
+
}), lang = _lg[0], setLang = _lg[1];
|
|
533
|
+
useEffect(function () {
|
|
534
|
+
UI_LANG = lang;
|
|
535
|
+
lsSet(LANG_KEY, lang);
|
|
536
|
+
}, [lang]);
|
|
537
|
+
useEffect(function () {
|
|
538
|
+
if (typeof document !== "undefined" && document.documentElement) {
|
|
539
|
+
document.documentElement.style.setProperty("--myt-zoom", fontScale);
|
|
540
|
+
}
|
|
541
|
+
lsSet(FONT_SCALE_KEY, fontScale);
|
|
542
|
+
}, [fontScale]);
|
|
494
543
|
|
|
495
544
|
// 选中校正:记忆的工作区必须仍在列表且 hasNovel,否则落回首个 hasNovel。
|
|
496
545
|
var effectiveWs = (function () {
|
|
@@ -523,16 +572,22 @@ window.__ModuleLoader__.load({
|
|
|
523
572
|
setWorkspaces([]);
|
|
524
573
|
});
|
|
525
574
|
}, []);
|
|
575
|
+
var stateSeqRef = useRef(0); // state 请求序号:快速切工作区时丢弃过期响应
|
|
526
576
|
var loadState = useCallback(function (root) {
|
|
577
|
+
var seq = ++stateSeqRef.current; // 先自增再发请求;早退分支也作废在途旧响应
|
|
527
578
|
if (!root) { setSnap(null); setStateErr(""); setLoading(false); return; }
|
|
528
579
|
setLoading(true);
|
|
529
580
|
setStateErr("");
|
|
530
581
|
return apiGet("state", { path: root }).then(function (d) {
|
|
582
|
+
if (seq !== stateSeqRef.current) return; // 过期响应:已有更新的请求在途,丢弃
|
|
531
583
|
setSnap(d && d.ok ? d : null);
|
|
532
584
|
}).catch(function (err) {
|
|
585
|
+
if (seq !== stateSeqRef.current) return;
|
|
533
586
|
setSnap(null);
|
|
534
587
|
setStateErr(err.message);
|
|
535
|
-
}).finally(function () {
|
|
588
|
+
}).finally(function () {
|
|
589
|
+
if (seq === stateSeqRef.current) setLoading(false); // 过期收尾不碰加载态
|
|
590
|
+
});
|
|
536
591
|
}, []);
|
|
537
592
|
var reload = useCallback(function () {
|
|
538
593
|
return effectiveWs ? loadState(effectiveWs) : Promise.resolve();
|
|
@@ -543,7 +598,7 @@ window.__ModuleLoader__.load({
|
|
|
543
598
|
var _ver = useState(null), ver = _ver[0], setVer = _ver[1];
|
|
544
599
|
useEffect(function () {
|
|
545
600
|
apiGet("update-check").then(function (d) {
|
|
546
|
-
if (d && d.
|
|
601
|
+
if (d && d.current) setVer(d); // 常态也显示当前版本号;有新版再亮徽章
|
|
547
602
|
}).catch(function () {});
|
|
548
603
|
}, []);
|
|
549
604
|
useEffect(function () { loadWorkspaces(); }, [loadWorkspaces]);
|
|
@@ -560,7 +615,7 @@ window.__ModuleLoader__.load({
|
|
|
560
615
|
}
|
|
561
616
|
function gotoTab(next, chapterFile) {
|
|
562
617
|
setTab(next);
|
|
563
|
-
|
|
618
|
+
setReaderFile(next === "chapters" && chapterFile ? chapterFile : null);
|
|
564
619
|
}
|
|
565
620
|
|
|
566
621
|
var novelWorkspaces = (workspaces || []).filter(function (w) { return w.hasNovel; });
|
|
@@ -570,29 +625,31 @@ window.__ModuleLoader__.load({
|
|
|
570
625
|
} else if (!effectiveWs) {
|
|
571
626
|
// 全无 novel 工作区:引导卡(maoying init + 复制)。
|
|
572
627
|
body = h("div", { className: "myt-stack" },
|
|
573
|
-
wsErr ? ErrorStrip({ text: "
|
|
628
|
+
wsErr ? ErrorStrip({ text: tf("shell.wsListFailed", { msg: wsErr }), onRetry: loadWorkspaces }) : null,
|
|
574
629
|
h(GuideCard, {
|
|
575
|
-
title: "
|
|
576
|
-
desc: "
|
|
630
|
+
title: tt("guide.noWsTitle"),
|
|
631
|
+
desc: tt("guide.noWsDesc"),
|
|
577
632
|
}));
|
|
578
633
|
} else if (loading && !snap) {
|
|
579
634
|
body = h(SkelPage);
|
|
580
635
|
} else if (stateErr) {
|
|
581
|
-
body = h(ErrorStrip, { text: "
|
|
636
|
+
body = h(ErrorStrip, { text: tf("shell.stateFailed", { msg: stateErr }), onRetry: reload });
|
|
582
637
|
} else if (snap && snap.found === false) {
|
|
583
638
|
body = h(GuideCard, {
|
|
584
|
-
title: "
|
|
585
|
-
desc: "
|
|
639
|
+
title: tt("guide.notInitTitle"),
|
|
640
|
+
desc: tt("guide.notInitDesc"),
|
|
586
641
|
});
|
|
587
642
|
} else if (snap) {
|
|
588
643
|
var tabProps = { snap: snap, reload: reload, wsPath: effectiveWs };
|
|
589
|
-
if (
|
|
644
|
+
if (readerFile) body = h(ChapterReader, { file: readerFile, snap: snap, reload: reload, wsPath: effectiveWs, onBack: function () { setReaderFile(null); } });
|
|
645
|
+
else if (tab === "chapters") body = h(ChaptersTab, Object.assign({ onOpenChapter: function (f) { setReaderFile(f); } }, tabProps));
|
|
590
646
|
else if (tab === "characters") body = h(CharactersTab, tabProps);
|
|
591
647
|
else if (tab === "foreshadows") body = h(ForeshadowsTab, tabProps);
|
|
592
648
|
else if (tab === "relations") body = h(RelationsTab, tabProps);
|
|
593
649
|
else if (tab === "timeline") body = h(TimelineTab, tabProps);
|
|
594
650
|
else if (tab === "world") body = h(WorldTab, tabProps);
|
|
595
651
|
else if (tab === "index") body = h(IndexTab, tabProps);
|
|
652
|
+
else if (tab === "settings") body = h(SettingsTab, tabProps);
|
|
596
653
|
else body = h(OverviewTab, Object.assign({ gotoTab: gotoTab }, tabProps));
|
|
597
654
|
}
|
|
598
655
|
|
|
@@ -600,33 +657,37 @@ window.__ModuleLoader__.load({
|
|
|
600
657
|
h("header", { className: "myt-header" },
|
|
601
658
|
h("div", null,
|
|
602
659
|
h("h1", { className: "myt-title" }, "毛颖创作台",
|
|
660
|
+
ver && ver.current ? h("span", { className: "myt-verTag", title: tt("header.curVerTitle") }, ver.current) : null,
|
|
603
661
|
ver && ver.updateAvailable
|
|
604
|
-
? h("a", { className: "myt-verBadge", href: ver.releaseUrl || "#", target: "_blank", rel: "noreferrer", title: "
|
|
605
|
-
"
|
|
662
|
+
? h("a", { className: "myt-verBadge", href: ver.releaseUrl || "#", target: "_blank", rel: "noreferrer", title: tf("header.upgradeTip", { cur: ver.current }) },
|
|
663
|
+
tf("header.newVersion", { ver: ver.latest }))
|
|
606
664
|
: null),
|
|
607
|
-
h("p", { className: "myt-subtitle" }, "
|
|
665
|
+
h("p", { className: "myt-subtitle" }, tt("header.subtitle"))),
|
|
608
666
|
h("div", { className: "myt-headActions" },
|
|
609
667
|
workspaces && novelWorkspaces.length
|
|
610
|
-
? [h("span", { className: "myt-wsLabel", key: "l" }, "
|
|
611
|
-
h("select", { className: "myt-wsSelect", key: "s", value: effectiveWs, onChange: chooseWs, "aria-label": "
|
|
668
|
+
? [h("span", { className: "myt-wsLabel", key: "l" }, tt("header.workspace")),
|
|
669
|
+
h("select", { className: "myt-wsSelect", key: "s", value: effectiveWs, onChange: chooseWs, "aria-label": tt("header.wsAria") },
|
|
612
670
|
workspaces.map(function (w, i) {
|
|
613
671
|
return h("option", { key: w.path || i, value: w.path || "", disabled: !w.hasNovel },
|
|
614
|
-
(w.hasNovel ? "" : "
|
|
672
|
+
(w.hasNovel ? "" : tt("header.noNovel")) + (w.name || w.path || tt("header.untitled")));
|
|
615
673
|
}))]
|
|
616
674
|
: null,
|
|
617
|
-
h("button", { type: "button", className: "myt-btn myt-btnIcon", title: "
|
|
618
|
-
h("button", { type: "button", className: "myt-
|
|
619
|
-
|
|
675
|
+
h("button", { type: "button", className: "myt-btn myt-btnIcon", title: tt("header.settings"), "aria-label": tt("header.settings"), onClick: function () { setShowSettings(true); } }, ic("IconSettingsOutlineRegular", 14)),
|
|
676
|
+
h("button", { type: "button", className: "myt-btn myt-btnIcon", title: tt("header.refresh"), "aria-label": tt("header.refresh"), disabled: loading, onClick: reload }, ic("IconRefreshOutlineRegular", 14)),
|
|
677
|
+
h("button", { type: "button", className: "myt-closeBtn", "aria-label": tt("header.closeAria"), title: tt("header.closeTitle"), onClick: onClose }, h("span", { className: "myt-closeGlyph", "aria-hidden": "true" }, "×")))),
|
|
678
|
+
readerFile ? null : h("nav", { className: "myt-tabs", "aria-label": tt("tabs.aria") },
|
|
620
679
|
TABS.map(function (t) {
|
|
621
680
|
return h("button", {
|
|
622
681
|
key: t[0], type: "button", className: "myt-tab", "data-active": tab === t[0] ? "true" : undefined,
|
|
623
682
|
onClick: function () { gotoTab(t[0]); },
|
|
624
|
-
}, t[1]);
|
|
683
|
+
}, tt(t[1]));
|
|
625
684
|
})),
|
|
626
685
|
h("div", { className: "myt-body" },
|
|
627
686
|
h("div", { className: "myt-page" },
|
|
628
687
|
snap && Array.isArray(snap.warnings) && snap.warnings.length ? WarningsStrip(snap.warnings) : null,
|
|
629
|
-
|
|
688
|
+
// 错误边界包住 tab 内容:key 随 tab / 阅读页变化重挂,切分区自动清错误态。
|
|
689
|
+
h(TabErrorBoundary, { key: readerFile ? "reader:" + readerFile : tab, onRetry: reload }, body))),
|
|
690
|
+
showSettings ? h(SettingsModal, { scale: fontScale, onPick: setFontScale, lang: lang, onPickLang: setLang, onClose: function () { setShowSettings(false); } }) : null,
|
|
630
691
|
h(ToastHost));
|
|
631
692
|
}
|
|
632
693
|
|
|
@@ -635,7 +696,7 @@ window.__ModuleLoader__.load({
|
|
|
635
696
|
return h("div", { className: "myt-strip myt-stripWarn" },
|
|
636
697
|
h("span", { className: "myt-stripIcon" }, ic("IconWarningOutlineRegular", 14)),
|
|
637
698
|
h("span", { className: "myt-stripText" },
|
|
638
|
-
h("div", null, h("strong", null, "
|
|
699
|
+
h("div", null, h("strong", null, tt("warnings.title"))),
|
|
639
700
|
warnings.map(function (w, i) { return h("div", { key: i }, "· " + String(w)); })));
|
|
640
701
|
}
|
|
641
702
|
|
|
@@ -659,50 +720,507 @@ window.__ModuleLoader__.load({
|
|
|
659
720
|
return h("div", { className: "myt-stack" },
|
|
660
721
|
h("div", { className: "myt-card myt-hero" },
|
|
661
722
|
h("div", { className: "myt-heroMeta" },
|
|
662
|
-
project.author ? h("span", { className: "myt-muted" }, "
|
|
723
|
+
project.author ? h("span", { className: "myt-muted" }, tf("overview.author", { name: project.author })) : null,
|
|
663
724
|
project.genre ? h("span", { className: "myt-badge" }, project.genre) : null,
|
|
664
725
|
project.length ? h("span", { className: "myt-badge" }, project.length) : null,
|
|
665
726
|
project.language ? h("span", { className: "myt-badge" }, project.language) : null),
|
|
666
727
|
h("h2", { className: "myt-heroTitle" }, orDash(project.title)),
|
|
667
728
|
h("div", { className: "myt-heroMeta" },
|
|
668
729
|
phasePill(project.phase),
|
|
669
|
-
nextPhase ? h("button", { type: "button", className: "myt-btn", disabled: phaseBusy, onClick: advancePhase, title: "
|
|
730
|
+
nextPhase ? h("button", { type: "button", className: "myt-btn", disabled: phaseBusy, onClick: advancePhase, title: tf("phase.advanceTitle", { next: tt("phase." + nextPhase) }) }, phaseBusy ? tt("phase.advancing") : tt("phase.advance")) : h("span", { className: "myt-muted" }, project.phase ? tt("phase.final") : null)),
|
|
670
731
|
project.description ? h("p", { className: "myt-heroDesc" }, project.description) : null),
|
|
671
732
|
h("div", { className: "myt-statRow" },
|
|
672
|
-
h("div", { className: "myt-card myt-stat" }, h("span", { className: "myt-statNum" }, fmtNum(totals.words)), h("span", { className: "myt-statLabel" }, "
|
|
673
|
-
h("div", { className: "myt-card myt-stat" }, h("span", { className: "myt-statNum" }, fmtNum(totals.chapters)), h("span", { className: "myt-statLabel" }, "
|
|
733
|
+
h("div", { className: "myt-card myt-stat" }, h("span", { className: "myt-statNum" }, fmtNum(totals.words)), h("span", { className: "myt-statLabel" }, tt("overview.totalWords"))),
|
|
734
|
+
h("div", { className: "myt-card myt-stat" }, h("span", { className: "myt-statNum" }, fmtNum(totals.chapters)), h("span", { className: "myt-statLabel" }, tt("overview.totalChapters"))),
|
|
674
735
|
h("div", { className: "myt-card myt-stat" },
|
|
675
736
|
h("span", { className: "myt-statNum" }, fmtNum((snap.characters || []).length)),
|
|
676
|
-
h("span", { className: "myt-statLabel" }, "
|
|
737
|
+
h("span", { className: "myt-statLabel" }, tt("overview.characters"))),
|
|
677
738
|
h("div", { className: "myt-card myt-stat" },
|
|
678
739
|
h("span", { className: "myt-statNum" }, fmtNum((snap.foreshadows || []).filter(function (f) { return f.status === "planted" || f.status === "progressing"; }).length)),
|
|
679
|
-
h("span", { className: "myt-statLabel" }, "
|
|
740
|
+
h("span", { className: "myt-statLabel" }, tt("overview.openForeshadows")))),
|
|
680
741
|
h("div", { className: "myt-card" },
|
|
681
|
-
h("div", { className: "myt-cardHead" }, h("h3", { className: "myt-cardTitle" }, "
|
|
742
|
+
h("div", { className: "myt-cardHead" }, h("h3", { className: "myt-cardTitle" }, tt("overview.statusDist"))),
|
|
682
743
|
h("div", { className: "myt-distBar", "aria-hidden": "true" }, STATUS_FLOW.map(function (s) {
|
|
683
744
|
var n = byStatus[s] || 0;
|
|
684
745
|
return n > 0 ? h("div", { key: s, className: "myt-distSeg", "data-status": s, style: { flexGrow: n } }) : null;
|
|
685
746
|
})),
|
|
686
747
|
h("div", { className: "myt-heroMeta" }, STATUS_FLOW.map(function (s) {
|
|
687
|
-
return h("span", { key: s, className: "myt-muted" },
|
|
748
|
+
return h("span", { key: s, className: "myt-muted" }, tt("status." + s) + " " + (byStatus[s] || 0));
|
|
688
749
|
}))),
|
|
689
750
|
h("div", { className: "myt-card" },
|
|
690
|
-
h("div", { className: "myt-cardHead" }, h("h3", { className: "myt-cardTitle" }, "
|
|
751
|
+
h("div", { className: "myt-cardHead" }, h("h3", { className: "myt-cardTitle" }, tt("overview.recent"))),
|
|
691
752
|
recent.length
|
|
692
753
|
? h("div", { className: "myt-recentList" }, recent.map(function (ch) {
|
|
693
754
|
return h("button", { key: ch.file, type: "button", className: "myt-recentRow", onClick: function () { gotoTab("chapters", ch.file); } },
|
|
694
755
|
statusBadge(ch.status),
|
|
695
756
|
h("span", { className: "myt-chTitle" }, orDash(ch.title)),
|
|
696
757
|
h("span", { className: "myt-chPath" }, chWhere(ch)),
|
|
697
|
-
h("span", { className: "myt-wordsCell" }, fmtNum(ch.words)
|
|
758
|
+
h("span", { className: "myt-wordsCell" }, tf("common.charCount", { n: fmtNum(ch.words) })));
|
|
698
759
|
}))
|
|
699
|
-
: h(Empty, { text: "chapters
|
|
760
|
+
: h(Empty, { text: tt("chapters.empty") })));
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// ── 轻量 Markdown 渲染(零依赖):先整体 HTML 转义再施加 md 变换,杜绝
|
|
764
|
+
// 注入;支持标题/加粗/斜体/删除线/行内码/围栏码/引用/有序无序列表/分隔线/
|
|
765
|
+
// 链接/段落换行。供章纲、世界观条目等 md 阅读位使用,正文手稿仍走 pre。 ──
|
|
766
|
+
function escapeHtml(s) {
|
|
767
|
+
return String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
768
|
+
}
|
|
769
|
+
// 链接 scheme 白名单:仅放行 http/https 绝对地址与无 scheme 的相对路径 /
|
|
770
|
+
// 锚点 / //协议相对;javascript:/data:/vbscript: 等一律拒绝(调用方落纯文本)。
|
|
771
|
+
function isSafeLinkUrl(url) {
|
|
772
|
+
var m = String(url || "").match(/^([a-zA-Z][a-zA-Z0-9+.\-]*):/);
|
|
773
|
+
return !m || /^https?$/i.test(m[1]);
|
|
774
|
+
}
|
|
775
|
+
function inlineMd(s) {
|
|
776
|
+
// 行内码占位保护:先抽出 `code` 区间换成哨兵占位符,加粗/斜体/删除线/
|
|
777
|
+
// 链接不再命中码内文本(否则 `*x*` 会被二次加格式),最后回填。
|
|
778
|
+
// 哨兵用 NUL 控制符 + 固定前缀:正文已过 escapeHtml,不可能出现该序列。
|
|
779
|
+
var codes = [];
|
|
780
|
+
var out = String(s).replace(/`([^`]+)`/g, function (_m, code) {
|
|
781
|
+
codes.push("<code>" + code + "</code>");
|
|
782
|
+
return "\u0000MYTCODE" + (codes.length - 1) + "\u0000";
|
|
783
|
+
})
|
|
784
|
+
.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
|
|
785
|
+
.replace(/__([^_]+)__/g, "<strong>$1</strong>")
|
|
786
|
+
.replace(/\*([^*\n]+)\*/g, "<em>$1</em>")
|
|
787
|
+
.replace(/~~([^~]+)~~/g, "<del>$1</del>")
|
|
788
|
+
.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, function (_m, txt, url) {
|
|
789
|
+
if (!isSafeLinkUrl(url)) return _m; // 白名单外:原样落纯文本,不生成 href
|
|
790
|
+
return '<a href="' + url + '" target="_blank" rel="noreferrer">' + txt + "</a>";
|
|
791
|
+
});
|
|
792
|
+
return out.replace(/\u0000MYTCODE(\d+)\u0000/g, function (_m, i) { return codes[Number(i)] || ""; });
|
|
793
|
+
}
|
|
794
|
+
function renderMd(src) {
|
|
795
|
+
var lines = escapeHtml(src).split(/\r?\n/);
|
|
796
|
+
var html = [];
|
|
797
|
+
var i = 0;
|
|
798
|
+
while (i < lines.length) {
|
|
799
|
+
var line = lines[i];
|
|
800
|
+
if (/^```/.test(line)) {
|
|
801
|
+
var code = [];
|
|
802
|
+
i++;
|
|
803
|
+
while (i < lines.length && !/^```/.test(lines[i])) { code.push(lines[i]); i++; }
|
|
804
|
+
i++; // 吃掉收尾 ```(缺失也吞到尾)
|
|
805
|
+
html.push('<pre class="myt-mdCode"><code>' + code.join("\n") + "</code></pre>");
|
|
806
|
+
continue;
|
|
807
|
+
}
|
|
808
|
+
if (/^\s*$/.test(line)) { i++; continue; }
|
|
809
|
+
var h2 = line.match(/^(#{1,6})\s+(.*)$/);
|
|
810
|
+
if (h2) {
|
|
811
|
+
var lv = h2[1].length;
|
|
812
|
+
html.push('<h' + lv + ' class="myt-mdH">' + inlineMd(h2[2]) + "</h" + lv + ">");
|
|
813
|
+
i++; continue;
|
|
814
|
+
}
|
|
815
|
+
if (/^ {0,3}(?:-{3,}|\*{3,}|_{3,})\s*$/.test(line)) { html.push('<hr class="myt-mdHr">'); i++; continue; }
|
|
816
|
+
if (/^\s*>\s?/.test(line)) {
|
|
817
|
+
var quote = [];
|
|
818
|
+
while (i < lines.length && /^\s*>\s?/.test(lines[i])) { quote.push(inlineMd(lines[i].replace(/^\s*>\s?/, ""))); i++; }
|
|
819
|
+
html.push('<blockquote class="myt-mdQuote">' + quote.join("<br>") + "</blockquote>");
|
|
820
|
+
continue;
|
|
821
|
+
}
|
|
822
|
+
var ul = line.match(/^\s*[-*+]\s+(.*)$/);
|
|
823
|
+
if (ul) {
|
|
824
|
+
var items = [];
|
|
825
|
+
while (i < lines.length) {
|
|
826
|
+
var m1 = lines[i].match(/^\s*[-*+]\s+(.*)$/);
|
|
827
|
+
if (!m1) break;
|
|
828
|
+
items.push("<li>" + inlineMd(m1[1]) + "</li>");
|
|
829
|
+
i++;
|
|
830
|
+
}
|
|
831
|
+
html.push('<ul class="myt-mdList">' + items.join("") + "</ul>");
|
|
832
|
+
continue;
|
|
833
|
+
}
|
|
834
|
+
var ol = line.match(/^\s*\d+[.)]\s+(.*)$/);
|
|
835
|
+
if (ol) {
|
|
836
|
+
var oitems = [];
|
|
837
|
+
while (i < lines.length) {
|
|
838
|
+
var m2 = lines[i].match(/^\s*\d+[.)]\s+(.*)$/);
|
|
839
|
+
if (!m2) break;
|
|
840
|
+
oitems.push("<li>" + inlineMd(m2[1]) + "</li>");
|
|
841
|
+
i++;
|
|
842
|
+
}
|
|
843
|
+
html.push('<ol class="myt-mdList">' + oitems.join("") + "</ol>");
|
|
844
|
+
continue;
|
|
845
|
+
}
|
|
846
|
+
var para = [line];
|
|
847
|
+
i++;
|
|
848
|
+
while (i < lines.length && !/^\s*$/.test(lines[i]) && !/^```|^\s*>|^\s*[-*+]\s|^\s*\d+[.)]\s|^#{1,6}\s/.test(lines[i])) {
|
|
849
|
+
para.push(lines[i]); i++;
|
|
850
|
+
}
|
|
851
|
+
html.push('<p class="myt-mdP">' + inlineMd(para.join("<br>")) + "</p>");
|
|
852
|
+
}
|
|
853
|
+
// 块间直接相连不补 "\n":落点容器(.myt-outlineBody / .myt-worldDesc)是
|
|
854
|
+
// white-space:pre-wrap,块间再塞换行会渲染成额外空行 → 双倍间距;
|
|
855
|
+
// 块间距已由 .myt-md* 各自 margin 承担。
|
|
856
|
+
return html.join("");
|
|
857
|
+
}
|
|
858
|
+
function MdView(props) {
|
|
859
|
+
return h("div", {
|
|
860
|
+
className: "myt-md" + (props.className ? " " + props.className : ""),
|
|
861
|
+
dangerouslySetInnerHTML: { __html: renderMd(props.text || "") },
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// ── 系统设置:字号五档(面板整体 zoom,抽屉/弹层 portal 节点同缩放)──────
|
|
866
|
+
var FONT_SCALE_KEY = "maoying.fontScale";
|
|
867
|
+
var FONT_SCALES = [["1", "100%"], ["1.1", "110%"], ["1.2", "120%"], ["1.3", "130%"], ["1.4", "140%"], ["1.5", "150%"]];
|
|
868
|
+
// ── 界面语言(系统设置可选中文/English):UI_LANG 为模块级当前语言,
|
|
869
|
+
// PageShell 在 lang state 变化时改写它并整树重渲。tt/tf 是唯一取词口:
|
|
870
|
+
// zh 词典的值必须与历史硬编码文案逐字一致(中文界面零回归),en 词典
|
|
871
|
+
// 不留中文;词典缺键回落 key 本身(便于渐进迁移,一眼看出漏网串)。 ──
|
|
872
|
+
var LANG_KEY = "maoying.lang";
|
|
873
|
+
var UI_LANGS = [["zh", "中文"], ["en", "English"]];
|
|
874
|
+
var UI_LANG = "zh";
|
|
875
|
+
var I18N = {
|
|
876
|
+
zh: {
|
|
877
|
+
"shell.title": "毛颖创作台", "header.subtitle": "网文项目可视化与状态流转 · 只读正文,写状态",
|
|
878
|
+
"header.curVerTitle": "当前版本", "header.upgradeTip": "当前 {cur},点击打开新版发布页", "header.newVersion": "有新版 {ver} ↗",
|
|
879
|
+
"header.workspace": "工作区", "header.wsAria": "选择工作区", "header.noNovel": "(无 novel)", "header.untitled": "未命名",
|
|
880
|
+
"header.settings": "系统设置", "header.refresh": "重新拉取", "header.closeAria": "关闭创作台", "header.closeTitle": "关闭",
|
|
881
|
+
"tabs.aria": "创作台分区",
|
|
882
|
+
"tabs.overview": "概览", "tabs.chapters": "章节", "tabs.characters": "角色", "tabs.foreshadows": "伏笔", "tabs.relations": "关系图",
|
|
883
|
+
"tabs.timeline": "时间线", "tabs.world": "设定", "tabs.index": "索引板", "tabs.settings": "小说设置",
|
|
884
|
+
"shell.wsListFailed": "工作区列表拉取失败:{msg}", "shell.stateFailed": "快照拉取失败:{msg}",
|
|
885
|
+
"guide.noWsTitle": "还没有可用的小说工作区", "guide.noWsDesc": "任一工作区根目录下出现 novel/project.yml 即自动入选;先用命令初始化目录骨架:",
|
|
886
|
+
"guide.notInitTitle": "该工作区尚未初始化小说项目", "guide.notInitDesc": "工作区根目录下没有 novel/project.yml。在该工作区根目录执行:",
|
|
887
|
+
"guide.defaultTitle": "该工作区还没有小说项目", "guide.defaultDesc": "在下面给出的命令里初始化目录骨架(或把已有 novel 目录放进工作区根),然后刷新本面板。",
|
|
888
|
+
"guide.copied": "已复制 maoying init", "guide.copyFailed": "复制失败,请手动选择命令文本", "guide.copying": "复制中…", "guide.copyCmd": "复制命令",
|
|
889
|
+
"common.retry": "重试", "common.empty": "暂无数据", "common.uncategorized": "未分类", "common.charCount": "{n} 字",
|
|
890
|
+
"common.closeTip": "关闭提示", "common.unknownErr": "未知错误", "common.priorityTitle": "优先级:{pri}", "common.priorityAria": "优先级{pri}",
|
|
891
|
+
"common.chapterNum": "第 {n} 章", "pri.high": "高", "pri.normal": "中", "pri.low": "低",
|
|
892
|
+
"toast.prefix": "毛颖创作台:{msg}", "toast.success": "操作成功", "toast.indexRebuilt": "索引已重建({n} 章)",
|
|
893
|
+
"toast.chapterStatus": "章节状态已更新", "toast.foreshadowStatus": "伏笔状态已更新", "toast.phaseAdvanced": "阶段已推进到「{phase}」",
|
|
894
|
+
"api.notJson": "响应不是 JSON(HTTP {code})", "api.timeout": "请求超时({s} 秒)",
|
|
895
|
+
"status.pending": "待写", "status.writing": "写作中", "status.review": "审校中", "status.accepted": "已定稿", "status.rawSuffix": "({raw})",
|
|
896
|
+
"phase.idea": "立意", "phase.setting": "设定", "phase.outline": "大纲", "phase.writing": "写作", "phase.review": "审校",
|
|
897
|
+
"phase.consistency": "一致性", "phase.export": "导出", "phase.unset": "未设阶段",
|
|
898
|
+
"phase.advanceTitle": "推进到「{next}」", "phase.advance": "推进阶段", "phase.advancing": "推进中…", "phase.final": "已是最终阶段",
|
|
899
|
+
"foreshadow.planted": "埋设", "foreshadow.progressing": "推进中", "foreshadow.resolved": "已回收", "foreshadow.abandoned": "已弃用",
|
|
900
|
+
"foreshadow.advance": "推进", "foreshadow.resolve": "回收", "foreshadow.abandon": "弃用", "foreshadow.advancing": "推进中…",
|
|
901
|
+
"foreshadow.plantedUnmarked": "埋设章未标", "foreshadow.resolvedAt": "回收于 {ch}", "foreshadow.priorityLabel": "优先级 {pri}",
|
|
902
|
+
"foreshadow.backfill": "回填", "foreshadow.backfillTip": "从作者正文回填", "foreshadow.missingStatus": "缺 status",
|
|
903
|
+
"foreshadow.noIdTip": "该条目缺 id,无法写状态",
|
|
904
|
+
"foreshadow.noStatusTip": "该条目缺 status 行,服务端会拒绝流转(409);请先在 foreshadows.yml 补上 status",
|
|
905
|
+
"foreshadow.empty": "foreshadows.yml 还没有伏笔条目", "foreshadow.colEmpty": "空",
|
|
906
|
+
"clue.clue": "线索", "clue.redHerring": "红鲱鱼", "clue.inference": "推理", "clue.question": "疑问",
|
|
907
|
+
"sci.known": "已确证", "sci.extrapolation": "推演", "sci.hypothesis": "假说", "world.unlabeled": "未标级",
|
|
908
|
+
"warnings.title": "部分文件解析异常(已跳过):",
|
|
909
|
+
"index.title": "索引板(clue-board / memories)", "index.rebuild": "重建索引", "index.rebuilding": "重建中…",
|
|
910
|
+
"index.rebuildTip": "按 CLI index 同一逻辑重建 index.yml", "index.clueBoard": "线索板", "index.clueEmpty": "clue-board.yml 还没有条目",
|
|
911
|
+
"index.memBoard": "记忆板", "index.memEmpty": "memories.yml 还没有条目", "index.promise": "承诺线",
|
|
912
|
+
"timeline.empty": "timeline.yml 还没有时间线条目", "timeline.importance": "重要度 {n}", "timeline.linkedChapter": "关联章节:{ch}",
|
|
913
|
+
"world.title": "世界观条目", "world.empty": "world/ 目录还没有设定条目", "world.science": "科学设定",
|
|
914
|
+
"world.scienceEmpty": "science/ 目录还没有科学设定", "world.research": "资料检索", "world.ref": "参考 {n}:{r}",
|
|
915
|
+
"relations.title": "角色关系图", "relations.counts": "{n} 节点 · {m} 关系",
|
|
916
|
+
"relations.help": "拖节点固定位置 · 滚轮缩放 · 双击复位 · 悬停边看关系描述",
|
|
917
|
+
"relations.unlinkedTitle": "未入图角色(无关系边)", "relations.noData": "没有角色或关系数据,无法绘图",
|
|
918
|
+
"relations.edgeTipPrefix": "{type}:", "relations.noDesc": "(无描述)",
|
|
919
|
+
"char.empty": "characters/ 目录还没有角色卡",
|
|
920
|
+
"char.field.age": "年龄", "char.field.gender": "性别", "char.field.appearance": "外貌", "char.field.personality": "性格",
|
|
921
|
+
"char.field.background": "背景", "char.field.motivation": "动机", "char.field.arc": "成长弧",
|
|
922
|
+
"char.years": "{n} 岁", "char.linePersonality": "性格 ", "char.lineMotivation": "动机 ",
|
|
923
|
+
"char.modalAria": "角色详情", "char.closeAria": "关闭角色卡", "char.markers": "标记", "char.file": "文件",
|
|
924
|
+
"overview.author": "作者:{name}", "overview.totalWords": "总字数(CJK)", "overview.totalChapters": "章节总数",
|
|
925
|
+
"overview.characters": "角色数", "overview.openForeshadows": "未回收伏笔", "overview.statusDist": "章节状态分布", "overview.recent": "最近章节",
|
|
926
|
+
"chapters.empty": "chapters/ 目录还没有章节文件", "chapters.writeQueue": "按单写作", "chapters.rowTip": "点击打开该章",
|
|
927
|
+
"chapters.outlineWords": "章纲 {n} 字", "chapters.all": "全部", "chapters.thVolume": "卷 · 章", "chapters.thTitle": "标题",
|
|
928
|
+
"chapters.thStatus": "状态", "chapters.thWords": "字数", "chapters.thSummary": "梗概", "chapters.rowReadTip": "点击阅读正文",
|
|
929
|
+
"chapters.emptyFiltered": "该状态下没有章节",
|
|
930
|
+
"reader.back": "返回", "reader.backTip": "返回章节列表", "reader.copyPath": "复制路径", "reader.copyPathTip": "复制章节文件路径",
|
|
931
|
+
"reader.copied": "已复制章节文件路径", "reader.copyFailed": "复制失败,请手动选择路径文本", "reader.loadFailed": "正文读取失败:{msg}",
|
|
932
|
+
"reader.noOutline": "暂无章纲", "reader.emptyBody": "(空章节)", "reader.finalState": "已定稿(终态)", "reader.statusFlow": "状态流转:",
|
|
933
|
+
"reader.flowing": "流转中…", "reader.advanceTo": "推进到「{next}」",
|
|
934
|
+
"outline.title": "章纲对照", "outline.empty": "(章纲为空)", "outline.collapseTip": "收起章纲", "outline.expandTip": "展开章纲",
|
|
935
|
+
"novel.metaTitle": "书稿元信息", "novel.metaNote": "对应 novel/project.yml · 行尾注释会原样保留",
|
|
936
|
+
"novel.fTitle": "书名", "novel.titlePh": "如:长夜余火", "novel.fAuthor": "作者", "novel.authorPh": "笔名",
|
|
937
|
+
"novel.fGenre": "题材", "novel.genrePh": "如:东方玄幻 / 都市异能 / 历史架空", "novel.fLength": "预期篇幅",
|
|
938
|
+
"novel.lengthPh": "如:约100万字 / 300章", "novel.fLanguage": "正文语言", "novel.fPhase": "工作流阶段", "novel.fMode": "共创模式",
|
|
939
|
+
"novel.fDesc": "一句话简介", "novel.descPh": "写封面文案时可直接引用", "novel.titleRequired": "书名不能为空",
|
|
940
|
+
"mode.aiDraft": "AI 代笔(AI 写正文)", "mode.coWrite": "逐章共创(按章指派谁写)", "mode.authorDraft": "作者亲笔(AI 只备料审稿)",
|
|
941
|
+
"novel.saving": "保存中…", "novel.save": "保存设置",
|
|
942
|
+
"settings.title": "系统设置", "settings.closeAria": "关闭设置", "settings.language": "语言",
|
|
943
|
+
"settings.languageHint": "界面文字随语言选项切换,立即生效并记忆。", "settings.fontScale": "字号",
|
|
944
|
+
"settings.fontScaleHint": "按当前字号的百分比缩放整个创作台,立即生效并记忆。",
|
|
945
|
+
"errBoundary.title": "该分区渲染出错", "errBoundary.summary": "错误摘要:{msg}",
|
|
946
|
+
"dim.cognitiveFrame": "认知框架", "dim.emotionalAnchor": "情感锚点", "dim.worldTexture": "世界质感",
|
|
947
|
+
"dim.suspense": "悬念", "dim.interpersonalTension": "人际张力",
|
|
948
|
+
},
|
|
949
|
+
en: {
|
|
950
|
+
"shell.title": "Maoying Studio", "header.subtitle": "Web-novel project dashboard & status flow · read-only text, writable status",
|
|
951
|
+
"header.curVerTitle": "Current version", "header.upgradeTip": "Current {cur} — click to open the release page", "header.newVersion": "New version {ver} ↗",
|
|
952
|
+
"header.workspace": "Workspace", "header.wsAria": "Select workspace", "header.noNovel": "(no novel)", "header.untitled": "Untitled",
|
|
953
|
+
"header.settings": "Settings", "header.refresh": "Refresh", "header.closeAria": "Close Maoying Studio", "header.closeTitle": "Close",
|
|
954
|
+
"tabs.aria": "Studio sections",
|
|
955
|
+
"tabs.overview": "Overview", "tabs.chapters": "Chapters", "tabs.characters": "Characters", "tabs.foreshadows": "Foreshadowing", "tabs.relations": "Relations",
|
|
956
|
+
"tabs.timeline": "Timeline", "tabs.world": "Worldbuilding", "tabs.index": "Index Board", "tabs.settings": "Novel Settings",
|
|
957
|
+
"shell.wsListFailed": "Failed to load workspace list: {msg}", "shell.stateFailed": "Failed to load snapshot: {msg}",
|
|
958
|
+
"guide.noWsTitle": "No novel workspace available yet", "guide.noWsDesc": "A workspace is picked up automatically once novel/project.yml exists at its root; initialize the directory scaffold with this command first:",
|
|
959
|
+
"guide.notInitTitle": "No novel project in this workspace yet", "guide.notInitDesc": "novel/project.yml is missing at the workspace root. Run this at the workspace root:",
|
|
960
|
+
"guide.defaultTitle": "No novel project in this workspace yet", "guide.defaultDesc": "Initialize the directory scaffold with the command below (or drop an existing novel/ directory into the workspace root), then refresh this panel.",
|
|
961
|
+
"guide.copied": "Copied maoying init", "guide.copyFailed": "Copy failed — select the command text manually", "guide.copying": "Copying…", "guide.copyCmd": "Copy command",
|
|
962
|
+
"common.retry": "Retry", "common.empty": "No data", "common.uncategorized": "Uncategorized", "common.charCount": "{n} chars",
|
|
963
|
+
"common.closeTip": "Dismiss notification", "common.unknownErr": "Unknown error", "common.priorityTitle": "Priority: {pri}", "common.priorityAria": "Priority {pri}",
|
|
964
|
+
"common.chapterNum": "Ch. {n}", "pri.high": "High", "pri.normal": "Normal", "pri.low": "Low",
|
|
965
|
+
"toast.prefix": "Maoying Studio: {msg}", "toast.success": "Success", "toast.indexRebuilt": "Index rebuilt ({n} chapters)",
|
|
966
|
+
"toast.chapterStatus": "Chapter status updated", "toast.foreshadowStatus": "Foreshadowing status updated", "toast.phaseAdvanced": "Phase advanced to {phase}",
|
|
967
|
+
"api.notJson": "Response is not JSON (HTTP {code})", "api.timeout": "Request timed out ({s} s)",
|
|
968
|
+
"status.pending": "Pending", "status.writing": "Writing", "status.review": "Review", "status.accepted": "Accepted", "status.rawSuffix": "({raw})",
|
|
969
|
+
"phase.idea": "Idea", "phase.setting": "Setting", "phase.outline": "Outline", "phase.writing": "Writing", "phase.review": "Review",
|
|
970
|
+
"phase.consistency": "Consistency", "phase.export": "Export", "phase.unset": "No phase",
|
|
971
|
+
"phase.advanceTitle": "Advance to {next}", "phase.advance": "Advance phase", "phase.advancing": "Advancing…", "phase.final": "Final phase",
|
|
972
|
+
"foreshadow.planted": "Planted", "foreshadow.progressing": "In Progress", "foreshadow.resolved": "Resolved", "foreshadow.abandoned": "Abandoned",
|
|
973
|
+
"foreshadow.advance": "Advance", "foreshadow.resolve": "Resolve", "foreshadow.abandon": "Abandon", "foreshadow.advancing": "Advancing…",
|
|
974
|
+
"foreshadow.plantedUnmarked": "Planting chapter unmarked", "foreshadow.resolvedAt": "Resolved in {ch}", "foreshadow.priorityLabel": "Priority {pri}",
|
|
975
|
+
"foreshadow.backfill": "Backfill", "foreshadow.backfillTip": "Backfilled from the author's manuscript", "foreshadow.missingStatus": "missing status",
|
|
976
|
+
"foreshadow.noIdTip": "This entry has no id — its status cannot be written",
|
|
977
|
+
"foreshadow.noStatusTip": "This entry has no status line, the server will reject the transition (409); add a status in foreshadows.yml first",
|
|
978
|
+
"foreshadow.empty": "No entries in foreshadows.yml yet", "foreshadow.colEmpty": "Empty",
|
|
979
|
+
"clue.clue": "Clue", "clue.redHerring": "Red herring", "clue.inference": "Inference", "clue.question": "Question",
|
|
980
|
+
"sci.known": "Confirmed", "sci.extrapolation": "Extrapolation", "sci.hypothesis": "Hypothesis", "world.unlabeled": "Unrated",
|
|
981
|
+
"warnings.title": "Some files failed to parse (skipped):",
|
|
982
|
+
"index.title": "Index Board (clue-board / memories)", "index.rebuild": "Rebuild index", "index.rebuilding": "Rebuilding…",
|
|
983
|
+
"index.rebuildTip": "Rebuild index.yml with the same logic as the CLI index command", "index.clueBoard": "Clue board", "index.clueEmpty": "No entries in clue-board.yml yet",
|
|
984
|
+
"index.memBoard": "Memory board", "index.memEmpty": "No entries in memories.yml yet", "index.promise": "Promise thread",
|
|
985
|
+
"timeline.empty": "No entries in timeline.yml yet", "timeline.importance": "Importance {n}", "timeline.linkedChapter": "Related chapter: {ch}",
|
|
986
|
+
"world.title": "Worldbuilding entries", "world.empty": "No worldbuilding entries in world/ yet", "world.science": "Science settings",
|
|
987
|
+
"world.scienceEmpty": "No science entries in science/ yet", "world.research": "Research", "world.ref": "Ref {n}: {r}",
|
|
988
|
+
"relations.title": "Character relations", "relations.counts": "{n} nodes · {m} relations",
|
|
989
|
+
"relations.help": "Drag a node to pin it · scroll to zoom · double-click to reset · hover an edge for its description",
|
|
990
|
+
"relations.unlinkedTitle": "Unlinked characters (no relation edges)", "relations.noData": "No character or relation data to draw",
|
|
991
|
+
"relations.edgeTipPrefix": "{type}: ", "relations.noDesc": "(no description)",
|
|
992
|
+
"char.empty": "No character cards in characters/ yet",
|
|
993
|
+
"char.field.age": "Age", "char.field.gender": "Gender", "char.field.appearance": "Appearance", "char.field.personality": "Personality",
|
|
994
|
+
"char.field.background": "Background", "char.field.motivation": "Motivation", "char.field.arc": "Character arc",
|
|
995
|
+
"char.years": "Age {n}", "char.linePersonality": "Personality ", "char.lineMotivation": "Motivation ",
|
|
996
|
+
"char.modalAria": "Character details", "char.closeAria": "Close character card", "char.markers": "Tags", "char.file": "File",
|
|
997
|
+
"overview.author": "Author: {name}", "overview.totalWords": "Total characters (CJK)", "overview.totalChapters": "Chapters",
|
|
998
|
+
"overview.characters": "Characters", "overview.openForeshadows": "Unresolved foreshadowing", "overview.statusDist": "Chapter status breakdown", "overview.recent": "Recent chapters",
|
|
999
|
+
"chapters.empty": "No chapter files in chapters/ yet", "chapters.writeQueue": "Writing Queue", "chapters.rowTip": "Click to open this chapter",
|
|
1000
|
+
"chapters.outlineWords": "Outline {n} chars", "chapters.all": "All", "chapters.thVolume": "Vol · Ch", "chapters.thTitle": "Title",
|
|
1001
|
+
"chapters.thStatus": "Status", "chapters.thWords": "Words", "chapters.thSummary": "Summary", "chapters.rowReadTip": "Click to read",
|
|
1002
|
+
"chapters.emptyFiltered": "No chapters with this status",
|
|
1003
|
+
"reader.back": "Back", "reader.backTip": "Back to chapter list", "reader.copyPath": "Copy path", "reader.copyPathTip": "Copy chapter file path",
|
|
1004
|
+
"reader.copied": "Copied chapter file path", "reader.copyFailed": "Copy failed — select the path text manually", "reader.loadFailed": "Failed to load chapter text: {msg}",
|
|
1005
|
+
"reader.noOutline": "No chapter outline", "reader.emptyBody": "(empty chapter)", "reader.finalState": "Accepted (final)", "reader.statusFlow": "Status:",
|
|
1006
|
+
"reader.flowing": "Transitioning…", "reader.advanceTo": "Advance to {next}",
|
|
1007
|
+
"outline.title": "Outline", "outline.empty": "(outline is empty)", "outline.collapseTip": "Collapse outline", "outline.expandTip": "Expand outline",
|
|
1008
|
+
"novel.metaTitle": "Manuscript metadata", "novel.metaNote": "Maps to novel/project.yml · trailing comments are preserved",
|
|
1009
|
+
"novel.fTitle": "Title", "novel.titlePh": "e.g. The Long Night", "novel.fAuthor": "Author", "novel.authorPh": "Pen name",
|
|
1010
|
+
"novel.fGenre": "Genre", "novel.genrePh": "e.g. Eastern fantasy / Urban powers / Alternate history", "novel.fLength": "Planned length",
|
|
1011
|
+
"novel.lengthPh": "e.g. ~1M characters / 300 chapters", "novel.fLanguage": "Text language", "novel.fPhase": "Workflow phase", "novel.fMode": "Co-writing mode",
|
|
1012
|
+
"novel.fDesc": "One-line synopsis", "novel.descPh": "Can be quoted directly in cover copy", "novel.titleRequired": "Title is required",
|
|
1013
|
+
"mode.aiDraft": "AI ghostwriting (AI drafts the prose)", "mode.coWrite": "Chapter-by-chapter co-writing (assign a writer per chapter)", "mode.authorDraft": "Author writes (AI only preps and reviews)",
|
|
1014
|
+
"novel.saving": "Saving…", "novel.save": "Save",
|
|
1015
|
+
"settings.title": "Settings", "settings.closeAria": "Close settings", "settings.language": "Language",
|
|
1016
|
+
"settings.languageHint": "UI text follows the language option; it applies immediately and is remembered.", "settings.fontScale": "Font size",
|
|
1017
|
+
"settings.fontScaleHint": "Scales the whole studio as a percentage of the current font size; applies immediately and is remembered.",
|
|
1018
|
+
"errBoundary.title": "This section failed to render", "errBoundary.summary": "Error summary: {msg}",
|
|
1019
|
+
"dim.cognitiveFrame": "Cognitive Frame", "dim.emotionalAnchor": "Emotional Anchor", "dim.worldTexture": "World Texture",
|
|
1020
|
+
"dim.suspense": "Suspense", "dim.interpersonalTension": "Interpersonal Tension",
|
|
1021
|
+
},
|
|
1022
|
+
};
|
|
1023
|
+
function tt(key) {
|
|
1024
|
+
var d = I18N[UI_LANG] || I18N.zh;
|
|
1025
|
+
var v = d[key];
|
|
1026
|
+
if (v === undefined) v = I18N.zh[key];
|
|
1027
|
+
return v === undefined ? key : v;
|
|
1028
|
+
}
|
|
1029
|
+
function tf(key, vars) {
|
|
1030
|
+
var s = tt(key);
|
|
1031
|
+
if (vars) for (var k in vars) s = s.split("{" + k + "}").join(String(vars[k]));
|
|
1032
|
+
return s;
|
|
1033
|
+
}
|
|
1034
|
+
function SettingsModal(props) {
|
|
1035
|
+
var scale = props.scale;
|
|
1036
|
+
var onPick = props.onPick;
|
|
1037
|
+
var lang = props.lang;
|
|
1038
|
+
var onPickLang = props.onPickLang;
|
|
1039
|
+
var onClose = props.onClose;
|
|
1040
|
+
return h(MaskPortal, {
|
|
1041
|
+
className: "myt-settingsMask",
|
|
1042
|
+
onMouseDown: function (e) { if (e.target === e.currentTarget) onClose(); },
|
|
1043
|
+
},
|
|
1044
|
+
h("div", { className: "myt-settings", role: "dialog", "aria-modal": "true", "aria-label": tt("settings.title") },
|
|
1045
|
+
h("div", { className: "myt-settingsHead" },
|
|
1046
|
+
h("h3", { className: "myt-settingsTitle" }, tt("settings.title")),
|
|
1047
|
+
h("button", { type: "button", className: "myt-closeBtn", "aria-label": tt("settings.closeAria"), onClick: onClose }, h("span", { className: "myt-closeGlyph", "aria-hidden": "true" }, "×"))),
|
|
1048
|
+
h("div", { className: "myt-settingsBody" },
|
|
1049
|
+
h("div", { className: "myt-settingsLabel" }, tt("settings.language")),
|
|
1050
|
+
h("div", { className: "myt-setRow" }, UI_LANGS.map(function (o) {
|
|
1051
|
+
return h("button", {
|
|
1052
|
+
key: o[0], type: "button", className: "myt-setOpt", "data-active": lang === o[0] ? "true" : undefined,
|
|
1053
|
+
onClick: function () { onPickLang(o[0]); },
|
|
1054
|
+
}, o[1]);
|
|
1055
|
+
})),
|
|
1056
|
+
h("div", { className: "myt-settingsHint" }, tt("settings.languageHint")),
|
|
1057
|
+
h("div", { className: "myt-settingsLabel" }, tt("settings.fontScale")),
|
|
1058
|
+
h("div", { className: "myt-setRow" }, FONT_SCALES.map(function (o) {
|
|
1059
|
+
return h("button", {
|
|
1060
|
+
key: o[0], type: "button", className: "myt-setOpt", "data-active": scale === o[0] ? "true" : undefined,
|
|
1061
|
+
onClick: function () { onPick(o[0]); },
|
|
1062
|
+
}, o[1]);
|
|
1063
|
+
})),
|
|
1064
|
+
h("div", { className: "myt-settingsHint" }, tt("settings.fontScaleHint")))));
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// ── 章节阅读页(整页视图,替代右侧抽屉):返回按钮 + 章纲对照 + 正文 +
|
|
1068
|
+
// 五维 + 状态流转。原抽屉是 position:fixed 侧弹层,在桌面端会被侧边栏
|
|
1069
|
+
// 遮挡;整页视图天然在面板文档流内,无遮挡问题。 ──
|
|
1070
|
+
function ChapterReader(props) {
|
|
1071
|
+
var file = props.file;
|
|
1072
|
+
var wsPath = props.wsPath;
|
|
1073
|
+
var snap = props.snap;
|
|
1074
|
+
var reload = props.reload;
|
|
1075
|
+
var onBack = props.onBack;
|
|
1076
|
+
var _d = useState(null), detail = _d[0], setDetail = _d[1]; // {loading, data, err}
|
|
1077
|
+
var _b = useState(false), flowBusy = _b[0], setFlowBusy = _b[1];
|
|
1078
|
+
var detailSeqRef = useRef(0); // chapter 请求序号:快速切章时丢弃过期响应
|
|
1079
|
+
var loadDetail = useCallback(function (f) {
|
|
1080
|
+
var seq = ++detailSeqRef.current;
|
|
1081
|
+
if (!f) { setDetail(null); return; }
|
|
1082
|
+
setDetail({ loading: true, data: null, err: "" });
|
|
1083
|
+
apiGet("chapter", { path: wsPath, file: f }).then(function (d) {
|
|
1084
|
+
if (seq !== detailSeqRef.current) return; // 过期响应:已在看另一章,丢弃
|
|
1085
|
+
setDetail({ loading: false, data: d, err: "" });
|
|
1086
|
+
}).catch(function (err) {
|
|
1087
|
+
if (seq !== detailSeqRef.current) return;
|
|
1088
|
+
setDetail({ loading: false, data: null, err: err.message });
|
|
1089
|
+
});
|
|
1090
|
+
}, [wsPath]);
|
|
1091
|
+
useEffect(function () { loadDetail(file); }, [file, loadDetail]);
|
|
1092
|
+
var ch = null;
|
|
1093
|
+
var chapters = (snap && snap.chapters) || [];
|
|
1094
|
+
for (var i = 0; i < chapters.length; i++) { if (chapters[i].file === file) { ch = chapters[i]; break; } }
|
|
1095
|
+
function advance() {
|
|
1096
|
+
if (!ch) return;
|
|
1097
|
+
var idx = STATUS_FLOW.indexOf(ch.status);
|
|
1098
|
+
var next = idx !== -1 && idx < STATUS_FLOW.length - 1 ? STATUS_FLOW[idx + 1] : null;
|
|
1099
|
+
if (!next || flowBusy) return;
|
|
1100
|
+
setFlowBusy(true);
|
|
1101
|
+
apiPost("chapter-status", { path: wsPath, file: ch.file, status: next })
|
|
1102
|
+
.then(function () { return reload(); })
|
|
1103
|
+
.then(function () { return loadDetail(ch.file); })
|
|
1104
|
+
.catch(function () {}).finally(function () { setFlowBusy(false); });
|
|
1105
|
+
}
|
|
1106
|
+
function copyChapterPath() {
|
|
1107
|
+
copyText(chapterAbsPath(snap && snap.novelDir, file)).then(function (ok) {
|
|
1108
|
+
if (ok) toast.ok(tt("reader.copied"));
|
|
1109
|
+
else toast.warn(tt("reader.copyFailed"));
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
return h("div", { className: "myt-reader" },
|
|
1113
|
+
h("div", { className: "myt-readerBar" },
|
|
1114
|
+
h("button", { type: "button", className: "myt-btn", onClick: onBack, title: tt("reader.backTip") },
|
|
1115
|
+
ic("IconChevronRightOutlineRegular", 13), tt("reader.back")),
|
|
1116
|
+
h("h2", { className: "myt-readerTitle" }, ch ? chFullTitle(ch) : file),
|
|
1117
|
+
ch ? statusBadge(ch.status) : null,
|
|
1118
|
+
h("div", { className: "myt-readerBarActions" },
|
|
1119
|
+
h("button", {
|
|
1120
|
+
type: "button", className: "myt-btn myt-copyBtn",
|
|
1121
|
+
title: tt("reader.copyPathTip"), "aria-label": tt("reader.copyPathTip"),
|
|
1122
|
+
onClick: copyChapterPath,
|
|
1123
|
+
}, ic("IconCopyOutlineRegular", 13), tt("reader.copyPath")))),
|
|
1124
|
+
h("div", { className: "myt-readerBody" },
|
|
1125
|
+
!detail || detail.loading ? h("div", { className: "myt-stack" }, h("div", { className: "myt-skel myt-skelRow" }), h("div", { className: "myt-skel myt-skelRow", style: { width: "80%" } }), h("div", { className: "myt-skel myt-skelRow", style: { width: "64%" } }))
|
|
1126
|
+
: detail.err ? h(ErrorStrip, { text: tf("reader.loadFailed", { msg: detail.err }), onRetry: function () { loadDetail(file); } })
|
|
1127
|
+
: [
|
|
1128
|
+
(function () {
|
|
1129
|
+
var ol = detail.data && detail.data.outline;
|
|
1130
|
+
return ol ? h(OutlinePanel, { key: "outline", outline: ol })
|
|
1131
|
+
: h("div", { key: "outline-none", className: "myt-outlineEmpty" }, tt("reader.noOutline"));
|
|
1132
|
+
})(),
|
|
1133
|
+
h("pre", { key: "body", className: "myt-chapterBody" }, (detail.data && detail.data.body) || tt("reader.emptyBody")),
|
|
1134
|
+
],
|
|
1135
|
+
ch ? h("div", { className: "myt-dimGrid" }, FIVE_DIMS.map(function (dim) {
|
|
1136
|
+
return h("div", { key: dim[0], className: "myt-dim" },
|
|
1137
|
+
h("div", { className: "myt-dimLabel" }, dim[1]),
|
|
1138
|
+
h("div", { className: "myt-dimText" }, orDash(ch[dim[0]])));
|
|
1139
|
+
})) : null),
|
|
1140
|
+
ch ? h("div", { className: "myt-readerFoot" },
|
|
1141
|
+
(function () {
|
|
1142
|
+
var idx = STATUS_FLOW.indexOf(ch.status);
|
|
1143
|
+
var next = idx !== -1 && idx < STATUS_FLOW.length - 1 ? STATUS_FLOW[idx + 1] : null;
|
|
1144
|
+
if (!next) return h("span", { className: "myt-muted" }, tt("reader.finalState"));
|
|
1145
|
+
return [h("span", { className: "myt-muted", key: "t" }, tt("reader.statusFlow")),
|
|
1146
|
+
h("button", { key: "b", type: "button", className: "myt-btn myt-btnPrimary", disabled: flowBusy, onClick: advance },
|
|
1147
|
+
flowBusy ? tt("reader.flowing") : tf("reader.advanceTo", { next: tt("status." + next) }))];
|
|
1148
|
+
})()) : null);
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
// ── 小说设置 tab:可视化 project.yml 平键并保存。mode/phase 走枚举
|
|
1152
|
+
// 下拉,自由文本走输入框;保存走 project-save 路由(服务端校验 + 保
|
|
1153
|
+
// 注释改写),成功后重拉快照让概览同步。 ──
|
|
1154
|
+
function SettingsTab(props) {
|
|
1155
|
+
var snap = props.snap;
|
|
1156
|
+
var wsPath = props.wsPath;
|
|
1157
|
+
var reload = props.reload;
|
|
1158
|
+
var project = snap.project || {};
|
|
1159
|
+
var _f = useState(function () {
|
|
1160
|
+
return {
|
|
1161
|
+
title: project.title || "", author: project.author || "", genre: project.genre || "",
|
|
1162
|
+
length: project.length || "", language: project.language || "zh",
|
|
1163
|
+
phase: PHASES.indexOf(project.phase) !== -1 ? project.phase : "idea",
|
|
1164
|
+
mode: project.mode || "ai-draft",
|
|
1165
|
+
description: project.description || "",
|
|
1166
|
+
};
|
|
1167
|
+
}), form = _f[0], setForm = _f[1];
|
|
1168
|
+
var _b = useState(false), busy = _b[0], setBusy = _b[1];
|
|
1169
|
+
var _e = useState(""), saveErr = _e[0], setSaveErr = _e[1];
|
|
1170
|
+
function set(k, v) { setForm(Object.assign({}, form, (function () { var o = {}; o[k] = v; return o; })())); }
|
|
1171
|
+
function save() {
|
|
1172
|
+
if (busy) return;
|
|
1173
|
+
if (!String(form.title || "").trim()) { setSaveErr(tt("novel.titleRequired")); return; }
|
|
1174
|
+
setBusy(true); setSaveErr("");
|
|
1175
|
+
// 成败提示统一走 apiPost 的 toast 通道(这里不再另弹,避免双 toast);
|
|
1176
|
+
// 卡内 saveErr 只承担表单旁的错误态。
|
|
1177
|
+
apiPost("project-save", { path: wsPath, values: form }).then(function () {
|
|
1178
|
+
return reload();
|
|
1179
|
+
}).catch(function (err) {
|
|
1180
|
+
setSaveErr(err.message);
|
|
1181
|
+
}).finally(function () { setBusy(false); });
|
|
1182
|
+
}
|
|
1183
|
+
function field(label, key, placeholder, textarea) {
|
|
1184
|
+
var value = form[key] || "";
|
|
1185
|
+
return h("label", { className: "myt-setField" },
|
|
1186
|
+
h("span", { className: "myt-setFieldLabel" }, label),
|
|
1187
|
+
textarea
|
|
1188
|
+
? h("textarea", { className: "myt-setInput myt-setArea", rows: 3, placeholder: placeholder, value: value,
|
|
1189
|
+
onChange: function (e) { set(key, e.target.value); } })
|
|
1190
|
+
: h("input", { type: "text", className: "myt-setInput", placeholder: placeholder, value: value,
|
|
1191
|
+
onChange: function (e) { set(key, e.target.value); } }));
|
|
1192
|
+
}
|
|
1193
|
+
function picker(label, key, opts) {
|
|
1194
|
+
var value = form[key] || "";
|
|
1195
|
+
return h("label", { className: "myt-setField" },
|
|
1196
|
+
h("span", { className: "myt-setFieldLabel" }, label),
|
|
1197
|
+
h("select", { className: "myt-setInput myt-setSelect", value: value, onChange: function (e) { set(key, e.target.value); } },
|
|
1198
|
+
opts.map(function (o) { return h("option", { key: o[0], value: o[0] }, o[1]); })));
|
|
1199
|
+
}
|
|
1200
|
+
return h("div", { className: "myt-stack" },
|
|
1201
|
+
h("div", { className: "myt-card" },
|
|
1202
|
+
h("div", { className: "myt-cardHead" },
|
|
1203
|
+
h("h3", { className: "myt-cardTitle" }, tt("novel.metaTitle")),
|
|
1204
|
+
h("span", { className: "myt-muted" }, tt("novel.metaNote"))),
|
|
1205
|
+
h("div", { className: "myt-setGrid" },
|
|
1206
|
+
field(tt("novel.fTitle"), "title", tt("novel.titlePh")),
|
|
1207
|
+
field(tt("novel.fAuthor"), "author", tt("novel.authorPh")),
|
|
1208
|
+
field(tt("novel.fGenre"), "genre", tt("novel.genrePh")),
|
|
1209
|
+
field(tt("novel.fLength"), "length", tt("novel.lengthPh")),
|
|
1210
|
+
picker(tt("novel.fLanguage"), "language", [["zh", "中文"], ["en", "English"], ["ja", "日本語"]]),
|
|
1211
|
+
picker(tt("novel.fPhase"), "phase", PHASES.map(function (p) { return [p, tt("phase." + p)]; })),
|
|
1212
|
+
picker(tt("novel.fMode"), "mode", [["ai-draft", tt("mode.aiDraft")], ["co-write", tt("mode.coWrite")], ["author-draft", tt("mode.authorDraft")]]),
|
|
1213
|
+
field(tt("novel.fDesc"), "description", tt("novel.descPh"), true))),
|
|
1214
|
+
saveErr ? h(ErrorStrip, { text: saveErr }) : null,
|
|
1215
|
+
h("div", { className: "myt-setActions" },
|
|
1216
|
+
h("button", { type: "button", className: "myt-btn myt-btnPrimary", disabled: busy, onClick: save },
|
|
1217
|
+
busy ? tt("novel.saving") : tt("novel.save"))));
|
|
700
1218
|
}
|
|
701
1219
|
|
|
702
1220
|
// ── 章节 tab:状态筛选 pills + 表格 + 行抽屉(只读正文 + 五维 + 流转)────
|
|
703
1221
|
var FIVE_DIMS = [
|
|
704
|
-
["cognitive_frame", "
|
|
705
|
-
["suspense", "
|
|
1222
|
+
["cognitive_frame", tt("dim.cognitiveFrame")], ["emotional_anchor", tt("dim.emotionalAnchor")], ["world_texture", tt("dim.worldTexture")],
|
|
1223
|
+
["suspense", tt("dim.suspense")], ["interpersonal_tension", tt("dim.interpersonalTension")],
|
|
706
1224
|
];
|
|
707
1225
|
// 章纲对照栏(五期):outline 非空时渲染可折叠原文(默认展开,只读,样式
|
|
708
1226
|
// 与正文区分);共享常量避免重渲染时折叠态意外重置——组件随抽屉挂载,
|
|
@@ -710,19 +1228,19 @@ window.__ModuleLoader__.load({
|
|
|
710
1228
|
function OutlinePanel(props) {
|
|
711
1229
|
var outline = props.outline;
|
|
712
1230
|
var _op = useState(true), open = _op[0], setOpen = _op[1];
|
|
713
|
-
var body = outline && typeof outline.body === "string" && outline.body.trim() !== "" ? outline.body : "
|
|
1231
|
+
var body = outline && typeof outline.body === "string" && outline.body.trim() !== "" ? outline.body : tt("outline.empty");
|
|
714
1232
|
return h("div", { className: "myt-outlinePanel" },
|
|
715
1233
|
h("button", {
|
|
716
1234
|
type: "button", className: "myt-outlineHead",
|
|
717
1235
|
"aria-expanded": open ? "true" : "false",
|
|
718
|
-
title: open ? "
|
|
1236
|
+
title: open ? tt("outline.collapseTip") : tt("outline.expandTip"),
|
|
719
1237
|
onClick: function () { setOpen(!open); },
|
|
720
1238
|
},
|
|
721
|
-
h("span", { className: "myt-outlineTitle" }, "
|
|
722
|
-
typeof (outline && outline.words) === "number" ? h("span", { className: "myt-outlineWords" }, fmtNum(outline.words)
|
|
1239
|
+
h("span", { className: "myt-outlineTitle" }, tt("outline.title")),
|
|
1240
|
+
typeof (outline && outline.words) === "number" ? h("span", { className: "myt-outlineWords" }, tf("common.charCount", { n: fmtNum(outline.words) })) : null,
|
|
723
1241
|
h("span", { className: "myt-outlineChevron", "aria-hidden": "true" },
|
|
724
1242
|
ic(open ? "IconChevronDownOutlineRegular" : "IconChevronRightOutlineRegular", 14))),
|
|
725
|
-
open ? h(
|
|
1243
|
+
open ? h(MdView, { text: body, className: "myt-outlineBody" }) : null);
|
|
726
1244
|
}
|
|
727
1245
|
// 抽屉/弹层专用 Portal:面板容器挂在宿主会话中栏里,中栏的 transform/
|
|
728
1246
|
// overflow 会把 position:fixed 劫持成相对自身并连同裁剪——抽屉渲染了也
|
|
@@ -738,45 +1256,11 @@ window.__ModuleLoader__.load({
|
|
|
738
1256
|
|
|
739
1257
|
function ChaptersTab(props) {
|
|
740
1258
|
var snap = props.snap;
|
|
741
|
-
var
|
|
742
|
-
var wsPath = props.wsPath;
|
|
1259
|
+
var onOpen = props.onOpenChapter; // 行点击 → 整页阅读(PageShell.readerFile)
|
|
743
1260
|
var _f = useState("all"), filter = _f[0], setFilter = _f[1];
|
|
744
|
-
var _o = useState(null), openFile = _o[0], setOpenFile = _o[1];
|
|
745
|
-
var _d = useState(null), detail = _d[0], setDetail = _d[1]; // {loading, data, err}
|
|
746
|
-
var _b = useState(false), flowBusy = _b[0], setFlowBusy = _b[1];
|
|
747
|
-
// 概览「最近章节」点进来:initialFile 只消费一次(吃掉后回置空,防止
|
|
748
|
-
// 下次进 tab 时把旧抽屉又顶开)。
|
|
749
|
-
useEffect(function () {
|
|
750
|
-
if (props.initialFile) {
|
|
751
|
-
setOpenFile(props.initialFile);
|
|
752
|
-
if (typeof props.onConsumeInitial === "function") props.onConsumeInitial();
|
|
753
|
-
}
|
|
754
|
-
}, [props.initialFile]);
|
|
755
|
-
var loadDetail = useCallback(function (file) {
|
|
756
|
-
if (!file) { setDetail(null); return; }
|
|
757
|
-
setDetail({ loading: true, data: null, err: "" });
|
|
758
|
-
apiGet("chapter", { path: wsPath, file: file }).then(function (d) {
|
|
759
|
-
setDetail({ loading: false, data: d, err: "" });
|
|
760
|
-
}).catch(function (err) {
|
|
761
|
-
setDetail({ loading: false, data: null, err: err.message });
|
|
762
|
-
});
|
|
763
|
-
}, [wsPath]);
|
|
764
|
-
useEffect(function () { loadDetail(openFile); }, [openFile, loadDetail]);
|
|
765
|
-
function advance(ch) {
|
|
766
|
-
var idx = STATUS_FLOW.indexOf(ch.status);
|
|
767
|
-
var next = idx !== -1 && idx < STATUS_FLOW.length - 1 ? STATUS_FLOW[idx + 1] : null;
|
|
768
|
-
if (!next || flowBusy) return;
|
|
769
|
-
setFlowBusy(true);
|
|
770
|
-
apiPost("chapter-status", { path: wsPath, file: ch.file, status: next })
|
|
771
|
-
.then(function () { return reload(); })
|
|
772
|
-
.then(function () { if (openFile === ch.file) return loadDetail(ch.file); })
|
|
773
|
-
.catch(function () {}).finally(function () { setFlowBusy(false); });
|
|
774
|
-
}
|
|
775
1261
|
var chapters = snap.chapters || [];
|
|
776
1262
|
var rows = filter === "all" ? chapters : chapters.filter(function (c) { return c.status === filter; });
|
|
777
1263
|
var counts = snap.totals && snap.totals.byStatus ? snap.totals.byStatus : {};
|
|
778
|
-
var openCh = null;
|
|
779
|
-
for (var i = 0; i < chapters.length; i++) { if (chapters[i].file === openFile) { openCh = chapters[i]; break; } }
|
|
780
1264
|
// 「按单写作」(五期):state.outlines 与 chapters 联交——有章纲且
|
|
781
1265
|
// status=pending 的章,按章纲(卷-章号)序列出,共创模式的主工作清单;
|
|
782
1266
|
// 空块(无待写章纲章)整块不渲染。孤儿章纲(chapter 无对应章节)联交
|
|
@@ -789,35 +1273,29 @@ window.__ModuleLoader__.load({
|
|
|
789
1273
|
var ch = ol && ol.chapter ? chapterByFile[ol.chapter] : null;
|
|
790
1274
|
if (ch && (ch.status || "pending") === "pending") writeOrder.push({ ch: ch, ol: ol });
|
|
791
1275
|
});
|
|
792
|
-
function copyChapterPath() {
|
|
793
|
-
copyText(chapterAbsPath(snap && snap.novelDir, openFile)).then(function (ok) {
|
|
794
|
-
if (ok) toast.ok("已复制章节文件路径");
|
|
795
|
-
else toast.warn("复制失败,请手动选择路径文本");
|
|
796
|
-
});
|
|
797
|
-
}
|
|
798
1276
|
return h("div", { className: "myt-stack" },
|
|
799
1277
|
writeOrder.length ? h("div", { className: "myt-card myt-writeOrder" },
|
|
800
1278
|
h("div", { className: "myt-cardHead" },
|
|
801
|
-
h("h3", { className: "myt-cardTitle" }, "
|
|
1279
|
+
h("h3", { className: "myt-cardTitle" }, tt("chapters.writeQueue")),
|
|
802
1280
|
h("span", { className: "myt-boardCount" }, writeOrder.length)),
|
|
803
1281
|
h("div", { className: "myt-writeOrderList" }, writeOrder.map(function (item) {
|
|
804
1282
|
var ch = item.ch;
|
|
805
1283
|
var ol = item.ol;
|
|
806
1284
|
return h("button", {
|
|
807
1285
|
key: ch.file, type: "button", className: "myt-writeOrderRow",
|
|
808
|
-
title: "
|
|
809
|
-
onClick: function () {
|
|
1286
|
+
title: tt("chapters.rowTip"),
|
|
1287
|
+
onClick: function () { onOpen(ch.file); },
|
|
810
1288
|
},
|
|
811
1289
|
h("span", { className: "myt-chPath" }, chWhere(ch) || ch.file),
|
|
812
1290
|
h("span", { className: "myt-chTitle" }, orDash(ch.title)),
|
|
813
1291
|
statusBadge(ch.status),
|
|
814
1292
|
ol && typeof ol.excerpt === "string" && ol.excerpt !== ""
|
|
815
1293
|
? h("span", { className: "myt-writeOrderExcerpt", title: ol.excerpt }, truncate(ol.excerpt, 60)) : null,
|
|
816
|
-
ol && ol.words ? h("span", { className: "myt-wordsCell" }, "
|
|
1294
|
+
ol && ol.words ? h("span", { className: "myt-wordsCell" }, tf("chapters.outlineWords", { n: fmtNum(ol.words) })) : null);
|
|
817
1295
|
}))) : null,
|
|
818
1296
|
h("div", { className: "myt-filterRow" },
|
|
819
|
-
[{ k: "all", label: "
|
|
820
|
-
return { k: s, label:
|
|
1297
|
+
[{ k: "all", label: tt("chapters.all"), n: chapters.length }].concat(STATUS_FLOW.map(function (s) {
|
|
1298
|
+
return { k: s, label: tt("status." + s), n: counts[s] || 0 };
|
|
821
1299
|
})).map(function (f) {
|
|
822
1300
|
return h("button", {
|
|
823
1301
|
key: f.k, type: "button", className: "myt-filterPill",
|
|
@@ -828,63 +1306,22 @@ window.__ModuleLoader__.load({
|
|
|
828
1306
|
rows.length ? h("div", { className: "myt-tableWrap" },
|
|
829
1307
|
h("table", { className: "myt-table" },
|
|
830
1308
|
h("thead", null, h("tr", null,
|
|
831
|
-
h("th", null, "
|
|
1309
|
+
h("th", null, tt("chapters.thVolume")), h("th", null, tt("chapters.thTitle")), h("th", null, tt("chapters.thStatus")), h("th", null, tt("chapters.thWords")), h("th", null, tt("chapters.thSummary")))),
|
|
832
1310
|
h("tbody", null, rows.map(function (ch) {
|
|
833
|
-
return h("tr", { key: ch.file, onClick: function () {
|
|
1311
|
+
return h("tr", { key: ch.file, onClick: function () { onOpen(ch.file); }, title: tt("chapters.rowReadTip") },
|
|
834
1312
|
h("td", null, h("span", { className: "myt-chPath" }, chWhere(ch) || "—")),
|
|
835
1313
|
h("td", null, h("span", { className: "myt-chTitle" }, orDash(ch.title))),
|
|
836
1314
|
h("td", null, statusBadge(ch.status)),
|
|
837
1315
|
h("td", null, h("span", { className: "myt-wordsCell" }, fmtNum(ch.words))),
|
|
838
1316
|
h("td", null, h("div", { className: "myt-summaryCell myt-muted" }, orDash(ch.summary))));
|
|
839
1317
|
}))))
|
|
840
|
-
: (chapters.length ? h(Empty, { text: "
|
|
841
|
-
openFile ? h(MaskPortal, {
|
|
842
|
-
className: "myt-drawerMask",
|
|
843
|
-
onMouseDown: function (e) { if (e.target === e.currentTarget) setOpenFile(null); },
|
|
844
|
-
},
|
|
845
|
-
h("aside", { className: "myt-drawer", role: "dialog", "aria-modal": "true", "aria-label": "章节详情" },
|
|
846
|
-
h("div", { className: "myt-drawerHead" },
|
|
847
|
-
h("h2", { className: "myt-drawerTitle" }, openCh ? chFullTitle(openCh) : openFile),
|
|
848
|
-
h("div", { className: "myt-heroMeta" }, openCh ? statusBadge(openCh.status) : null,
|
|
849
|
-
h("button", {
|
|
850
|
-
type: "button", className: "myt-btn myt-copyBtn",
|
|
851
|
-
title: "复制章节文件路径", "aria-label": "复制章节文件路径",
|
|
852
|
-
onClick: copyChapterPath,
|
|
853
|
-
}, ic("IconCopyOutlineRegular", 13), "复制路径"),
|
|
854
|
-
h("button", { type: "button", className: "myt-closeBtn", "aria-label": "关闭抽屉", onClick: function () { setOpenFile(null); } }, h("span", { className: "myt-closeGlyph", "aria-hidden": "true" }, "×")))),
|
|
855
|
-
h("div", { className: "myt-drawerBody" },
|
|
856
|
-
!detail || detail.loading ? h("div", { className: "myt-stack" }, h("div", { className: "myt-skel myt-skelRow" }), h("div", { className: "myt-skel myt-skelRow", style: { width: "80%" } }), h("div", { className: "myt-skel myt-skelRow", style: { width: "64%" } }))
|
|
857
|
-
: detail.err ? h(ErrorStrip, { text: "正文读取失败:" + detail.err, onRetry: function () { loadDetail(openFile); } })
|
|
858
|
-
: [
|
|
859
|
-
(function () {
|
|
860
|
-
var ol = detail.data && detail.data.outline;
|
|
861
|
-
// 有章纲:对照栏置顶(可折叠,默认展开);无章纲:一行
|
|
862
|
-
// 淡字提示——共创模式下这是「先要章纲」的信号。
|
|
863
|
-
return ol ? h(OutlinePanel, { key: "outline", outline: ol })
|
|
864
|
-
: h("div", { key: "outline-none", className: "myt-outlineEmpty" }, "暂无章纲");
|
|
865
|
-
})(),
|
|
866
|
-
h("pre", { key: "body", className: "myt-chapterBody" }, (detail.data && detail.data.body) || "(空章节)"),
|
|
867
|
-
],
|
|
868
|
-
openCh ? h("div", { className: "myt-dimGrid" }, FIVE_DIMS.map(function (dim) {
|
|
869
|
-
return h("div", { key: dim[0], className: "myt-dim" },
|
|
870
|
-
h("div", { className: "myt-dimLabel" }, dim[1]),
|
|
871
|
-
h("div", { className: "myt-dimText" }, orDash(openCh[dim[0]])));
|
|
872
|
-
})) : null),
|
|
873
|
-
openCh ? h("div", { className: "myt-drawerFoot" },
|
|
874
|
-
(function () {
|
|
875
|
-
var idx = STATUS_FLOW.indexOf(openCh.status);
|
|
876
|
-
var next = idx !== -1 && idx < STATUS_FLOW.length - 1 ? STATUS_FLOW[idx + 1] : null;
|
|
877
|
-
if (!next) return h("span", { className: "myt-muted" }, "已定稿(终态)");
|
|
878
|
-
return [h("span", { className: "myt-muted", key: "t" }, "状态流转:"),
|
|
879
|
-
h("button", { key: "b", type: "button", className: "myt-btn myt-btnPrimary", disabled: flowBusy, onClick: function () { advance(openCh); } },
|
|
880
|
-
flowBusy ? "流转中…" : "推进到「" + STATUS_LABEL[next] + "」")];
|
|
881
|
-
})()) : null)) : null);
|
|
1318
|
+
: (chapters.length ? h(Empty, { text: tt("chapters.emptyFiltered") }) : h(Empty, { text: tt("chapters.empty") })));
|
|
882
1319
|
}
|
|
883
1320
|
|
|
884
1321
|
// ── 角色 tab:卡墙 + 全卡弹层 ───────────────────────────────────────────
|
|
885
1322
|
var CHAR_FULL_ROWS = [
|
|
886
|
-
["age", "
|
|
887
|
-
["background", "
|
|
1323
|
+
["age", tt("char.field.age")], ["gender", tt("char.field.gender")], ["appearance", tt("char.field.appearance")], ["personality", tt("char.field.personality")],
|
|
1324
|
+
["background", tt("char.field.background")], ["motivation", tt("char.field.motivation")], ["arc", tt("char.field.arc")],
|
|
888
1325
|
];
|
|
889
1326
|
function CharactersTab(props) {
|
|
890
1327
|
var characters = props.snap.characters || [];
|
|
@@ -898,20 +1335,20 @@ window.__ModuleLoader__.load({
|
|
|
898
1335
|
h("span", { className: "myt-charName" }, orDash(c.name)),
|
|
899
1336
|
roleBadge(c.role)),
|
|
900
1337
|
h("div", { className: "myt-charMeta" },
|
|
901
|
-
[c.age !== null && c.age !== undefined ? c.age
|
|
902
|
-
c.personality ? h("div", { className: "myt-charLine" }, h("span", { className: "myt-charLabel" }, "
|
|
903
|
-
c.motivation ? h("div", { className: "myt-charLine" }, h("span", { className: "myt-charLabel" }, "
|
|
904
|
-
})) : h(Empty, { text: "
|
|
1338
|
+
[c.age !== null && c.age !== undefined ? tf("char.years", { n: c.age }) : null, c.gender || null].filter(Boolean).join(" · ") || "—"),
|
|
1339
|
+
c.personality ? h("div", { className: "myt-charLine" }, h("span", { className: "myt-charLabel" }, tt("char.linePersonality")), truncate(c.personality, 60)) : null,
|
|
1340
|
+
c.motivation ? h("div", { className: "myt-charLine" }, h("span", { className: "myt-charLabel" }, tt("char.lineMotivation")), truncate(c.motivation, 60)) : null);
|
|
1341
|
+
})) : h(Empty, { text: tt("char.empty") }),
|
|
905
1342
|
modal ? h(MaskPortal, {
|
|
906
1343
|
className: "myt-modalMask",
|
|
907
1344
|
onMouseDown: function (e) { if (e.target === e.currentTarget) setModalFile(null); },
|
|
908
1345
|
},
|
|
909
|
-
h("div", { className: "myt-modalCard", role: "dialog", "aria-modal": "true", "aria-label": "
|
|
1346
|
+
h("div", { className: "myt-modalCard", role: "dialog", "aria-modal": "true", "aria-label": tt("char.modalAria") },
|
|
910
1347
|
h("div", { className: "myt-modalHead" },
|
|
911
1348
|
h("h2", { className: "myt-modalTitle" }, orDash(modal.name)),
|
|
912
1349
|
h("div", { className: "myt-heroMeta" },
|
|
913
1350
|
roleBadge(modal.role),
|
|
914
|
-
h("button", { type: "button", className: "myt-closeBtn", "aria-label": "
|
|
1351
|
+
h("button", { type: "button", className: "myt-closeBtn", "aria-label": tt("char.closeAria"), onClick: function () { setModalFile(null); } }, h("span", { className: "myt-closeGlyph", "aria-hidden": "true" }, "×")))),
|
|
915
1352
|
h("div", { className: "myt-modalBody" },
|
|
916
1353
|
CHAR_FULL_ROWS.map(function (row) {
|
|
917
1354
|
return h("div", { key: row[0], className: "myt-kvRow" },
|
|
@@ -919,13 +1356,13 @@ window.__ModuleLoader__.load({
|
|
|
919
1356
|
h("span", { className: "myt-kvVal" }, orDash(modal[row[0]])));
|
|
920
1357
|
}),
|
|
921
1358
|
h("div", { className: "myt-kvRow" },
|
|
922
|
-
h("span", { className: "myt-kvKey" }, "
|
|
1359
|
+
h("span", { className: "myt-kvKey" }, tt("char.markers")),
|
|
923
1360
|
h("span", { className: "myt-kvVal myt-tagRow" },
|
|
924
1361
|
(modal.markers || []).length
|
|
925
1362
|
? modal.markers.map(function (m, i) { return h("span", { key: i, className: "myt-markerTag" }, m); })
|
|
926
1363
|
: "—")),
|
|
927
1364
|
h("div", { className: "myt-kvRow" },
|
|
928
|
-
h("span", { className: "myt-kvKey" }, "
|
|
1365
|
+
h("span", { className: "myt-kvKey" }, tt("char.file")),
|
|
929
1366
|
h("span", { className: "myt-kvVal" }, modal.file || "—"))))) : null);
|
|
930
1367
|
}
|
|
931
1368
|
|
|
@@ -947,28 +1384,36 @@ window.__ModuleLoader__.load({
|
|
|
947
1384
|
var items = list.filter(function (f) { return (f.status || "planted") === col; });
|
|
948
1385
|
return h("div", { key: col, className: "myt-boardCol" },
|
|
949
1386
|
h("div", { className: "myt-boardHead" },
|
|
950
|
-
h("span", { className: "myt-boardTitle" },
|
|
1387
|
+
h("span", { className: "myt-boardTitle" }, tt("foreshadow." + col)),
|
|
951
1388
|
h("span", { className: "myt-boardCount" }, items.length)),
|
|
952
1389
|
items.map(function (fs) {
|
|
953
1390
|
var next = FS_FLOW[col] || null;
|
|
954
1391
|
var busy = busyId === fs.id;
|
|
955
|
-
|
|
1392
|
+
// status 行缺失(服务端要求每条恰一处 status,写必 409):禁用流
|
|
1393
|
+
// 转/弃用按钮,降级为只读展示并给提示,避免「可点但必失败」。
|
|
1394
|
+
var noStatus = !fs.status;
|
|
1395
|
+
var noWrite = !fs.id || noStatus;
|
|
1396
|
+
var noWriteTip = !fs.id
|
|
1397
|
+
? tt("foreshadow.noIdTip")
|
|
1398
|
+
: tt("foreshadow.noStatusTip");
|
|
1399
|
+
var chRange = [fs.planted_chapter || tt("foreshadow.plantedUnmarked"), fs.expected_resolve].filter(Boolean).join(" → ");
|
|
956
1400
|
return h("div", { key: fs.id || fs.title, className: "myt-fsCard" },
|
|
957
1401
|
h("div", { className: "myt-fsTitle" },
|
|
958
1402
|
priDot(fs.priority), h("span", null, orDash(fs.title) || orDash(fs.id)),
|
|
959
1403
|
// 回填徽章(五期账本回填):区别于写前规划条目,悬浮见来源。
|
|
960
|
-
fs.source === "backfill" ? h("span", { className: "myt-backfillBadge", title: "
|
|
1404
|
+
fs.source === "backfill" ? h("span", { className: "myt-backfillBadge", title: tt("foreshadow.backfillTip") }, tt("foreshadow.backfill")) : null,
|
|
1405
|
+
noStatus ? h("span", { className: "myt-fsNoStatus", title: noWriteTip }, tt("foreshadow.missingStatus")) : null),
|
|
961
1406
|
h("div", { className: "myt-fsMeta" },
|
|
962
|
-
h("span", null, col === "resolved" && fs.resolved_chapter ? "
|
|
963
|
-
fs.priority && fs.priority !== "normal" ? h("span", null, "
|
|
1407
|
+
h("span", null, col === "resolved" && fs.resolved_chapter ? tf("foreshadow.resolvedAt", { ch: fs.resolved_chapter }) : chRange),
|
|
1408
|
+
fs.priority && fs.priority !== "normal" ? h("span", null, tf("foreshadow.priorityLabel", { pri: ["high", "low", "normal"].indexOf(fs.priority) !== -1 ? tt("pri." + fs.priority) : fs.priority })) : null),
|
|
964
1409
|
fs.notes ? h("div", { className: "myt-fsNotes", title: fs.notes }, fs.notes) : null,
|
|
965
1410
|
h("div", { className: "myt-fsActions" },
|
|
966
|
-
next ? h("button", { type: "button", className: "myt-btn", disabled: busy ||
|
|
1411
|
+
next ? h("button", { type: "button", className: "myt-btn", disabled: busy || noWrite, title: noWriteTip, onClick: function () { setStatus(fs, next); } }, busy ? tt("foreshadow.advancing") : (col === "planted" ? tt("foreshadow.advance") : tt("foreshadow.resolve"))) : null,
|
|
967
1412
|
col !== "resolved" && col !== "abandoned"
|
|
968
|
-
? h("button", { type: "button", className: "myt-btn myt-btnDanger", disabled: busy ||
|
|
1413
|
+
? h("button", { type: "button", className: "myt-btn myt-btnDanger", disabled: busy || noWrite, title: noWriteTip, onClick: function () { setStatus(fs, "abandoned"); } }, tt("foreshadow.abandon")) : null));
|
|
969
1414
|
}),
|
|
970
|
-
items.length ? null : h("div", { className: "myt-muted" }, "
|
|
971
|
-
})) : h(Empty, { text: "
|
|
1415
|
+
items.length ? null : h("div", { className: "myt-muted" }, tt("foreshadow.colEmpty")));
|
|
1416
|
+
})) : h(Empty, { text: tt("foreshadow.empty") }));
|
|
972
1417
|
}
|
|
973
1418
|
|
|
974
1419
|
// ── 时间线 tab:垂直轴 + importance 点位(1-5 → 大小/深浅)──────────────
|
|
@@ -977,17 +1422,17 @@ window.__ModuleLoader__.load({
|
|
|
977
1422
|
if (items.length) {
|
|
978
1423
|
items.sort(function (a, b) { return String(a.year || "").localeCompare(String(b.year || ""), "zh-CN", { numeric: true }); });
|
|
979
1424
|
}
|
|
980
|
-
if (!items.length) return h(Empty, { text: "timeline.
|
|
1425
|
+
if (!items.length) return h(Empty, { text: tt("timeline.empty") });
|
|
981
1426
|
return h("div", { className: "myt-card" },
|
|
982
1427
|
h("div", { className: "myt-timeline" }, items.map(function (t, i) {
|
|
983
1428
|
var imp = Math.min(5, Math.max(1, typeof t.importance === "number" ? t.importance : 3));
|
|
984
1429
|
return h("div", { key: i, className: "myt-tlRow" },
|
|
985
1430
|
h("div", { className: "myt-tlYear" }, orDash(t.year)),
|
|
986
|
-
h("div", { className: "myt-tlAxis" }, h("span", { className: "myt-tlDot", "data-imp": String(imp), title: "
|
|
1431
|
+
h("div", { className: "myt-tlAxis" }, h("span", { className: "myt-tlDot", "data-imp": String(imp), title: tf("timeline.importance", { n: imp }) })),
|
|
987
1432
|
h("div", { className: "myt-tlCard" },
|
|
988
1433
|
h("div", { className: "myt-tlTitle" }, orDash(t.title)),
|
|
989
1434
|
t.description ? h("div", { className: "myt-tlDesc" }, t.description) : null,
|
|
990
|
-
t.chapter ? h("div", { className: "myt-tlChapter" }, "
|
|
1435
|
+
t.chapter ? h("div", { className: "myt-tlChapter" }, tf("timeline.linkedChapter", { ch: t.chapter })) : null));
|
|
991
1436
|
})));
|
|
992
1437
|
}
|
|
993
1438
|
|
|
@@ -997,16 +1442,19 @@ window.__ModuleLoader__.load({
|
|
|
997
1442
|
var world = snap.world || [];
|
|
998
1443
|
var science = snap.science || [];
|
|
999
1444
|
var research = snap.research || [];
|
|
1000
|
-
|
|
1445
|
+
// 无原型容器:key 来自数据文件的自由文本(category),值为
|
|
1446
|
+
// constructor/__proto__/toString 等时若用普通 {} 会命中 Object.prototype
|
|
1447
|
+
// 真值 → TypeError → 整面板白屏;Object.create(null) 杜绝。
|
|
1448
|
+
var groups = Object.create(null);
|
|
1001
1449
|
world.forEach(function (w) {
|
|
1002
|
-
var key = w.category || "
|
|
1450
|
+
var key = w.category || tt("common.uncategorized");
|
|
1003
1451
|
if (!groups[key]) groups[key] = [];
|
|
1004
1452
|
groups[key].push(w);
|
|
1005
1453
|
});
|
|
1006
1454
|
var groupKeys = Object.keys(groups).sort(function (a, b) { return a.localeCompare(b, "zh-CN"); });
|
|
1007
1455
|
return h("div", { className: "myt-stack" },
|
|
1008
1456
|
h("div", { className: "myt-card" },
|
|
1009
|
-
h("div", { className: "myt-cardHead" }, h("h3", { className: "myt-cardTitle" }, "
|
|
1457
|
+
h("div", { className: "myt-cardHead" }, h("h3", { className: "myt-cardTitle" }, tt("world.title"))),
|
|
1010
1458
|
groupKeys.length ? h("div", { className: "myt-stack" }, groupKeys.map(function (key) {
|
|
1011
1459
|
return h("div", { key: key, className: "myt-worldGroup" },
|
|
1012
1460
|
h("div", { className: "myt-listHead" }, key, h("span", { className: "myt-boardCount" }, groups[key].length)),
|
|
@@ -1015,25 +1463,25 @@ window.__ModuleLoader__.load({
|
|
|
1015
1463
|
h("div", { className: "myt-worldHead" },
|
|
1016
1464
|
h("span", { className: "myt-worldName" }, orDash(w.name)),
|
|
1017
1465
|
(w.tags || []).length ? h("span", { className: "myt-tagRow" }, w.tags.map(function (t, j) { return h("span", { key: j, className: "myt-tag" }, t); })) : null),
|
|
1018
|
-
w.description ? h(
|
|
1466
|
+
w.description ? h(MdView, { text: w.description, className: "myt-worldDesc" }) : null);
|
|
1019
1467
|
}));
|
|
1020
|
-
})) : h(Empty, { text: "world
|
|
1468
|
+
})) : h(Empty, { text: tt("world.empty") })),
|
|
1021
1469
|
h("div", { className: "myt-card" },
|
|
1022
|
-
h("div", { className: "myt-cardHead" }, h("h3", { className: "myt-cardTitle" }, "
|
|
1470
|
+
h("div", { className: "myt-cardHead" }, h("h3", { className: "myt-cardTitle" }, tt("world.science")),
|
|
1023
1471
|
h("span", { className: "myt-boardCount" }, science.length)),
|
|
1024
1472
|
science.length ? h("div", { className: "myt-stack" }, science.map(function (s, i) {
|
|
1025
|
-
var label =
|
|
1473
|
+
var label = ["known", "extrapolation", "hypothesis"].indexOf(s.label) !== -1 ? s.label : null;
|
|
1026
1474
|
return h("div", { key: s.file || i, className: "myt-worldItem" },
|
|
1027
1475
|
h("div", { className: "myt-worldHead" },
|
|
1028
1476
|
h("span", { className: "myt-worldName" }, orDash(s.name)),
|
|
1029
1477
|
h("span", { className: "myt-badge myt-sciTag", "data-label": label || "hypothesis", title: s.label && !label ? s.label : undefined },
|
|
1030
|
-
label ?
|
|
1031
|
-
s.description ? h(
|
|
1478
|
+
label ? tt("sci." + label) : (s.label || tt("world.unlabeled")))),
|
|
1479
|
+
s.description ? h(MdView, { text: s.description, className: "myt-worldDesc" }) : null,
|
|
1032
1480
|
(s.references || []).length ? h("div", { className: "myt-refList" },
|
|
1033
|
-
s.references.map(function (r, j) { return h("span", { key: j, className: "myt-refItem" }, "
|
|
1034
|
-
})) : h(Empty, { text: "
|
|
1481
|
+
s.references.map(function (r, j) { return h("span", { key: j, className: "myt-refItem" }, tf("world.ref", { n: j + 1, r: r })); })) : null);
|
|
1482
|
+
})) : h(Empty, { text: tt("world.scienceEmpty") })),
|
|
1035
1483
|
research.length ? h("div", { className: "myt-card" },
|
|
1036
|
-
h("div", { className: "myt-cardHead" }, h("h3", { className: "myt-cardTitle" }, "
|
|
1484
|
+
h("div", { className: "myt-cardHead" }, h("h3", { className: "myt-cardTitle" }, tt("world.research")),
|
|
1037
1485
|
h("span", { className: "myt-boardCount" }, research.length)),
|
|
1038
1486
|
h("div", { className: "myt-tagRow" }, research.map(function (r, i) {
|
|
1039
1487
|
return h("span", { key: r.file || i, className: "myt-tag", title: r.file || "" }, orDash(r.topic) || r.file);
|
|
@@ -1054,29 +1502,31 @@ window.__ModuleLoader__.load({
|
|
|
1054
1502
|
}
|
|
1055
1503
|
var clues = snap.clues || [];
|
|
1056
1504
|
var memories = snap.memories || [];
|
|
1057
|
-
|
|
1505
|
+
// 无原型容器:kind/category 是数据文件自由文本,见 WorldTab 处说明。
|
|
1506
|
+
var clueGroups = Object.create(null);
|
|
1058
1507
|
clues.forEach(function (c) {
|
|
1059
|
-
var key = c.kind || "
|
|
1508
|
+
var key = c.kind || tt("common.uncategorized");
|
|
1060
1509
|
if (!clueGroups[key]) clueGroups[key] = [];
|
|
1061
1510
|
clueGroups[key].push(c);
|
|
1062
1511
|
});
|
|
1063
|
-
var memGroups =
|
|
1512
|
+
var memGroups = Object.create(null);
|
|
1064
1513
|
memories.forEach(function (m) {
|
|
1065
|
-
var key = m.category || "
|
|
1514
|
+
var key = m.category || tt("common.uncategorized");
|
|
1066
1515
|
if (!memGroups[key]) memGroups[key] = [];
|
|
1067
1516
|
memGroups[key].push(m);
|
|
1068
1517
|
});
|
|
1069
1518
|
function groupKey(a, b) { return a.localeCompare(b, "zh-CN"); }
|
|
1070
1519
|
return h("div", { className: "myt-stack" },
|
|
1071
1520
|
h("div", { className: "myt-cardHead" },
|
|
1072
|
-
h("h3", { className: "myt-cardTitle" }, "
|
|
1073
|
-
h("button", { type: "button", className: "myt-btn", disabled: rebuildBusy, onClick: rebuild, title: "
|
|
1074
|
-
rebuildBusy ? "
|
|
1521
|
+
h("h3", { className: "myt-cardTitle" }, tt("index.title")),
|
|
1522
|
+
h("button", { type: "button", className: "myt-btn", disabled: rebuildBusy, onClick: rebuild, title: tt("index.rebuildTip") },
|
|
1523
|
+
rebuildBusy ? tt("index.rebuilding") : tt("index.rebuild"))),
|
|
1075
1524
|
h("div", { className: "myt-indexCols" },
|
|
1076
1525
|
h("div", { className: "myt-card" },
|
|
1077
|
-
h("div", { className: "myt-cardHead" }, h("h3", { className: "myt-cardTitle" }, "
|
|
1526
|
+
h("div", { className: "myt-cardHead" }, h("h3", { className: "myt-cardTitle" }, tt("index.clueBoard")), h("span", { className: "myt-boardCount" }, clues.length)),
|
|
1078
1527
|
clues.length ? Object.keys(clueGroups).sort(groupKey).map(function (kind) {
|
|
1079
|
-
var
|
|
1528
|
+
var ck = CLUE_KIND_KEYS[kind];
|
|
1529
|
+
var label = ck ? tt("clue." + ck) : kind;
|
|
1080
1530
|
return h("div", { key: kind, className: "myt-worldGroup" },
|
|
1081
1531
|
h("div", { className: "myt-listHead" }, label, h("span", { className: "myt-boardCount" }, clueGroups[kind].length)),
|
|
1082
1532
|
clueGroups[kind].map(function (c, i) {
|
|
@@ -1084,21 +1534,21 @@ window.__ModuleLoader__.load({
|
|
|
1084
1534
|
h("span", { className: "myt-clueText" }, orDash(c.content)),
|
|
1085
1535
|
c.related_chapter ? h("span", { className: "myt-clueChapter" }, c.related_chapter) : null);
|
|
1086
1536
|
}));
|
|
1087
|
-
}) : h(Empty, { text: "
|
|
1537
|
+
}) : h(Empty, { text: tt("index.clueEmpty") })),
|
|
1088
1538
|
h("div", { className: "myt-card" },
|
|
1089
|
-
h("div", { className: "myt-cardHead" }, h("h3", { className: "myt-cardTitle" }, "
|
|
1539
|
+
h("div", { className: "myt-cardHead" }, h("h3", { className: "myt-cardTitle" }, tt("index.memBoard")), h("span", { className: "myt-boardCount" }, memories.length)),
|
|
1090
1540
|
memories.length ? Object.keys(memGroups).sort(groupKey).map(function (cat) {
|
|
1091
1541
|
var isPromise = /promise|承诺/i.test(cat);
|
|
1092
1542
|
return h("div", { key: cat, className: "myt-worldGroup" },
|
|
1093
1543
|
h("div", { className: "myt-listHead" },
|
|
1094
|
-
cat, isPromise ? h("span", { className: "myt-markerTag" }, "
|
|
1544
|
+
cat, isPromise ? h("span", { className: "myt-markerTag" }, tt("index.promise")) : null,
|
|
1095
1545
|
h("span", { className: "myt-boardCount" }, memGroups[cat].length)),
|
|
1096
1546
|
memGroups[cat].map(function (m, i) {
|
|
1097
1547
|
return h("div", { key: i, className: "myt-memItem", "data-promise": isPromise ? "true" : undefined },
|
|
1098
1548
|
h("span", { className: "myt-clueText" }, orDash(m.content)),
|
|
1099
1549
|
m.source_chapter ? h("span", { className: "myt-memChapter" }, m.source_chapter) : null);
|
|
1100
1550
|
}));
|
|
1101
|
-
}) : h(Empty, { text: "
|
|
1551
|
+
}) : h(Empty, { text: tt("index.memEmpty") }))));
|
|
1102
1552
|
}
|
|
1103
1553
|
|
|
1104
1554
|
// ── 关系图 tab:canvas 力导向(斥力 + 弹簧 + 中心引力,rAF 循环)─────────
|
|
@@ -1117,24 +1567,26 @@ window.__ModuleLoader__.load({
|
|
|
1117
1567
|
var _tip = useState(null), tip = _tip[0], setTip = _tip[1]; // {x, y, text}
|
|
1118
1568
|
var wrapRef = useRef(null);
|
|
1119
1569
|
var canvasRef = useRef(null);
|
|
1120
|
-
var
|
|
1570
|
+
var tipRafRef = useRef(0); // hover 提示 rAF 句柄:卸载/换图时取消在途合帧
|
|
1121
1571
|
// 数据准备:>30 角色只留 relations 相关节点;角色空时退化为关系端点名。
|
|
1122
|
-
|
|
1123
|
-
|
|
1572
|
+
// useMemo 缓存:hover 提示等重渲染不再重复建图(原渲染体内 IIFE 每帧重建)。
|
|
1573
|
+
// 容器用无原型对象:角色名/关系端点是数据文件自由文本,见 WorldTab 处说明。
|
|
1574
|
+
var graph = useMemo(function () {
|
|
1575
|
+
var byName = Object.create(null);
|
|
1124
1576
|
chars.forEach(function (c) { if (c && c.name && !byName[c.name]) byName[c.name] = c; });
|
|
1125
|
-
var deg =
|
|
1577
|
+
var deg = Object.create(null);
|
|
1126
1578
|
rels.forEach(function (r) { deg[r.a] = (deg[r.a] || 0) + 1; deg[r.b] = (deg[r.b] || 0) + 1; });
|
|
1127
1579
|
var names = chars.length ? chars.map(function (c) { return c.name; }) : Object.keys(deg);
|
|
1128
1580
|
var linked = names.filter(function (n) { return deg[n]; });
|
|
1129
1581
|
var drawNames = names.length > 30 ? linked : names;
|
|
1130
1582
|
var unlinked = names.filter(function (n) { return drawNames.indexOf(n) === -1; });
|
|
1131
1583
|
var nodes = drawNames.map(function (n) { return { id: n, role: byName[n] ? byName[n].role : null, x: 0, y: 0, vx: 0, vy: 0, fixed: false }; });
|
|
1132
|
-
var idx =
|
|
1584
|
+
var idx = Object.create(null);
|
|
1133
1585
|
nodes.forEach(function (nd, i) { idx[nd.id] = i; });
|
|
1134
1586
|
var edges = rels.filter(function (r) { return idx[r.a] !== undefined && idx[r.b] !== undefined; })
|
|
1135
1587
|
.map(function (r) { return { a: idx[r.a], b: idx[r.b], type: r.type || "", desc: r.description || "", w: edgeWidth(r.intensity) }; });
|
|
1136
1588
|
return { nodes: nodes, edges: edges, unlinked: unlinked };
|
|
1137
|
-
})
|
|
1589
|
+
}, [snap]); // chars/rels 均由 snap 派生,snap 不变则图不变
|
|
1138
1590
|
useEffect(function () {
|
|
1139
1591
|
var wrap = wrapRef.current;
|
|
1140
1592
|
var canvas = canvasRef.current;
|
|
@@ -1142,8 +1594,7 @@ window.__ModuleLoader__.load({
|
|
|
1142
1594
|
var ctx = canvas.getContext("2d");
|
|
1143
1595
|
if (!ctx) return undefined;
|
|
1144
1596
|
var dpr = (window.devicePixelRatio || 1) > 0 ? window.devicePixelRatio || 1 : 1;
|
|
1145
|
-
var sim = { zoom: 1, drag: null, raf: 0, frame: 0, w: 0, h: 0, tokens: null
|
|
1146
|
-
simRef.current = sim;
|
|
1597
|
+
var sim = { zoom: 1, drag: null, raf: 0, frame: 0, w: 0, h: 0, tokens: null };
|
|
1147
1598
|
// 令牌缓存:每 30 帧重读 computed style,浅/深色切换约半秒内跟上。
|
|
1148
1599
|
function readTokens() {
|
|
1149
1600
|
var cs = getComputedStyle(document.body);
|
|
@@ -1294,18 +1745,23 @@ window.__ModuleLoader__.load({
|
|
|
1294
1745
|
}
|
|
1295
1746
|
function onMove(e) {
|
|
1296
1747
|
var p = localXY(e);
|
|
1297
|
-
sim.mx = p.x; sim.my = p.y;
|
|
1298
1748
|
if (sim.drag) {
|
|
1299
1749
|
var w = toWorld(p.x, p.y);
|
|
1300
1750
|
sim.drag.node.x = w.x; sim.drag.node.y = w.y;
|
|
1301
1751
|
return;
|
|
1302
1752
|
}
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1753
|
+
// hover 提示 rAF 节流:mousemove 高频触发,合帧后再命中检测 + setTip,
|
|
1754
|
+
// 避免逐事件整 tab 重渲染(建图已 useMemo,这里再省掉无效重绘)。
|
|
1755
|
+
if (tipRafRef.current) cancelAnimationFrame(tipRafRef.current);
|
|
1756
|
+
tipRafRef.current = requestAnimationFrame(function () {
|
|
1757
|
+
tipRafRef.current = 0;
|
|
1758
|
+
var e2 = hitEdge(p.x, p.y);
|
|
1759
|
+
if (e2) {
|
|
1760
|
+
setTip({ x: p.x, y: p.y, text: (e2.type ? e2.type + ":" : "") + (e2.desc || tt("relations.noDesc")) });
|
|
1761
|
+
} else {
|
|
1762
|
+
setTip(function (prev) { return prev ? null : prev; });
|
|
1763
|
+
}
|
|
1764
|
+
});
|
|
1309
1765
|
}
|
|
1310
1766
|
function onUp() { sim.drag = null; canvas.dataset.dragging = "false"; }
|
|
1311
1767
|
function onWheel(e) {
|
|
@@ -1325,27 +1781,27 @@ window.__ModuleLoader__.load({
|
|
|
1325
1781
|
canvas.addEventListener("dblclick", onDbl);
|
|
1326
1782
|
return function () {
|
|
1327
1783
|
cancelAnimationFrame(sim.raf);
|
|
1784
|
+
if (tipRafRef.current) { cancelAnimationFrame(tipRafRef.current); tipRafRef.current = 0; }
|
|
1328
1785
|
if (ro) ro.disconnect();
|
|
1329
1786
|
canvas.removeEventListener("mousedown", onDown);
|
|
1330
1787
|
window.removeEventListener("mousemove", onMove);
|
|
1331
1788
|
window.removeEventListener("mouseup", onUp);
|
|
1332
1789
|
canvas.removeEventListener("wheel", onWheel);
|
|
1333
1790
|
canvas.removeEventListener("dblclick", onDbl);
|
|
1334
|
-
simRef.current = null;
|
|
1335
1791
|
};
|
|
1336
1792
|
}, [snap]);
|
|
1337
|
-
if (!graph.nodes.length) return h(Empty, { text: "
|
|
1793
|
+
if (!graph.nodes.length) return h(Empty, { text: tt("relations.noData") });
|
|
1338
1794
|
return h("div", { className: "myt-stack" },
|
|
1339
1795
|
h("div", { className: "myt-card" },
|
|
1340
1796
|
h("div", { className: "myt-cardHead" },
|
|
1341
|
-
h("h3", { className: "myt-cardTitle" }, "
|
|
1342
|
-
h("span", { className: "myt-muted" }, graph.nodes.length
|
|
1797
|
+
h("h3", { className: "myt-cardTitle" }, tt("relations.title")),
|
|
1798
|
+
h("span", { className: "myt-muted" }, tf("relations.counts", { n: graph.nodes.length, m: graph.edges.length }))),
|
|
1343
1799
|
h("div", { className: "myt-graphWrap", ref: wrapRef },
|
|
1344
1800
|
h("canvas", { className: "myt-graphCanvas", ref: canvasRef, "data-dragging": "false" }),
|
|
1345
1801
|
tip ? h("div", { className: "myt-graphTip", style: { left: Math.min(tip.x + 12, 320) + "px", top: Math.max(tip.y - 34, 4) + "px" } }, tip.text) : null),
|
|
1346
|
-
h("div", { className: "myt-graphFoot" }, "
|
|
1802
|
+
h("div", { className: "myt-graphFoot" }, tt("relations.help"))),
|
|
1347
1803
|
graph.unlinked.length ? h("div", { className: "myt-card" },
|
|
1348
|
-
h("div", { className: "myt-cardHead" }, h("h3", { className: "myt-cardTitle" }, "
|
|
1804
|
+
h("div", { className: "myt-cardHead" }, h("h3", { className: "myt-cardTitle" }, tt("relations.unlinkedTitle"))),
|
|
1349
1805
|
h("div", { className: "myt-unlinked" }, graph.unlinked.map(function (n, i) {
|
|
1350
1806
|
return h("span", { key: n || i, className: "myt-unlinkedTag" }, n);
|
|
1351
1807
|
}))) : null);
|