@musnows/scriverse 0.4.4 → 0.4.6
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 -6
- package/dist/app.js.map +1 -1
- package/dist/cli-contract.js +0 -1
- package/dist/cli-contract.js.map +1 -1
- package/dist/database.js +15 -2
- package/dist/database.js.map +1 -1
- package/dist/public/app.js +162 -64
- package/dist/public/character-filters.d.ts +24 -0
- package/dist/public/character-filters.js +35 -0
- package/dist/public/character-version.js +0 -1
- package/dist/public/display-labels.d.ts +0 -1
- package/dist/public/display-labels.js +0 -4
- package/dist/public/index.html +6 -4
- package/dist/public/relationship-graph.js +108 -26
- package/dist/public/styles.css +47 -17
- package/dist/store.js +66 -47
- 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";
|
|
@@ -12,11 +12,10 @@ import { formatAiContextUsageTooltip } from "/ai-context-meter.js?v=20260718-lay
|
|
|
12
12
|
import { copyAiRawMarkdown } from "/ai-message-actions.js?v=20260713-copy-raw-markdown";
|
|
13
13
|
import { THEME_STORAGE_KEY, nextTheme, normalizeTheme, themeToggleLabel } from "/theme.js?v=20260713-dark-mode";
|
|
14
14
|
import { buildCharacterDetails, buildCharacterState, characterStateEntries, normalizeCharacterDetails, normalizeCharacterSections } from "/character-profile.js?v=20260713-character-editor";
|
|
15
|
-
import { characterVersionSourceLabel, describeCharacterVersionChanges } from "/character-version.js?v=
|
|
15
|
+
import { characterVersionSourceLabel, describeCharacterVersionChanges } from "/character-version.js?v=20260725-unified-permissions";
|
|
16
16
|
import { VERSIONED_ENTITY_LABELS, entityVersionSnapshotSummary, entityVersionSourceLabel } from "/entity-version.js?v=20260725-enum-labels-zh";
|
|
17
17
|
import {
|
|
18
18
|
chapterVersionSourceLabel,
|
|
19
|
-
characterVisibilityLabel,
|
|
20
19
|
foreshadowStatusLabel,
|
|
21
20
|
levelLabel,
|
|
22
21
|
occurrenceRoleLabel,
|
|
@@ -31,7 +30,7 @@ import {
|
|
|
31
30
|
settingStatusLabel,
|
|
32
31
|
taskScopeLabel,
|
|
33
32
|
timelineStatusLabel
|
|
34
|
-
} from "/display-labels.js?v=20260725-
|
|
33
|
+
} from "/display-labels.js?v=20260725-unified-permissions";
|
|
35
34
|
import { parsePageRoute, serializePageRoute } from "/page-route.js?v=20260723-knowledge-editor-page";
|
|
36
35
|
import { splitRelationshipKeywordInput, splitRelationshipKeywords, uniqueRelationshipKeywords } from "/relationship-keywords.js?v=20260720-relationship-keyword-chips";
|
|
37
36
|
import { tokenizeVisibleSpaces } from "/whitespace-visualization.js?v=20260718-visible-whitespace";
|
|
@@ -40,6 +39,7 @@ import { ANALYSIS_TYPES, analysisTypeDescription } from "/analysis-types.js?v=20
|
|
|
40
39
|
import { WORK_PERMISSION_MODULES, canReadPermissionModule, canReadUiModule, canWritePermissionModule, canWriteUiModule, emptyModulePermissions, firstReadableUiModule, normalizeModulePermissions, permissionSummary } from "/work-permissions.js?v=20260724-outline-title";
|
|
41
40
|
import { MODULE_LAYOUT_STORAGE_KEY, LEGACY_SETTINGS_LAYOUT_STORAGE_KEY, normalizeModuleLayout } from "/module-layout.js?v=20260723-module-layout-toggle";
|
|
42
41
|
import { isGlobalSearchShortcut } from "/keyboard-shortcuts.js?v=20260723-global-search";
|
|
42
|
+
import { filterCharacters, paginateCharacters } from "/character-filters.js?v=20260725-character-filters";
|
|
43
43
|
|
|
44
44
|
const state = {
|
|
45
45
|
user: null,
|
|
@@ -705,6 +705,8 @@ let entityEditorType = null;
|
|
|
705
705
|
let entityEditorDirty = false;
|
|
706
706
|
let entityEditorReadOnly = false;
|
|
707
707
|
let characterListPage = 1;
|
|
708
|
+
const characterFilters = { raceIds: [], organizationIds: [] };
|
|
709
|
+
let characterFiltersPanelOpen = false;
|
|
708
710
|
let settingEditorItem = null;
|
|
709
711
|
let characterEditorItem = null;
|
|
710
712
|
let knowledgeEditorItem = null;
|
|
@@ -723,11 +725,14 @@ let settingEditorVditor = null;
|
|
|
723
725
|
let knowledgeSectionVditor = null;
|
|
724
726
|
let characterSectionVditor = null;
|
|
725
727
|
let entityHistoryContext = null;
|
|
728
|
+
let moduleContentInteractionsBound = false;
|
|
726
729
|
|
|
727
730
|
function showEntityEditorPage(type, { readOnly = false } = {}) {
|
|
731
|
+
const module = type === "setting" ? "settings" : type === "character" ? "characters" : type === "race" ? "races" : "organizations";
|
|
732
|
+
const viewOnly = readOnly || !canEditModule(module);
|
|
728
733
|
entityEditorType = type;
|
|
729
734
|
entityEditorDirty = false;
|
|
730
|
-
entityEditorReadOnly =
|
|
735
|
+
entityEditorReadOnly = viewOnly;
|
|
731
736
|
characterSectionEditorDirty = false;
|
|
732
737
|
knowledgeSectionEditorDirty = false;
|
|
733
738
|
$("#entity-editor-view").classList.remove("hidden");
|
|
@@ -736,6 +741,7 @@ function showEntityEditorPage(type, { readOnly = false } = {}) {
|
|
|
736
741
|
$("#knowledge-editor-form").classList.toggle("hidden", !["race", "organization"].includes(type));
|
|
737
742
|
$("#character-section-editor-view").classList.add("hidden");
|
|
738
743
|
$("#knowledge-section-editor-view").classList.add("hidden");
|
|
744
|
+
["setting", "character", "knowledge"].forEach((editor) => $(`#${editor}-editor-readonly-badge`).classList.toggle("hidden", !(viewOnly && (editor === "knowledge" ? ["race", "organization"].includes(type) : editor === type))));
|
|
739
745
|
$("#app").inert = true;
|
|
740
746
|
document.body.classList.add("entity-editor-open");
|
|
741
747
|
replacePageRoute(currentPageRoute());
|
|
@@ -1782,7 +1788,9 @@ async function api(path, options = {}) {
|
|
|
1782
1788
|
state.csrfToken = null;
|
|
1783
1789
|
showAuth(false);
|
|
1784
1790
|
}
|
|
1785
|
-
|
|
1791
|
+
const error = new Error(payload.error?.message ?? `请求失败:${response.status}`);
|
|
1792
|
+
error.code = payload.error?.code;
|
|
1793
|
+
throw error;
|
|
1786
1794
|
}
|
|
1787
1795
|
if (response.status === 204) return null;
|
|
1788
1796
|
const payload = await response.json();
|
|
@@ -2264,11 +2272,8 @@ async function initializePage() {
|
|
|
2264
2272
|
}
|
|
2265
2273
|
if (route.view === "module") return;
|
|
2266
2274
|
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
2275
|
const item = route.entityId
|
|
2269
|
-
? route.entity === "character"
|
|
2270
|
-
? await api(`/api/characters/${encodeURIComponent(route.entityId)}`)
|
|
2271
|
-
: records.find((record) => record.id === route.entityId)
|
|
2276
|
+
? await api(`/api/${route.entity === "setting" ? "settings" : route.entity === "character" ? "characters" : route.entity === "race" ? "races" : "organizations"}/${encodeURIComponent(route.entityId)}`)
|
|
2272
2277
|
: null;
|
|
2273
2278
|
if (route.entityId && !item) {
|
|
2274
2279
|
toast(({ setting: "未找到要编辑的设定", character: "未找到要编辑的角色", race: "未找到要编辑的种族", organization: "未找到要编辑的组织" }[route.entity] ?? "未找到要编辑的档案"), "error");
|
|
@@ -2903,6 +2908,7 @@ async function showModule(module) {
|
|
|
2903
2908
|
$("#module-create-button").textContent = meta[3];
|
|
2904
2909
|
$("#module-create-button").classList.toggle("hidden", module === "ai-settings" || !canEditModule(module));
|
|
2905
2910
|
$("#module-content").innerHTML = '<div class="empty-state">正在载入……</div>';
|
|
2911
|
+
bindModuleContentInteractions();
|
|
2906
2912
|
try {
|
|
2907
2913
|
if (module === "settings") await renderSettings();
|
|
2908
2914
|
if (module === "characters") await renderCharacters(characterListPage);
|
|
@@ -3126,9 +3132,18 @@ function bindModuleLayoutToggle(refresh) {
|
|
|
3126
3132
|
}));
|
|
3127
3133
|
}
|
|
3128
3134
|
|
|
3135
|
+
function mountCharacterFilterToggle() {
|
|
3136
|
+
$("#module-header-actions").querySelector('[data-module-header-action="character-filter-toggle"]')?.remove();
|
|
3137
|
+
$("#module-header-actions").insertAdjacentHTML("afterbegin", `<button type="button" class="module-filter-toggle" data-module-header-action="character-filter-toggle" aria-label="筛选角色" aria-controls="character-filter-panel" aria-expanded="${characterFiltersPanelOpen}" title="筛选角色"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4 5h16l-6.5 7.2v5.3l-3 1.5v-6.8L4 5Z"></path></svg></button>`);
|
|
3138
|
+
$("#module-header-actions").querySelector('[data-module-header-action="character-filter-toggle"]')?.addEventListener("click", async () => {
|
|
3139
|
+
characterFiltersPanelOpen = !characterFiltersPanelOpen;
|
|
3140
|
+
await renderCharacters(characterListPage);
|
|
3141
|
+
});
|
|
3142
|
+
}
|
|
3143
|
+
|
|
3129
3144
|
function bindRecordPreview(selector, open) {
|
|
3130
3145
|
$("#module-content").querySelectorAll(selector).forEach((card) => {
|
|
3131
|
-
const id = card.dataset.openSetting ?? card.dataset.openCharacter ?? card.dataset.openRace ?? card.dataset.openReview;
|
|
3146
|
+
const id = card.dataset.openSetting ?? card.dataset.openCharacter ?? card.dataset.openRace ?? card.dataset.openOrganization ?? card.dataset.openReview;
|
|
3132
3147
|
card.addEventListener("click", (event) => {
|
|
3133
3148
|
if (!event.target.closest("button, a, summary")) void open(id);
|
|
3134
3149
|
});
|
|
@@ -3141,6 +3156,36 @@ function bindRecordPreview(selector, open) {
|
|
|
3141
3156
|
});
|
|
3142
3157
|
}
|
|
3143
3158
|
|
|
3159
|
+
function bindModuleContentInteractions() {
|
|
3160
|
+
if (moduleContentInteractionsBound) return;
|
|
3161
|
+
moduleContentInteractionsBound = true;
|
|
3162
|
+
const host = $("#module-content");
|
|
3163
|
+
const open = async (card) => {
|
|
3164
|
+
const id = card.dataset.openSetting ?? card.dataset.openCharacter ?? card.dataset.openRace ?? card.dataset.openOrganization;
|
|
3165
|
+
if (!id) return;
|
|
3166
|
+
if (card.dataset.openSetting) return openSettingEditor(await api(`/api/settings/${encodeURIComponent(id)}`), { readOnly: true });
|
|
3167
|
+
if (card.dataset.openCharacter) return openCharacterEditor(await api(`/api/characters/${encodeURIComponent(id)}`), { readOnly: true });
|
|
3168
|
+
if (card.dataset.openRace) return openRaceDialog(await api(`/api/races/${encodeURIComponent(id)}`), { readOnly: true });
|
|
3169
|
+
if (card.dataset.openOrganization) return openOrganizationDialog(await api(`/api/organizations/${encodeURIComponent(id)}`), { readOnly: true });
|
|
3170
|
+
};
|
|
3171
|
+
const findCard = (target) => target instanceof Element
|
|
3172
|
+
? target.closest("[data-open-setting], [data-open-character], [data-open-race], [data-open-organization]")
|
|
3173
|
+
: null;
|
|
3174
|
+
host.addEventListener("click", (event) => {
|
|
3175
|
+
if (event.target instanceof Element && event.target.closest("button, a, summary")) return;
|
|
3176
|
+
const card = findCard(event.target);
|
|
3177
|
+
if (card) void open(card);
|
|
3178
|
+
});
|
|
3179
|
+
host.addEventListener("keydown", (event) => {
|
|
3180
|
+
if (event.key !== "Enter" && event.key !== " ") return;
|
|
3181
|
+
if (event.target instanceof Element && event.target.closest("button, a, summary")) return;
|
|
3182
|
+
const card = findCard(event.target);
|
|
3183
|
+
if (!card) return;
|
|
3184
|
+
event.preventDefault();
|
|
3185
|
+
void open(card);
|
|
3186
|
+
});
|
|
3187
|
+
}
|
|
3188
|
+
|
|
3144
3189
|
function openReviewDetailDialog(item) {
|
|
3145
3190
|
if (!item) return;
|
|
3146
3191
|
const evidence = Array.isArray(item.evidence) ? item.evidence : [];
|
|
@@ -3185,13 +3230,13 @@ function settingRecordActions(item) {
|
|
|
3185
3230
|
function renderSettingCards(records) {
|
|
3186
3231
|
return `<div class="card-grid">${records.map((item) => `
|
|
3187
3232
|
<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
|
|
3233
|
+
<h3>${esc(item.title)}</h3><div class="record-markdown-preview">${esc(item.contentPreview || "暂无正文预览")}</div>
|
|
3189
3234
|
<div class="card-actions">${settingRecordActions(item)}</div></article>`).join("")}</div>`;
|
|
3190
3235
|
}
|
|
3191
3236
|
|
|
3192
3237
|
function renderSettingRows(records) {
|
|
3193
3238
|
return `<div class="module-row-list">${records.map((item) => {
|
|
3194
|
-
const preview = moduleRowPreview(item.
|
|
3239
|
+
const preview = moduleRowPreview(item.contentPreview);
|
|
3195
3240
|
return `
|
|
3196
3241
|
<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
3242
|
<h3>${esc(item.title)}</h3><p class="module-row-preview" title="${esc(preview)}">${esc(preview)}</p>
|
|
@@ -3208,26 +3253,30 @@ async function renderSettings() {
|
|
|
3208
3253
|
? `${layout === "rows" ? renderSettingRows(records) : renderSettingCards(records)}`
|
|
3209
3254
|
: emptyModule("还没有世界观设定", "新建规则、地点、组织、科技或创作约束。AI 提取的候选也会进入这里。");
|
|
3210
3255
|
bindModuleLayoutToggle(renderSettings);
|
|
3211
|
-
|
|
3212
|
-
$("#module-content").querySelectorAll("[data-edit-setting]").forEach((button) => button.addEventListener("click", () =>
|
|
3256
|
+
const openSetting = async (id, readOnly) => openSettingEditor(await api(`/api/settings/${encodeURIComponent(id)}`), { readOnly });
|
|
3257
|
+
$("#module-content").querySelectorAll("[data-edit-setting]").forEach((button) => button.addEventListener("click", () => { void openSetting(button.dataset.editSetting, false); }));
|
|
3213
3258
|
bindEntityHistoryButtons(async () => { await renderSettings(); await loadAiReferences(); });
|
|
3214
3259
|
}
|
|
3215
3260
|
|
|
3216
3261
|
async function renderCharacters(page = characterListPage) {
|
|
3217
|
-
const
|
|
3218
|
-
|
|
3262
|
+
const hasCharacterFilters = characterFilters.raceIds.length > 0 || characterFilters.organizationIds.length > 0;
|
|
3263
|
+
const [characterSource, races, organizations] = await Promise.all([
|
|
3264
|
+
hasCharacterFilters ? apiAllPages(`/api/works/${state.work.id}/characters`) : apiPage(`/api/works/${state.work.id}/characters`, page),
|
|
3219
3265
|
canReadModule("races") ? apiAllPages(`/api/works/${state.work.id}/races`) : Promise.resolve([]),
|
|
3220
3266
|
canReadModule("organizations") ? apiAllPages(`/api/works/${state.work.id}/organizations`) : Promise.resolve([])
|
|
3221
3267
|
]);
|
|
3268
|
+
const characterPage = hasCharacterFilters
|
|
3269
|
+
? paginateCharacters(filterCharacters(characterSource, characterFilters), page, 50)
|
|
3270
|
+
: characterSource;
|
|
3222
3271
|
if (!characterPage.items.length && page > 1) return renderCharacters(page - 1);
|
|
3223
3272
|
characterListPage = characterPage.page;
|
|
3224
|
-
|
|
3273
|
+
state.characters = characterPage.items;
|
|
3225
3274
|
const layout = readModuleLayout();
|
|
3226
3275
|
const characterActions = (item) => recordCardEditButton("edit-character", item.id, `角色“${item.name}”`);
|
|
3227
3276
|
const characterCards = () => `<div class="card-grid">${state.characters.map((item) => {
|
|
3228
3277
|
const details = normalizeCharacterDetails(item.attributes?.details);
|
|
3229
3278
|
return `
|
|
3230
|
-
<article class="record-card character-card preview-record-card has-card-edit" data-open-character="${esc(item.id)}" role="button" tabindex="0" aria-label="查看角色 ${esc(item.name)}">${recordCardEditButton("edit-character", item.id, `角色“${item.name}”`)}
|
|
3279
|
+
<article class="record-card character-card preview-record-card has-card-edit" data-open-character="${esc(item.id)}" role="button" tabindex="0" aria-label="查看角色 ${esc(item.name)}">${recordCardEditButton("edit-character", item.id, `角色“${item.name}”`)}${item.lockedFields.length ? `<small>锁定 ${item.lockedFields.length} 项</small>` : ""}
|
|
3231
3280
|
<h3>${esc(item.name)}</h3>
|
|
3232
3281
|
${item.attributes?.identity ? `<p class="character-identity">${esc(item.attributes.identity)}</p>` : ""}
|
|
3233
3282
|
${item.aliases.length ? `<div class="character-aliases">${item.aliases.map((alias) => `<span class="pill">${esc(alias)}</span>`).join("")}</div>` : ""}
|
|
@@ -3250,14 +3299,12 @@ async function renderCharacters(page = characterListPage) {
|
|
|
3250
3299
|
const line = meta ? `${meta} · ${preview}` : preview;
|
|
3251
3300
|
return `
|
|
3252
3301
|
<article class="record-card module-row character-card preview-record-card" data-open-character="${esc(item.id)}" role="button" tabindex="0" aria-label="查看角色 ${esc(item.name)}">
|
|
3253
|
-
|
|
3302
|
+
${item.lockedFields.length ? `<small>锁定 ${item.lockedFields.length} 项</small>` : ""}
|
|
3254
3303
|
<h3>${esc(item.name)}</h3>
|
|
3255
3304
|
<p class="module-row-preview" title="${esc(line)}">${esc(line)}</p>
|
|
3256
3305
|
<div class="card-actions">${characterActions(item)}</div>
|
|
3257
3306
|
</article>`;
|
|
3258
3307
|
}).join("")}</div>`;
|
|
3259
|
-
const hasMultipleCharacters = characterPage.page > 1 || characterPage.hasMore || state.characters.length > 1;
|
|
3260
|
-
const auditPanel = canEditModule("tasks") ? `<section class="character-audit-panel"><div><strong>角色身份确认</strong><small>让 AI 查询角色档案并搜索正文,找出可能被误建成两个档案的同一角色。AI 只提交审核建议,不会自动合并。</small></div><button id="create-character-audit-task" class="ghost-button" type="button" ${hasMultipleCharacters ? "" : "disabled"}>AI 角色查重</button></section>` : "";
|
|
3261
3308
|
const pagination = state.characters.length && (characterPage.page > 1 || characterPage.hasMore)
|
|
3262
3309
|
? `<nav class="module-pagination" aria-label="角色列表分页">
|
|
3263
3310
|
<button type="button" data-character-page="${characterPage.page - 1}" ${characterPage.page <= 1 ? "disabled" : ""}>上一页</button>
|
|
@@ -3265,37 +3312,57 @@ async function renderCharacters(page = characterListPage) {
|
|
|
3265
3312
|
<button type="button" data-character-page="${characterPage.nextPage ?? characterPage.page + 1}" ${characterPage.hasMore ? "" : "disabled"}>下一页</button>
|
|
3266
3313
|
</nav>`
|
|
3267
3314
|
: "";
|
|
3315
|
+
const selectedRaceIds = new Set(characterFilters.raceIds);
|
|
3316
|
+
const selectedOrganizationIds = new Set(characterFilters.organizationIds);
|
|
3317
|
+
const selectedRaceNames = races.filter((race) => selectedRaceIds.has(String(race.id))).map((race) => racePathLabel(race) || race.name);
|
|
3318
|
+
const selectedOrganizationNames = organizations.filter((organization) => selectedOrganizationIds.has(String(organization.id))).map((organization) => organization.name);
|
|
3319
|
+
const filterOptionList = (items, selectedIds, valueKey = "id") => items.map((item) => {
|
|
3320
|
+
const value = String(item[valueKey]);
|
|
3321
|
+
const label = valueKey === "id" ? (racePathLabel(item) || item.name) : item.name;
|
|
3322
|
+
return `<label class="character-filter-option"><input type="checkbox" value="${esc(value)}" ${selectedIds.has(value) ? "checked" : ""}><span>${esc(label)}</span></label>`;
|
|
3323
|
+
}).join("");
|
|
3324
|
+
const filterToolbar = `<section id="character-filter-panel" class="character-filter-toolbar${characterFiltersPanelOpen ? "" : " hidden"}" aria-label="角色筛选">
|
|
3325
|
+
<details class="character-filter-dropdown"><summary><span>按种族筛选</span><strong>${selectedRaceNames.length ? `已选 ${selectedRaceNames.length} 项` : "全部种族"}</strong></summary><div id="character-race-filter" class="character-filter-options">${filterOptionList(races, selectedRaceIds)}</div></details>
|
|
3326
|
+
<details class="character-filter-dropdown"><summary><span>按组织筛选</span><strong>${selectedOrganizationNames.length ? `已选 ${selectedOrganizationNames.length} 项` : "全部组织"}</strong></summary><div id="character-organization-filter" class="character-filter-options">${filterOptionList(organizations, selectedOrganizationIds, "id")}</div></details>
|
|
3327
|
+
<div class="character-filter-toolbar-actions">${hasCharacterFilters ? `<span class="character-filter-result-count" aria-live="polite">筛选后剩余 ${characterPage.total} 个角色</span>` : ""}<button id="clear-character-filters" class="ghost-button" type="button" ${hasCharacterFilters ? "" : "disabled"}>重置筛选</button></div>
|
|
3328
|
+
</section>`;
|
|
3329
|
+
mountCharacterFilterToggle();
|
|
3268
3330
|
if (state.characters.length) mountModuleLayoutToggle(layout, "角色列表样式");
|
|
3269
|
-
$("#module-content").innerHTML =
|
|
3331
|
+
$("#module-content").innerHTML = filterToolbar + (state.characters.length
|
|
3270
3332
|
? `${layout === "rows" ? characterRows() : characterCards()}${pagination}`
|
|
3271
3333
|
: emptyModule("还没有角色档案", "创建主要人物,并维护别名、身份、动机和当前状态。"));
|
|
3272
3334
|
bindModuleLayoutToggle(renderCharacters);
|
|
3335
|
+
const readSelectedValues = (selector) => [...$(selector).querySelectorAll('input[type="checkbox"]:checked')].map((input) => input.value);
|
|
3336
|
+
$("#character-race-filter").addEventListener("change", async () => {
|
|
3337
|
+
characterFiltersPanelOpen = true;
|
|
3338
|
+
characterFilters.raceIds = readSelectedValues("#character-race-filter");
|
|
3339
|
+
characterListPage = 1;
|
|
3340
|
+
await renderCharacters(1);
|
|
3341
|
+
});
|
|
3342
|
+
$("#character-organization-filter").addEventListener("change", async () => {
|
|
3343
|
+
characterFiltersPanelOpen = true;
|
|
3344
|
+
characterFilters.organizationIds = readSelectedValues("#character-organization-filter");
|
|
3345
|
+
characterListPage = 1;
|
|
3346
|
+
await renderCharacters(1);
|
|
3347
|
+
});
|
|
3348
|
+
$("#clear-character-filters")?.addEventListener("click", async () => {
|
|
3349
|
+
characterFiltersPanelOpen = true;
|
|
3350
|
+
characterFilters.raceIds = [];
|
|
3351
|
+
characterFilters.organizationIds = [];
|
|
3352
|
+
characterListPage = 1;
|
|
3353
|
+
await renderCharacters(1);
|
|
3354
|
+
});
|
|
3273
3355
|
$("#module-content").querySelectorAll("[data-character-page]").forEach((button) => button.addEventListener("click", async () => {
|
|
3274
3356
|
if (button.disabled) return;
|
|
3275
3357
|
$("#module-content").querySelectorAll("[data-character-page]").forEach((control) => { control.disabled = true; });
|
|
3276
3358
|
await renderCharacters(Number(button.dataset.characterPage));
|
|
3277
3359
|
}));
|
|
3278
|
-
$("#create-character-audit-task")?.addEventListener("click", async () => {
|
|
3279
|
-
const button = $("#create-character-audit-task");
|
|
3280
|
-
button.disabled = true;
|
|
3281
|
-
try {
|
|
3282
|
-
await api(`/api/works/${state.work.id}/tasks`, { method: "POST", body: { taskType: "character-identity-audit", scope: { type: "book" } } });
|
|
3283
|
-
toast("角色查重任务已加入分析队列");
|
|
3284
|
-
await showModule("tasks");
|
|
3285
|
-
} catch (error) {
|
|
3286
|
-
toast(error.message, "error");
|
|
3287
|
-
button.disabled = false;
|
|
3288
|
-
}
|
|
3289
|
-
});
|
|
3290
3360
|
bindRecordPreview("[data-open-character]", (id) => openCharacterEditor(state.characters.find((item) => item.id === id), { readOnly: true }));
|
|
3291
3361
|
$("#module-content").querySelectorAll("[data-edit-character]").forEach((button) => button.addEventListener("click", () => openCharacterEditor(state.characters.find((item) => item.id === button.dataset.editCharacter))));
|
|
3292
3362
|
}
|
|
3293
3363
|
|
|
3294
3364
|
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
|
-
]);
|
|
3365
|
+
state.races = await apiAllPages(`/api/works/${state.work.id}/races`);
|
|
3299
3366
|
const layout = readModuleLayout();
|
|
3300
3367
|
const canEditRaces = canEditModule("races");
|
|
3301
3368
|
const raceActions = (item) => canEditRaces
|
|
@@ -3307,10 +3374,10 @@ async function renderRaces() {
|
|
|
3307
3374
|
const renderRaceNode = (item) => `<details class="race-tree-node" open data-race-node="${esc(item.id)}">
|
|
3308
3375
|
<summary><span>${esc(item.name)}</span><small>${item.children.length} 个直接子种族</small></summary>
|
|
3309
3376
|
<div class="race-tree-branch">
|
|
3310
|
-
|
|
3377
|
+
<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
3378
|
<div class="race-path" aria-label="种族路径">${esc(racePathLabel(item))}</div>
|
|
3312
3379
|
<p>${esc(item.description || "尚未填写种族简介")}</p>
|
|
3313
|
-
<div class="race-settings">${item.
|
|
3380
|
+
<div class="race-settings">${item.settingsCount ? `<span class="pill">${item.settingsCount} 条共同设定,打开查看详情</span>` : '<span class="pill">暂无共同设定</span>'}</div>
|
|
3314
3381
|
<p class="race-members">直接角色:${item.members.length ? item.members.map((member) => esc(member.name)).join("、") : "暂无绑定角色"}</p>
|
|
3315
3382
|
${raceCardActions(item)}
|
|
3316
3383
|
</article>
|
|
@@ -3319,7 +3386,7 @@ async function renderRaces() {
|
|
|
3319
3386
|
</details>`;
|
|
3320
3387
|
const raceRows = () => `<div class="module-row-list">${state.races.map((item) => {
|
|
3321
3388
|
const preview = moduleRowPreview(item.description || "尚未填写种族简介");
|
|
3322
|
-
const meta = `${item.memberIds.length} 位直接角色 · ${item.settings
|
|
3389
|
+
const meta = `${item.memberIds.length} 位直接角色 · ${(item.settingsCount ?? item.settings?.length ?? 0) ? "已填写共同设定" : "暂无共同设定"}`;
|
|
3323
3390
|
return `
|
|
3324
3391
|
<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
3392
|
<small>${esc(meta)}</small>
|
|
@@ -3333,16 +3400,13 @@ async function renderRaces() {
|
|
|
3333
3400
|
? `${layout === "rows" ? raceRows() : `<section class="race-tree" aria-label="种族层级">${buildRaceForest(state.races).map(renderRaceNode).join("")}</section>`}`
|
|
3334
3401
|
: emptyModule("还没有种族档案", "先创建种族及共同设定,之后角色编辑器才能选择该种族。");
|
|
3335
3402
|
bindModuleLayoutToggle(renderRaces);
|
|
3336
|
-
|
|
3337
|
-
$("#module-content").querySelectorAll("[data-edit-race]").forEach((button) => button.addEventListener("click", () =>
|
|
3403
|
+
const openRace = async (id, readOnly) => openRaceDialog(await api(`/api/races/${encodeURIComponent(id)}`), { readOnly });
|
|
3404
|
+
$("#module-content").querySelectorAll("[data-edit-race]").forEach((button) => button.addEventListener("click", () => { void openRace(button.dataset.editRace, false); }));
|
|
3338
3405
|
bindEntityHistoryButtons(async () => { await renderRaces(); await loadAiReferences(); });
|
|
3339
3406
|
}
|
|
3340
3407
|
|
|
3341
3408
|
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
|
-
]);
|
|
3409
|
+
state.organizations = await apiAllPages(`/api/works/${state.work.id}/organizations`);
|
|
3346
3410
|
const layout = readModuleLayout();
|
|
3347
3411
|
const canEditOrganizations = canEditModule("organizations");
|
|
3348
3412
|
const organizationActions = (item) => canEditOrganizations
|
|
@@ -3352,9 +3416,9 @@ async function renderOrganizations() {
|
|
|
3352
3416
|
? organizationActions(item)
|
|
3353
3417
|
: `<div class="card-actions">${organizationActions(item)}</div>`;
|
|
3354
3418
|
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
|
|
3419
|
+
<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
3420
|
<h3>${esc(item.name)}</h3><p>${esc(item.description || "尚未填写组织简介")}</p>
|
|
3357
|
-
<div class="organization-settings">${item.
|
|
3421
|
+
<div class="organization-settings">${item.settingsCount ? `<span class="pill">${item.settingsCount} 条组织设定,打开查看详情</span>` : '<span class="pill">暂无组织设定</span>'}</div>
|
|
3358
3422
|
<p class="organization-members">成员:${item.members.length ? item.members.map((member) => esc(member.name)).join("、") : "暂无绑定角色"}</p>
|
|
3359
3423
|
${organizationCardActions(item)}
|
|
3360
3424
|
</article>`).join("")}</div>`;
|
|
@@ -3362,8 +3426,8 @@ async function renderOrganizations() {
|
|
|
3362
3426
|
const preview = moduleRowPreview(item.description || "尚未填写组织简介");
|
|
3363
3427
|
const members = item.members.length ? item.members.map((member) => member.name).join("、") : "暂无绑定角色";
|
|
3364
3428
|
return `
|
|
3365
|
-
<article class="record-card module-row organization-card">
|
|
3366
|
-
<small>${item.memberIds.length} 位成员 · ${item.settings
|
|
3429
|
+
<article class="record-card module-row organization-card" data-open-organization="${esc(item.id)}" role="button" tabindex="0" aria-label="查看组织 ${esc(item.name)}">
|
|
3430
|
+
<small>${item.memberIds.length} 位成员 · ${(item.settingsCount ?? item.settings?.length ?? 0) ? "已填写组织设定" : "暂无组织设定"}</small>
|
|
3367
3431
|
<h3>${esc(item.name)}</h3>
|
|
3368
3432
|
<p class="module-row-preview" title="${esc(`${preview} · 成员:${members}`)}">${esc(preview)} · ${esc(members)}</p>
|
|
3369
3433
|
<div class="card-actions">${organizationActions(item)}</div>
|
|
@@ -3374,7 +3438,8 @@ async function renderOrganizations() {
|
|
|
3374
3438
|
? `${layout === "rows" ? organizationRows() : organizationCards()}`
|
|
3375
3439
|
: emptyModule("还没有组织", "创建国家、机构、阵营或团队,并维护组织设定与成员。");
|
|
3376
3440
|
bindModuleLayoutToggle(renderOrganizations);
|
|
3377
|
-
|
|
3441
|
+
const openOrganization = async (id, readOnly) => openOrganizationDialog(await api(`/api/organizations/${encodeURIComponent(id)}`), { readOnly });
|
|
3442
|
+
$("#module-content").querySelectorAll("[data-edit-organization]").forEach((button) => button.addEventListener("click", () => { void openOrganization(button.dataset.editOrganization, false); }));
|
|
3378
3443
|
bindEntityHistoryButtons(async () => { await renderOrganizations(); await loadAiReferences(); });
|
|
3379
3444
|
}
|
|
3380
3445
|
|
|
@@ -4063,7 +4128,7 @@ async function loadAiReferences() {
|
|
|
4063
4128
|
const generation = workScopedUiGeneration;
|
|
4064
4129
|
const [characters, settings] = await Promise.all([
|
|
4065
4130
|
canReadModule("characters") ? apiAllPages(`/api/works/${workId}/characters`) : Promise.resolve([]),
|
|
4066
|
-
canReadModule("settings") ?
|
|
4131
|
+
canReadModule("settings") ? api(`/api/works/${workId}/settings/context`) : Promise.resolve([])
|
|
4067
4132
|
]);
|
|
4068
4133
|
if (state.work?.id !== workId || generation !== workScopedUiGeneration) return;
|
|
4069
4134
|
state.characters = characters;
|
|
@@ -4740,7 +4805,14 @@ function createVditorEditor(host, value, { onInput = () => {}, uploadAttachment
|
|
|
4740
4805
|
placeholder,
|
|
4741
4806
|
preview: { transform: transformVditorPreview },
|
|
4742
4807
|
cache: { enable: false },
|
|
4743
|
-
toolbar: ["headings", "bold", "italic", "strike", "|", "line", "quote", "list", "ordered-list", "check", "|", "code", "inline-code", "link", "table", "upload", "|", "undo", "redo",
|
|
4808
|
+
toolbar: ["headings", "bold", "italic", "strike", "|", "line", "quote", "list", "ordered-list", "check", "|", "code", "inline-code", "link", "table", "upload", "|", "undo", "redo", {
|
|
4809
|
+
name: "line-number",
|
|
4810
|
+
tip: "显示/隐藏行号",
|
|
4811
|
+
tipPosition: "s",
|
|
4812
|
+
className: "vditor-line-number-button",
|
|
4813
|
+
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>',
|
|
4814
|
+
click: () => toggleVditorLineNumbers(editor)
|
|
4815
|
+
}, "edit-mode", "fullscreen"],
|
|
4744
4816
|
upload: {
|
|
4745
4817
|
accept: "image/*",
|
|
4746
4818
|
max: 10 * 1024 * 1024,
|
|
@@ -4750,11 +4822,13 @@ function createVditorEditor(host, value, { onInput = () => {}, uploadAttachment
|
|
|
4750
4822
|
input: (markdown) => {
|
|
4751
4823
|
normalizeVditorAttachmentImages(editor);
|
|
4752
4824
|
updateVditorWordCount(editor, markdown);
|
|
4825
|
+
updateVditorLineNumbers(editor, markdown);
|
|
4753
4826
|
onInput(markdown);
|
|
4754
4827
|
},
|
|
4755
4828
|
after: () => {
|
|
4756
4829
|
normalizeVditorAttachmentImages(editor);
|
|
4757
4830
|
updateVditorWordCount(editor, value);
|
|
4831
|
+
updateVditorLineNumbers(editor, value);
|
|
4758
4832
|
if (readOnly) editor?.disabled();
|
|
4759
4833
|
}
|
|
4760
4834
|
});
|
|
@@ -4765,6 +4839,34 @@ function createVditorEditor(host, value, { onInput = () => {}, uploadAttachment
|
|
|
4765
4839
|
return editor;
|
|
4766
4840
|
}
|
|
4767
4841
|
|
|
4842
|
+
function toggleVditorLineNumbers(editor) {
|
|
4843
|
+
const host = editor?.vditor?.element;
|
|
4844
|
+
if (!host) return;
|
|
4845
|
+
const enabled = host.classList.toggle("show-line-numbers");
|
|
4846
|
+
const button = host.querySelector(".vditor-line-number-button");
|
|
4847
|
+
if (button) button.setAttribute("aria-pressed", String(enabled));
|
|
4848
|
+
}
|
|
4849
|
+
|
|
4850
|
+
function updateVditorLineNumbers(editor, markdown) {
|
|
4851
|
+
const content = editor?.vditor?.element?.querySelector(".vditor-content");
|
|
4852
|
+
if (!content) return;
|
|
4853
|
+
let gutter = content.querySelector(".vditor-line-numbers");
|
|
4854
|
+
if (!gutter) {
|
|
4855
|
+
gutter = document.createElement("div");
|
|
4856
|
+
gutter.className = "vditor-line-numbers";
|
|
4857
|
+
gutter.setAttribute("aria-hidden", "true");
|
|
4858
|
+
content.prepend(gutter);
|
|
4859
|
+
}
|
|
4860
|
+
const lineCount = Math.max(1, String(markdown ?? "").split(/\r?\n/gu).length);
|
|
4861
|
+
const fragment = document.createDocumentFragment();
|
|
4862
|
+
for (let line = 1; line <= lineCount; line += 1) {
|
|
4863
|
+
const number = document.createElement("span");
|
|
4864
|
+
number.textContent = String(line);
|
|
4865
|
+
fragment.append(number);
|
|
4866
|
+
}
|
|
4867
|
+
gutter.replaceChildren(fragment);
|
|
4868
|
+
}
|
|
4869
|
+
|
|
4768
4870
|
function updateVditorWordCount(editor, markdown) {
|
|
4769
4871
|
const toolbar = editor?.vditor?.toolbar?.element;
|
|
4770
4872
|
if (!toolbar) return;
|
|
@@ -4861,13 +4963,10 @@ function knowledgeSectionEditorHtml(section = null) {
|
|
|
4861
4963
|
const label = knowledgeEditorKind === "race" ? "种族" : "组织";
|
|
4862
4964
|
return `<div class="character-section-editor-shell knowledge-section-editor-shell">
|
|
4863
4965
|
<header class="character-section-editor-header">
|
|
4864
|
-
<div><span class="eyebrow">${label} Markdown 设定</span><
|
|
4966
|
+
<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
4967
|
<button class="entity-editor-back" type="button" data-knowledge-section-edit-close>返回设定列表</button>
|
|
4866
4968
|
</header>
|
|
4867
4969
|
<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
4970
|
<div id="knowledge-section-markdown" class="vditor-editor-host" data-vditor-editor aria-label="Markdown 编辑器"></div>
|
|
4872
4971
|
<div class="character-markdown-editor-footer">
|
|
4873
4972
|
<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 +5038,12 @@ function characterSectionEditorHtml(section = null) {
|
|
|
4939
5038
|
const options = Object.entries(characterSectionTypeLabels).map(([value, label]) => `<option value="${value}" ${section?.sectionType === value ? "selected" : ""}>${esc(label)}</option>`).join("");
|
|
4940
5039
|
return `<div class="character-section-editor-shell">
|
|
4941
5040
|
<header class="character-section-editor-header">
|
|
4942
|
-
<div><span class="eyebrow">人物 Markdown 档案</span><
|
|
5041
|
+
<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
5042
|
<button class="entity-editor-back" type="button" data-character-section-edit-close>返回人物档案</button>
|
|
4944
5043
|
</header>
|
|
4945
5044
|
<section class="character-markdown-editor" aria-label="${section ? "编辑" : "新建"}人物 Markdown 章节">
|
|
4946
5045
|
<div class="character-markdown-editor-meta">
|
|
4947
5046
|
<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
5047
|
<label class="character-markdown-summary-field">章节摘要<textarea id="character-section-summary" maxlength="20000" placeholder="用于角色列表和 AI 快速定位,不会替代正文">${esc(section?.summary ?? "")}</textarea></label>
|
|
4950
5048
|
</div>
|
|
4951
5049
|
<div id="character-section-markdown" class="vditor-editor-host" data-vditor-editor aria-label="Markdown 编辑器"></div>
|
|
@@ -5120,7 +5218,6 @@ function renderCharacterEditorFields(item) {
|
|
|
5120
5218
|
: organizationOptions.length
|
|
5121
5219
|
? field("organizationIds", "所属组织(可多选)", "chips", item?.organizationIds ?? [], organizationOptions)
|
|
5122
5220
|
: '<div class="character-editor-empty-field"><b>所属组织</b><span>尚未创建组织,可稍后在“组织”模块中补充。</span></div>') +
|
|
5123
|
-
field("visibility", "可见范围", "select", item?.visibility ?? "author", [["author", "仅作者"], ["collaborators", "协作者"], ["public", "公开"]]) +
|
|
5124
5221
|
(canReadModule("editor")
|
|
5125
5222
|
? field("firstChapterId", "首次登场章节", "select", item?.firstChapterId ?? "", chapterOptions)
|
|
5126
5223
|
: '<div class="character-editor-empty-field"><b>首次登场章节</b><span>当前账户没有正文读取权限,原有绑定不会被修改。</span></div>')),
|
|
@@ -5176,7 +5273,6 @@ function collectCharacterBody(form) {
|
|
|
5176
5273
|
},
|
|
5177
5274
|
currentState: buildCharacterState(form.getAll("stateKey"), form.getAll("stateValue"), item?.currentState ?? {}),
|
|
5178
5275
|
lockedFields: form.getAll("lockedFields").map((value) => String(value).trim()).filter(Boolean),
|
|
5179
|
-
visibility: String(form.get("visibility") ?? "author"),
|
|
5180
5276
|
changeNote: String(form.get("changeNote") ?? "").trim()
|
|
5181
5277
|
};
|
|
5182
5278
|
if (canReadModule("races")) body.raceId = form.get("raceId") || null;
|
|
@@ -6176,7 +6272,9 @@ $("#profile-form").addEventListener("submit", async (event) => {
|
|
|
6176
6272
|
state.user = updated;
|
|
6177
6273
|
applyAuthenticatedUser({ user: updated, csrfToken: state.csrfToken });
|
|
6178
6274
|
toast("显示名称已更新");
|
|
6179
|
-
} catch (error) {
|
|
6275
|
+
} catch (error) {
|
|
6276
|
+
toast(error.code === "INVALID_CURRENT_PASSWORD" ? "当前密码错误,请重新输入" : error.message, "error");
|
|
6277
|
+
}
|
|
6180
6278
|
});
|
|
6181
6279
|
$("#password-form").addEventListener("submit", async (event) => {
|
|
6182
6280
|
event.preventDefault();
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
type CharacterFilterItem = {
|
|
2
|
+
id?: string;
|
|
3
|
+
race?: { id?: string | null } | null;
|
|
4
|
+
raceId?: string | null;
|
|
5
|
+
organizations?: Array<{ id?: string | null }> | null;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export declare function filterCharacters<T extends CharacterFilterItem>(
|
|
9
|
+
characters: T[],
|
|
10
|
+
filters?: { raceIds?: string[]; organizationIds?: string[] }
|
|
11
|
+
): T[];
|
|
12
|
+
|
|
13
|
+
export declare function paginateCharacters<T>(
|
|
14
|
+
characters: T[],
|
|
15
|
+
page: number,
|
|
16
|
+
limit: number
|
|
17
|
+
): {
|
|
18
|
+
items: T[];
|
|
19
|
+
page: number;
|
|
20
|
+
limit: number;
|
|
21
|
+
hasMore: boolean;
|
|
22
|
+
nextPage: number | null;
|
|
23
|
+
total: number;
|
|
24
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
function characterRaceId(character) {
|
|
2
|
+
return String(character?.race?.id ?? character?.raceId ?? "");
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function characterOrganizationIds(character) {
|
|
6
|
+
return (Array.isArray(character?.organizations) ? character.organizations : [])
|
|
7
|
+
.map((organization) => String(organization?.organizationId ?? organization?.id ?? ""))
|
|
8
|
+
.filter(Boolean);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function filterCharacters(characters, { raceIds = [], organizationIds = [] } = {}) {
|
|
12
|
+
const selectedRaceIds = new Set(raceIds.map(String).filter(Boolean));
|
|
13
|
+
const selectedOrganizationIds = new Set(organizationIds.map(String).filter(Boolean));
|
|
14
|
+
return characters.filter((character) => {
|
|
15
|
+
const matchesRace = selectedRaceIds.size === 0 || selectedRaceIds.has(characterRaceId(character));
|
|
16
|
+
const matchesOrganization = selectedOrganizationIds.size === 0
|
|
17
|
+
|| characterOrganizationIds(character).some((organizationId) => selectedOrganizationIds.has(organizationId));
|
|
18
|
+
return matchesRace && matchesOrganization;
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function paginateCharacters(characters, page, limit) {
|
|
23
|
+
const safeLimit = Math.max(1, Number(limit) || 1);
|
|
24
|
+
const safePage = Math.max(1, Number(page) || 1);
|
|
25
|
+
const start = (safePage - 1) * safeLimit;
|
|
26
|
+
const items = characters.slice(start, start + safeLimit);
|
|
27
|
+
return {
|
|
28
|
+
items,
|
|
29
|
+
page: safePage,
|
|
30
|
+
limit: safeLimit,
|
|
31
|
+
hasMore: start + safeLimit < characters.length,
|
|
32
|
+
nextPage: start + safeLimit < characters.length ? safePage + 1 : null,
|
|
33
|
+
total: characters.length
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
export function settingStatusLabel(value: unknown): string;
|
|
2
|
-
export function characterVisibilityLabel(value: unknown): string;
|
|
3
2
|
export function timelineStatusLabel(value: unknown): string;
|
|
4
3
|
export function levelLabel(value: unknown): string;
|
|
5
4
|
export function foreshadowStatusLabel(value: unknown): string;
|
|
@@ -9,10 +9,6 @@ export function settingStatusLabel(value) {
|
|
|
9
9
|
return enumLabel({ draft: "草稿", pending: "待确认", confirmed: "已确认", deprecated: "已弃用" }, value, "未知状态");
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
export function characterVisibilityLabel(value) {
|
|
13
|
-
return enumLabel({ public: "公开", author: "仅作者", collaborators: "协作者可见" }, value, "未知范围");
|
|
14
|
-
}
|
|
15
|
-
|
|
16
12
|
export function timelineStatusLabel(value) {
|
|
17
13
|
return enumLabel({ candidate: "候选", pending: "待确认", confirmed: "已确认", deprecated: "已弃用" }, value, "未知状态");
|
|
18
14
|
}
|