dsh-maoying 0.1.13 → 0.1.15
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 +3 -3
- package/README.md +3 -3
- package/docs/creator-panel.md +19 -9
- package/docs/design.md +2 -2
- package/index.mjs +56 -2
- package/lib/base.css +45 -2
- package/lib/client.js +338 -48
- package/lib/novel-parse.mjs +6 -2
- package/lib/panel-core.mjs +135 -10
- package/lib/self-update.mjs +263 -0
- package/lib/version.mjs +11 -0
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -53,17 +53,22 @@ window.__ModuleLoader__.load({
|
|
|
53
53
|
var CSS_KEY = "maoying-creator/base";
|
|
54
54
|
var WS_STORAGE_KEY = "maoying.workspace";
|
|
55
55
|
var API_TIMEOUT_MS = 60000;
|
|
56
|
+
// 一键升级单独放宽:服务端 pnpm add 超时上限 180s,统一 60s 会先掐断,
|
|
57
|
+
// 故 self-update 请求走 200s(留 20s 余量),其余请求仍用 API_TIMEOUT_MS。
|
|
58
|
+
var SELF_UPDATE_TIMEOUT_MS = 200000;
|
|
56
59
|
|
|
57
60
|
var STATUS_FLOW = ["pending", "writing", "review", "accepted"];
|
|
58
61
|
var FS_ORDER = ["planted", "progressing", "resolved", "abandoned"];
|
|
59
62
|
var FS_FLOW = { planted: "progressing", progressing: "resolved" };
|
|
60
63
|
var PHASES = ["idea", "setting", "outline", "writing", "review", "consistency", "export"];
|
|
61
|
-
//
|
|
62
|
-
|
|
64
|
+
// clue.* 词典键是小驼峰,数据里的 kind 按契约(templates/novel/README.md、
|
|
65
|
+
// docs/spec.md)是 clue|red-herring|deduction|question——此前键表误按
|
|
66
|
+
// snake_case + inference 写,红鲱鱼/推理两组永远查不中、只能裸显英文原值。
|
|
67
|
+
// 尾两个旧键是历史手写数据的容错别名,契约值优先。
|
|
68
|
+
var CLUE_KIND_KEYS = { clue: "clue", "red-herring": "redHerring", deduction: "inference", question: "question", red_herring: "redHerring", inference: "inference" };
|
|
63
69
|
var TABS = [
|
|
64
70
|
["overview", "tabs.overview"], ["chapters", "tabs.chapters"], ["characters", "tabs.characters"],
|
|
65
71
|
["foreshadows", "tabs.foreshadows"], ["relations", "tabs.relations"], ["timeline", "tabs.timeline"],
|
|
66
|
-
["world", "tabs.world"], ["index", "tabs.index"], ["settings", "tabs.settings"],
|
|
67
72
|
];
|
|
68
73
|
|
|
69
74
|
// 角色徽章四色(role 是自由文本):按关键词归 tone,命中即返。
|
|
@@ -74,10 +79,15 @@ window.__ModuleLoader__.load({
|
|
|
74
79
|
if (/配角|支持|盟友|friend|ally/i.test(r)) return "c";
|
|
75
80
|
return "d";
|
|
76
81
|
}
|
|
82
|
+
// 契约四态 role(protagonist|antagonist|supporting|minor)按界面语言出标签,
|
|
83
|
+
// role 是自由文本,契约外取值原样展示不翻译。
|
|
84
|
+
var ROLE_CONTRACT = { protagonist: 1, antagonist: 1, supporting: 1, minor: 1 };
|
|
77
85
|
function roleBadge(role) {
|
|
78
86
|
var text = String(role || "").trim();
|
|
79
87
|
if (!text) return null;
|
|
80
|
-
|
|
88
|
+
var key = text.toLowerCase();
|
|
89
|
+
if (!ROLE_CONTRACT[key]) key = null;
|
|
90
|
+
return h("span", { className: "myt-badge myt-roleBadge", "data-tone": roleTone(text) }, key ? tt("role." + key) : text);
|
|
81
91
|
}
|
|
82
92
|
function statusBadge(status) {
|
|
83
93
|
var s = STATUS_FLOW.indexOf(status) !== -1 ? status : "pending";
|
|
@@ -113,6 +123,15 @@ window.__ModuleLoader__.load({
|
|
|
113
123
|
var num = typeof ch.num === "number" ? tf("common.chapterNum", { n: ch.num }) : "";
|
|
114
124
|
return ch.volume ? String(ch.volume) + " · " + num : num;
|
|
115
125
|
}
|
|
126
|
+
// 孤儿章纲的「卷-章」定位(0.1.15):从章纲文件名 stem(v01-c003.md)
|
|
127
|
+
// 反推,显示格式与 chWhere 对齐;不合规范的文件名返回空串(调用方退回
|
|
128
|
+
// 文件名兜底显示)。
|
|
129
|
+
function olWhere(ol) {
|
|
130
|
+
var stem = ol && ol.chapter ? String(ol.chapter).replace(/\.md$/i, "") : "";
|
|
131
|
+
var m = /^v(\d+)-c(\d+)$/i.exec(stem);
|
|
132
|
+
if (!m) return "";
|
|
133
|
+
return "v" + m[1] + " · " + tf("common.chapterNum", { n: Number(m[2]) });
|
|
134
|
+
}
|
|
116
135
|
function chFullTitle(ch) {
|
|
117
136
|
if (!ch) return "";
|
|
118
137
|
var where = chWhere(ch);
|
|
@@ -156,7 +175,9 @@ window.__ModuleLoader__.load({
|
|
|
156
175
|
|
|
157
176
|
// ── 服务端 API 助手(同源 /api/maoying/*,非 2xx 抛错)─────────────────
|
|
158
177
|
// 错误统一 toast(读路由调用方另留卡内错误态);总超时防按钮永久转圈。
|
|
159
|
-
function maoyingApi(path, body) {
|
|
178
|
+
function maoyingApi(path, body, timeoutMs) {
|
|
179
|
+
// timeoutMs 缺省用统一 60s;一键升级单独放宽到 200s(见常量处注释)。
|
|
180
|
+
var limit = typeof timeoutMs === "number" && timeoutMs > 0 ? timeoutMs : API_TIMEOUT_MS;
|
|
160
181
|
var opts = body === undefined ? {} : {
|
|
161
182
|
method: "POST",
|
|
162
183
|
headers: { "content-type": "application/json" },
|
|
@@ -166,19 +187,25 @@ window.__ModuleLoader__.load({
|
|
|
166
187
|
var timer = null;
|
|
167
188
|
if (ctrl) {
|
|
168
189
|
opts.signal = ctrl.signal;
|
|
169
|
-
timer = setTimeout(function () { ctrl.abort(); },
|
|
190
|
+
timer = setTimeout(function () { ctrl.abort(); }, limit);
|
|
170
191
|
}
|
|
171
192
|
return fetch(API_BASE + path, opts).then(function (r) {
|
|
172
193
|
// 先取全文再 JSON.parse:代理错误页 / 空体不再抛 SyntaxError。
|
|
173
194
|
return r.text().then(function (raw) {
|
|
174
195
|
var d = null;
|
|
175
196
|
try { d = raw ? JSON.parse(raw) : null; } catch (err) { d = null; }
|
|
176
|
-
if (!r.ok)
|
|
197
|
+
if (!r.ok) {
|
|
198
|
+
// 错误对象带状态码:调用方按码分流(如设置保存只在 409 外部改动
|
|
199
|
+
// 冲突时重置表单,其余失败保留用户正在编辑的内容)。
|
|
200
|
+
var e = new Error((d && d.error) || ("HTTP " + r.status));
|
|
201
|
+
e.status = r.status;
|
|
202
|
+
throw e;
|
|
203
|
+
}
|
|
177
204
|
if (!d) throw new Error(tf("api.notJson", { code: r.status }));
|
|
178
205
|
return d;
|
|
179
206
|
});
|
|
180
207
|
}).catch(function (err) {
|
|
181
|
-
if (ctrl && err && err.name === "AbortError") throw new Error(tf("api.timeout", { s:
|
|
208
|
+
if (ctrl && err && err.name === "AbortError") throw new Error(tf("api.timeout", { s: Math.round(limit / 1000) }));
|
|
182
209
|
throw err;
|
|
183
210
|
}).finally(function () {
|
|
184
211
|
if (timer !== null) clearTimeout(timer);
|
|
@@ -209,6 +236,7 @@ window.__ModuleLoader__.load({
|
|
|
209
236
|
function apiPostOkText(path, d) {
|
|
210
237
|
if (path === "rebuild-index") return tf("toast.indexRebuilt", { n: fmtNum(d && d.chapters) });
|
|
211
238
|
if (path === "chapter-status") return tt("toast.chapterStatus");
|
|
239
|
+
if (path === "chapter-create") return tf("toast.chapterCreated", { title: (d && d.title) || "—" });
|
|
212
240
|
if (path === "foreshadow-status") return tt("toast.foreshadowStatus");
|
|
213
241
|
if (path === "phase") return tf("toast.phaseAdvanced", { phase: d && d.phase && PHASES.indexOf(d.phase) !== -1 ? tt("phase." + d.phase) : (d && d.phase) || "—" });
|
|
214
242
|
return tt("toast.success");
|
|
@@ -507,7 +535,8 @@ window.__ModuleLoader__.load({
|
|
|
507
535
|
}
|
|
508
536
|
|
|
509
537
|
// ── 页面骨架:顶栏(工作区下拉 + 刷新 + 关闭)+ tab 条 + 内容区 ─────────
|
|
510
|
-
// 数据流:workspaces
|
|
538
|
+
// 数据流:workspaces 每次打开面板都重拉(懒挂载下关闭不卸载,见下方
|
|
539
|
+
// activate 监听);state 跟随选中工作区;写操作 POST 成功后
|
|
511
540
|
// 整份重拉 state(不乐观更新)。选择记 localStorage('maoying.workspace')。
|
|
512
541
|
function PageShell(props) {
|
|
513
542
|
var onClose = props.onClose;
|
|
@@ -601,7 +630,50 @@ window.__ModuleLoader__.load({
|
|
|
601
630
|
if (d && d.current) setVer(d); // 常态也显示当前版本号;有新版再亮徽章
|
|
602
631
|
}).catch(function () {});
|
|
603
632
|
}, []);
|
|
633
|
+
// 一键升级状态机:""(可点)→ "installing"(安装中,按钮禁用)→
|
|
634
|
+
// "installed"(已装完待重启,按钮永久禁用、新版徽章收起)。可预期失败
|
|
635
|
+
// (dev-link / pnpm-missing / pnpm 出错)回落 "" 允许重试。
|
|
636
|
+
var _up = useState(""), upState = _up[0], setUpState = _up[1];
|
|
637
|
+
function runSelfUpdate() {
|
|
638
|
+
if (upState !== "") return;
|
|
639
|
+
setUpState("installing");
|
|
640
|
+
// 不走 apiPost:成功提示要带新版本号、失败按 kind 分流文案,且超时
|
|
641
|
+
// 单独放宽(服务端 pnpm 最长 180s,统一 60s 会先掐断,见常量处注释)。
|
|
642
|
+
maoyingApi("self-update", {}, SELF_UPDATE_TIMEOUT_MS).then(function (d) {
|
|
643
|
+
if (d && d.ok) {
|
|
644
|
+
setUpState("installed");
|
|
645
|
+
// 硬约束照实说:装完只是磁盘换新,运行中的进程仍是旧代码。
|
|
646
|
+
toast.ok(tf("toast.selfUpdate.ok", { ver: (d && d.to) || "?" }));
|
|
647
|
+
} else if (d && d.kind === "dev-link") {
|
|
648
|
+
setUpState("");
|
|
649
|
+
toast.warn(tt("toast.selfUpdate.devLink"));
|
|
650
|
+
} else if (d && d.kind === "pnpm-missing") {
|
|
651
|
+
setUpState("");
|
|
652
|
+
toast.warn(tt("toast.selfUpdate.pnpmMissing"));
|
|
653
|
+
} else {
|
|
654
|
+
setUpState("");
|
|
655
|
+
toast.err(tf("toast.prefix", { msg: (d && d.error) || tt("common.unknownErr") }));
|
|
656
|
+
}
|
|
657
|
+
}).catch(function (err) {
|
|
658
|
+
setUpState("");
|
|
659
|
+
toast.err(tf("toast.prefix", { msg: err.message }));
|
|
660
|
+
});
|
|
661
|
+
}
|
|
604
662
|
useEffect(function () { loadWorkspaces(); }, [loadWorkspaces]);
|
|
663
|
+
// 每次打开面板都重拉工作区列表:懒挂载下关闭只是摘掉 active 属性,
|
|
664
|
+
// 组件不卸载,上面的 mount effect 整个页面生命周期只跑一次,期间外部
|
|
665
|
+
// (CLI / 其他面板)新建或删除的工作区在重开时看不见。mountPanel 每次
|
|
666
|
+
// 打开都会广播 dsh-panel-activate(source: "maoying"),借它触发重拉;
|
|
667
|
+
// 首次打开的广播早于本组件挂载(render 异步调度),收不到也无妨——
|
|
668
|
+
// mount effect 已覆盖首拉。其余面板的同名广播按 source 过滤掉。
|
|
669
|
+
useEffect(function () {
|
|
670
|
+
var onActivate = function (event) {
|
|
671
|
+
var source = event && event.detail && event.detail.source;
|
|
672
|
+
if (source === "maoying") loadWorkspaces();
|
|
673
|
+
};
|
|
674
|
+
document.addEventListener(ACTIVATE_EVENT, onActivate);
|
|
675
|
+
return function () { document.removeEventListener(ACTIVATE_EVENT, onActivate); };
|
|
676
|
+
}, [loadWorkspaces]);
|
|
605
677
|
useEffect(function () { loadState(effectiveWs); }, [effectiveWs, loadState]);
|
|
606
678
|
// 校正后的选择回写记忆(首次打开 / 记忆失效时自动落位)。
|
|
607
679
|
useEffect(function () {
|
|
@@ -649,7 +721,6 @@ window.__ModuleLoader__.load({
|
|
|
649
721
|
else if (tab === "timeline") body = h(TimelineTab, tabProps);
|
|
650
722
|
else if (tab === "world") body = h(WorldTab, tabProps);
|
|
651
723
|
else if (tab === "index") body = h(IndexTab, tabProps);
|
|
652
|
-
else if (tab === "settings") body = h(SettingsTab, tabProps);
|
|
653
724
|
else body = h(OverviewTab, Object.assign({ gotoTab: gotoTab }, tabProps));
|
|
654
725
|
}
|
|
655
726
|
|
|
@@ -659,8 +730,22 @@ window.__ModuleLoader__.load({
|
|
|
659
730
|
h("h1", { className: "myt-title" }, "毛颖创作台",
|
|
660
731
|
ver && ver.current ? h("span", { className: "myt-verTag", title: tt("header.curVerTitle") }, ver.current) : null,
|
|
661
732
|
ver && ver.updateAvailable
|
|
662
|
-
?
|
|
663
|
-
|
|
733
|
+
? [
|
|
734
|
+
// 徽章(跳发布页)+「一键升级」按钮双元素;装完(installed)
|
|
735
|
+
// 徽章收起——磁盘已是新版,别再引导去发布页——按钮变
|
|
736
|
+
// 「已安装,待重启」保持禁用,重启 DeepSeek 换新进程后才消失。
|
|
737
|
+
upState !== "installed"
|
|
738
|
+
? h("a", { key: "b", className: "myt-verBadge", href: ver.releaseUrl || "#", target: "_blank", rel: "noreferrer", title: tf("header.upgradeTip", { cur: ver.current }) },
|
|
739
|
+
tf("header.newVersion", { ver: ver.latest }))
|
|
740
|
+
: null,
|
|
741
|
+
h("button", {
|
|
742
|
+
key: "u", type: "button", className: "myt-verUpgrade",
|
|
743
|
+
disabled: upState !== "",
|
|
744
|
+
title: upState === "installed" ? tt("header.upgradedTitle") : tt("header.upgradeTitle"),
|
|
745
|
+
onClick: runSelfUpdate,
|
|
746
|
+
},
|
|
747
|
+
upState === "installing" ? tt("header.upgrading") : upState === "installed" ? tt("header.upgraded") : tt("header.upgradeBtn")),
|
|
748
|
+
]
|
|
664
749
|
: null),
|
|
665
750
|
h("p", { className: "myt-subtitle" }, tt("header.subtitle"))),
|
|
666
751
|
h("div", { className: "myt-headActions" },
|
|
@@ -700,17 +785,53 @@ window.__ModuleLoader__.load({
|
|
|
700
785
|
warnings.map(function (w, i) { return h("div", { key: i }, "· " + String(w)); })));
|
|
701
786
|
}
|
|
702
787
|
|
|
703
|
-
//
|
|
788
|
+
// 静态体检黄条:checkProject 的原始问题清单(快照侧会把越枚举 status
|
|
789
|
+
// 归一成 pending,人工改坏在快照里看不出来,这里照 CLI check 同源透出)。
|
|
790
|
+
// 含 error 级问题时叠加强调 class(纯 CSS,见 base.css)。
|
|
791
|
+
function CheckStrip(problems) {
|
|
792
|
+
var errs = 0;
|
|
793
|
+
for (var i = 0; i < problems.length; i++) { if (problems[i] && problems[i].level === "error") errs++; }
|
|
794
|
+
return h("div", { className: "myt-strip myt-stripWarn" + (errs > 0 ? " myt-stripCheckErr" : "") },
|
|
795
|
+
h("span", { className: "myt-stripIcon" }, ic("IconWarningOutlineRegular", 14)),
|
|
796
|
+
h("span", { className: "myt-stripText" },
|
|
797
|
+
h("div", null, h("strong", null, tf("check.title", { n: fmtNum(problems.length), err: fmtNum(errs), warn: fmtNum(problems.length - errs) }))),
|
|
798
|
+
problems.slice(0, 8).map(function (p, i) {
|
|
799
|
+
return h("div", { key: i }, "· [" + String((p && p.level) || "") + "] " + String((p && p.file) || "") + ": " + String((p && p.message) || ""));
|
|
800
|
+
}),
|
|
801
|
+
h("div", null, tt("check.fullHint"))));
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// ── 概览 tab:project 卡(含「编辑」书稿元信息,原小说设置并入)+ 统计卡
|
|
805
|
+
// + 最近更新 ─────────────────────────────────────────────────────────────
|
|
704
806
|
function OverviewTab(props) {
|
|
705
807
|
var snap = props.snap;
|
|
706
808
|
var reload = props.reload;
|
|
707
809
|
var gotoTab = props.gotoTab;
|
|
810
|
+
var wsPath = props.wsPath;
|
|
708
811
|
var project = snap.project || {};
|
|
812
|
+
// 编辑态:true 时 hero 卡切换为书稿元信息表单(ProjectMetaForm)。
|
|
813
|
+
var _ed = useState(false), editing = _ed[0], setEditing = _ed[1];
|
|
709
814
|
var totals = snap.totals || {};
|
|
710
815
|
var byStatus = totals.byStatus || {};
|
|
711
816
|
var _b = useState(false), phaseBusy = _b[0], setPhaseBusy = _b[1];
|
|
712
817
|
var phaseIdx = PHASES.indexOf(project.phase);
|
|
713
818
|
var nextPhase = phaseIdx !== -1 && phaseIdx < PHASES.length - 1 ? PHASES[phaseIdx + 1] : null;
|
|
819
|
+
// 静态体检黄条:仅 found 时拉 check 路由(未初始化没有可查的东西)。
|
|
820
|
+
// 工作区切换与每次 reload 都换新 snap → effect 重跑即重查;请求序号防
|
|
821
|
+
// 过期响应(快速切工作区时在途旧响应不得落进新工作区的黄条)。
|
|
822
|
+
var _ck = useState(null), checkProblems = _ck[0], setCheckProblems = _ck[1];
|
|
823
|
+
var checkSeqRef = useRef(0);
|
|
824
|
+
useEffect(function () {
|
|
825
|
+
if (!snap || !snap.found || !wsPath) { setCheckProblems(null); return; }
|
|
826
|
+
var seq = ++checkSeqRef.current; // 先自增再发请求;早退分支也作废在途旧响应
|
|
827
|
+
apiGet("check", { path: wsPath }).then(function (d) {
|
|
828
|
+
if (seq !== checkSeqRef.current) return; // 过期响应:已有更新的请求在途,丢弃
|
|
829
|
+
setCheckProblems(d && d.ok && Array.isArray(d.problems) ? d.problems : []);
|
|
830
|
+
}).catch(function () {
|
|
831
|
+
if (seq !== checkSeqRef.current) return;
|
|
832
|
+
setCheckProblems(null); // 拉取失败不显示黄条(toast 已由 apiGet 统一提示)
|
|
833
|
+
});
|
|
834
|
+
}, [snap, wsPath]);
|
|
714
835
|
function advancePhase() {
|
|
715
836
|
if (!nextPhase || phaseBusy) return;
|
|
716
837
|
setPhaseBusy(true);
|
|
@@ -718,17 +839,25 @@ window.__ModuleLoader__.load({
|
|
|
718
839
|
}
|
|
719
840
|
var recent = (snap.chapters || []).slice().sort(function (a, b) { return (b.num || 0) - (a.num || 0); }).slice(0, 5);
|
|
720
841
|
return h("div", { className: "myt-stack" },
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
842
|
+
checkProblems && checkProblems.length ? CheckStrip(checkProblems) : null,
|
|
843
|
+
editing
|
|
844
|
+
// 编辑态:hero 卡整体换成元信息表单(保存/取消在表单卡内)。
|
|
845
|
+
? h(ProjectMetaForm, { snap: snap, wsPath: wsPath, reload: reload, onDone: function () { setEditing(false); } })
|
|
846
|
+
: h("div", { className: "myt-card myt-hero" },
|
|
847
|
+
[
|
|
848
|
+
h("div", { className: "myt-heroMeta", key: "meta" },
|
|
849
|
+
project.author ? h("span", { className: "myt-muted" }, tf("overview.author", { name: project.author })) : null,
|
|
850
|
+
project.genre ? h("span", { className: "myt-badge" }, project.genre) : null,
|
|
851
|
+
project.length ? h("span", { className: "myt-badge" }, project.length) : null,
|
|
852
|
+
project.language ? h("span", { className: "myt-badge" }, project.language) : null,
|
|
853
|
+
h("button", { type: "button", className: "myt-btn myt-heroEdit", title: tt("overview.editTip"), "aria-label": tt("overview.editTip"), onClick: function () { setEditing(true); } },
|
|
854
|
+
ic("IconEditOutlineRegular", 13), tt("overview.edit"))),
|
|
855
|
+
h("h2", { className: "myt-heroTitle", key: "title" }, orDash(project.title)),
|
|
856
|
+
h("div", { className: "myt-heroMeta", key: "phase" },
|
|
857
|
+
phasePill(project.phase),
|
|
858
|
+
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)),
|
|
859
|
+
project.description ? h("p", { className: "myt-heroDesc", key: "desc" }, project.description) : null,
|
|
860
|
+
]),
|
|
732
861
|
h("div", { className: "myt-statRow" },
|
|
733
862
|
h("div", { className: "myt-card myt-stat" }, h("span", { className: "myt-statNum" }, fmtNum(totals.words)), h("span", { className: "myt-statLabel" }, tt("overview.totalWords"))),
|
|
734
863
|
h("div", { className: "myt-card myt-stat" }, h("span", { className: "myt-statNum" }, fmtNum(totals.chapters)), h("span", { className: "myt-statLabel" }, tt("overview.totalChapters"))),
|
|
@@ -876,11 +1005,16 @@ window.__ModuleLoader__.load({
|
|
|
876
1005
|
zh: {
|
|
877
1006
|
"shell.title": "毛颖创作台", "header.subtitle": "网文项目可视化与状态流转 · 只读正文,写状态",
|
|
878
1007
|
"header.curVerTitle": "当前版本", "header.upgradeTip": "当前 {cur},点击打开新版发布页", "header.newVersion": "有新版 {ver} ↗",
|
|
1008
|
+
"header.upgradeBtn": "一键升级", "header.upgrading": "安装中…", "header.upgraded": "已安装,待重启",
|
|
1009
|
+
"header.upgradeTitle": "用 pnpm 安装 npm 最新版(最长约 3 分钟),装完需重启 DeepSeek 生效",
|
|
1010
|
+
"header.upgradedTitle": "已安装新版,重启 DeepSeek 后生效",
|
|
1011
|
+
"toast.selfUpdate.ok": "已安装 {ver},重启 DeepSeek 后生效",
|
|
1012
|
+
"toast.selfUpdate.devLink": "开发目录安装,请 git pull 更新", "toast.selfUpdate.pnpmMissing": "未找到 pnpm,请手动安装",
|
|
879
1013
|
"header.workspace": "工作区", "header.wsAria": "选择工作区", "header.noNovel": "(无 novel)", "header.untitled": "未命名",
|
|
880
1014
|
"header.settings": "系统设置", "header.refresh": "重新拉取", "header.closeAria": "关闭创作台", "header.closeTitle": "关闭",
|
|
881
1015
|
"tabs.aria": "创作台分区",
|
|
882
1016
|
"tabs.overview": "概览", "tabs.chapters": "章节", "tabs.characters": "角色", "tabs.foreshadows": "伏笔", "tabs.relations": "关系图",
|
|
883
|
-
"tabs.timeline": "时间线", "tabs.world": "设定", "tabs.index": "索引板",
|
|
1017
|
+
"tabs.timeline": "时间线", "tabs.world": "设定", "tabs.index": "索引板",
|
|
884
1018
|
"shell.wsListFailed": "工作区列表拉取失败:{msg}", "shell.stateFailed": "快照拉取失败:{msg}",
|
|
885
1019
|
"guide.noWsTitle": "还没有可用的小说工作区", "guide.noWsDesc": "任一工作区根目录下出现 novel/project.yml 即自动入选;先用命令初始化目录骨架:",
|
|
886
1020
|
"guide.notInitTitle": "该工作区尚未初始化小说项目", "guide.notInitDesc": "工作区根目录下没有 novel/project.yml。在该工作区根目录执行:",
|
|
@@ -890,7 +1024,7 @@ window.__ModuleLoader__.load({
|
|
|
890
1024
|
"common.closeTip": "关闭提示", "common.unknownErr": "未知错误", "common.priorityTitle": "优先级:{pri}", "common.priorityAria": "优先级{pri}",
|
|
891
1025
|
"common.chapterNum": "第 {n} 章", "pri.high": "高", "pri.normal": "中", "pri.low": "低",
|
|
892
1026
|
"toast.prefix": "毛颖创作台:{msg}", "toast.success": "操作成功", "toast.indexRebuilt": "索引已重建({n} 章)",
|
|
893
|
-
"toast.chapterStatus": "章节状态已更新", "toast.foreshadowStatus": "伏笔状态已更新", "toast.phaseAdvanced": "阶段已推进到「{phase}」",
|
|
1027
|
+
"toast.chapterStatus": "章节状态已更新", "toast.chapterCreated": "已从章纲建章「{title}」(待写)", "toast.foreshadowStatus": "伏笔状态已更新", "toast.phaseAdvanced": "阶段已推进到「{phase}」",
|
|
894
1028
|
"api.notJson": "响应不是 JSON(HTTP {code})", "api.timeout": "请求超时({s} 秒)",
|
|
895
1029
|
"status.pending": "待写", "status.writing": "写作中", "status.review": "审校中", "status.accepted": "已定稿", "status.rawSuffix": "({raw})",
|
|
896
1030
|
"phase.idea": "立意", "phase.setting": "设定", "phase.outline": "大纲", "phase.writing": "写作", "phase.review": "审校",
|
|
@@ -906,6 +1040,7 @@ window.__ModuleLoader__.load({
|
|
|
906
1040
|
"clue.clue": "线索", "clue.redHerring": "红鲱鱼", "clue.inference": "推理", "clue.question": "疑问",
|
|
907
1041
|
"sci.known": "已确证", "sci.extrapolation": "推演", "sci.hypothesis": "假说", "world.unlabeled": "未标级",
|
|
908
1042
|
"warnings.title": "部分文件解析异常(已跳过):",
|
|
1043
|
+
"check.title": "静态体检:{n} 项(错误 {err} / 警告 {warn})", "check.fullHint": "全量结果请在终端运行 maoying check 查看",
|
|
909
1044
|
"index.title": "索引板(clue-board / memories)", "index.rebuild": "重建索引", "index.rebuilding": "重建中…",
|
|
910
1045
|
"index.rebuildTip": "按 CLI index 同一逻辑重建 index.yml", "index.clueBoard": "线索板", "index.clueEmpty": "clue-board.yml 还没有条目",
|
|
911
1046
|
"index.memBoard": "记忆板", "index.memEmpty": "memories.yml 还没有条目", "index.promise": "承诺线",
|
|
@@ -917,28 +1052,34 @@ window.__ModuleLoader__.load({
|
|
|
917
1052
|
"relations.unlinkedTitle": "未入图角色(无关系边)", "relations.noData": "没有角色或关系数据,无法绘图",
|
|
918
1053
|
"relations.edgeTipPrefix": "{type}:", "relations.noDesc": "(无描述)",
|
|
919
1054
|
"char.empty": "characters/ 目录还没有角色卡",
|
|
1055
|
+
"role.protagonist": "主角", "role.antagonist": "反派", "role.supporting": "配角", "role.minor": "龙套",
|
|
920
1056
|
"char.field.age": "年龄", "char.field.gender": "性别", "char.field.appearance": "外貌", "char.field.personality": "性格",
|
|
921
1057
|
"char.field.background": "背景", "char.field.motivation": "动机", "char.field.arc": "成长弧",
|
|
922
1058
|
"char.years": "{n} 岁", "char.linePersonality": "性格 ", "char.lineMotivation": "动机 ",
|
|
923
1059
|
"char.modalAria": "角色详情", "char.closeAria": "关闭角色卡", "char.markers": "标记", "char.file": "文件",
|
|
924
1060
|
"overview.author": "作者:{name}", "overview.totalWords": "总字数(CJK)", "overview.totalChapters": "章节总数",
|
|
925
1061
|
"overview.characters": "角色数", "overview.openForeshadows": "未回收伏笔", "overview.statusDist": "章节状态分布", "overview.recent": "最近章节",
|
|
1062
|
+
"overview.edit": "编辑", "overview.editTip": "编辑书稿元信息",
|
|
926
1063
|
"chapters.empty": "chapters/ 目录还没有章节文件", "chapters.writeQueue": "按单写作", "chapters.rowTip": "点击打开该章",
|
|
927
1064
|
"chapters.outlineWords": "章纲 {n} 字", "chapters.all": "全部", "chapters.thVolume": "卷 · 章", "chapters.thTitle": "标题",
|
|
928
1065
|
"chapters.thStatus": "状态", "chapters.thWords": "字数", "chapters.thSummary": "梗概", "chapters.rowReadTip": "点击阅读正文",
|
|
929
1066
|
"chapters.emptyFiltered": "该状态下没有章节",
|
|
1067
|
+
"chapters.outlineRowTip": "点击查看章纲全文", "chapters.noChapter": "未建章", "chapters.createBtn": "建章", "chapters.creating": "建章中…",
|
|
1068
|
+
"chapters.createTip": "在 chapters/ 下按章纲建一章(status=pending,正文待写)",
|
|
1069
|
+
"chapters.emptyWithOutlines": "chapters/ 目录还没有章节文件;下方「按单写作」已列出未建章章纲",
|
|
930
1070
|
"reader.back": "返回", "reader.backTip": "返回章节列表", "reader.copyPath": "复制路径", "reader.copyPathTip": "复制章节文件路径",
|
|
931
1071
|
"reader.copied": "已复制章节文件路径", "reader.copyFailed": "复制失败,请手动选择路径文本", "reader.loadFailed": "正文读取失败:{msg}",
|
|
932
1072
|
"reader.noOutline": "暂无章纲", "reader.emptyBody": "(空章节)", "reader.finalState": "已定稿(终态)", "reader.statusFlow": "状态流转:",
|
|
933
1073
|
"reader.flowing": "流转中…", "reader.advanceTo": "推进到「{next}」",
|
|
934
1074
|
"outline.title": "章纲对照", "outline.empty": "(章纲为空)", "outline.collapseTip": "收起章纲", "outline.expandTip": "展开章纲",
|
|
1075
|
+
"outline.modalAria": "章纲全文", "outline.closeAria": "关闭章纲", "outline.loading": "章纲加载中…", "outline.loadFailed": "章纲读取失败:{msg}",
|
|
935
1076
|
"novel.metaTitle": "书稿元信息", "novel.metaNote": "对应 novel/project.yml · 行尾注释会原样保留",
|
|
936
1077
|
"novel.fTitle": "书名", "novel.titlePh": "如:长夜余火", "novel.fAuthor": "作者", "novel.authorPh": "笔名",
|
|
937
1078
|
"novel.fGenre": "题材", "novel.genrePh": "如:东方玄幻 / 都市异能 / 历史架空", "novel.fLength": "预期篇幅",
|
|
938
1079
|
"novel.lengthPh": "如:约100万字 / 300章", "novel.fLanguage": "正文语言", "novel.fPhase": "工作流阶段", "novel.fMode": "共创模式",
|
|
939
1080
|
"novel.fDesc": "一句话简介", "novel.descPh": "写封面文案时可直接引用", "novel.titleRequired": "书名不能为空",
|
|
940
1081
|
"mode.aiDraft": "AI 代笔(AI 写正文)", "mode.coWrite": "逐章共创(按章指派谁写)", "mode.authorDraft": "作者亲笔(AI 只备料审稿)",
|
|
941
|
-
"novel.saving": "保存中…", "novel.save": "保存设置",
|
|
1082
|
+
"novel.saving": "保存中…", "novel.save": "保存设置", "novel.cancel": "取消",
|
|
942
1083
|
"settings.title": "系统设置", "settings.closeAria": "关闭设置", "settings.language": "语言",
|
|
943
1084
|
"settings.languageHint": "界面文字随语言选项切换,立即生效并记忆。", "settings.fontScale": "字号",
|
|
944
1085
|
"settings.fontScaleHint": "按当前字号的百分比缩放整个创作台,立即生效并记忆。",
|
|
@@ -949,11 +1090,16 @@ window.__ModuleLoader__.load({
|
|
|
949
1090
|
en: {
|
|
950
1091
|
"shell.title": "Maoying Studio", "header.subtitle": "Web-novel project dashboard & status flow · read-only text, writable status",
|
|
951
1092
|
"header.curVerTitle": "Current version", "header.upgradeTip": "Current {cur} — click to open the release page", "header.newVersion": "New version {ver} ↗",
|
|
1093
|
+
"header.upgradeBtn": "Upgrade now", "header.upgrading": "Installing…", "header.upgraded": "Installed — restart pending",
|
|
1094
|
+
"header.upgradeTitle": "Install the latest npm release with pnpm (up to ~3 min); takes effect after restarting DeepSeek",
|
|
1095
|
+
"header.upgradedTitle": "New version installed — takes effect after restarting DeepSeek",
|
|
1096
|
+
"toast.selfUpdate.ok": "Installed {ver} — takes effect after restarting DeepSeek",
|
|
1097
|
+
"toast.selfUpdate.devLink": "Installed from a dev directory — update with git pull", "toast.selfUpdate.pnpmMissing": "pnpm not found — install it manually",
|
|
952
1098
|
"header.workspace": "Workspace", "header.wsAria": "Select workspace", "header.noNovel": "(no novel)", "header.untitled": "Untitled",
|
|
953
1099
|
"header.settings": "Settings", "header.refresh": "Refresh", "header.closeAria": "Close Maoying Studio", "header.closeTitle": "Close",
|
|
954
1100
|
"tabs.aria": "Studio sections",
|
|
955
1101
|
"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",
|
|
1102
|
+
"tabs.timeline": "Timeline", "tabs.world": "Worldbuilding", "tabs.index": "Index Board",
|
|
957
1103
|
"shell.wsListFailed": "Failed to load workspace list: {msg}", "shell.stateFailed": "Failed to load snapshot: {msg}",
|
|
958
1104
|
"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
1105
|
"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:",
|
|
@@ -963,7 +1109,7 @@ window.__ModuleLoader__.load({
|
|
|
963
1109
|
"common.closeTip": "Dismiss notification", "common.unknownErr": "Unknown error", "common.priorityTitle": "Priority: {pri}", "common.priorityAria": "Priority {pri}",
|
|
964
1110
|
"common.chapterNum": "Ch. {n}", "pri.high": "High", "pri.normal": "Normal", "pri.low": "Low",
|
|
965
1111
|
"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}",
|
|
1112
|
+
"toast.chapterStatus": "Chapter status updated", "toast.chapterCreated": "Created chapter \"{title}\" from the outline (pending)", "toast.foreshadowStatus": "Foreshadowing status updated", "toast.phaseAdvanced": "Phase advanced to {phase}",
|
|
967
1113
|
"api.notJson": "Response is not JSON (HTTP {code})", "api.timeout": "Request timed out ({s} s)",
|
|
968
1114
|
"status.pending": "Pending", "status.writing": "Writing", "status.review": "Review", "status.accepted": "Accepted", "status.rawSuffix": "({raw})",
|
|
969
1115
|
"phase.idea": "Idea", "phase.setting": "Setting", "phase.outline": "Outline", "phase.writing": "Writing", "phase.review": "Review",
|
|
@@ -979,6 +1125,7 @@ window.__ModuleLoader__.load({
|
|
|
979
1125
|
"clue.clue": "Clue", "clue.redHerring": "Red herring", "clue.inference": "Inference", "clue.question": "Question",
|
|
980
1126
|
"sci.known": "Confirmed", "sci.extrapolation": "Extrapolation", "sci.hypothesis": "Hypothesis", "world.unlabeled": "Unrated",
|
|
981
1127
|
"warnings.title": "Some files failed to parse (skipped):",
|
|
1128
|
+
"check.title": "Static check: {n} issue(s) ({err} errors / {warn} warnings)", "check.fullHint": "Run maoying check in a terminal for the full report",
|
|
982
1129
|
"index.title": "Index Board (clue-board / memories)", "index.rebuild": "Rebuild index", "index.rebuilding": "Rebuilding…",
|
|
983
1130
|
"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
1131
|
"index.memBoard": "Memory board", "index.memEmpty": "No entries in memories.yml yet", "index.promise": "Promise thread",
|
|
@@ -990,28 +1137,34 @@ window.__ModuleLoader__.load({
|
|
|
990
1137
|
"relations.unlinkedTitle": "Unlinked characters (no relation edges)", "relations.noData": "No character or relation data to draw",
|
|
991
1138
|
"relations.edgeTipPrefix": "{type}: ", "relations.noDesc": "(no description)",
|
|
992
1139
|
"char.empty": "No character cards in characters/ yet",
|
|
1140
|
+
"role.protagonist": "Protagonist", "role.antagonist": "Antagonist", "role.supporting": "Supporting", "role.minor": "Minor",
|
|
993
1141
|
"char.field.age": "Age", "char.field.gender": "Gender", "char.field.appearance": "Appearance", "char.field.personality": "Personality",
|
|
994
1142
|
"char.field.background": "Background", "char.field.motivation": "Motivation", "char.field.arc": "Character arc",
|
|
995
1143
|
"char.years": "Age {n}", "char.linePersonality": "Personality ", "char.lineMotivation": "Motivation ",
|
|
996
1144
|
"char.modalAria": "Character details", "char.closeAria": "Close character card", "char.markers": "Tags", "char.file": "File",
|
|
997
1145
|
"overview.author": "Author: {name}", "overview.totalWords": "Total characters (CJK)", "overview.totalChapters": "Chapters",
|
|
998
1146
|
"overview.characters": "Characters", "overview.openForeshadows": "Unresolved foreshadowing", "overview.statusDist": "Chapter status breakdown", "overview.recent": "Recent chapters",
|
|
1147
|
+
"overview.edit": "Edit", "overview.editTip": "Edit manuscript metadata",
|
|
999
1148
|
"chapters.empty": "No chapter files in chapters/ yet", "chapters.writeQueue": "Writing Queue", "chapters.rowTip": "Click to open this chapter",
|
|
1000
1149
|
"chapters.outlineWords": "Outline {n} chars", "chapters.all": "All", "chapters.thVolume": "Vol · Ch", "chapters.thTitle": "Title",
|
|
1001
1150
|
"chapters.thStatus": "Status", "chapters.thWords": "Words", "chapters.thSummary": "Summary", "chapters.rowReadTip": "Click to read",
|
|
1002
1151
|
"chapters.emptyFiltered": "No chapters with this status",
|
|
1152
|
+
"chapters.outlineRowTip": "Click to view the full outline", "chapters.noChapter": "No chapter", "chapters.createBtn": "Create", "chapters.creating": "Creating…",
|
|
1153
|
+
"chapters.createTip": "Create a pending chapter in chapters/ from this outline (body to be written)",
|
|
1154
|
+
"chapters.emptyWithOutlines": "No chapter files in chapters/ yet — the Writing Queue below lists outlines without chapters",
|
|
1003
1155
|
"reader.back": "Back", "reader.backTip": "Back to chapter list", "reader.copyPath": "Copy path", "reader.copyPathTip": "Copy chapter file path",
|
|
1004
1156
|
"reader.copied": "Copied chapter file path", "reader.copyFailed": "Copy failed — select the path text manually", "reader.loadFailed": "Failed to load chapter text: {msg}",
|
|
1005
1157
|
"reader.noOutline": "No chapter outline", "reader.emptyBody": "(empty chapter)", "reader.finalState": "Accepted (final)", "reader.statusFlow": "Status:",
|
|
1006
1158
|
"reader.flowing": "Transitioning…", "reader.advanceTo": "Advance to {next}",
|
|
1007
1159
|
"outline.title": "Outline", "outline.empty": "(outline is empty)", "outline.collapseTip": "Collapse outline", "outline.expandTip": "Expand outline",
|
|
1160
|
+
"outline.modalAria": "Chapter outline", "outline.closeAria": "Close outline", "outline.loading": "Loading outline…", "outline.loadFailed": "Failed to load outline: {msg}",
|
|
1008
1161
|
"novel.metaTitle": "Manuscript metadata", "novel.metaNote": "Maps to novel/project.yml · trailing comments are preserved",
|
|
1009
1162
|
"novel.fTitle": "Title", "novel.titlePh": "e.g. The Long Night", "novel.fAuthor": "Author", "novel.authorPh": "Pen name",
|
|
1010
1163
|
"novel.fGenre": "Genre", "novel.genrePh": "e.g. Eastern fantasy / Urban powers / Alternate history", "novel.fLength": "Planned length",
|
|
1011
1164
|
"novel.lengthPh": "e.g. ~1M characters / 300 chapters", "novel.fLanguage": "Text language", "novel.fPhase": "Workflow phase", "novel.fMode": "Co-writing mode",
|
|
1012
1165
|
"novel.fDesc": "One-line synopsis", "novel.descPh": "Can be quoted directly in cover copy", "novel.titleRequired": "Title is required",
|
|
1013
1166
|
"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",
|
|
1167
|
+
"novel.saving": "Saving…", "novel.save": "Save", "novel.cancel": "Cancel",
|
|
1015
1168
|
"settings.title": "Settings", "settings.closeAria": "Close settings", "settings.language": "Language",
|
|
1016
1169
|
"settings.languageHint": "UI text follows the language option; it applies immediately and is remembered.", "settings.fontScale": "Font size",
|
|
1017
1170
|
"settings.fontScaleHint": "Scales the whole studio as a percentage of the current font size; applies immediately and is remembered.",
|
|
@@ -1148,23 +1301,55 @@ window.__ModuleLoader__.load({
|
|
|
1148
1301
|
})()) : null);
|
|
1149
1302
|
}
|
|
1150
1303
|
|
|
1151
|
-
// ──
|
|
1152
|
-
//
|
|
1153
|
-
//
|
|
1154
|
-
|
|
1304
|
+
// ── 书稿元信息编辑表单(概览「编辑」按钮展开):可视化 project.yml 平
|
|
1305
|
+
// 键并保存。mode/phase 走枚举下拉,自由文本走输入框;保存走 project-save
|
|
1306
|
+
// 路由(服务端校验 + 保注释改写),成功后重拉快照并经 onDone 退出编辑态。
|
|
1307
|
+
// 表单初值(与磁盘真值同源):首次载入与 409 重置共用同一形状。
|
|
1308
|
+
function settingsFormOf(project) {
|
|
1309
|
+
project = project || {};
|
|
1310
|
+
return {
|
|
1311
|
+
title: project.title || "", author: project.author || "", genre: project.genre || "",
|
|
1312
|
+
length: project.length || "", language: project.language || "zh",
|
|
1313
|
+
phase: PHASES.indexOf(project.phase) !== -1 ? project.phase : "idea",
|
|
1314
|
+
mode: project.mode || "ai-draft",
|
|
1315
|
+
description: project.description || "",
|
|
1316
|
+
};
|
|
1317
|
+
}
|
|
1318
|
+
// 快照 root 与工作区 path 同源但形态可能有差(尾斜杠等),比对前归一。
|
|
1319
|
+
function normRoot(s) { return String(s || "").replace(/[\\/]+$/, ""); }
|
|
1320
|
+
function ProjectMetaForm(props) {
|
|
1155
1321
|
var snap = props.snap;
|
|
1156
1322
|
var wsPath = props.wsPath;
|
|
1157
1323
|
var reload = props.reload;
|
|
1324
|
+
var onDone = props.onDone;
|
|
1158
1325
|
var project = snap.project || {};
|
|
1159
|
-
var _f = useState(function () {
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1326
|
+
var _f = useState(function () { return settingsFormOf(project); }), form = _f[0], setForm = _f[1];
|
|
1327
|
+
// 保存基线:表单载入时的 project.yml 指纹(snap.project_hash)。服务端
|
|
1328
|
+
// 拿它检测表单载入后的外部改动(作者手改 / agent 推进 phase),命中即
|
|
1329
|
+
// 409 拒写——没有它,保存会静默覆盖同名键、丢掉人工改动。
|
|
1330
|
+
var baseHashRef = useRef(snap && typeof snap.project_hash === "string" ? snap.project_hash : null);
|
|
1331
|
+
// 409 后的一次性重置标记:reload 带回新快照时把表单与基线放回磁盘真值。
|
|
1332
|
+
// 只在保存失败路径置位——顶栏刷新等其他 reload 不碰正在编辑的表单。
|
|
1333
|
+
var pendingResetRef = useRef(false);
|
|
1334
|
+
useEffect(function () {
|
|
1335
|
+
var hash = snap && typeof snap.project_hash === "string" ? snap.project_hash : null;
|
|
1336
|
+
if (pendingResetRef.current) {
|
|
1337
|
+
pendingResetRef.current = false;
|
|
1338
|
+
setForm(settingsFormOf(snap && snap.project));
|
|
1339
|
+
baseHashRef.current = hash;
|
|
1340
|
+
return;
|
|
1341
|
+
}
|
|
1342
|
+
// 空闲跟随:快照确属当前工作区(root 对得上,跨工作区切换的在途旧
|
|
1343
|
+
// 快照不认)且表单未被用户动过(与磁盘逐键同值)时,把基线对齐本快照
|
|
1344
|
+
// ——本组件不随工作区切换重挂,基线若还攥着旧工作区的 hash,保存必被
|
|
1345
|
+
// 自家 409 拦住(假冲突)。用户已编辑则不打扰,外部改动由保存 409 兜底。
|
|
1346
|
+
if (!snap || !snap.found || normRoot(snap.root) !== normRoot(wsPath)) return;
|
|
1347
|
+
var disk = settingsFormOf(snap.project);
|
|
1348
|
+
for (var k in disk) {
|
|
1349
|
+
if (String(form[k]) !== String(disk[k])) return;
|
|
1350
|
+
}
|
|
1351
|
+
baseHashRef.current = hash;
|
|
1352
|
+
}, [snap, wsPath]);
|
|
1168
1353
|
var _b = useState(false), busy = _b[0], setBusy = _b[1];
|
|
1169
1354
|
var _e = useState(""), saveErr = _e[0], setSaveErr = _e[1];
|
|
1170
1355
|
function set(k, v) { setForm(Object.assign({}, form, (function () { var o = {}; o[k] = v; return o; })())); }
|
|
@@ -1174,10 +1359,20 @@ window.__ModuleLoader__.load({
|
|
|
1174
1359
|
setBusy(true); setSaveErr("");
|
|
1175
1360
|
// 成败提示统一走 apiPost 的 toast 通道(这里不再另弹,避免双 toast);
|
|
1176
1361
|
// 卡内 saveErr 只承担表单旁的错误态。
|
|
1177
|
-
apiPost("project-save", { path: wsPath, values: form }).then(function () {
|
|
1362
|
+
apiPost("project-save", { path: wsPath, values: form, base_hash: baseHashRef.current }).then(function (res) {
|
|
1363
|
+
// 写后指纹即下一轮基线:不等 reload 快照就先接住——连续两次保存时
|
|
1364
|
+
// 第二次若还回传载入时的旧指纹,会被自家 409 拦住(假冲突)。
|
|
1365
|
+
if (res && typeof res.project_hash === "string") baseHashRef.current = res.project_hash;
|
|
1178
1366
|
return reload();
|
|
1179
1367
|
}).catch(function (err) {
|
|
1180
1368
|
setSaveErr(err.message);
|
|
1369
|
+
// 409 = project.yml 在表单载入后被外部改过:重拉快照并一次性把表单
|
|
1370
|
+
// 放回磁盘真值,别让用户在过期基线上继续盲改。其他失败(网络抖动
|
|
1371
|
+
// 等)不动表单,保留正在编辑的内容。
|
|
1372
|
+
if (err && err.status === 409) {
|
|
1373
|
+
pendingResetRef.current = true;
|
|
1374
|
+
reload();
|
|
1375
|
+
}
|
|
1181
1376
|
}).finally(function () { setBusy(false); });
|
|
1182
1377
|
}
|
|
1183
1378
|
function field(label, key, placeholder, textarea) {
|
|
@@ -1197,6 +1392,10 @@ window.__ModuleLoader__.load({
|
|
|
1197
1392
|
h("select", { className: "myt-setInput myt-setSelect", value: value, onChange: function (e) { set(key, e.target.value); } },
|
|
1198
1393
|
opts.map(function (o) { return h("option", { key: o[0], value: o[0] }, o[1]); })));
|
|
1199
1394
|
}
|
|
1395
|
+
function cancel() {
|
|
1396
|
+
if (busy) return; // 保存在途不许退出:避免卸载后 setState
|
|
1397
|
+
onDone();
|
|
1398
|
+
}
|
|
1200
1399
|
return h("div", { className: "myt-stack" },
|
|
1201
1400
|
h("div", { className: "myt-card" },
|
|
1202
1401
|
h("div", { className: "myt-cardHead" },
|
|
@@ -1213,6 +1412,8 @@ window.__ModuleLoader__.load({
|
|
|
1213
1412
|
field(tt("novel.fDesc"), "description", tt("novel.descPh"), true))),
|
|
1214
1413
|
saveErr ? h(ErrorStrip, { text: saveErr }) : null,
|
|
1215
1414
|
h("div", { className: "myt-setActions" },
|
|
1415
|
+
h("button", { type: "button", className: "myt-btn", disabled: busy, onClick: cancel },
|
|
1416
|
+
tt("novel.cancel")),
|
|
1216
1417
|
h("button", { type: "button", className: "myt-btn myt-btnPrimary", disabled: busy, onClick: save },
|
|
1217
1418
|
busy ? tt("novel.saving") : tt("novel.save"))));
|
|
1218
1419
|
}
|
|
@@ -1254,17 +1455,60 @@ window.__ModuleLoader__.load({
|
|
|
1254
1455
|
return node;
|
|
1255
1456
|
}
|
|
1256
1457
|
|
|
1458
|
+
// ── 章纲全文弹层(0.1.15):按单写作的孤儿章纲行点击展开——GET outline
|
|
1459
|
+
// 路由取全文,MdView 渲染,words 一并显示;加载中/失败给简单文案,失败
|
|
1460
|
+
// 态可重试。请求序号防快速开关弹层时的过期响应(对照 ChapterReader)。──
|
|
1461
|
+
function OutlineModal(props) {
|
|
1462
|
+
var wsPath = props.wsPath;
|
|
1463
|
+
var file = props.file;
|
|
1464
|
+
var onClose = props.onClose;
|
|
1465
|
+
var _d = useState(null), detail = _d[0], setDetail = _d[1]; // {loading, data, err}
|
|
1466
|
+
var seqRef = useRef(0);
|
|
1467
|
+
var load = useCallback(function (f) {
|
|
1468
|
+
var seq = ++seqRef.current;
|
|
1469
|
+
if (!f) { setDetail(null); return; }
|
|
1470
|
+
setDetail({ loading: true, data: null, err: "" });
|
|
1471
|
+
apiGet("outline", { path: wsPath, file: f }).then(function (d) {
|
|
1472
|
+
if (seq !== seqRef.current) return; // 过期响应:弹层已换文件/关闭,丢弃
|
|
1473
|
+
setDetail({ loading: false, data: d, err: "" });
|
|
1474
|
+
}).catch(function (err) {
|
|
1475
|
+
if (seq !== seqRef.current) return;
|
|
1476
|
+
setDetail({ loading: false, data: null, err: err.message });
|
|
1477
|
+
});
|
|
1478
|
+
}, [wsPath]);
|
|
1479
|
+
useEffect(function () { load(file); }, [file, load]);
|
|
1480
|
+
var words = detail && detail.data && typeof detail.data.words === "number" ? detail.data.words : null;
|
|
1481
|
+
return h(MaskPortal, {
|
|
1482
|
+
className: "myt-modalMask",
|
|
1483
|
+
onMouseDown: function (e) { if (e.target === e.currentTarget) onClose(); },
|
|
1484
|
+
},
|
|
1485
|
+
h("div", { className: "myt-modalCard", role: "dialog", "aria-modal": "true", "aria-label": tt("outline.modalAria") },
|
|
1486
|
+
h("div", { className: "myt-modalHead" },
|
|
1487
|
+
h("h2", { className: "myt-modalTitle" }, props.title || file),
|
|
1488
|
+
h("div", { className: "myt-heroMeta" },
|
|
1489
|
+
words !== null ? h("span", { className: "myt-outlineWords" }, tf("common.charCount", { n: fmtNum(words) })) : null,
|
|
1490
|
+
h("button", { type: "button", className: "myt-closeBtn", "aria-label": tt("outline.closeAria"), onClick: onClose }, h("span", { className: "myt-closeGlyph", "aria-hidden": "true" }, "×")))),
|
|
1491
|
+
h("div", { className: "myt-modalBody" },
|
|
1492
|
+
!detail || detail.loading ? h("div", { className: "myt-muted" }, tt("outline.loading"))
|
|
1493
|
+
: detail.err ? h(ErrorStrip, { text: tf("outline.loadFailed", { msg: detail.err }), onRetry: function () { load(file); } })
|
|
1494
|
+
: h(MdView, { text: (detail.data && detail.data.body) || tt("outline.empty"), className: "myt-outlineBody" }))));
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1257
1497
|
function ChaptersTab(props) {
|
|
1258
1498
|
var snap = props.snap;
|
|
1499
|
+
var wsPath = props.wsPath;
|
|
1500
|
+
var reload = props.reload;
|
|
1259
1501
|
var onOpen = props.onOpenChapter; // 行点击 → 整页阅读(PageShell.readerFile)
|
|
1260
1502
|
var _f = useState("all"), filter = _f[0], setFilter = _f[1];
|
|
1261
1503
|
var chapters = snap.chapters || [];
|
|
1262
1504
|
var rows = filter === "all" ? chapters : chapters.filter(function (c) { return c.status === filter; });
|
|
1263
1505
|
var counts = snap.totals && snap.totals.byStatus ? snap.totals.byStatus : {};
|
|
1264
1506
|
// 「按单写作」(五期):state.outlines 与 chapters 联交——有章纲且
|
|
1265
|
-
// status=pending
|
|
1266
|
-
//
|
|
1267
|
-
//
|
|
1507
|
+
// status=pending 的章,按章纲(卷-章号)序列出,共创模式的主工作清单。
|
|
1508
|
+
// 孤儿章纲(chapter 无对应章节文件)照列(0.1.15):章纲先于正文是共
|
|
1509
|
+
// 创常态,未建章行挂「未建章」徽章,行点击看章纲全文、卡上「建章」一
|
|
1510
|
+
// 键落 pending 空章(建后走既有 pending 流转)。outlines 快照已按卷-章
|
|
1511
|
+
// 号升序,孤儿行与待写章直接保序混排。
|
|
1268
1512
|
var outlines = snap.outlines || [];
|
|
1269
1513
|
var chapterByFile = {};
|
|
1270
1514
|
chapters.forEach(function (c) { chapterByFile[c.file] = c; });
|
|
@@ -1272,7 +1516,22 @@ window.__ModuleLoader__.load({
|
|
|
1272
1516
|
outlines.forEach(function (ol) {
|
|
1273
1517
|
var ch = ol && ol.chapter ? chapterByFile[ol.chapter] : null;
|
|
1274
1518
|
if (ch && (ch.status || "pending") === "pending") writeOrder.push({ ch: ch, ol: ol });
|
|
1519
|
+
else if (!ch) writeOrder.push({ ol: ol }); // 孤儿章纲:正文未建也入清单
|
|
1275
1520
|
});
|
|
1521
|
+
// 章纲全文弹层:outline 文件名,null 不渲染;建章 in-flight 的文件名。
|
|
1522
|
+
var _mo = useState(null), outlineModalFile = _mo[0], setOutlineModalFile = _mo[1];
|
|
1523
|
+
var _busy = useState(null), creatingFile = _busy[0], setCreatingFile = _busy[1];
|
|
1524
|
+
function createFromOutline(ol, e) {
|
|
1525
|
+
if (e && typeof e.stopPropagation === "function") e.stopPropagation(); // 别触发行的弹层
|
|
1526
|
+
if (!ol || !ol.file || creatingFile) return;
|
|
1527
|
+
setCreatingFile(ol.file);
|
|
1528
|
+
// 成败提示走 apiPost 的 toast 通道;成功后重拉快照——新章节进表格、
|
|
1529
|
+
// 孤儿行按快照最新联交结果自然消失(与 chapter-status 流转同一模式)。
|
|
1530
|
+
apiPost("chapter-create", { path: wsPath, file: ol.file })
|
|
1531
|
+
.then(function () { return reload(); })
|
|
1532
|
+
.catch(function () {})
|
|
1533
|
+
.finally(function () { setCreatingFile(null); });
|
|
1534
|
+
}
|
|
1276
1535
|
return h("div", { className: "myt-stack" },
|
|
1277
1536
|
writeOrder.length ? h("div", { className: "myt-card myt-writeOrder" },
|
|
1278
1537
|
h("div", { className: "myt-cardHead" },
|
|
@@ -1281,6 +1540,27 @@ window.__ModuleLoader__.load({
|
|
|
1281
1540
|
h("div", { className: "myt-writeOrderList" }, writeOrder.map(function (item) {
|
|
1282
1541
|
var ch = item.ch;
|
|
1283
1542
|
var ol = item.ol;
|
|
1543
|
+
if (!ch) {
|
|
1544
|
+
// 孤儿章纲行:div 承担行点击(button 里不能再嵌 button),键盘
|
|
1545
|
+
// Enter/Space 与 role/tabIndex 补齐可达性。
|
|
1546
|
+
var busy = creatingFile === ol.file;
|
|
1547
|
+
return h("div", {
|
|
1548
|
+
key: "ol:" + ol.file, className: "myt-writeOrderRow", role: "button", tabIndex: 0,
|
|
1549
|
+
title: tt("chapters.outlineRowTip"),
|
|
1550
|
+
onClick: function () { setOutlineModalFile(ol.file); },
|
|
1551
|
+
onKeyDown: function (e) { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); setOutlineModalFile(ol.file); } },
|
|
1552
|
+
},
|
|
1553
|
+
h("span", { className: "myt-chPath" }, olWhere(ol) || ol.file),
|
|
1554
|
+
h("span", { className: "myt-noChapterBadge", title: tt("chapters.createTip") }, tt("chapters.noChapter")),
|
|
1555
|
+
ol && typeof ol.excerpt === "string" && ol.excerpt !== ""
|
|
1556
|
+
? h("span", { className: "myt-writeOrderExcerpt", title: ol.excerpt }, truncate(ol.excerpt, 60)) : null,
|
|
1557
|
+
ol && ol.words ? h("span", { className: "myt-wordsCell" }, tf("chapters.outlineWords", { n: fmtNum(ol.words) })) : null,
|
|
1558
|
+
h("button", {
|
|
1559
|
+
type: "button", className: "myt-btn", disabled: creatingFile !== null,
|
|
1560
|
+
title: tt("chapters.createTip"),
|
|
1561
|
+
onClick: function (e) { createFromOutline(ol, e); },
|
|
1562
|
+
}, busy ? tt("chapters.creating") : tt("chapters.createBtn")));
|
|
1563
|
+
}
|
|
1284
1564
|
return h("button", {
|
|
1285
1565
|
key: ch.file, type: "button", className: "myt-writeOrderRow",
|
|
1286
1566
|
title: tt("chapters.rowTip"),
|
|
@@ -1315,7 +1595,17 @@ window.__ModuleLoader__.load({
|
|
|
1315
1595
|
h("td", null, h("span", { className: "myt-wordsCell" }, fmtNum(ch.words))),
|
|
1316
1596
|
h("td", null, h("div", { className: "myt-summaryCell myt-muted" }, orDash(ch.summary))));
|
|
1317
1597
|
}))))
|
|
1318
|
-
: (chapters.length ? h(Empty, { text: tt("chapters.emptyFiltered") }) : h(Empty, { text: tt("chapters.empty") }))
|
|
1598
|
+
: (chapters.length ? h(Empty, { text: tt("chapters.emptyFiltered") }) : h(Empty, { text: outlines.length ? tt("chapters.emptyWithOutlines") : tt("chapters.empty") })),
|
|
1599
|
+
outlineModalFile ? h(OutlineModal, {
|
|
1600
|
+
wsPath: wsPath,
|
|
1601
|
+
file: outlineModalFile,
|
|
1602
|
+
// 弹层标题用「卷-章」定位(对照 olWhere 显示格式),解析不了退文件名
|
|
1603
|
+
title: (function () {
|
|
1604
|
+
for (var i = 0; i < outlines.length; i++) { if (outlines[i].file === outlineModalFile) return olWhere(outlines[i]) || outlines[i].file; }
|
|
1605
|
+
return outlineModalFile;
|
|
1606
|
+
})(),
|
|
1607
|
+
onClose: function () { setOutlineModalFile(null); },
|
|
1608
|
+
}) : null);
|
|
1319
1609
|
}
|
|
1320
1610
|
|
|
1321
1611
|
// ── 角色 tab:卡墙 + 全卡弹层 ───────────────────────────────────────────
|