@musnows/scriverse 0.4.3 → 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 +15 -5
- package/dist/app.js.map +1 -1
- package/dist/public/app.js +176 -53
- package/dist/public/index.html +9 -4
- package/dist/public/relationship-graph.js +108 -26
- package/dist/public/styles.css +253 -8
- 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";
|
|
@@ -287,6 +287,10 @@ function hasCompletedOnboarding() {
|
|
|
287
287
|
return state.user?.onboardingCompleted === true;
|
|
288
288
|
}
|
|
289
289
|
|
|
290
|
+
function isMobileViewport() {
|
|
291
|
+
return window.matchMedia("(max-width: 850px)").matches;
|
|
292
|
+
}
|
|
293
|
+
|
|
290
294
|
function persistOnboardingCompletion() {
|
|
291
295
|
if (!state.user || state.user.onboardingCompleted) return;
|
|
292
296
|
state.user = { ...state.user, onboardingCompleted: true };
|
|
@@ -409,6 +413,7 @@ function renderOnboardingStep(step, focusTitle = false) {
|
|
|
409
413
|
}
|
|
410
414
|
|
|
411
415
|
function openOnboarding(force = false) {
|
|
416
|
+
if (isMobileViewport()) return;
|
|
412
417
|
const dialog = $("#onboarding-dialog");
|
|
413
418
|
if (!force && hasCompletedOnboarding()) return;
|
|
414
419
|
onboardingSteps = currentOnboardingSteps();
|
|
@@ -427,7 +432,7 @@ function completeOnboarding() {
|
|
|
427
432
|
}
|
|
428
433
|
|
|
429
434
|
function scheduleFirstUseOnboarding() {
|
|
430
|
-
if (onboardingAutoScheduled || hasCompletedOnboarding()) return;
|
|
435
|
+
if (isMobileViewport() || onboardingAutoScheduled || hasCompletedOnboarding()) return;
|
|
431
436
|
onboardingAutoScheduled = true;
|
|
432
437
|
window.requestAnimationFrame(() => {
|
|
433
438
|
onboardingAutoScheduled = false;
|
|
@@ -601,6 +606,11 @@ function loadPanelLayout() {
|
|
|
601
606
|
}
|
|
602
607
|
|
|
603
608
|
let panelLayout = loadPanelLayout();
|
|
609
|
+
if (window.matchMedia("(max-width: 850px)").matches) {
|
|
610
|
+
// 手机上默认收起创作助手,避免遮挡正文;用户可通过底部把手随时展开。
|
|
611
|
+
panelLayout.aiCollapsed = true;
|
|
612
|
+
panelLayout.leftCollapsed = true;
|
|
613
|
+
}
|
|
604
614
|
|
|
605
615
|
function constrainPanelLayout() {
|
|
606
616
|
const minimumMainWidth = 480;
|
|
@@ -621,9 +631,13 @@ function applyPanelLayout(persist = false) {
|
|
|
621
631
|
app.style.setProperty("--ai-panel-width", `${panelLayout.aiWidth}px`);
|
|
622
632
|
app.classList.toggle("left-panel-collapsed", panelLayout.leftCollapsed);
|
|
623
633
|
app.classList.toggle("ai-panel-collapsed", panelLayout.aiCollapsed);
|
|
624
|
-
|
|
634
|
+
const mobileViewport = isMobileViewport();
|
|
635
|
+
$("#left-panel-toggle").textContent = mobileViewport ? "›" : (panelLayout.leftCollapsed ? "›" : "‹");
|
|
625
636
|
$("#left-panel-toggle").setAttribute("aria-expanded", String(!panelLayout.leftCollapsed));
|
|
626
|
-
$("#left-panel-toggle").setAttribute("aria-label",
|
|
637
|
+
$("#left-panel-toggle").setAttribute("aria-label", mobileViewport
|
|
638
|
+
? (panelLayout.leftCollapsed ? "打开作品模块" : "关闭作品模块")
|
|
639
|
+
: (panelLayout.leftCollapsed ? "展开作品侧栏" : "收起作品侧栏"));
|
|
640
|
+
$("#mobile-module-tab").setAttribute("aria-expanded", String(!panelLayout.leftCollapsed));
|
|
627
641
|
$("#ai-panel-toggle").textContent = panelLayout.aiCollapsed ? "‹" : "›";
|
|
628
642
|
$("#ai-panel-toggle").setAttribute("aria-expanded", String(!panelLayout.aiCollapsed));
|
|
629
643
|
$("#ai-panel-toggle").setAttribute("aria-label", panelLayout.aiCollapsed ? "展开创作助手" : "收起创作助手");
|
|
@@ -709,11 +723,14 @@ let settingEditorVditor = null;
|
|
|
709
723
|
let knowledgeSectionVditor = null;
|
|
710
724
|
let characterSectionVditor = null;
|
|
711
725
|
let entityHistoryContext = null;
|
|
726
|
+
let moduleContentInteractionsBound = false;
|
|
712
727
|
|
|
713
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);
|
|
714
731
|
entityEditorType = type;
|
|
715
732
|
entityEditorDirty = false;
|
|
716
|
-
entityEditorReadOnly =
|
|
733
|
+
entityEditorReadOnly = viewOnly;
|
|
717
734
|
characterSectionEditorDirty = false;
|
|
718
735
|
knowledgeSectionEditorDirty = false;
|
|
719
736
|
$("#entity-editor-view").classList.remove("hidden");
|
|
@@ -722,6 +739,7 @@ function showEntityEditorPage(type, { readOnly = false } = {}) {
|
|
|
722
739
|
$("#knowledge-editor-form").classList.toggle("hidden", !["race", "organization"].includes(type));
|
|
723
740
|
$("#character-section-editor-view").classList.add("hidden");
|
|
724
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))));
|
|
725
743
|
$("#app").inert = true;
|
|
726
744
|
document.body.classList.add("entity-editor-open");
|
|
727
745
|
replacePageRoute(currentPageRoute());
|
|
@@ -1763,8 +1781,14 @@ async function api(path, options = {}) {
|
|
|
1763
1781
|
});
|
|
1764
1782
|
if (!response.ok) {
|
|
1765
1783
|
const payload = await response.json().catch(() => ({ error: { message: `请求失败:${response.status}` } }));
|
|
1766
|
-
if (response.status === 401 && !path.startsWith("/api/auth/"))
|
|
1767
|
-
|
|
1784
|
+
if (response.status === 401 && !path.startsWith("/api/auth/")) {
|
|
1785
|
+
state.user = null;
|
|
1786
|
+
state.csrfToken = null;
|
|
1787
|
+
showAuth(false);
|
|
1788
|
+
}
|
|
1789
|
+
const error = new Error(payload.error?.message ?? `请求失败:${response.status}`);
|
|
1790
|
+
error.code = payload.error?.code;
|
|
1791
|
+
throw error;
|
|
1768
1792
|
}
|
|
1769
1793
|
if (response.status === 204) return null;
|
|
1770
1794
|
const payload = await response.json();
|
|
@@ -1844,6 +1868,7 @@ async function refreshAuthCaptcha(target = "login") {
|
|
|
1844
1868
|
}
|
|
1845
1869
|
|
|
1846
1870
|
function showAuth(setupRequired, registrationOpen = false) {
|
|
1871
|
+
if (state.user) return;
|
|
1847
1872
|
document.body.classList.add("auth-pending");
|
|
1848
1873
|
$("#auth-view").classList.remove("hidden");
|
|
1849
1874
|
const canRegister = registrationOpen === true;
|
|
@@ -1872,6 +1897,7 @@ function applyAuthenticatedUser(session) {
|
|
|
1872
1897
|
$("#account-menu-role").textContent = session.user.role === "admin" ? "系统管理员" : "普通用户";
|
|
1873
1898
|
$("#auth-view").classList.add("hidden");
|
|
1874
1899
|
document.documentElement.classList.remove("login-route");
|
|
1900
|
+
if (!session.csrfToken) document.body.classList.remove("auth-pending");
|
|
1875
1901
|
// 注意:auth-pending 由 initializePage 路由完成后才移除,
|
|
1876
1902
|
// 避免会话确认后、目标视图渲染前露出无内容的编辑器外壳
|
|
1877
1903
|
}
|
|
@@ -2239,15 +2265,13 @@ async function initializePage() {
|
|
|
2239
2265
|
|
|
2240
2266
|
if (route.view === "editor") {
|
|
2241
2267
|
if (route.chapterId && state.chapter?.id !== route.chapterId) await selectChapter(route.chapterId);
|
|
2268
|
+
if (state.chapter?.id === route.chapterId && $("#editor-view").classList.contains("hidden")) await selectChapter(state.chapter.id);
|
|
2242
2269
|
return;
|
|
2243
2270
|
}
|
|
2244
2271
|
if (route.view === "module") return;
|
|
2245
2272
|
if (route.view === "entity-editor") {
|
|
2246
|
-
const records = route.entity === "setting" ? state.settings : route.entity === "character" ? state.characters : route.entity === "race" ? state.races : state.organizations;
|
|
2247
2273
|
const item = route.entityId
|
|
2248
|
-
? route.entity === "character"
|
|
2249
|
-
? await api(`/api/characters/${encodeURIComponent(route.entityId)}`)
|
|
2250
|
-
: 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)}`)
|
|
2251
2275
|
: null;
|
|
2252
2276
|
if (route.entityId && !item) {
|
|
2253
2277
|
toast(({ setting: "未找到要编辑的设定", character: "未找到要编辑的角色", race: "未找到要编辑的种族", organization: "未找到要编辑的组织" }[route.entity] ?? "未找到要编辑的档案"), "error");
|
|
@@ -2882,6 +2906,7 @@ async function showModule(module) {
|
|
|
2882
2906
|
$("#module-create-button").textContent = meta[3];
|
|
2883
2907
|
$("#module-create-button").classList.toggle("hidden", module === "ai-settings" || !canEditModule(module));
|
|
2884
2908
|
$("#module-content").innerHTML = '<div class="empty-state">正在载入……</div>';
|
|
2909
|
+
bindModuleContentInteractions();
|
|
2885
2910
|
try {
|
|
2886
2911
|
if (module === "settings") await renderSettings();
|
|
2887
2912
|
if (module === "characters") await renderCharacters(characterListPage);
|
|
@@ -3107,7 +3132,7 @@ function bindModuleLayoutToggle(refresh) {
|
|
|
3107
3132
|
|
|
3108
3133
|
function bindRecordPreview(selector, open) {
|
|
3109
3134
|
$("#module-content").querySelectorAll(selector).forEach((card) => {
|
|
3110
|
-
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;
|
|
3111
3136
|
card.addEventListener("click", (event) => {
|
|
3112
3137
|
if (!event.target.closest("button, a, summary")) void open(id);
|
|
3113
3138
|
});
|
|
@@ -3120,6 +3145,36 @@ function bindRecordPreview(selector, open) {
|
|
|
3120
3145
|
});
|
|
3121
3146
|
}
|
|
3122
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
|
+
|
|
3123
3178
|
function openReviewDetailDialog(item) {
|
|
3124
3179
|
if (!item) return;
|
|
3125
3180
|
const evidence = Array.isArray(item.evidence) ? item.evidence : [];
|
|
@@ -3164,13 +3219,13 @@ function settingRecordActions(item) {
|
|
|
3164
3219
|
function renderSettingCards(records) {
|
|
3165
3220
|
return `<div class="card-grid">${records.map((item) => `
|
|
3166
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>
|
|
3167
|
-
<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>
|
|
3168
3223
|
<div class="card-actions">${settingRecordActions(item)}</div></article>`).join("")}</div>`;
|
|
3169
3224
|
}
|
|
3170
3225
|
|
|
3171
3226
|
function renderSettingRows(records) {
|
|
3172
3227
|
return `<div class="module-row-list">${records.map((item) => {
|
|
3173
|
-
const preview = moduleRowPreview(item.
|
|
3228
|
+
const preview = moduleRowPreview(item.contentPreview);
|
|
3174
3229
|
return `
|
|
3175
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>
|
|
3176
3231
|
<h3>${esc(item.title)}</h3><p class="module-row-preview" title="${esc(preview)}">${esc(preview)}</p>
|
|
@@ -3187,20 +3242,16 @@ async function renderSettings() {
|
|
|
3187
3242
|
? `${layout === "rows" ? renderSettingRows(records) : renderSettingCards(records)}`
|
|
3188
3243
|
: emptyModule("还没有世界观设定", "新建规则、地点、组织、科技或创作约束。AI 提取的候选也会进入这里。");
|
|
3189
3244
|
bindModuleLayoutToggle(renderSettings);
|
|
3190
|
-
|
|
3191
|
-
$("#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); }));
|
|
3192
3247
|
bindEntityHistoryButtons(async () => { await renderSettings(); await loadAiReferences(); });
|
|
3193
3248
|
}
|
|
3194
3249
|
|
|
3195
3250
|
async function renderCharacters(page = characterListPage) {
|
|
3196
|
-
const
|
|
3197
|
-
apiPage(`/api/works/${state.work.id}/characters`, page),
|
|
3198
|
-
canReadModule("races") ? apiAllPages(`/api/works/${state.work.id}/races`) : Promise.resolve([]),
|
|
3199
|
-
canReadModule("organizations") ? apiAllPages(`/api/works/${state.work.id}/organizations`) : Promise.resolve([])
|
|
3200
|
-
]);
|
|
3251
|
+
const characterPage = await apiPage(`/api/works/${state.work.id}/characters`, page);
|
|
3201
3252
|
if (!characterPage.items.length && page > 1) return renderCharacters(page - 1);
|
|
3202
3253
|
characterListPage = characterPage.page;
|
|
3203
|
-
|
|
3254
|
+
state.characters = characterPage.items;
|
|
3204
3255
|
const layout = readModuleLayout();
|
|
3205
3256
|
const characterActions = (item) => recordCardEditButton("edit-character", item.id, `角色“${item.name}”`);
|
|
3206
3257
|
const characterCards = () => `<div class="card-grid">${state.characters.map((item) => {
|
|
@@ -3266,15 +3317,12 @@ async function renderCharacters(page = characterListPage) {
|
|
|
3266
3317
|
button.disabled = false;
|
|
3267
3318
|
}
|
|
3268
3319
|
});
|
|
3269
|
-
|
|
3270
|
-
$("#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); }));
|
|
3271
3322
|
}
|
|
3272
3323
|
|
|
3273
3324
|
async function renderRaces() {
|
|
3274
|
-
|
|
3275
|
-
apiAllPages(`/api/works/${state.work.id}/races`),
|
|
3276
|
-
canReadModule("characters") ? apiAllPages(`/api/works/${state.work.id}/characters`) : Promise.resolve([])
|
|
3277
|
-
]);
|
|
3325
|
+
state.races = await apiAllPages(`/api/works/${state.work.id}/races`);
|
|
3278
3326
|
const layout = readModuleLayout();
|
|
3279
3327
|
const canEditRaces = canEditModule("races");
|
|
3280
3328
|
const raceActions = (item) => canEditRaces
|
|
@@ -3286,10 +3334,10 @@ async function renderRaces() {
|
|
|
3286
3334
|
const renderRaceNode = (item) => `<details class="race-tree-node" open data-race-node="${esc(item.id)}">
|
|
3287
3335
|
<summary><span>${esc(item.name)}</span><small>${item.children.length} 个直接子种族</small></summary>
|
|
3288
3336
|
<div class="race-tree-branch">
|
|
3289
|
-
|
|
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>
|
|
3290
3338
|
<div class="race-path" aria-label="种族路径">${esc(racePathLabel(item))}</div>
|
|
3291
3339
|
<p>${esc(item.description || "尚未填写种族简介")}</p>
|
|
3292
|
-
<div class="race-settings">${item.
|
|
3340
|
+
<div class="race-settings">${item.settingsCount ? `<span class="pill">${item.settingsCount} 条共同设定,打开查看详情</span>` : '<span class="pill">暂无共同设定</span>'}</div>
|
|
3293
3341
|
<p class="race-members">直接角色:${item.members.length ? item.members.map((member) => esc(member.name)).join("、") : "暂无绑定角色"}</p>
|
|
3294
3342
|
${raceCardActions(item)}
|
|
3295
3343
|
</article>
|
|
@@ -3298,7 +3346,7 @@ async function renderRaces() {
|
|
|
3298
3346
|
</details>`;
|
|
3299
3347
|
const raceRows = () => `<div class="module-row-list">${state.races.map((item) => {
|
|
3300
3348
|
const preview = moduleRowPreview(item.description || "尚未填写种族简介");
|
|
3301
|
-
const meta = `${item.memberIds.length} 位直接角色 · ${item.settings
|
|
3349
|
+
const meta = `${item.memberIds.length} 位直接角色 · ${(item.settingsCount ?? item.settings?.length ?? 0) ? "已填写共同设定" : "暂无共同设定"}`;
|
|
3302
3350
|
return `
|
|
3303
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)}">
|
|
3304
3352
|
<small>${esc(meta)}</small>
|
|
@@ -3312,16 +3360,13 @@ async function renderRaces() {
|
|
|
3312
3360
|
? `${layout === "rows" ? raceRows() : `<section class="race-tree" aria-label="种族层级">${buildRaceForest(state.races).map(renderRaceNode).join("")}</section>`}`
|
|
3313
3361
|
: emptyModule("还没有种族档案", "先创建种族及共同设定,之后角色编辑器才能选择该种族。");
|
|
3314
3362
|
bindModuleLayoutToggle(renderRaces);
|
|
3315
|
-
|
|
3316
|
-
$("#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); }));
|
|
3317
3365
|
bindEntityHistoryButtons(async () => { await renderRaces(); await loadAiReferences(); });
|
|
3318
3366
|
}
|
|
3319
3367
|
|
|
3320
3368
|
async function renderOrganizations() {
|
|
3321
|
-
|
|
3322
|
-
apiAllPages(`/api/works/${state.work.id}/organizations`),
|
|
3323
|
-
canReadModule("characters") ? apiAllPages(`/api/works/${state.work.id}/characters`) : Promise.resolve([])
|
|
3324
|
-
]);
|
|
3369
|
+
state.organizations = await apiAllPages(`/api/works/${state.work.id}/organizations`);
|
|
3325
3370
|
const layout = readModuleLayout();
|
|
3326
3371
|
const canEditOrganizations = canEditModule("organizations");
|
|
3327
3372
|
const organizationActions = (item) => canEditOrganizations
|
|
@@ -3331,9 +3376,9 @@ async function renderOrganizations() {
|
|
|
3331
3376
|
? organizationActions(item)
|
|
3332
3377
|
: `<div class="card-actions">${organizationActions(item)}</div>`;
|
|
3333
3378
|
const organizationCards = () => `<div class="card-grid organization-grid">${state.organizations.map((item) => `
|
|
3334
|
-
<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>
|
|
3335
3380
|
<h3>${esc(item.name)}</h3><p>${esc(item.description || "尚未填写组织简介")}</p>
|
|
3336
|
-
<div class="organization-settings">${item.
|
|
3381
|
+
<div class="organization-settings">${item.settingsCount ? `<span class="pill">${item.settingsCount} 条组织设定,打开查看详情</span>` : '<span class="pill">暂无组织设定</span>'}</div>
|
|
3337
3382
|
<p class="organization-members">成员:${item.members.length ? item.members.map((member) => esc(member.name)).join("、") : "暂无绑定角色"}</p>
|
|
3338
3383
|
${organizationCardActions(item)}
|
|
3339
3384
|
</article>`).join("")}</div>`;
|
|
@@ -3341,8 +3386,8 @@ async function renderOrganizations() {
|
|
|
3341
3386
|
const preview = moduleRowPreview(item.description || "尚未填写组织简介");
|
|
3342
3387
|
const members = item.members.length ? item.members.map((member) => member.name).join("、") : "暂无绑定角色";
|
|
3343
3388
|
return `
|
|
3344
|
-
<article class="record-card module-row organization-card">
|
|
3345
|
-
<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>
|
|
3346
3391
|
<h3>${esc(item.name)}</h3>
|
|
3347
3392
|
<p class="module-row-preview" title="${esc(`${preview} · 成员:${members}`)}">${esc(preview)} · ${esc(members)}</p>
|
|
3348
3393
|
<div class="card-actions">${organizationActions(item)}</div>
|
|
@@ -3353,7 +3398,8 @@ async function renderOrganizations() {
|
|
|
3353
3398
|
? `${layout === "rows" ? organizationRows() : organizationCards()}`
|
|
3354
3399
|
: emptyModule("还没有组织", "创建国家、机构、阵营或团队,并维护组织设定与成员。");
|
|
3355
3400
|
bindModuleLayoutToggle(renderOrganizations);
|
|
3356
|
-
|
|
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); }));
|
|
3357
3403
|
bindEntityHistoryButtons(async () => { await renderOrganizations(); await loadAiReferences(); });
|
|
3358
3404
|
}
|
|
3359
3405
|
|
|
@@ -4042,7 +4088,7 @@ async function loadAiReferences() {
|
|
|
4042
4088
|
const generation = workScopedUiGeneration;
|
|
4043
4089
|
const [characters, settings] = await Promise.all([
|
|
4044
4090
|
canReadModule("characters") ? apiAllPages(`/api/works/${workId}/characters`) : Promise.resolve([]),
|
|
4045
|
-
canReadModule("settings") ?
|
|
4091
|
+
canReadModule("settings") ? api(`/api/works/${workId}/settings/context`) : Promise.resolve([])
|
|
4046
4092
|
]);
|
|
4047
4093
|
if (state.work?.id !== workId || generation !== workScopedUiGeneration) return;
|
|
4048
4094
|
state.characters = characters;
|
|
@@ -4719,7 +4765,14 @@ function createVditorEditor(host, value, { onInput = () => {}, uploadAttachment
|
|
|
4719
4765
|
placeholder,
|
|
4720
4766
|
preview: { transform: transformVditorPreview },
|
|
4721
4767
|
cache: { enable: false },
|
|
4722
|
-
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"],
|
|
4723
4776
|
upload: {
|
|
4724
4777
|
accept: "image/*",
|
|
4725
4778
|
max: 10 * 1024 * 1024,
|
|
@@ -4728,10 +4781,14 @@ function createVditorEditor(host, value, { onInput = () => {}, uploadAttachment
|
|
|
4728
4781
|
},
|
|
4729
4782
|
input: (markdown) => {
|
|
4730
4783
|
normalizeVditorAttachmentImages(editor);
|
|
4784
|
+
updateVditorWordCount(editor, markdown);
|
|
4785
|
+
updateVditorLineNumbers(editor, markdown);
|
|
4731
4786
|
onInput(markdown);
|
|
4732
4787
|
},
|
|
4733
4788
|
after: () => {
|
|
4734
4789
|
normalizeVditorAttachmentImages(editor);
|
|
4790
|
+
updateVditorWordCount(editor, value);
|
|
4791
|
+
updateVditorLineNumbers(editor, value);
|
|
4735
4792
|
if (readOnly) editor?.disabled();
|
|
4736
4793
|
}
|
|
4737
4794
|
});
|
|
@@ -4742,6 +4799,51 @@ function createVditorEditor(host, value, { onInput = () => {}, uploadAttachment
|
|
|
4742
4799
|
return editor;
|
|
4743
4800
|
}
|
|
4744
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
|
+
|
|
4830
|
+
function updateVditorWordCount(editor, markdown) {
|
|
4831
|
+
const toolbar = editor?.vditor?.toolbar?.element;
|
|
4832
|
+
if (!toolbar) return;
|
|
4833
|
+
let counter = toolbar.querySelector("[data-markdown-word-count]");
|
|
4834
|
+
if (!counter) {
|
|
4835
|
+
counter = document.createElement("span");
|
|
4836
|
+
counter.className = "markdown-word-count";
|
|
4837
|
+
counter.dataset.markdownWordCount = "true";
|
|
4838
|
+
counter.setAttribute("role", "status");
|
|
4839
|
+
counter.setAttribute("aria-live", "polite");
|
|
4840
|
+
counter.title = "当前 Markdown 正文的字数,不计空白字符";
|
|
4841
|
+
toolbar.append(counter);
|
|
4842
|
+
}
|
|
4843
|
+
const count = Array.from(String(markdown ?? "").replace(/\s/gu, "")).length;
|
|
4844
|
+
counter.textContent = `${count.toLocaleString("zh-CN")} 字`;
|
|
4845
|
+
}
|
|
4846
|
+
|
|
4745
4847
|
function transformVditorPreview(html) {
|
|
4746
4848
|
return String(html ?? "").replace(/(<img\b[^>]*\bsrc\s*=\s*)(["'])attachment:\/\/([A-Za-z0-9_-]{1,300})\2/giu, (_match, prefix, quote, attachmentId) => `${prefix}${quote}/api/attachments/${encodeURIComponent(attachmentId)}/content${quote}`);
|
|
4747
4849
|
}
|
|
@@ -4821,13 +4923,10 @@ function knowledgeSectionEditorHtml(section = null) {
|
|
|
4821
4923
|
const label = knowledgeEditorKind === "race" ? "种族" : "组织";
|
|
4822
4924
|
return `<div class="character-section-editor-shell knowledge-section-editor-shell">
|
|
4823
4925
|
<header class="character-section-editor-header">
|
|
4824
|
-
<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>
|
|
4825
4927
|
<button class="entity-editor-back" type="button" data-knowledge-section-edit-close>返回设定列表</button>
|
|
4826
4928
|
</header>
|
|
4827
4929
|
<section class="character-markdown-editor" aria-label="${section ? "编辑" : "新建"}${label} Markdown 设定">
|
|
4828
|
-
<div class="character-markdown-editor-meta">
|
|
4829
|
-
<label>设定标题<input id="knowledge-section-title" maxlength="200" value="${esc(section?.title ?? "")}" placeholder="例如:组织章程、种族特征" required></label>
|
|
4830
|
-
</div>
|
|
4831
4930
|
<div id="knowledge-section-markdown" class="vditor-editor-host" data-vditor-editor aria-label="Markdown 编辑器"></div>
|
|
4832
4931
|
<div class="character-markdown-editor-footer">
|
|
4833
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>
|
|
@@ -4899,13 +4998,12 @@ function characterSectionEditorHtml(section = null) {
|
|
|
4899
4998
|
const options = Object.entries(characterSectionTypeLabels).map(([value, label]) => `<option value="${value}" ${section?.sectionType === value ? "selected" : ""}>${esc(label)}</option>`).join("");
|
|
4900
4999
|
return `<div class="character-section-editor-shell">
|
|
4901
5000
|
<header class="character-section-editor-header">
|
|
4902
|
-
<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>
|
|
4903
5002
|
<button class="entity-editor-back" type="button" data-character-section-edit-close>返回人物档案</button>
|
|
4904
5003
|
</header>
|
|
4905
5004
|
<section class="character-markdown-editor" aria-label="${section ? "编辑" : "新建"}人物 Markdown 章节">
|
|
4906
5005
|
<div class="character-markdown-editor-meta">
|
|
4907
5006
|
<label>章节类型<select id="character-section-type">${options}</select></label>
|
|
4908
|
-
<label>章节标题<input id="character-section-title" maxlength="200" value="${esc(section?.title ?? "")}" placeholder="例如:背景故事" required></label>
|
|
4909
5007
|
<label class="character-markdown-summary-field">章节摘要<textarea id="character-section-summary" maxlength="20000" placeholder="用于角色列表和 AI 快速定位,不会替代正文">${esc(section?.summary ?? "")}</textarea></label>
|
|
4910
5008
|
</div>
|
|
4911
5009
|
<div id="character-section-markdown" class="vditor-editor-host" data-vditor-editor aria-label="Markdown 编辑器"></div>
|
|
@@ -6136,7 +6234,9 @@ $("#profile-form").addEventListener("submit", async (event) => {
|
|
|
6136
6234
|
state.user = updated;
|
|
6137
6235
|
applyAuthenticatedUser({ user: updated, csrfToken: state.csrfToken });
|
|
6138
6236
|
toast("显示名称已更新");
|
|
6139
|
-
} catch (error) {
|
|
6237
|
+
} catch (error) {
|
|
6238
|
+
toast(error.code === "INVALID_CURRENT_PASSWORD" ? "当前密码错误,请重新输入" : error.message, "error");
|
|
6239
|
+
}
|
|
6140
6240
|
});
|
|
6141
6241
|
$("#password-form").addEventListener("submit", async (event) => {
|
|
6142
6242
|
event.preventDefault();
|
|
@@ -6447,6 +6547,14 @@ $("#left-panel-toggle").addEventListener("click", () => {
|
|
|
6447
6547
|
panelLayout.leftCollapsed = !panelLayout.leftCollapsed;
|
|
6448
6548
|
applyPanelLayout(true);
|
|
6449
6549
|
});
|
|
6550
|
+
$("#mobile-module-tab").addEventListener("click", () => {
|
|
6551
|
+
panelLayout.leftCollapsed = !panelLayout.leftCollapsed;
|
|
6552
|
+
applyPanelLayout(true);
|
|
6553
|
+
});
|
|
6554
|
+
$("#mobile-panel-backdrop").addEventListener("click", () => {
|
|
6555
|
+
panelLayout.leftCollapsed = true;
|
|
6556
|
+
applyPanelLayout(true);
|
|
6557
|
+
});
|
|
6450
6558
|
$("#ai-panel-toggle").addEventListener("click", () => {
|
|
6451
6559
|
panelLayout.aiCollapsed = !panelLayout.aiCollapsed;
|
|
6452
6560
|
applyPanelLayout(true);
|
|
@@ -6454,7 +6562,11 @@ $("#ai-panel-toggle").addEventListener("click", () => {
|
|
|
6454
6562
|
setupPanelResize($("#left-panel-resize"), "left");
|
|
6455
6563
|
setupPanelResize($("#ai-panel-resize"), "ai");
|
|
6456
6564
|
if (typeof ResizeObserver !== "undefined") new ResizeObserver(scheduleChapterLineNumbers).observe($("#chapter-content"));
|
|
6457
|
-
window.addEventListener("resize", () => {
|
|
6565
|
+
window.addEventListener("resize", () => {
|
|
6566
|
+
if (isMobileViewport() && $("#onboarding-dialog").open) completeOnboarding();
|
|
6567
|
+
applyPanelLayout();
|
|
6568
|
+
scheduleChapterLineNumbers();
|
|
6569
|
+
});
|
|
6458
6570
|
$("#module-nav").addEventListener("click", (event) => {
|
|
6459
6571
|
const button = event.target.closest("button");
|
|
6460
6572
|
if (!button || button.id === "module-more-button") return;
|
|
@@ -6463,7 +6575,14 @@ $("#module-nav").addEventListener("click", (event) => {
|
|
|
6463
6575
|
openWorkSettingsDialog(work);
|
|
6464
6576
|
return;
|
|
6465
6577
|
}
|
|
6466
|
-
if (button.dataset.module)
|
|
6578
|
+
if (button.dataset.module) {
|
|
6579
|
+
void showModule(button.dataset.module).finally(() => {
|
|
6580
|
+
if (window.matchMedia("(max-width: 850px)").matches) {
|
|
6581
|
+
panelLayout.leftCollapsed = true;
|
|
6582
|
+
applyPanelLayout(true);
|
|
6583
|
+
}
|
|
6584
|
+
});
|
|
6585
|
+
}
|
|
6467
6586
|
});
|
|
6468
6587
|
$("#module-more-button").addEventListener("click", () => setModuleNavExpanded(!moduleNavExpanded));
|
|
6469
6588
|
$("#module-create-button").addEventListener("click", () => ({ settings: openSettingEditor, characters: openCharacterEditor, races: openRaceDialog, organizations: openOrganizationDialog, timeline: openTimelineDialog, outlines: openForeshadowDialog, relationships: openRelationshipDialog, reviews: openReviewDialog, tasks: openTaskDialog })[state.module]?.());
|
|
@@ -6719,6 +6838,10 @@ window.addEventListener("beforeunload", (event) => { if (state.dirty || entityEd
|
|
|
6719
6838
|
|
|
6720
6839
|
initializePage().catch((error) => {
|
|
6721
6840
|
restoringPageRoute = false;
|
|
6841
|
+
if (state.user) {
|
|
6842
|
+
document.body.classList.remove("auth-pending");
|
|
6843
|
+
$("#auth-view").classList.add("hidden");
|
|
6844
|
+
}
|
|
6722
6845
|
showShelf();
|
|
6723
6846
|
toast(`系统初始化失败:${error.message}`, "error");
|
|
6724
6847
|
});
|
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">
|
|
@@ -70,6 +70,7 @@
|
|
|
70
70
|
<small>小说 AI 创作工作台</small>
|
|
71
71
|
</div>
|
|
72
72
|
</button>
|
|
73
|
+
<button id="mobile-module-tab" class="mobile-module-tab" type="button" aria-label="打开作品模块" aria-expanded="false">模块</button>
|
|
73
74
|
<div id="work-meta" class="work-meta">尚未选择作品</div>
|
|
74
75
|
<div class="top-actions">
|
|
75
76
|
<span id="save-state" class="save-state">就绪</span>
|
|
@@ -100,6 +101,8 @@
|
|
|
100
101
|
</div>
|
|
101
102
|
</header>
|
|
102
103
|
|
|
104
|
+
<button id="mobile-panel-backdrop" class="mobile-panel-backdrop" type="button" aria-label="关闭作品模块侧栏"></button>
|
|
105
|
+
|
|
103
106
|
<aside class="left-panel">
|
|
104
107
|
<div id="left-panel-resize" class="panel-resize-handle" role="separator" aria-label="调整作品侧栏宽度" aria-orientation="vertical" tabindex="0"></div>
|
|
105
108
|
<div class="left-actions">
|
|
@@ -373,6 +376,7 @@
|
|
|
373
376
|
<button id="setting-editor-back" class="entity-editor-back" type="button">返回设定库</button>
|
|
374
377
|
<div class="entity-editor-heading">
|
|
375
378
|
<span id="setting-editor-eyebrow" class="eyebrow">新建设定</span>
|
|
379
|
+
<span id="setting-editor-readonly-badge" class="entity-editor-readonly-badge hidden">只读模式</span>
|
|
376
380
|
<input id="setting-editor-name" class="setting-editor-title-input" name="title" maxlength="200" placeholder="新建设定" aria-label="设定标题">
|
|
377
381
|
<div class="setting-editor-header-fields">
|
|
378
382
|
<label>分类<select id="setting-editor-category" name="category">
|
|
@@ -405,7 +409,7 @@
|
|
|
405
409
|
<button id="character-editor-close" class="entity-editor-back" type="button">返回角色列表</button>
|
|
406
410
|
<div>
|
|
407
411
|
<span id="character-editor-eyebrow" class="eyebrow">人物主档案</span>
|
|
408
|
-
<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>
|
|
409
413
|
</div>
|
|
410
414
|
<div class="character-editor-header-actions">
|
|
411
415
|
<button id="character-editor-edit" class="primary-button hidden" type="button">编辑角色</button>
|
|
@@ -439,7 +443,7 @@
|
|
|
439
443
|
<button id="knowledge-editor-close" class="entity-editor-back" type="button">返回列表</button>
|
|
440
444
|
<div>
|
|
441
445
|
<span id="knowledge-editor-eyebrow" class="eyebrow">世界档案</span>
|
|
442
|
-
<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>
|
|
443
447
|
</div>
|
|
444
448
|
<div class="character-editor-header-actions">
|
|
445
449
|
<span id="knowledge-editor-header-note" class="knowledge-editor-header-note">独立资料页</span>
|
|
@@ -679,6 +683,7 @@
|
|
|
679
683
|
<output id="galaxy-stats" class="galaxy-stats" aria-live="polite" data-testid="galaxy-stats"></output>
|
|
680
684
|
<aside class="galaxy-controls" aria-label="银河图控制">
|
|
681
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>
|
|
682
687
|
<button id="galaxy-rotation" type="button" aria-pressed="false" data-testid="galaxy-rotation">暂停旋转</button>
|
|
683
688
|
<button id="galaxy-zoom-out" type="button" aria-label="缩小">−</button>
|
|
684
689
|
<button id="galaxy-zoom-in" type="button" aria-label="放大">+</button>
|
|
@@ -694,6 +699,6 @@
|
|
|
694
699
|
<div id="auth-loading" class="auth-loading" role="status" aria-label="正在载入工作台"></div>
|
|
695
700
|
<script id="vditorIconScript" src="/vendor/vditor/dist/js/icons/ant.js?v=3.11.2"></script>
|
|
696
701
|
<script src="/vendor/vditor/dist/index.min.js?v=3.11.2"></script>
|
|
697
|
-
<script type="module" src="/app.js?v=20260725-
|
|
702
|
+
<script type="module" src="/app.js?v=20260725-develop-galaxy-ripple"></script>
|
|
698
703
|
</body>
|
|
699
704
|
</html>
|