@musnows/scriverse 0.4.4 → 0.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.js +10 -5
- package/dist/app.js.map +1 -1
- package/dist/public/app.js +107 -47
- package/dist/public/index.html +6 -4
- package/dist/public/relationship-graph.js +108 -26
- package/dist/public/styles.css +17 -7
- package/dist/store.js +61 -40
- package/dist/store.js.map +1 -1
- package/dist/user-auth.js +1 -1
- package/dist/user-auth.js.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/public/app.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { buildRelationshipGraph, createGalaxyRenderer, renderRelationshipMindMap } from "/relationship-graph.js?v=
|
|
1
|
+
import { buildRelationshipGraph, createGalaxyRenderer, renderRelationshipMindMap } from "/relationship-graph.js?v=20260725-galaxy-ripple";
|
|
2
2
|
import { collapseExcessBlankLines, formatDateTime, normalizeParagraphSpacing } from "/text-formatting.js?v=20260713-saved-at-seconds";
|
|
3
3
|
import { renderMarkdown } from "/markdown.js?v=20260722-inline-code";
|
|
4
4
|
import { buildAiReferenceScope, findAiMention, listAiMentionOptions } from "/ai-mentions.js?v=20260716-chapter-references";
|
|
@@ -723,11 +723,14 @@ let settingEditorVditor = null;
|
|
|
723
723
|
let knowledgeSectionVditor = null;
|
|
724
724
|
let characterSectionVditor = null;
|
|
725
725
|
let entityHistoryContext = null;
|
|
726
|
+
let moduleContentInteractionsBound = false;
|
|
726
727
|
|
|
727
728
|
function showEntityEditorPage(type, { readOnly = false } = {}) {
|
|
729
|
+
const module = type === "setting" ? "settings" : type === "character" ? "characters" : type === "race" ? "races" : "organizations";
|
|
730
|
+
const viewOnly = readOnly || !canEditModule(module);
|
|
728
731
|
entityEditorType = type;
|
|
729
732
|
entityEditorDirty = false;
|
|
730
|
-
entityEditorReadOnly =
|
|
733
|
+
entityEditorReadOnly = viewOnly;
|
|
731
734
|
characterSectionEditorDirty = false;
|
|
732
735
|
knowledgeSectionEditorDirty = false;
|
|
733
736
|
$("#entity-editor-view").classList.remove("hidden");
|
|
@@ -736,6 +739,7 @@ function showEntityEditorPage(type, { readOnly = false } = {}) {
|
|
|
736
739
|
$("#knowledge-editor-form").classList.toggle("hidden", !["race", "organization"].includes(type));
|
|
737
740
|
$("#character-section-editor-view").classList.add("hidden");
|
|
738
741
|
$("#knowledge-section-editor-view").classList.add("hidden");
|
|
742
|
+
["setting", "character", "knowledge"].forEach((editor) => $(`#${editor}-editor-readonly-badge`).classList.toggle("hidden", !(viewOnly && (editor === "knowledge" ? ["race", "organization"].includes(type) : editor === type))));
|
|
739
743
|
$("#app").inert = true;
|
|
740
744
|
document.body.classList.add("entity-editor-open");
|
|
741
745
|
replacePageRoute(currentPageRoute());
|
|
@@ -1782,7 +1786,9 @@ async function api(path, options = {}) {
|
|
|
1782
1786
|
state.csrfToken = null;
|
|
1783
1787
|
showAuth(false);
|
|
1784
1788
|
}
|
|
1785
|
-
|
|
1789
|
+
const error = new Error(payload.error?.message ?? `请求失败:${response.status}`);
|
|
1790
|
+
error.code = payload.error?.code;
|
|
1791
|
+
throw error;
|
|
1786
1792
|
}
|
|
1787
1793
|
if (response.status === 204) return null;
|
|
1788
1794
|
const payload = await response.json();
|
|
@@ -2264,11 +2270,8 @@ async function initializePage() {
|
|
|
2264
2270
|
}
|
|
2265
2271
|
if (route.view === "module") return;
|
|
2266
2272
|
if (route.view === "entity-editor") {
|
|
2267
|
-
const records = route.entity === "setting" ? state.settings : route.entity === "character" ? state.characters : route.entity === "race" ? state.races : state.organizations;
|
|
2268
2273
|
const item = route.entityId
|
|
2269
|
-
? route.entity === "character"
|
|
2270
|
-
? await api(`/api/characters/${encodeURIComponent(route.entityId)}`)
|
|
2271
|
-
: records.find((record) => record.id === route.entityId)
|
|
2274
|
+
? await api(`/api/${route.entity === "setting" ? "settings" : route.entity === "character" ? "characters" : route.entity === "race" ? "races" : "organizations"}/${encodeURIComponent(route.entityId)}`)
|
|
2272
2275
|
: null;
|
|
2273
2276
|
if (route.entityId && !item) {
|
|
2274
2277
|
toast(({ setting: "未找到要编辑的设定", character: "未找到要编辑的角色", race: "未找到要编辑的种族", organization: "未找到要编辑的组织" }[route.entity] ?? "未找到要编辑的档案"), "error");
|
|
@@ -2903,6 +2906,7 @@ async function showModule(module) {
|
|
|
2903
2906
|
$("#module-create-button").textContent = meta[3];
|
|
2904
2907
|
$("#module-create-button").classList.toggle("hidden", module === "ai-settings" || !canEditModule(module));
|
|
2905
2908
|
$("#module-content").innerHTML = '<div class="empty-state">正在载入……</div>';
|
|
2909
|
+
bindModuleContentInteractions();
|
|
2906
2910
|
try {
|
|
2907
2911
|
if (module === "settings") await renderSettings();
|
|
2908
2912
|
if (module === "characters") await renderCharacters(characterListPage);
|
|
@@ -3128,7 +3132,7 @@ function bindModuleLayoutToggle(refresh) {
|
|
|
3128
3132
|
|
|
3129
3133
|
function bindRecordPreview(selector, open) {
|
|
3130
3134
|
$("#module-content").querySelectorAll(selector).forEach((card) => {
|
|
3131
|
-
const id = card.dataset.openSetting ?? card.dataset.openCharacter ?? card.dataset.openRace ?? card.dataset.openReview;
|
|
3135
|
+
const id = card.dataset.openSetting ?? card.dataset.openCharacter ?? card.dataset.openRace ?? card.dataset.openOrganization ?? card.dataset.openReview;
|
|
3132
3136
|
card.addEventListener("click", (event) => {
|
|
3133
3137
|
if (!event.target.closest("button, a, summary")) void open(id);
|
|
3134
3138
|
});
|
|
@@ -3141,6 +3145,36 @@ function bindRecordPreview(selector, open) {
|
|
|
3141
3145
|
});
|
|
3142
3146
|
}
|
|
3143
3147
|
|
|
3148
|
+
function bindModuleContentInteractions() {
|
|
3149
|
+
if (moduleContentInteractionsBound) return;
|
|
3150
|
+
moduleContentInteractionsBound = true;
|
|
3151
|
+
const host = $("#module-content");
|
|
3152
|
+
const open = async (card) => {
|
|
3153
|
+
const id = card.dataset.openSetting ?? card.dataset.openCharacter ?? card.dataset.openRace ?? card.dataset.openOrganization;
|
|
3154
|
+
if (!id) return;
|
|
3155
|
+
if (card.dataset.openSetting) return openSettingEditor(await api(`/api/settings/${encodeURIComponent(id)}`), { readOnly: true });
|
|
3156
|
+
if (card.dataset.openCharacter) return openCharacterEditor(await api(`/api/characters/${encodeURIComponent(id)}`), { readOnly: true });
|
|
3157
|
+
if (card.dataset.openRace) return openRaceDialog(await api(`/api/races/${encodeURIComponent(id)}`), { readOnly: true });
|
|
3158
|
+
if (card.dataset.openOrganization) return openOrganizationDialog(await api(`/api/organizations/${encodeURIComponent(id)}`), { readOnly: true });
|
|
3159
|
+
};
|
|
3160
|
+
const findCard = (target) => target instanceof Element
|
|
3161
|
+
? target.closest("[data-open-setting], [data-open-character], [data-open-race], [data-open-organization]")
|
|
3162
|
+
: null;
|
|
3163
|
+
host.addEventListener("click", (event) => {
|
|
3164
|
+
if (event.target instanceof Element && event.target.closest("button, a, summary")) return;
|
|
3165
|
+
const card = findCard(event.target);
|
|
3166
|
+
if (card) void open(card);
|
|
3167
|
+
});
|
|
3168
|
+
host.addEventListener("keydown", (event) => {
|
|
3169
|
+
if (event.key !== "Enter" && event.key !== " ") return;
|
|
3170
|
+
if (event.target instanceof Element && event.target.closest("button, a, summary")) return;
|
|
3171
|
+
const card = findCard(event.target);
|
|
3172
|
+
if (!card) return;
|
|
3173
|
+
event.preventDefault();
|
|
3174
|
+
void open(card);
|
|
3175
|
+
});
|
|
3176
|
+
}
|
|
3177
|
+
|
|
3144
3178
|
function openReviewDetailDialog(item) {
|
|
3145
3179
|
if (!item) return;
|
|
3146
3180
|
const evidence = Array.isArray(item.evidence) ? item.evidence : [];
|
|
@@ -3185,13 +3219,13 @@ function settingRecordActions(item) {
|
|
|
3185
3219
|
function renderSettingCards(records) {
|
|
3186
3220
|
return `<div class="card-grid">${records.map((item) => `
|
|
3187
3221
|
<article class="record-card preview-record-card" data-open-setting="${esc(item.id)}" role="button" tabindex="0" aria-label="查看设定 ${esc(item.title)}"><small>${esc(item.category)} · ${item.locked ? "已锁定" : esc(settingStatusLabel(item.status))}</small>
|
|
3188
|
-
<h3>${esc(item.title)}</h3><div class="record-markdown-preview
|
|
3222
|
+
<h3>${esc(item.title)}</h3><div class="record-markdown-preview">${esc(item.contentPreview || "暂无正文预览")}</div>
|
|
3189
3223
|
<div class="card-actions">${settingRecordActions(item)}</div></article>`).join("")}</div>`;
|
|
3190
3224
|
}
|
|
3191
3225
|
|
|
3192
3226
|
function renderSettingRows(records) {
|
|
3193
3227
|
return `<div class="module-row-list">${records.map((item) => {
|
|
3194
|
-
const preview = moduleRowPreview(item.
|
|
3228
|
+
const preview = moduleRowPreview(item.contentPreview);
|
|
3195
3229
|
return `
|
|
3196
3230
|
<article class="record-card module-row preview-record-card" data-open-setting="${esc(item.id)}" role="button" tabindex="0" aria-label="查看设定 ${esc(item.title)}"><small>${esc(item.category)} · ${item.locked ? "已锁定" : esc(settingStatusLabel(item.status))}</small>
|
|
3197
3231
|
<h3>${esc(item.title)}</h3><p class="module-row-preview" title="${esc(preview)}">${esc(preview)}</p>
|
|
@@ -3208,20 +3242,16 @@ async function renderSettings() {
|
|
|
3208
3242
|
? `${layout === "rows" ? renderSettingRows(records) : renderSettingCards(records)}`
|
|
3209
3243
|
: emptyModule("还没有世界观设定", "新建规则、地点、组织、科技或创作约束。AI 提取的候选也会进入这里。");
|
|
3210
3244
|
bindModuleLayoutToggle(renderSettings);
|
|
3211
|
-
|
|
3212
|
-
$("#module-content").querySelectorAll("[data-edit-setting]").forEach((button) => button.addEventListener("click", () =>
|
|
3245
|
+
const openSetting = async (id, readOnly) => openSettingEditor(await api(`/api/settings/${encodeURIComponent(id)}`), { readOnly });
|
|
3246
|
+
$("#module-content").querySelectorAll("[data-edit-setting]").forEach((button) => button.addEventListener("click", () => { void openSetting(button.dataset.editSetting, false); }));
|
|
3213
3247
|
bindEntityHistoryButtons(async () => { await renderSettings(); await loadAiReferences(); });
|
|
3214
3248
|
}
|
|
3215
3249
|
|
|
3216
3250
|
async function renderCharacters(page = characterListPage) {
|
|
3217
|
-
const
|
|
3218
|
-
apiPage(`/api/works/${state.work.id}/characters`, page),
|
|
3219
|
-
canReadModule("races") ? apiAllPages(`/api/works/${state.work.id}/races`) : Promise.resolve([]),
|
|
3220
|
-
canReadModule("organizations") ? apiAllPages(`/api/works/${state.work.id}/organizations`) : Promise.resolve([])
|
|
3221
|
-
]);
|
|
3251
|
+
const characterPage = await apiPage(`/api/works/${state.work.id}/characters`, page);
|
|
3222
3252
|
if (!characterPage.items.length && page > 1) return renderCharacters(page - 1);
|
|
3223
3253
|
characterListPage = characterPage.page;
|
|
3224
|
-
|
|
3254
|
+
state.characters = characterPage.items;
|
|
3225
3255
|
const layout = readModuleLayout();
|
|
3226
3256
|
const characterActions = (item) => recordCardEditButton("edit-character", item.id, `角色“${item.name}”`);
|
|
3227
3257
|
const characterCards = () => `<div class="card-grid">${state.characters.map((item) => {
|
|
@@ -3287,15 +3317,12 @@ async function renderCharacters(page = characterListPage) {
|
|
|
3287
3317
|
button.disabled = false;
|
|
3288
3318
|
}
|
|
3289
3319
|
});
|
|
3290
|
-
|
|
3291
|
-
$("#module-content").querySelectorAll("[data-edit-character]").forEach((button) => button.addEventListener("click", () =>
|
|
3320
|
+
const openCharacter = async (id, readOnly) => openCharacterEditor(await api(`/api/characters/${encodeURIComponent(id)}`), { readOnly });
|
|
3321
|
+
$("#module-content").querySelectorAll("[data-edit-character]").forEach((button) => button.addEventListener("click", () => { void openCharacter(button.dataset.editCharacter, false); }));
|
|
3292
3322
|
}
|
|
3293
3323
|
|
|
3294
3324
|
async function renderRaces() {
|
|
3295
|
-
|
|
3296
|
-
apiAllPages(`/api/works/${state.work.id}/races`),
|
|
3297
|
-
canReadModule("characters") ? apiAllPages(`/api/works/${state.work.id}/characters`) : Promise.resolve([])
|
|
3298
|
-
]);
|
|
3325
|
+
state.races = await apiAllPages(`/api/works/${state.work.id}/races`);
|
|
3299
3326
|
const layout = readModuleLayout();
|
|
3300
3327
|
const canEditRaces = canEditModule("races");
|
|
3301
3328
|
const raceActions = (item) => canEditRaces
|
|
@@ -3307,10 +3334,10 @@ async function renderRaces() {
|
|
|
3307
3334
|
const renderRaceNode = (item) => `<details class="race-tree-node" open data-race-node="${esc(item.id)}">
|
|
3308
3335
|
<summary><span>${esc(item.name)}</span><small>${item.children.length} 个直接子种族</small></summary>
|
|
3309
3336
|
<div class="race-tree-branch">
|
|
3310
|
-
|
|
3337
|
+
<article class="record-card race-card preview-record-card${canEditRaces ? " has-card-edit" : ""}" data-open-race="${esc(item.id)}" role="button" tabindex="0" aria-label="查看种族 ${esc(item.name)}"><small>${item.memberIds.length} 位直接角色 · ${item.settingsCount ?? item.settings?.length ?? 0} 条自身设定</small>
|
|
3311
3338
|
<div class="race-path" aria-label="种族路径">${esc(racePathLabel(item))}</div>
|
|
3312
3339
|
<p>${esc(item.description || "尚未填写种族简介")}</p>
|
|
3313
|
-
<div class="race-settings">${item.
|
|
3340
|
+
<div class="race-settings">${item.settingsCount ? `<span class="pill">${item.settingsCount} 条共同设定,打开查看详情</span>` : '<span class="pill">暂无共同设定</span>'}</div>
|
|
3314
3341
|
<p class="race-members">直接角色:${item.members.length ? item.members.map((member) => esc(member.name)).join("、") : "暂无绑定角色"}</p>
|
|
3315
3342
|
${raceCardActions(item)}
|
|
3316
3343
|
</article>
|
|
@@ -3319,7 +3346,7 @@ async function renderRaces() {
|
|
|
3319
3346
|
</details>`;
|
|
3320
3347
|
const raceRows = () => `<div class="module-row-list">${state.races.map((item) => {
|
|
3321
3348
|
const preview = moduleRowPreview(item.description || "尚未填写种族简介");
|
|
3322
|
-
const meta = `${item.memberIds.length} 位直接角色 · ${item.settings
|
|
3349
|
+
const meta = `${item.memberIds.length} 位直接角色 · ${(item.settingsCount ?? item.settings?.length ?? 0) ? "已填写共同设定" : "暂无共同设定"}`;
|
|
3323
3350
|
return `
|
|
3324
3351
|
<article class="record-card module-row race-card preview-record-card" data-open-race="${esc(item.id)}" role="button" tabindex="0" aria-label="查看种族 ${esc(item.name)}">
|
|
3325
3352
|
<small>${esc(meta)}</small>
|
|
@@ -3333,16 +3360,13 @@ async function renderRaces() {
|
|
|
3333
3360
|
? `${layout === "rows" ? raceRows() : `<section class="race-tree" aria-label="种族层级">${buildRaceForest(state.races).map(renderRaceNode).join("")}</section>`}`
|
|
3334
3361
|
: emptyModule("还没有种族档案", "先创建种族及共同设定,之后角色编辑器才能选择该种族。");
|
|
3335
3362
|
bindModuleLayoutToggle(renderRaces);
|
|
3336
|
-
|
|
3337
|
-
$("#module-content").querySelectorAll("[data-edit-race]").forEach((button) => button.addEventListener("click", () =>
|
|
3363
|
+
const openRace = async (id, readOnly) => openRaceDialog(await api(`/api/races/${encodeURIComponent(id)}`), { readOnly });
|
|
3364
|
+
$("#module-content").querySelectorAll("[data-edit-race]").forEach((button) => button.addEventListener("click", () => { void openRace(button.dataset.editRace, false); }));
|
|
3338
3365
|
bindEntityHistoryButtons(async () => { await renderRaces(); await loadAiReferences(); });
|
|
3339
3366
|
}
|
|
3340
3367
|
|
|
3341
3368
|
async function renderOrganizations() {
|
|
3342
|
-
|
|
3343
|
-
apiAllPages(`/api/works/${state.work.id}/organizations`),
|
|
3344
|
-
canReadModule("characters") ? apiAllPages(`/api/works/${state.work.id}/characters`) : Promise.resolve([])
|
|
3345
|
-
]);
|
|
3369
|
+
state.organizations = await apiAllPages(`/api/works/${state.work.id}/organizations`);
|
|
3346
3370
|
const layout = readModuleLayout();
|
|
3347
3371
|
const canEditOrganizations = canEditModule("organizations");
|
|
3348
3372
|
const organizationActions = (item) => canEditOrganizations
|
|
@@ -3352,9 +3376,9 @@ async function renderOrganizations() {
|
|
|
3352
3376
|
? organizationActions(item)
|
|
3353
3377
|
: `<div class="card-actions">${organizationActions(item)}</div>`;
|
|
3354
3378
|
const organizationCards = () => `<div class="card-grid organization-grid">${state.organizations.map((item) => `
|
|
3355
|
-
<article class="record-card organization-card${canEditOrganizations ? " has-card-edit" : ""}"><small>${item.memberIds.length} 位成员 · ${item.settings
|
|
3379
|
+
<article class="record-card organization-card preview-record-card${canEditOrganizations ? " has-card-edit" : ""}" data-open-organization="${esc(item.id)}" role="button" tabindex="0" aria-label="查看组织 ${esc(item.name)}"><small>${item.memberIds.length} 位成员 · ${(item.settingsCount ?? item.settings?.length ?? 0) ? "已填写组织设定" : "暂无组织设定"}</small>
|
|
3356
3380
|
<h3>${esc(item.name)}</h3><p>${esc(item.description || "尚未填写组织简介")}</p>
|
|
3357
|
-
<div class="organization-settings">${item.
|
|
3381
|
+
<div class="organization-settings">${item.settingsCount ? `<span class="pill">${item.settingsCount} 条组织设定,打开查看详情</span>` : '<span class="pill">暂无组织设定</span>'}</div>
|
|
3358
3382
|
<p class="organization-members">成员:${item.members.length ? item.members.map((member) => esc(member.name)).join("、") : "暂无绑定角色"}</p>
|
|
3359
3383
|
${organizationCardActions(item)}
|
|
3360
3384
|
</article>`).join("")}</div>`;
|
|
@@ -3362,8 +3386,8 @@ async function renderOrganizations() {
|
|
|
3362
3386
|
const preview = moduleRowPreview(item.description || "尚未填写组织简介");
|
|
3363
3387
|
const members = item.members.length ? item.members.map((member) => member.name).join("、") : "暂无绑定角色";
|
|
3364
3388
|
return `
|
|
3365
|
-
<article class="record-card module-row organization-card">
|
|
3366
|
-
<small>${item.memberIds.length} 位成员 · ${item.settings
|
|
3389
|
+
<article class="record-card module-row organization-card" data-open-organization="${esc(item.id)}" role="button" tabindex="0" aria-label="查看组织 ${esc(item.name)}">
|
|
3390
|
+
<small>${item.memberIds.length} 位成员 · ${(item.settingsCount ?? item.settings?.length ?? 0) ? "已填写组织设定" : "暂无组织设定"}</small>
|
|
3367
3391
|
<h3>${esc(item.name)}</h3>
|
|
3368
3392
|
<p class="module-row-preview" title="${esc(`${preview} · 成员:${members}`)}">${esc(preview)} · ${esc(members)}</p>
|
|
3369
3393
|
<div class="card-actions">${organizationActions(item)}</div>
|
|
@@ -3374,7 +3398,8 @@ async function renderOrganizations() {
|
|
|
3374
3398
|
? `${layout === "rows" ? organizationRows() : organizationCards()}`
|
|
3375
3399
|
: emptyModule("还没有组织", "创建国家、机构、阵营或团队,并维护组织设定与成员。");
|
|
3376
3400
|
bindModuleLayoutToggle(renderOrganizations);
|
|
3377
|
-
|
|
3401
|
+
const openOrganization = async (id, readOnly) => openOrganizationDialog(await api(`/api/organizations/${encodeURIComponent(id)}`), { readOnly });
|
|
3402
|
+
$("#module-content").querySelectorAll("[data-edit-organization]").forEach((button) => button.addEventListener("click", () => { void openOrganization(button.dataset.editOrganization, false); }));
|
|
3378
3403
|
bindEntityHistoryButtons(async () => { await renderOrganizations(); await loadAiReferences(); });
|
|
3379
3404
|
}
|
|
3380
3405
|
|
|
@@ -4063,7 +4088,7 @@ async function loadAiReferences() {
|
|
|
4063
4088
|
const generation = workScopedUiGeneration;
|
|
4064
4089
|
const [characters, settings] = await Promise.all([
|
|
4065
4090
|
canReadModule("characters") ? apiAllPages(`/api/works/${workId}/characters`) : Promise.resolve([]),
|
|
4066
|
-
canReadModule("settings") ?
|
|
4091
|
+
canReadModule("settings") ? api(`/api/works/${workId}/settings/context`) : Promise.resolve([])
|
|
4067
4092
|
]);
|
|
4068
4093
|
if (state.work?.id !== workId || generation !== workScopedUiGeneration) return;
|
|
4069
4094
|
state.characters = characters;
|
|
@@ -4740,7 +4765,14 @@ function createVditorEditor(host, value, { onInput = () => {}, uploadAttachment
|
|
|
4740
4765
|
placeholder,
|
|
4741
4766
|
preview: { transform: transformVditorPreview },
|
|
4742
4767
|
cache: { enable: false },
|
|
4743
|
-
toolbar: ["headings", "bold", "italic", "strike", "|", "line", "quote", "list", "ordered-list", "check", "|", "code", "inline-code", "link", "table", "upload", "|", "undo", "redo",
|
|
4768
|
+
toolbar: ["headings", "bold", "italic", "strike", "|", "line", "quote", "list", "ordered-list", "check", "|", "code", "inline-code", "link", "table", "upload", "|", "undo", "redo", {
|
|
4769
|
+
name: "line-number",
|
|
4770
|
+
tip: "显示/隐藏行号",
|
|
4771
|
+
tipPosition: "s",
|
|
4772
|
+
className: "vditor-line-number-button",
|
|
4773
|
+
icon: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 5h3M4 9h3M4 13h3M4 17h3M10 5h10M10 9h10M10 13h10M10 17h10" fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="1.6"/></svg>',
|
|
4774
|
+
click: () => toggleVditorLineNumbers(editor)
|
|
4775
|
+
}, "edit-mode", "fullscreen"],
|
|
4744
4776
|
upload: {
|
|
4745
4777
|
accept: "image/*",
|
|
4746
4778
|
max: 10 * 1024 * 1024,
|
|
@@ -4750,11 +4782,13 @@ function createVditorEditor(host, value, { onInput = () => {}, uploadAttachment
|
|
|
4750
4782
|
input: (markdown) => {
|
|
4751
4783
|
normalizeVditorAttachmentImages(editor);
|
|
4752
4784
|
updateVditorWordCount(editor, markdown);
|
|
4785
|
+
updateVditorLineNumbers(editor, markdown);
|
|
4753
4786
|
onInput(markdown);
|
|
4754
4787
|
},
|
|
4755
4788
|
after: () => {
|
|
4756
4789
|
normalizeVditorAttachmentImages(editor);
|
|
4757
4790
|
updateVditorWordCount(editor, value);
|
|
4791
|
+
updateVditorLineNumbers(editor, value);
|
|
4758
4792
|
if (readOnly) editor?.disabled();
|
|
4759
4793
|
}
|
|
4760
4794
|
});
|
|
@@ -4765,6 +4799,34 @@ function createVditorEditor(host, value, { onInput = () => {}, uploadAttachment
|
|
|
4765
4799
|
return editor;
|
|
4766
4800
|
}
|
|
4767
4801
|
|
|
4802
|
+
function toggleVditorLineNumbers(editor) {
|
|
4803
|
+
const host = editor?.vditor?.element;
|
|
4804
|
+
if (!host) return;
|
|
4805
|
+
const enabled = host.classList.toggle("show-line-numbers");
|
|
4806
|
+
const button = host.querySelector(".vditor-line-number-button");
|
|
4807
|
+
if (button) button.setAttribute("aria-pressed", String(enabled));
|
|
4808
|
+
}
|
|
4809
|
+
|
|
4810
|
+
function updateVditorLineNumbers(editor, markdown) {
|
|
4811
|
+
const content = editor?.vditor?.element?.querySelector(".vditor-content");
|
|
4812
|
+
if (!content) return;
|
|
4813
|
+
let gutter = content.querySelector(".vditor-line-numbers");
|
|
4814
|
+
if (!gutter) {
|
|
4815
|
+
gutter = document.createElement("div");
|
|
4816
|
+
gutter.className = "vditor-line-numbers";
|
|
4817
|
+
gutter.setAttribute("aria-hidden", "true");
|
|
4818
|
+
content.prepend(gutter);
|
|
4819
|
+
}
|
|
4820
|
+
const lineCount = Math.max(1, String(markdown ?? "").split(/\r?\n/gu).length);
|
|
4821
|
+
const fragment = document.createDocumentFragment();
|
|
4822
|
+
for (let line = 1; line <= lineCount; line += 1) {
|
|
4823
|
+
const number = document.createElement("span");
|
|
4824
|
+
number.textContent = String(line);
|
|
4825
|
+
fragment.append(number);
|
|
4826
|
+
}
|
|
4827
|
+
gutter.replaceChildren(fragment);
|
|
4828
|
+
}
|
|
4829
|
+
|
|
4768
4830
|
function updateVditorWordCount(editor, markdown) {
|
|
4769
4831
|
const toolbar = editor?.vditor?.toolbar?.element;
|
|
4770
4832
|
if (!toolbar) return;
|
|
@@ -4861,13 +4923,10 @@ function knowledgeSectionEditorHtml(section = null) {
|
|
|
4861
4923
|
const label = knowledgeEditorKind === "race" ? "种族" : "组织";
|
|
4862
4924
|
return `<div class="character-section-editor-shell knowledge-section-editor-shell">
|
|
4863
4925
|
<header class="character-section-editor-header">
|
|
4864
|
-
<div><span class="eyebrow">${label} Markdown 设定</span><
|
|
4926
|
+
<div><span class="eyebrow">${label} Markdown 设定</span><input id="knowledge-section-title" class="character-section-editor-title-input" maxlength="200" value="${esc(section?.title ?? "")}" placeholder="新建设定" aria-label="设定标题" required></div>
|
|
4865
4927
|
<button class="entity-editor-back" type="button" data-knowledge-section-edit-close>返回设定列表</button>
|
|
4866
4928
|
</header>
|
|
4867
4929
|
<section class="character-markdown-editor" aria-label="${section ? "编辑" : "新建"}${label} Markdown 设定">
|
|
4868
|
-
<div class="character-markdown-editor-meta">
|
|
4869
|
-
<label>设定标题<input id="knowledge-section-title" maxlength="200" value="${esc(section?.title ?? "")}" placeholder="例如:组织章程、种族特征" required></label>
|
|
4870
|
-
</div>
|
|
4871
4930
|
<div id="knowledge-section-markdown" class="vditor-editor-host" data-vditor-editor aria-label="Markdown 编辑器"></div>
|
|
4872
4931
|
<div class="character-markdown-editor-footer">
|
|
4873
4932
|
<div class="character-markdown-editor-actions"><button type="button" data-knowledge-section-edit-cancel>取消</button><button type="button" class="primary-button" data-knowledge-section-edit-save>${section ? "保存设定" : "添加设定"}</button></div>
|
|
@@ -4939,13 +4998,12 @@ function characterSectionEditorHtml(section = null) {
|
|
|
4939
4998
|
const options = Object.entries(characterSectionTypeLabels).map(([value, label]) => `<option value="${value}" ${section?.sectionType === value ? "selected" : ""}>${esc(label)}</option>`).join("");
|
|
4940
4999
|
return `<div class="character-section-editor-shell">
|
|
4941
5000
|
<header class="character-section-editor-header">
|
|
4942
|
-
<div><span class="eyebrow">人物 Markdown 档案</span><
|
|
5001
|
+
<div><span class="eyebrow">人物 Markdown 档案</span><input id="character-section-title" class="character-section-editor-title-input" maxlength="200" value="${esc(section?.title ?? "")}" placeholder="新建档案章节" aria-label="章节标题" required></div>
|
|
4943
5002
|
<button class="entity-editor-back" type="button" data-character-section-edit-close>返回人物档案</button>
|
|
4944
5003
|
</header>
|
|
4945
5004
|
<section class="character-markdown-editor" aria-label="${section ? "编辑" : "新建"}人物 Markdown 章节">
|
|
4946
5005
|
<div class="character-markdown-editor-meta">
|
|
4947
5006
|
<label>章节类型<select id="character-section-type">${options}</select></label>
|
|
4948
|
-
<label>章节标题<input id="character-section-title" maxlength="200" value="${esc(section?.title ?? "")}" placeholder="例如:背景故事" required></label>
|
|
4949
5007
|
<label class="character-markdown-summary-field">章节摘要<textarea id="character-section-summary" maxlength="20000" placeholder="用于角色列表和 AI 快速定位,不会替代正文">${esc(section?.summary ?? "")}</textarea></label>
|
|
4950
5008
|
</div>
|
|
4951
5009
|
<div id="character-section-markdown" class="vditor-editor-host" data-vditor-editor aria-label="Markdown 编辑器"></div>
|
|
@@ -6176,7 +6234,9 @@ $("#profile-form").addEventListener("submit", async (event) => {
|
|
|
6176
6234
|
state.user = updated;
|
|
6177
6235
|
applyAuthenticatedUser({ user: updated, csrfToken: state.csrfToken });
|
|
6178
6236
|
toast("显示名称已更新");
|
|
6179
|
-
} catch (error) {
|
|
6237
|
+
} catch (error) {
|
|
6238
|
+
toast(error.code === "INVALID_CURRENT_PASSWORD" ? "当前密码错误,请重新输入" : error.message, "error");
|
|
6239
|
+
}
|
|
6180
6240
|
});
|
|
6181
6241
|
$("#password-form").addEventListener("submit", async (event) => {
|
|
6182
6242
|
event.preventDefault();
|
package/dist/public/index.html
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
<link rel="icon" href="/icon.svg?v=20260712" type="image/svg+xml">
|
|
11
11
|
<link rel="manifest" href="/site.webmanifest">
|
|
12
12
|
<link rel="stylesheet" href="/vendor/vditor/dist/index.css?v=3.11.2">
|
|
13
|
-
<link rel="stylesheet" href="/styles.css?v=20260725-
|
|
13
|
+
<link rel="stylesheet" href="/styles.css?v=20260725-develop-galaxy-ripple">
|
|
14
14
|
</head>
|
|
15
15
|
<body class="auth-pending">
|
|
16
16
|
<section id="auth-view" class="auth-view hidden" aria-labelledby="auth-title">
|
|
@@ -376,6 +376,7 @@
|
|
|
376
376
|
<button id="setting-editor-back" class="entity-editor-back" type="button">返回设定库</button>
|
|
377
377
|
<div class="entity-editor-heading">
|
|
378
378
|
<span id="setting-editor-eyebrow" class="eyebrow">新建设定</span>
|
|
379
|
+
<span id="setting-editor-readonly-badge" class="entity-editor-readonly-badge hidden">只读模式</span>
|
|
379
380
|
<input id="setting-editor-name" class="setting-editor-title-input" name="title" maxlength="200" placeholder="新建设定" aria-label="设定标题">
|
|
380
381
|
<div class="setting-editor-header-fields">
|
|
381
382
|
<label>分类<select id="setting-editor-category" name="category">
|
|
@@ -408,7 +409,7 @@
|
|
|
408
409
|
<button id="character-editor-close" class="entity-editor-back" type="button">返回角色列表</button>
|
|
409
410
|
<div>
|
|
410
411
|
<span id="character-editor-eyebrow" class="eyebrow">人物主档案</span>
|
|
411
|
-
<div class="character-editor-title-row"><h1 id="character-editor-title">新建角色</h1><span id="character-editor-version" class="character-version-badge">新档案</span></div>
|
|
412
|
+
<div class="character-editor-title-row"><h1 id="character-editor-title">新建角色</h1><span id="character-editor-version" class="character-version-badge">新档案</span><span id="character-editor-readonly-badge" class="entity-editor-readonly-badge hidden">只读模式</span></div>
|
|
412
413
|
</div>
|
|
413
414
|
<div class="character-editor-header-actions">
|
|
414
415
|
<button id="character-editor-edit" class="primary-button hidden" type="button">编辑角色</button>
|
|
@@ -442,7 +443,7 @@
|
|
|
442
443
|
<button id="knowledge-editor-close" class="entity-editor-back" type="button">返回列表</button>
|
|
443
444
|
<div>
|
|
444
445
|
<span id="knowledge-editor-eyebrow" class="eyebrow">世界档案</span>
|
|
445
|
-
<div class="character-editor-title-row"><h1 id="knowledge-editor-title">新建档案</h1><span id="knowledge-editor-version" class="character-version-badge">新档案</span></div>
|
|
446
|
+
<div class="character-editor-title-row"><h1 id="knowledge-editor-title">新建档案</h1><span id="knowledge-editor-version" class="character-version-badge">新档案</span><span id="knowledge-editor-readonly-badge" class="entity-editor-readonly-badge hidden">只读模式</span></div>
|
|
446
447
|
</div>
|
|
447
448
|
<div class="character-editor-header-actions">
|
|
448
449
|
<span id="knowledge-editor-header-note" class="knowledge-editor-header-note">独立资料页</span>
|
|
@@ -682,6 +683,7 @@
|
|
|
682
683
|
<output id="galaxy-stats" class="galaxy-stats" aria-live="polite" data-testid="galaxy-stats"></output>
|
|
683
684
|
<aside class="galaxy-controls" aria-label="银河图控制">
|
|
684
685
|
<button id="galaxy-stars" type="button" aria-pressed="true" data-testid="galaxy-stars">隐藏背景星点</button>
|
|
686
|
+
<button id="galaxy-grid" type="button" aria-pressed="false" data-testid="galaxy-grid">显示空间网格</button>
|
|
685
687
|
<button id="galaxy-rotation" type="button" aria-pressed="false" data-testid="galaxy-rotation">暂停旋转</button>
|
|
686
688
|
<button id="galaxy-zoom-out" type="button" aria-label="缩小">−</button>
|
|
687
689
|
<button id="galaxy-zoom-in" type="button" aria-label="放大">+</button>
|
|
@@ -697,6 +699,6 @@
|
|
|
697
699
|
<div id="auth-loading" class="auth-loading" role="status" aria-label="正在载入工作台"></div>
|
|
698
700
|
<script id="vditorIconScript" src="/vendor/vditor/dist/js/icons/ant.js?v=3.11.2"></script>
|
|
699
701
|
<script src="/vendor/vditor/dist/index.min.js?v=3.11.2"></script>
|
|
700
|
-
<script type="module" src="/app.js?v=20260725-
|
|
702
|
+
<script type="module" src="/app.js?v=20260725-develop-galaxy-ripple"></script>
|
|
701
703
|
</body>
|
|
702
704
|
</html>
|
|
@@ -1551,29 +1551,87 @@ export function layoutGalaxy(graph, seed) {
|
|
|
1551
1551
|
return { nodes, byId };
|
|
1552
1552
|
}
|
|
1553
1553
|
|
|
1554
|
-
export function createGalaxyStarfield(seed, count =
|
|
1554
|
+
export function createGalaxyStarfield(seed, count = 7200) {
|
|
1555
1555
|
const random = seededRandom(hashString(seed));
|
|
1556
1556
|
const stars = [];
|
|
1557
1557
|
const armCount = 4;
|
|
1558
1558
|
for (let index = 0; index < count; index += 1) {
|
|
1559
|
-
const
|
|
1559
|
+
const population = random();
|
|
1560
|
+
const isCore = population < 0.62;
|
|
1561
|
+
const isHalo = !isCore && population > 0.9;
|
|
1562
|
+
const radius = isCore
|
|
1563
|
+
? 32 + Math.pow(random(), 1.72) * 720
|
|
1564
|
+
: 160 + Math.pow(random(), 0.68) * 1510;
|
|
1560
1565
|
const arm = index % armCount;
|
|
1561
1566
|
const armAngle = arm / armCount * Math.PI * 2;
|
|
1562
|
-
const angle =
|
|
1563
|
-
|
|
1567
|
+
const angle = isHalo
|
|
1568
|
+
? random() * Math.PI * 2
|
|
1569
|
+
: armAngle + radius * 0.0065 + (random() - 0.5) * (isCore ? 0.72 : 0.42 + radius / 1100);
|
|
1570
|
+
const thickness = isHalo ? 70 + radius * 0.2 : (isCore ? 8 + radius * 0.045 : 22 + radius * 0.105);
|
|
1564
1571
|
const temperature = random();
|
|
1572
|
+
const x = Math.cos(angle) * radius + (random() - 0.5) * (isCore ? 42 : 78);
|
|
1573
|
+
const z = Math.sin(angle) * radius + (random() - 0.5) * (isCore ? 42 : 78);
|
|
1565
1574
|
stars.push({
|
|
1566
|
-
x
|
|
1575
|
+
x,
|
|
1567
1576
|
y: (random() + random() + random() - 1.5) * thickness,
|
|
1568
|
-
z
|
|
1569
|
-
|
|
1570
|
-
|
|
1577
|
+
z,
|
|
1578
|
+
originX: x,
|
|
1579
|
+
originZ: z,
|
|
1580
|
+
vx: 0,
|
|
1581
|
+
vz: 0,
|
|
1582
|
+
size: isCore && random() > 0.94 ? 1.25 + random() * 1.25 : 0.38 + random() * 0.82,
|
|
1583
|
+
brightness: isCore ? 0.3 + random() * 0.7 : 0.16 + random() * 0.66,
|
|
1571
1584
|
color: temperature < 0.2 ? "255,218,176" : temperature > 0.78 ? "174,211,255" : "226,237,255"
|
|
1572
1585
|
});
|
|
1573
1586
|
}
|
|
1574
1587
|
return stars;
|
|
1575
1588
|
}
|
|
1576
1589
|
|
|
1590
|
+
export function stepGalaxyStarfieldPhysics(stars, attractor = null, options = {}) {
|
|
1591
|
+
const deltaMs = clamp(Number(options.deltaMs) || 16.667, 1, 50);
|
|
1592
|
+
const timeScale = deltaMs / 16.667;
|
|
1593
|
+
const influenceRadius = Math.max(1, Number(options.influenceRadius) || 210);
|
|
1594
|
+
const repulsionStrength = Number(options.repulsionStrength) || 5.5;
|
|
1595
|
+
const springStrength = Number(options.springStrength) || 0.006;
|
|
1596
|
+
const damping = clamp(Number(options.damping) || 0.84, 0, 1);
|
|
1597
|
+
let energy = 0;
|
|
1598
|
+
|
|
1599
|
+
for (const star of stars) {
|
|
1600
|
+
const originX = Number(star.originX ?? star.x);
|
|
1601
|
+
const originZ = Number(star.originZ ?? star.z);
|
|
1602
|
+
star.originX = originX;
|
|
1603
|
+
star.originZ = originZ;
|
|
1604
|
+
star.vx = Number(star.vx) || 0;
|
|
1605
|
+
star.vz = Number(star.vz) || 0;
|
|
1606
|
+
|
|
1607
|
+
if (attractor) {
|
|
1608
|
+
const deltaX = star.x - attractor.x;
|
|
1609
|
+
const deltaZ = star.z - attractor.z;
|
|
1610
|
+
const distance = Math.hypot(deltaX, deltaZ);
|
|
1611
|
+
if (distance < influenceRadius) {
|
|
1612
|
+
const angle = Math.atan2(star.originZ, star.originX);
|
|
1613
|
+
const normalX = distance > 0.001 ? deltaX / distance : Math.cos(angle);
|
|
1614
|
+
const normalZ = distance > 0.001 ? deltaZ / distance : Math.sin(angle);
|
|
1615
|
+
const falloff = 1 - distance / influenceRadius;
|
|
1616
|
+
const force = falloff * falloff * repulsionStrength * timeScale;
|
|
1617
|
+
star.vx += normalX * force;
|
|
1618
|
+
star.vz += normalZ * force;
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
star.vx += (originX - star.x) * springStrength * timeScale;
|
|
1623
|
+
star.vz += (originZ - star.z) * springStrength * timeScale;
|
|
1624
|
+
const frameDamping = Math.pow(damping, timeScale);
|
|
1625
|
+
star.vx *= frameDamping;
|
|
1626
|
+
star.vz *= frameDamping;
|
|
1627
|
+
star.x += star.vx * timeScale;
|
|
1628
|
+
star.z += star.vz * timeScale;
|
|
1629
|
+
energy += Math.abs(star.vx) + Math.abs(star.vz) + Math.abs(originX - star.x) + Math.abs(originZ - star.z);
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
return energy;
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1577
1635
|
export function projectGalaxyPoint(point, camera, viewport) {
|
|
1578
1636
|
const relativeX = point.x - Number(camera.targetX ?? 0);
|
|
1579
1637
|
const relativeY = point.y - Number(camera.targetY ?? 0);
|
|
@@ -1697,13 +1755,18 @@ export function createGalaxyRenderer(dialog, graph, options = {}) {
|
|
|
1697
1755
|
let animationFrame = 0;
|
|
1698
1756
|
let previousFrameTime = 0;
|
|
1699
1757
|
let cameraFocus = null;
|
|
1758
|
+
let draggedNode = null;
|
|
1759
|
+
let starPhysicsEnergy = 0;
|
|
1700
1760
|
let paused = false;
|
|
1701
1761
|
let starsVisible = true;
|
|
1762
|
+
let gridVisible = false;
|
|
1702
1763
|
let destroyed = false;
|
|
1703
1764
|
|
|
1704
1765
|
shell.classList.add("is-three-dimensional");
|
|
1705
1766
|
shell.dataset.sceneDimension = "3";
|
|
1706
1767
|
shell.dataset.starCount = String(stars.length);
|
|
1768
|
+
shell.dataset.gridVisible = "false";
|
|
1769
|
+
shell.dataset.starPhysicsEnergy = "0";
|
|
1707
1770
|
shell.dataset.rotationSpeed = String(GALAXY_ROTATION_RADIANS_PER_MS);
|
|
1708
1771
|
shell.dataset.layoutMinimumRadius = String(GALAXY_LAYOUT_CONFIG.minimumRadius);
|
|
1709
1772
|
shell.dataset.layoutRadialSpan = String(GALAXY_LAYOUT_CONFIG.radialSpan);
|
|
@@ -1743,24 +1806,26 @@ export function createGalaxyRenderer(dialog, graph, options = {}) {
|
|
|
1743
1806
|
context.fillStyle = backdrop;
|
|
1744
1807
|
context.fillRect(0, 0, width, height);
|
|
1745
1808
|
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1809
|
+
if (gridVisible) {
|
|
1810
|
+
context.lineWidth = 1;
|
|
1811
|
+
context.strokeStyle = "rgba(105,142,182,.06)";
|
|
1812
|
+
for (let offset = -1200; offset <= 1200; offset += 160) {
|
|
1813
|
+
const horizontalStart = project({ x: -1200, y: 0, z: offset }, width, height);
|
|
1814
|
+
const horizontalEnd = project({ x: 1200, y: 0, z: offset }, width, height);
|
|
1815
|
+
const verticalStart = project({ x: offset, y: 0, z: -1200 }, width, height);
|
|
1816
|
+
const verticalEnd = project({ x: offset, y: 0, z: 1200 }, width, height);
|
|
1817
|
+
if (horizontalStart.visible && horizontalEnd.visible) {
|
|
1818
|
+
context.beginPath();
|
|
1819
|
+
context.moveTo(horizontalStart.x, horizontalStart.y);
|
|
1820
|
+
context.lineTo(horizontalEnd.x, horizontalEnd.y);
|
|
1821
|
+
context.stroke();
|
|
1822
|
+
}
|
|
1823
|
+
if (verticalStart.visible && verticalEnd.visible) {
|
|
1824
|
+
context.beginPath();
|
|
1825
|
+
context.moveTo(verticalStart.x, verticalStart.y);
|
|
1826
|
+
context.lineTo(verticalEnd.x, verticalEnd.y);
|
|
1827
|
+
context.stroke();
|
|
1828
|
+
}
|
|
1764
1829
|
}
|
|
1765
1830
|
}
|
|
1766
1831
|
|
|
@@ -1938,6 +2003,10 @@ export function createGalaxyRenderer(dialog, graph, options = {}) {
|
|
|
1938
2003
|
}
|
|
1939
2004
|
}
|
|
1940
2005
|
if (!paused && !cameraDrag) camera.yaw += elapsed * GALAXY_ROTATION_RADIANS_PER_MS;
|
|
2006
|
+
if (draggedNode || starPhysicsEnergy > 0.01) {
|
|
2007
|
+
starPhysicsEnergy = stepGalaxyStarfieldPhysics(stars, draggedNode, { deltaMs: elapsed || 16.667 });
|
|
2008
|
+
shell.dataset.starPhysicsEnergy = starPhysicsEnergy.toFixed(3);
|
|
2009
|
+
}
|
|
1941
2010
|
drawScene();
|
|
1942
2011
|
animationFrame = window.requestAnimationFrame(renderFrame);
|
|
1943
2012
|
};
|
|
@@ -2072,6 +2141,7 @@ export function createGalaxyRenderer(dialog, graph, options = {}) {
|
|
|
2072
2141
|
scale: Number(button.dataset.projectedScale) || 1,
|
|
2073
2142
|
dragged: false
|
|
2074
2143
|
};
|
|
2144
|
+
draggedNode = node;
|
|
2075
2145
|
button.setPointerCapture(event.pointerId);
|
|
2076
2146
|
button.classList.add("is-dragging");
|
|
2077
2147
|
button.setAttribute("aria-grabbed", "true");
|
|
@@ -2099,6 +2169,7 @@ export function createGalaxyRenderer(dialog, graph, options = {}) {
|
|
|
2099
2169
|
if (!nodeDrag || event.pointerId !== nodeDrag.pointerId) return;
|
|
2100
2170
|
suppressClick = nodeDrag.dragged;
|
|
2101
2171
|
nodeDrag = null;
|
|
2172
|
+
if (draggedNode === node) draggedNode = null;
|
|
2102
2173
|
button.classList.remove("is-dragging");
|
|
2103
2174
|
button.setAttribute("aria-grabbed", "false");
|
|
2104
2175
|
if (button.hasPointerCapture(event.pointerId)) button.releasePointerCapture(event.pointerId);
|
|
@@ -2156,9 +2227,13 @@ export function createGalaxyRenderer(dialog, graph, options = {}) {
|
|
|
2156
2227
|
if (!dialog.open) dialog.showModal();
|
|
2157
2228
|
paused = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
2158
2229
|
starsVisible = true;
|
|
2230
|
+
gridVisible = false;
|
|
2159
2231
|
background.classList.remove("hidden-stars");
|
|
2232
|
+
shell.dataset.gridVisible = "false";
|
|
2160
2233
|
dialog.querySelector("#galaxy-stars").setAttribute("aria-pressed", "true");
|
|
2161
2234
|
dialog.querySelector("#galaxy-stars").textContent = "隐藏背景星点";
|
|
2235
|
+
dialog.querySelector("#galaxy-grid").setAttribute("aria-pressed", "false");
|
|
2236
|
+
dialog.querySelector("#galaxy-grid").textContent = "显示空间网格";
|
|
2162
2237
|
updateRotationControl();
|
|
2163
2238
|
stats.value = `${graph.stats.nodeCount} 个节点 / ${graph.stats.edgeCount} 条关系`;
|
|
2164
2239
|
renderNodes();
|
|
@@ -2182,6 +2257,13 @@ export function createGalaxyRenderer(dialog, graph, options = {}) {
|
|
|
2182
2257
|
event.currentTarget.textContent = starsVisible ? "隐藏背景星点" : "显示背景星点";
|
|
2183
2258
|
drawScene();
|
|
2184
2259
|
});
|
|
2260
|
+
listen(dialog.querySelector("#galaxy-grid"), "click", (event) => {
|
|
2261
|
+
gridVisible = !gridVisible;
|
|
2262
|
+
shell.dataset.gridVisible = String(gridVisible);
|
|
2263
|
+
event.currentTarget.setAttribute("aria-pressed", String(gridVisible));
|
|
2264
|
+
event.currentTarget.textContent = gridVisible ? "隐藏空间网格" : "显示空间网格";
|
|
2265
|
+
drawScene();
|
|
2266
|
+
});
|
|
2185
2267
|
listen(dialog.querySelector("#galaxy-rotation"), "click", () => {
|
|
2186
2268
|
paused = !paused;
|
|
2187
2269
|
updateRotationControl();
|