mirror of
https://github.com/SilenceLurker/ST-Amily2-Chat-Optimisation.git
synced 2026-06-06 07:45:51 +00:00
Update WorldEditor.js
This commit is contained in:
@@ -1,20 +1,29 @@
|
|||||||
/**
|
|
||||||
* 世界书编辑器 - 最终稳定版
|
import { world_names, loadWorldInfo, saveWorldInfo, deleteWorldInfo, updateWorldInfoList } from "/scripts/world-info.js";
|
||||||
*/
|
|
||||||
import { world_names, loadWorldInfo, saveWorldInfo } from "/scripts/world-info.js";
|
|
||||||
import { eventSource, event_types } from '/script.js';
|
import { eventSource, event_types } from '/script.js';
|
||||||
import { showHtmlModal } from '/scripts/extensions/third-party/ST-Amily2-Chat-Optimisation/ui/page-window.js';
|
import { showHtmlModal } from '/scripts/extensions/third-party/ST-Amily2-Chat-Optimisation/ui/page-window.js';
|
||||||
|
import { safeLorebooks, safeLorebookEntries, safeUpdateLorebookEntries } from '../core/tavernhelper-compatibility.js';
|
||||||
|
import { writeToLorebookWithTavernHelper } from '../core/lore.js';
|
||||||
const { SillyTavern, TavernHelper } = window;
|
const { SillyTavern, TavernHelper } = window;
|
||||||
|
|
||||||
class WorldEditor {
|
class WorldEditor {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
// 通用状态
|
||||||
|
this.isLoading = false;
|
||||||
|
|
||||||
|
// 世界书视图状态
|
||||||
|
this.allWorldBooks = [];
|
||||||
|
this.filteredWorldBooks = [];
|
||||||
|
this.selectedWorldBooks = new Set();
|
||||||
|
|
||||||
|
// 条目视图状态
|
||||||
this.currentWorldBook = null;
|
this.currentWorldBook = null;
|
||||||
this.entries = [];
|
this.entries = [];
|
||||||
this.selectedEntries = new Set();
|
this.selectedEntries = new Set();
|
||||||
this.filteredEntries = [];
|
this.filteredEntries = [];
|
||||||
this.isLoading = false;
|
|
||||||
this.currentEditingEntry = null;
|
this.currentEditingEntry = null;
|
||||||
this.sortState = { key: 'order', asc: true };
|
this.sortState = { key: 'order', asc: true };
|
||||||
|
|
||||||
this.init();
|
this.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,13 +35,22 @@ class WorldEditor {
|
|||||||
}
|
}
|
||||||
this.bindEvents();
|
this.bindEvents();
|
||||||
this.loadAvailableWorldBooks();
|
this.loadAvailableWorldBooks();
|
||||||
this.bindExternalEvents(); // 绑定外部事件监听
|
this.bindExternalEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
initializeComponents() {
|
initializeComponents() {
|
||||||
const ids = [
|
const ids = [
|
||||||
'world-editor-world-select', 'world-editor-refresh-btn', 'world-editor-create-entry-btn',
|
// 主视图
|
||||||
'world-editor-search-box', 'world-editor-search-btn', 'world-editor-entry-count',
|
'world-book-selection-view', 'world-editor-entry-view',
|
||||||
|
// 顶部按钮
|
||||||
|
'world-editor-refresh-btn', 'world-editor-create-book-btn', 'world-editor-create-entry-btn',
|
||||||
|
// 世界书视图
|
||||||
|
'world-book-search-box', 'world-book-search-btn', 'world-book-count',
|
||||||
|
'world-book-batch-actions', 'world-book-selected-count', 'world-book-clone-btn', 'world-book-delete-btn',
|
||||||
|
'world-book-list-container',
|
||||||
|
// 条目视图
|
||||||
|
'world-editor-current-book-title', 'world-editor-back-to-list-btn',
|
||||||
|
'world-editor-search-type', 'world-editor-search-box', 'world-editor-search-btn', 'world-editor-entry-count',
|
||||||
'world-editor-select-all', 'world-editor-selected-count', 'world-editor-batch-actions',
|
'world-editor-select-all', 'world-editor-selected-count', 'world-editor-batch-actions',
|
||||||
'world-editor-entries-container',
|
'world-editor-entries-container',
|
||||||
'world-editor-enable-selected-btn', 'world-editor-disable-selected-btn',
|
'world-editor-enable-selected-btn', 'world-editor-disable-selected-btn',
|
||||||
@@ -44,23 +62,35 @@ class WorldEditor {
|
|||||||
for (const id of ids) {
|
for (const id of ids) {
|
||||||
const camelCaseId = id.replace(/-(\w)/g, (_, c) => c.toUpperCase());
|
const camelCaseId = id.replace(/-(\w)/g, (_, c) => c.toUpperCase());
|
||||||
this.elements[camelCaseId] = document.getElementById(id);
|
this.elements[camelCaseId] = document.getElementById(id);
|
||||||
if (!this.elements[camelCaseId] && id.endsWith('container')) { // Only container is critical
|
if (!this.elements[camelCaseId]) {
|
||||||
console.error(`[世界书编辑器] 关键元素缺失: ${id}`);
|
console.warn(`[世界书编辑器] UI元素缺失: ${id}`);
|
||||||
missing = true;
|
if (id.endsWith('container') || id.endsWith('view')) {
|
||||||
|
missing = true; // 关键元素缺失
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return !missing;
|
return !missing;
|
||||||
}
|
}
|
||||||
|
|
||||||
bindEvents() {
|
bindEvents() {
|
||||||
this.elements.worldEditorWorldSelect.addEventListener('change', (e) => this.loadWorldBookEntries(e.target.value));
|
// 视图切换
|
||||||
|
this.elements.worldEditorBackToListBtn.addEventListener('click', () => this.switchToBookListView());
|
||||||
|
|
||||||
|
// 顶部按钮
|
||||||
this.elements.worldEditorRefreshBtn.addEventListener('click', () => this.loadAvailableWorldBooks());
|
this.elements.worldEditorRefreshBtn.addEventListener('click', () => this.loadAvailableWorldBooks());
|
||||||
document.querySelector('#world-editor-container .world-editor-entries-header').addEventListener('click', (e) => {
|
this.elements.worldEditorCreateBookBtn.addEventListener('click', () => this.createNewWorldBook());
|
||||||
if (e.target.dataset.sort) {
|
|
||||||
this.sortEntries(e.target.dataset.sort);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.elements.worldEditorCreateEntryBtn.addEventListener('click', () => this.openCreateModal());
|
this.elements.worldEditorCreateEntryBtn.addEventListener('click', () => this.openCreateModal());
|
||||||
|
|
||||||
|
// 世界书视图事件
|
||||||
|
this.elements.worldBookSearchBox.addEventListener('input', () => this.filterWorldBooks());
|
||||||
|
this.elements.worldBookSearchBtn.addEventListener('click', () => this.filterWorldBooks());
|
||||||
|
this.elements.worldBookCloneBtn.addEventListener('click', () => this.cloneSelectedBooks());
|
||||||
|
this.elements.worldBookDeleteBtn.addEventListener('click', () => this.deleteSelectedBooks());
|
||||||
|
|
||||||
|
// 条目视图事件
|
||||||
|
document.querySelector('#world-editor-entry-view .world-editor-entries-header').addEventListener('click', (e) => {
|
||||||
|
if (e.target.dataset.sort) this.sortEntries(e.target.dataset.sort);
|
||||||
|
});
|
||||||
this.elements.worldEditorSearchBox.addEventListener('input', () => this.filterEntries());
|
this.elements.worldEditorSearchBox.addEventListener('input', () => this.filterEntries());
|
||||||
this.elements.worldEditorSearchBtn.addEventListener('click', () => this.filterEntries());
|
this.elements.worldEditorSearchBtn.addEventListener('click', () => this.filterEntries());
|
||||||
this.elements.worldEditorSelectAll.addEventListener('change', (e) => this.toggleSelectAll(e.target.checked));
|
this.elements.worldEditorSelectAll.addEventListener('change', (e) => this.toggleSelectAll(e.target.checked));
|
||||||
@@ -73,19 +103,29 @@ class WorldEditor {
|
|||||||
this.elements.worldEditorSetPreventRecursionBtn.addEventListener('click', () => this.toggleBatchRecursion('prevent_recursion', '防止递归'));
|
this.elements.worldEditorSetPreventRecursionBtn.addEventListener('click', () => this.toggleBatchRecursion('prevent_recursion', '防止递归'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 视图管理
|
||||||
|
switchToBookListView() {
|
||||||
|
this.elements.worldBookSelectionView.style.display = 'block';
|
||||||
|
this.elements.worldEditorEntryView.style.display = 'none';
|
||||||
|
this.elements.worldEditorCreateEntryBtn.disabled = true;
|
||||||
|
this.currentWorldBook = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
switchToEntryView(bookName) {
|
||||||
|
this.elements.worldBookSelectionView.style.display = 'none';
|
||||||
|
this.elements.worldEditorEntryView.style.display = 'block';
|
||||||
|
this.elements.worldEditorCreateEntryBtn.disabled = false;
|
||||||
|
this.elements.worldEditorCurrentBookTitle.textContent = `当前编辑:${bookName}`;
|
||||||
|
this.loadWorldBookEntries(bookName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 世界书数据处理
|
||||||
async loadAvailableWorldBooks() {
|
async loadAvailableWorldBooks() {
|
||||||
this.setLoading(true);
|
this.setLoading(true);
|
||||||
try {
|
try {
|
||||||
const books = await this.getAllWorldBooks();
|
const books = await this.getAllWorldBooks();
|
||||||
const select = this.elements.worldEditorWorldSelect;
|
this.allWorldBooks = books.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
select.innerHTML = '<option value="">请选择世界书...</option>';
|
this.filterWorldBooks(); // 这会渲染列表
|
||||||
books.forEach(book => {
|
|
||||||
const option = document.createElement('option');
|
|
||||||
option.value = book.name;
|
|
||||||
option.textContent = book.name;
|
|
||||||
select.appendChild(option);
|
|
||||||
});
|
|
||||||
await this.selectCurrentCharacterWorldBook();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.showError('加载世界书列表失败: ' + error.message);
|
this.showError('加载世界书列表失败: ' + error.message);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -94,39 +134,174 @@ class WorldEditor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getAllWorldBooks() {
|
async getAllWorldBooks() {
|
||||||
if (TavernHelper?.getLorebooks) {
|
const books = await safeLorebooks();
|
||||||
const books = await TavernHelper.getLorebooks();
|
return books.map(name => ({ name }));
|
||||||
if (Array.isArray(books) && books.length > 0) return books.map(name => ({ name }));
|
|
||||||
}
|
|
||||||
return (world_names || []).map(name => ({ name }));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async selectCurrentCharacterWorldBook() {
|
filterWorldBooks() {
|
||||||
if (TavernHelper?.getCurrentCharPrimaryLorebook) {
|
const term = this.elements.worldBookSearchBox.value.toLowerCase();
|
||||||
const primaryBook = await TavernHelper.getCurrentCharPrimaryLorebook();
|
this.filteredWorldBooks = this.allWorldBooks.filter(book => book.name.toLowerCase().includes(term));
|
||||||
if (primaryBook) {
|
this.renderWorldBookList();
|
||||||
this.elements.worldEditorWorldSelect.value = primaryBook;
|
this.updateWorldBookCount();
|
||||||
await this.loadWorldBookEntries(primaryBook);
|
}
|
||||||
|
|
||||||
|
renderWorldBookList() {
|
||||||
|
const container = this.elements.worldBookListContainer;
|
||||||
|
container.innerHTML = ''; // 清空
|
||||||
|
if (this.filteredWorldBooks.length === 0) {
|
||||||
|
container.innerHTML = '<p class="world-editor-empty-state">没有找到世界书</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fragment = document.createDocumentFragment();
|
||||||
|
this.filteredWorldBooks.forEach(book => {
|
||||||
|
const isSelected = this.selectedWorldBooks.has(book.name);
|
||||||
|
const row = document.createElement('div');
|
||||||
|
row.className = `world-book-row ${isSelected ? 'selected' : ''}`;
|
||||||
|
row.dataset.bookName = book.name;
|
||||||
|
row.innerHTML = `
|
||||||
|
<input type="checkbox" class="world-book-checkbox" ${isSelected ? 'checked' : ''}>
|
||||||
|
<span class="world-book-name">${book.name}</span>
|
||||||
|
<div class="world-book-actions">
|
||||||
|
<button class="world-editor-btn small-btn" data-action="edit"><i class="fas fa-pencil-alt"></i> 编辑</button>
|
||||||
|
<button class="world-editor-btn small-btn" data-action="rename"><i class="fas fa-i-cursor"></i> 重命名</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
fragment.appendChild(row);
|
||||||
|
});
|
||||||
|
container.appendChild(fragment);
|
||||||
|
this.bindWorldBookListEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
bindWorldBookListEvents() {
|
||||||
|
this.elements.worldBookListContainer.querySelectorAll('.world-book-row').forEach(row => {
|
||||||
|
const bookName = row.dataset.bookName;
|
||||||
|
// 复选框事件
|
||||||
|
row.querySelector('.world-book-checkbox').addEventListener('change', (e) => {
|
||||||
|
if (e.target.checked) {
|
||||||
|
this.selectedWorldBooks.add(bookName);
|
||||||
|
} else {
|
||||||
|
this.selectedWorldBooks.delete(bookName);
|
||||||
|
}
|
||||||
|
row.classList.toggle('selected', e.target.checked);
|
||||||
|
this.updateWorldBookSelectionUI();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 按钮事件
|
||||||
|
row.querySelector('[data-action="edit"]').addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
this.switchToEntryView(bookName);
|
||||||
|
});
|
||||||
|
row.querySelector('[data-action="rename"]').addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
this.renameWorldBook(bookName);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async createNewWorldBook() {
|
||||||
|
const bookName = prompt("请输入新的世界书名称:");
|
||||||
|
if (bookName && bookName.trim()) {
|
||||||
|
const trimmedBookName = bookName.trim();
|
||||||
|
try {
|
||||||
|
await writeToLorebookWithTavernHelper(trimmedBookName, '新条目', () => '这是一个新条目', {});
|
||||||
|
if (window.toastr) window.toastr.success(`世界书 "${trimmedBookName}" 创建成功!`);
|
||||||
|
this.loadAvailableWorldBooks();
|
||||||
|
} catch (error) {
|
||||||
|
this.showError(`创建失败: ${error.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async renameWorldBook(oldName) {
|
||||||
|
const newName = prompt(`重命名世界书 "${oldName}":`, oldName);
|
||||||
|
if (newName && newName.trim() && newName !== oldName) {
|
||||||
|
const trimmedNewName = newName.trim();
|
||||||
|
try {
|
||||||
|
const bookData = await loadWorldInfo(oldName);
|
||||||
|
await saveWorldInfo(trimmedNewName, bookData);
|
||||||
|
await deleteWorldInfo(oldName);
|
||||||
|
if (window.toastr) window.toastr.success('重命名成功!');
|
||||||
|
|
||||||
|
await updateWorldInfoList();
|
||||||
|
eventSource.emit(event_types.CHARACTER_PAGE_LOADED);
|
||||||
|
this.loadAvailableWorldBooks();
|
||||||
|
} catch (error) {
|
||||||
|
this.showError(`重命名失败: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async cloneSelectedBooks() {
|
||||||
|
if (this.selectedWorldBooks.size === 0) return;
|
||||||
|
if (!confirm(`确定要为 ${this.selectedWorldBooks.size} 个世界书创建备份吗?`)) return;
|
||||||
|
|
||||||
|
this.setLoading(true);
|
||||||
|
try {
|
||||||
|
for (const bookName of this.selectedWorldBooks) {
|
||||||
|
const newName = `${bookName}_备份_${Date.now()}`;
|
||||||
|
const bookData = await loadWorldInfo(bookName);
|
||||||
|
await saveWorldInfo(newName, bookData);
|
||||||
|
}
|
||||||
|
if (window.toastr) window.toastr.success('备份创建成功!');
|
||||||
|
|
||||||
|
await updateWorldInfoList();
|
||||||
|
eventSource.emit(event_types.CHARACTER_PAGE_LOADED);
|
||||||
|
this.loadAvailableWorldBooks();
|
||||||
|
} catch (error) {
|
||||||
|
this.showError(`备份失败: ${error.message}`);
|
||||||
|
} finally {
|
||||||
|
this.setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteSelectedBooks() {
|
||||||
|
if (this.selectedWorldBooks.size === 0) return;
|
||||||
|
if (!confirm(`警告:这将永久删除 ${this.selectedWorldBooks.size} 个世界书及其所有内容!确定要继续吗?`)) return;
|
||||||
|
|
||||||
|
this.setLoading(true);
|
||||||
|
try {
|
||||||
|
for (const bookName of this.selectedWorldBooks) {
|
||||||
|
await deleteWorldInfo(bookName);
|
||||||
|
}
|
||||||
|
if (window.toastr) window.toastr.success('批量删除成功!');
|
||||||
|
|
||||||
|
await updateWorldInfoList();
|
||||||
|
eventSource.emit(event_types.CHARACTER_PAGE_LOADED);
|
||||||
|
this.loadAvailableWorldBooks();
|
||||||
|
} catch (error) {
|
||||||
|
this.showError(`删除失败: ${error.message}`);
|
||||||
|
} finally {
|
||||||
|
this.setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateWorldBookCount() {
|
||||||
|
this.elements.worldBookCount.textContent = `世界书:${this.allWorldBooks.length}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateWorldBookSelectionUI() {
|
||||||
|
const count = this.selectedWorldBooks.size;
|
||||||
|
this.elements.worldBookSelectedCount.textContent = `已选择 ${count} 项`;
|
||||||
|
this.elements.worldBookBatchActions.classList.toggle('active', count > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 条目数据处理 (大部分逻辑与旧版相似)
|
||||||
async loadWorldBookEntries(worldBookName) {
|
async loadWorldBookEntries(worldBookName) {
|
||||||
if (!worldBookName) { this.entries = []; this.renderEntries(); return; }
|
if (!worldBookName) {
|
||||||
|
this.entries = [];
|
||||||
|
this.filteredEntries = [];
|
||||||
|
this.selectedEntries.clear();
|
||||||
|
this.renderEntries();
|
||||||
|
this.updateEntryCount();
|
||||||
|
this.updateSelectionUI();
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.setLoading(true);
|
this.setLoading(true);
|
||||||
this.currentWorldBook = worldBookName;
|
this.currentWorldBook = worldBookName;
|
||||||
try {
|
try {
|
||||||
let rawEntries = await TavernHelper?.getLorebookEntries?.(worldBookName);
|
const rawEntries = await safeLorebookEntries(worldBookName);
|
||||||
if (!rawEntries || rawEntries.length === 0) {
|
|
||||||
const bookData = await loadWorldInfo(worldBookName);
|
|
||||||
if (bookData?.entries) {
|
|
||||||
rawEntries = Object.entries(bookData.entries).map(([uid, entry]) => ({
|
|
||||||
uid: parseInt(uid), enabled: !entry.disable, type: entry.constant ? 'constant' : 'selective',
|
|
||||||
keys: entry.key || [], content: entry.content || '', position: this.convertPositionFromNative(entry.position),
|
|
||||||
depth: entry.depth, order: entry.order, comment: entry.comment || ''
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.entries = (rawEntries || []).map(e => ({
|
this.entries = (rawEntries || []).map(e => ({
|
||||||
uid: e.uid, enabled: e.enabled, type: e.type || (e.constant ? 'constant' : 'selective'),
|
uid: e.uid, enabled: e.enabled, type: e.type || (e.constant ? 'constant' : 'selective'),
|
||||||
keys: e.keys || [], content: e.content || '', position: e.position || 'before_character_definition',
|
keys: e.keys || [], content: e.content || '', position: e.position || 'before_character_definition',
|
||||||
@@ -138,8 +313,11 @@ class WorldEditor {
|
|||||||
this.updateEntryCount();
|
this.updateEntryCount();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.showError(`加载条目失败: ${error.message}`);
|
this.showError(`加载条目失败: ${error.message}`);
|
||||||
this.entries = []; this.renderEntries();
|
this.entries = [];
|
||||||
|
this.filteredEntries = [];
|
||||||
} finally {
|
} finally {
|
||||||
|
this.selectedEntries.clear();
|
||||||
|
this.updateSelectionUI();
|
||||||
this.setLoading(false);
|
this.setLoading(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -153,10 +331,6 @@ class WorldEditor {
|
|||||||
const container = this.elements.worldEditorEntriesContainer;
|
const container = this.elements.worldEditorEntriesContainer;
|
||||||
const header = container.querySelector('.world-editor-entries-header');
|
const header = container.querySelector('.world-editor-entries-header');
|
||||||
|
|
||||||
// Clear only the entry rows, not the header
|
|
||||||
while (container.firstChild && container.firstChild !== header) {
|
|
||||||
container.removeChild(container.firstChild);
|
|
||||||
}
|
|
||||||
while (header && header.nextSibling) {
|
while (header && header.nextSibling) {
|
||||||
container.removeChild(header.nextSibling);
|
container.removeChild(header.nextSibling);
|
||||||
}
|
}
|
||||||
@@ -178,7 +352,6 @@ class WorldEditor {
|
|||||||
tempDiv.innerHTML = rowHTML;
|
tempDiv.innerHTML = rowHTML;
|
||||||
const rowElement = tempDiv.firstChild;
|
const rowElement = tempDiv.firstChild;
|
||||||
|
|
||||||
// Safely set the content to prevent HTML rendering
|
|
||||||
const contentCell = rowElement.querySelector('.world-editor-entry-content');
|
const contentCell = rowElement.querySelector('.world-editor-entry-content');
|
||||||
if (contentCell) {
|
if (contentCell) {
|
||||||
contentCell.textContent = e.content || '';
|
contentCell.textContent = e.content || '';
|
||||||
@@ -216,19 +389,13 @@ class WorldEditor {
|
|||||||
bindEntryEvents() {
|
bindEntryEvents() {
|
||||||
this.elements.worldEditorEntriesContainer.querySelectorAll('.world-editor-entry-row').forEach(row => {
|
this.elements.worldEditorEntriesContainer.querySelectorAll('.world-editor-entry-row').forEach(row => {
|
||||||
const uid = parseInt(row.dataset.uid);
|
const uid = parseInt(row.dataset.uid);
|
||||||
|
|
||||||
// Checkbox
|
|
||||||
const checkbox = row.querySelector('.world-editor-entry-checkbox');
|
const checkbox = row.querySelector('.world-editor-entry-checkbox');
|
||||||
checkbox.addEventListener('change', (e) => {
|
checkbox.addEventListener('change', (e) => {
|
||||||
if (e.target.checked) this.selectedEntries.add(uid); else this.selectedEntries.delete(uid);
|
if (e.target.checked) this.selectedEntries.add(uid); else this.selectedEntries.delete(uid);
|
||||||
row.classList.toggle('selected', e.target.checked);
|
row.classList.toggle('selected', e.target.checked);
|
||||||
this.updateSelectionUI();
|
this.updateSelectionUI();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Content click to open modal (for longer edits)
|
|
||||||
row.querySelector('[data-action="open-editor"]').addEventListener('click', () => this.openEditModal(uid));
|
row.querySelector('[data-action="open-editor"]').addEventListener('click', () => this.openEditModal(uid));
|
||||||
|
|
||||||
// Inline toggles (enabled, type)
|
|
||||||
row.querySelectorAll('.inline-toggle').forEach(toggle => {
|
row.querySelectorAll('.inline-toggle').forEach(toggle => {
|
||||||
toggle.addEventListener('click', (e) => {
|
toggle.addEventListener('click', (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -240,54 +407,106 @@ class WorldEditor {
|
|||||||
this.updateSingleEntry(uid, { [field]: newValue });
|
this.updateSingleEntry(uid, { [field]: newValue });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Inline edits (inputs, selects)
|
|
||||||
row.querySelectorAll('.inline-edit').forEach(input => {
|
row.querySelectorAll('.inline-edit').forEach(input => {
|
||||||
input.addEventListener('change', (e) => {
|
input.addEventListener('change', (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const field = input.dataset.field;
|
const field = input.dataset.field;
|
||||||
let value = input.value;
|
let value = input.value;
|
||||||
if (input.type === 'number') value = parseInt(value, 10);
|
if (input.type === 'number') value = parseInt(value, 10);
|
||||||
// The 'keys' field is no longer inline editable, so that specific logic can be removed.
|
|
||||||
|
|
||||||
const updates = { [field]: value };
|
const updates = { [field]: value };
|
||||||
// If position changes, re-evaluate depth disable state
|
|
||||||
if (field === 'position') {
|
if (field === 'position') {
|
||||||
const depthInput = row.querySelector('[data-field="depth"]');
|
const depthInput = row.querySelector('[data-field="depth"]');
|
||||||
if (depthInput) depthInput.disabled = !value.startsWith('at_depth');
|
if (depthInput) depthInput.disabled = !value.startsWith('at_depth');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.updateSingleEntry(uid, updates);
|
this.updateSingleEntry(uid, updates);
|
||||||
});
|
});
|
||||||
input.addEventListener('click', e => e.stopPropagation()); // Prevent row selection when clicking input
|
input.addEventListener('click', e => e.stopPropagation());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用原生 saveWorldInfo 更新条目,避免界面跳转
|
||||||
|
* @param {Array<object>} entriesToUpdate - 需要更新的条目对象数组
|
||||||
|
*/
|
||||||
|
async updateEntriesWithNativeMethod(entriesToUpdate) {
|
||||||
|
try {
|
||||||
|
const bookData = await loadWorldInfo(this.currentWorldBook);
|
||||||
|
if (!bookData || !bookData.entries) {
|
||||||
|
throw new Error("无法加载世界书数据。");
|
||||||
|
}
|
||||||
|
|
||||||
|
const uidsToUpdate = new Set(entriesToUpdate.map(e => e.uid));
|
||||||
|
const updatedUIDs = new Set();
|
||||||
|
|
||||||
|
// 更新 bookData.entries
|
||||||
|
for (const entry of entriesToUpdate) {
|
||||||
|
if (bookData.entries[entry.uid]) {
|
||||||
|
const nativeEntry = bookData.entries[entry.uid];
|
||||||
|
nativeEntry.comment = entry.comment;
|
||||||
|
nativeEntry.content = entry.content;
|
||||||
|
nativeEntry.key = entry.keys;
|
||||||
|
nativeEntry.disable = !entry.enabled;
|
||||||
|
nativeEntry.constant = entry.type === 'constant';
|
||||||
|
nativeEntry.position = this.convertPositionToNative(entry.position);
|
||||||
|
nativeEntry.depth = entry.depth;
|
||||||
|
nativeEntry.order = entry.order;
|
||||||
|
nativeEntry.exclude_recursion = entry.exclude_recursion;
|
||||||
|
nativeEntry.prevent_recursion = entry.prevent_recursion;
|
||||||
|
updatedUIDs.add(entry.uid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updatedUIDs.size !== uidsToUpdate.size) {
|
||||||
|
console.warn("[世界书编辑器] 部分条目更新失败,UID可能不存在。");
|
||||||
|
}
|
||||||
|
|
||||||
|
await saveWorldInfo(this.currentWorldBook, bookData, true); // true 表示静默保存
|
||||||
|
|
||||||
|
// Optimistic UI update in local state
|
||||||
|
for (const updatedEntry of entriesToUpdate) {
|
||||||
|
const localEntry = this.entries.find(e => e.uid === updatedEntry.uid);
|
||||||
|
if (localEntry) {
|
||||||
|
Object.assign(localEntry, updatedEntry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.renderEntries();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
this.showError(`更新失败: ${error.message}`);
|
||||||
|
this.loadWorldBookEntries(this.currentWorldBook); // On error, re-sync with truth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to convert string position to native number format
|
||||||
|
convertPositionToNative(posStr) {
|
||||||
|
const map = {
|
||||||
|
'before_character_definition': 0,
|
||||||
|
'after_character_definition': 1,
|
||||||
|
'before_author_note': 2,
|
||||||
|
'after_author_note': 3,
|
||||||
|
'at_depth': 4,
|
||||||
|
'at_depth_as_system': 4
|
||||||
|
};
|
||||||
|
return map[posStr] !== undefined ? map[posStr] : 4;
|
||||||
|
}
|
||||||
|
|
||||||
async updateSingleEntry(uid, updates) {
|
async updateSingleEntry(uid, updates) {
|
||||||
const entry = this.entries.find(e => e.uid === uid);
|
const entry = this.entries.find(e => e.uid === uid);
|
||||||
if (!entry) return;
|
if (!entry) return;
|
||||||
|
const updatedEntry = { ...entry, ...updates };
|
||||||
// Optimistic UI update
|
await this.updateEntriesWithNativeMethod([updatedEntry]);
|
||||||
Object.assign(entry, updates);
|
|
||||||
this.renderEntries(); // Re-render to reflect changes immediately
|
|
||||||
|
|
||||||
try {
|
|
||||||
await TavernHelper.setLorebookEntries(this.currentWorldBook, [{ ...entry, ...updates }]);
|
|
||||||
} catch (error) {
|
|
||||||
this.showError(`更新失败: ${error.message}`);
|
|
||||||
// Revert on failure
|
|
||||||
this.loadWorldBookEntries(this.currentWorldBook);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async batchUpdateEntries(updates, confirmation = null) {
|
async batchUpdateEntries(updates, confirmation = null) {
|
||||||
if (this.selectedEntries.size === 0) return;
|
if (this.selectedEntries.size === 0) return;
|
||||||
if (confirmation && !confirm(confirmation)) return;
|
if (confirmation && !confirm(confirmation)) return;
|
||||||
|
|
||||||
const entries = this.entries.filter(e => this.selectedEntries.has(e.uid)).map(e => ({ ...e, ...updates }));
|
const entriesToUpdate = this.entries
|
||||||
await TavernHelper.setLorebookEntries(this.currentWorldBook, entries);
|
.filter(e => this.selectedEntries.has(e.uid))
|
||||||
this.loadWorldBookEntries(this.currentWorldBook); // Refresh
|
.map(e => ({ ...e, ...updates }));
|
||||||
|
|
||||||
|
await this.updateEntriesWithNativeMethod(entriesToUpdate);
|
||||||
if (window.toastr) window.toastr.success('批量更新成功!');
|
if (window.toastr) window.toastr.success('批量更新成功!');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -303,9 +522,12 @@ class WorldEditor {
|
|||||||
|
|
||||||
async batchDeleteEntries() {
|
async batchDeleteEntries() {
|
||||||
if (this.selectedEntries.size === 0 || !confirm(`删除 ${this.selectedEntries.size} 个条目?`)) return;
|
if (this.selectedEntries.size === 0 || !confirm(`删除 ${this.selectedEntries.size} 个条目?`)) return;
|
||||||
await TavernHelper.deleteLorebookEntries(this.currentWorldBook, Array.from(this.selectedEntries));
|
try {
|
||||||
this.selectedEntries.clear();
|
await TavernHelper.deleteLorebookEntries(this.currentWorldBook, Array.from(this.selectedEntries));
|
||||||
this.loadWorldBookEntries(this.currentWorldBook); // Refresh
|
this.loadWorldBookEntries(this.currentWorldBook);
|
||||||
|
} catch (error) {
|
||||||
|
this.showError(`删除失败: ${error.message}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleSelectAll(checked) {
|
toggleSelectAll(checked) {
|
||||||
@@ -327,16 +549,8 @@ class WorldEditor {
|
|||||||
|
|
||||||
filterEntries() {
|
filterEntries() {
|
||||||
const term = this.elements.worldEditorSearchBox.value.toLowerCase();
|
const term = this.elements.worldEditorSearchBox.value.toLowerCase();
|
||||||
const searchType = document.getElementById('world-editor-search-type').value;
|
const searchType = this.elements.worldEditorSearchType.value;
|
||||||
|
this.filteredEntries = !term ? [...this.entries] : this.entries.filter(e => (e[searchType] || '').toLowerCase().includes(term));
|
||||||
if (!term) {
|
|
||||||
this.filteredEntries = [...this.entries];
|
|
||||||
} else {
|
|
||||||
this.filteredEntries = this.entries.filter(e => {
|
|
||||||
const targetField = e[searchType] || '';
|
|
||||||
return targetField.toLowerCase().includes(term);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.renderEntries();
|
this.renderEntries();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -355,146 +569,84 @@ class WorldEditor {
|
|||||||
|
|
||||||
showEditModal(title, entry) {
|
showEditModal(title, entry) {
|
||||||
const formHtml = this.getEditFormHtml(entry);
|
const formHtml = this.getEditFormHtml(entry);
|
||||||
showHtmlModal(title, formHtml, {
|
showHtmlModal(title, formHtml, { onOk: (d) => { this.saveEntry(d); return true; } });
|
||||||
onOk: (dialog) => {
|
|
||||||
this.saveEntry(dialog);
|
|
||||||
return true; // Close the modal
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getEditFormHtml(entry) {
|
getEditFormHtml(entry) {
|
||||||
return `
|
return `
|
||||||
<style>
|
<style>
|
||||||
.world-editor-form-grid {
|
.world-editor-form-grid { display: grid; grid-template-columns: 120px 1fr; gap: 15px; align-items: center; }
|
||||||
display: grid;
|
.world-editor-form-grid label { text-align: right; color: #ccc; }
|
||||||
grid-template-columns: 120px 1fr;
|
.world-editor-form-grid .form-control { width: 100%; padding: 8px; background-color: #404040; color: white; border: 1px solid #555; border-radius: 4px; box-sizing: border-box; }
|
||||||
gap: 15px;
|
.world-editor-form-grid textarea.form-control { min-height: 100px; resize: vertical; }
|
||||||
align-items: center;
|
.world-editor-form-grid .full-width { grid-column: 1 / -1; }
|
||||||
}
|
.world-editor-form-grid .checkbox-group { grid-column: 2 / -1; display: flex; align-items: center; gap: 8px; }
|
||||||
.world-editor-form-grid label {
|
|
||||||
text-align: right;
|
|
||||||
color: #ccc;
|
|
||||||
}
|
|
||||||
.world-editor-form-grid .form-control {
|
|
||||||
width: 100%;
|
|
||||||
padding: 8px;
|
|
||||||
background-color: #404040;
|
|
||||||
color: white;
|
|
||||||
border: 1px solid #555;
|
|
||||||
border-radius: 4px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
.world-editor-form-grid textarea.form-control {
|
|
||||||
min-height: 100px;
|
|
||||||
resize: vertical;
|
|
||||||
}
|
|
||||||
.world-editor-form-grid .full-width {
|
|
||||||
grid-column: 1 / -1;
|
|
||||||
}
|
|
||||||
.world-editor-form-grid .checkbox-group {
|
|
||||||
grid-column: 2 / -1;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
<form id="world-editor-edit-form" class="world-editor-form-grid">
|
<form id="world-editor-edit-form" class="world-editor-form-grid">
|
||||||
<div class="checkbox-group">
|
<div class="checkbox-group"><input type="checkbox" id="world-editor-entry-enabled" ${entry.enabled ? 'checked' : ''}><label for="world-editor-entry-enabled">启用条目</label></div>
|
||||||
<input type="checkbox" id="world-editor-entry-enabled" ${entry.enabled ? 'checked' : ''}>
|
<label for="world-editor-entry-type">激活模式:</label><select id="world-editor-entry-type" class="form-control"><option value="selective" ${entry.type === 'selective' ? 'selected' : ''}>🟢 绿灯 (关键词触发)</option><option value="constant" ${entry.type === 'constant' ? 'selected' : ''}>🔵 蓝灯 (始终激活)</option></select>
|
||||||
<label for="world-editor-entry-enabled">启用条目</label>
|
<label for="world-editor-entry-keys" class="full-width" style="text-align: left; grid-column: 1 / -1;">关键词 (每行一个):</label><textarea id="world-editor-entry-keys" class="form-control full-width" placeholder="输入关键词,每行一个">${(entry.keys || []).join('\n')}</textarea>
|
||||||
</div>
|
<label for="world-editor-entry-content" class="full-width" style="text-align: left; grid-column: 1 / -1;">内容:</label><textarea id="world-editor-entry-content" class="form-control full-width" placeholder="输入条目内容">${entry.content || ''}</textarea>
|
||||||
|
<label for="world-editor-entry-position">插入位置:</label><select id="world-editor-entry-position" class="form-control"><option value="before_character_definition" ${entry.position === 'before_character_definition' ? 'selected' : ''}>角色定义之前</option><option value="after_character_definition" ${entry.position === 'after_character_definition' ? 'selected' : ''}>角色定义之后</option><option value="before_author_note" ${entry.position === 'before_author_note' ? 'selected' : ''}>作者注释之前</option><option value="after_author_note" ${entry.position === 'after_author_note' ? 'selected' : ''}>作者注释之后</option><option value="at_depth" ${entry.position === 'at_depth' ? 'selected' : ''}>@D 注入指定深度</option></select>
|
||||||
<label for="world-editor-entry-type">激活模式:</label>
|
<label for="world-editor-entry-depth">深度:</label><input type="number" id="world-editor-entry-depth" class="form-control" min="0" max="9999" value="${entry.depth || 4}">
|
||||||
<select id="world-editor-entry-type" class="form-control">
|
<label for="world-editor-entry-order">顺序:</label><input type="number" id="world-editor-entry-order" class="form-control" min="0" max="9999" value="${entry.order || 100}">
|
||||||
<option value="selective" ${entry.type === 'selective' ? 'selected' : ''}>🟢 绿灯 (关键词触发)</option>
|
<label for="world-editor-entry-comment">备注:</label><input type="text" id="world-editor-entry-comment" class="form-control" placeholder="可选的备注信息" value="${entry.comment || ''}">
|
||||||
<option value="constant" ${entry.type === 'constant' ? 'selected' : ''}>🔵 蓝灯 (始终激活)</option>
|
<div class="checkbox-group"><input type="checkbox" id="world-editor-entry-disable-recursion" ${entry.exclude_recursion ? 'checked' : ''}><label for="world-editor-entry-disable-recursion">不可递归 (不会被其他条目激活)</label></div>
|
||||||
</select>
|
<div class="checkbox-group"><input type="checkbox" id="world-editor-entry-prevent-recursion" ${entry.prevent_recursion ? 'checked' : ''}><label for="world-editor-entry-prevent-recursion">防止进一步递归 (本条目将不会激活其他条目)</label></div>
|
||||||
|
|
||||||
<label for="world-editor-entry-keys" class="full-width" style="text-align: left; grid-column: 1 / -1;">关键词 (每行一个):</label>
|
|
||||||
<textarea id="world-editor-entry-keys" class="form-control full-width" placeholder="输入关键词,每行一个">${(entry.keys || []).join('\n')}</textarea>
|
|
||||||
|
|
||||||
<label for="world-editor-entry-content" class="full-width" style="text-align: left; grid-column: 1 / -1;">内容:</label>
|
|
||||||
<textarea id="world-editor-entry-content" class="form-control full-width" placeholder="输入条目内容">${entry.content || ''}</textarea>
|
|
||||||
|
|
||||||
<label for="world-editor-entry-position">插入位置:</label>
|
|
||||||
<select id="world-editor-entry-position" class="form-control">
|
|
||||||
<option value="before_character_definition" ${entry.position === 'before_character_definition' ? 'selected' : ''}>角色定义之前</option>
|
|
||||||
<option value="after_character_definition" ${entry.position === 'after_character_definition' ? 'selected' : ''}>角色定义之后</option>
|
|
||||||
<option value="before_author_note" ${entry.position === 'before_author_note' ? 'selected' : ''}>作者注释之前</option>
|
|
||||||
<option value="after_author_note" ${entry.position === 'after_author_note' ? 'selected' : ''}>作者注释之后</option>
|
|
||||||
<option value="at_depth" ${entry.position === 'at_depth' ? 'selected' : ''}>@D 注入指定深度</option>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<label for="world-editor-entry-depth">深度:</label>
|
|
||||||
<input type="number" id="world-editor-entry-depth" class="form-control" min="0" max="9999" value="${entry.depth || 4}">
|
|
||||||
|
|
||||||
<label for="world-editor-entry-order">顺序:</label>
|
|
||||||
<input type="number" id="world-editor-entry-order" class="form-control" min="0" max="9999" value="${entry.order || 100}">
|
|
||||||
|
|
||||||
<label for="world-editor-entry-comment">备注:</label>
|
|
||||||
<input type="text" id="world-editor-entry-comment" class="form-control" placeholder="可选的备注信息" value="${entry.comment || ''}">
|
|
||||||
|
|
||||||
<div class="checkbox-group">
|
|
||||||
<input type="checkbox" id="world-editor-entry-disable-recursion" ${entry.exclude_recursion ? 'checked' : ''}>
|
|
||||||
<label for="world-editor-entry-disable-recursion">不可递归 (不会被其他条目激活)</label>
|
|
||||||
</div>
|
|
||||||
<div class="checkbox-group">
|
|
||||||
<input type="checkbox" id="world-editor-entry-prevent-recursion" ${entry.prevent_recursion ? 'checked' : ''}>
|
|
||||||
<label for="world-editor-entry-prevent-recursion">防止进一步递归 (本条目将不会激活其他条目)</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveEntry(dialog) {
|
async saveEntry(dialog) {
|
||||||
const formData = this.getFormDataFromModal(dialog);
|
const formData = this.getFormDataFromModal(dialog);
|
||||||
if (this.currentEditingEntry) {
|
try {
|
||||||
await TavernHelper.setLorebookEntries(this.currentWorldBook, [{ ...this.currentEditingEntry, ...formData }]);
|
if (this.currentEditingEntry) {
|
||||||
} else {
|
// 使用改造后的原生方法更新
|
||||||
await TavernHelper.createLorebookEntries(this.currentWorldBook, [formData]);
|
await this.updateEntriesWithNativeMethod([{ ...this.currentEditingEntry, ...formData }]);
|
||||||
|
} else {
|
||||||
|
// 创建条目仍然可以使用TavernHelper,因为它通常不会触发跳转
|
||||||
|
await TavernHelper.createLorebookEntries(this.currentWorldBook, [formData]);
|
||||||
|
}
|
||||||
|
// 刷新当前视图
|
||||||
|
this.loadWorldBookEntries(this.currentWorldBook);
|
||||||
|
} catch (error) {
|
||||||
|
this.showError(`保存失败: ${error.message}`);
|
||||||
}
|
}
|
||||||
this.loadWorldBookEntries(this.currentWorldBook);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getFormDataFromModal(dialog) {
|
getFormDataFromModal(dialog) {
|
||||||
const data = {};
|
return {
|
||||||
data.enabled = dialog.find('#world-editor-entry-enabled').is(':checked');
|
enabled: dialog.find('#world-editor-entry-enabled').is(':checked'),
|
||||||
data.type = dialog.find('#world-editor-entry-type').val();
|
type: dialog.find('#world-editor-entry-type').val(),
|
||||||
data.keys = dialog.find('#world-editor-entry-keys').val().split('\n').map(k => k.trim()).filter(Boolean);
|
keys: dialog.find('#world-editor-entry-keys').val().split('\n').map(k => k.trim()).filter(Boolean),
|
||||||
data.content = dialog.find('#world-editor-entry-content').val();
|
content: dialog.find('#world-editor-entry-content').val(),
|
||||||
data.position = dialog.find('#world-editor-entry-position').val();
|
position: dialog.find('#world-editor-entry-position').val(),
|
||||||
data.depth = parseInt(dialog.find('#world-editor-entry-depth').val());
|
depth: parseInt(dialog.find('#world-editor-entry-depth').val()),
|
||||||
data.order = parseInt(dialog.find('#world-editor-entry-order').val());
|
order: parseInt(dialog.find('#world-editor-entry-order').val()),
|
||||||
data.comment = dialog.find('#world-editor-entry-comment').val();
|
comment: dialog.find('#world-editor-entry-comment').val(),
|
||||||
data.exclude_recursion = dialog.find('#world-editor-entry-disable-recursion').is(':checked');
|
exclude_recursion: dialog.find('#world-editor-entry-disable-recursion').is(':checked'),
|
||||||
data.prevent_recursion = dialog.find('#world-editor-entry-prevent-recursion').is(':checked');
|
prevent_recursion: dialog.find('#world-editor-entry-prevent-recursion').is(':checked')
|
||||||
return data;
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoading(loading) { this.elements.worldEditorEntriesContainer.classList.toggle('loading', loading); }
|
setLoading(loading) {
|
||||||
|
this.isLoading = loading;
|
||||||
|
document.getElementById('world-editor-container').classList.toggle('loading', loading);
|
||||||
|
}
|
||||||
showError(msg) { if (window.toastr) window.toastr.error(msg); console.error(msg); }
|
showError(msg) { if (window.toastr) window.toastr.error(msg); console.error(msg); }
|
||||||
|
|
||||||
sortEntries(key) {
|
sortEntries(key) {
|
||||||
if (this.sortState.key === key) {
|
if (this.sortState.key === key) this.sortState.asc = !this.sortState.asc;
|
||||||
this.sortState.asc = !this.sortState.asc;
|
else { this.sortState.key = key; this.sortState.asc = true; }
|
||||||
} else {
|
|
||||||
this.sortState.key = key;
|
|
||||||
this.sortState.asc = true;
|
|
||||||
}
|
|
||||||
this.renderEntries();
|
this.renderEntries();
|
||||||
}
|
}
|
||||||
|
|
||||||
sortFilteredEntries() {
|
sortFilteredEntries() {
|
||||||
const { key, asc } = this.sortState;
|
const { key, asc } = this.sortState;
|
||||||
this.filteredEntries.sort((a, b) => {
|
this.filteredEntries.sort((a, b) => {
|
||||||
let valA = a[key];
|
let valA = a[key], valB = b[key];
|
||||||
let valB = b[key];
|
|
||||||
|
|
||||||
if (typeof valA === 'string') valA = valA.toLowerCase();
|
if (typeof valA === 'string') valA = valA.toLowerCase();
|
||||||
if (typeof valB === 'string') valB = valB.toLowerCase();
|
if (typeof valB === 'string') valB = valB.toLowerCase();
|
||||||
|
|
||||||
if (valA < valB) return asc ? -1 : 1;
|
if (valA < valB) return asc ? -1 : 1;
|
||||||
if (valA > valB) return asc ? 1 : -1;
|
if (valA > valB) return asc ? 1 : -1;
|
||||||
return 0;
|
return 0;
|
||||||
@@ -503,32 +655,33 @@ class WorldEditor {
|
|||||||
|
|
||||||
bindExternalEvents() {
|
bindExternalEvents() {
|
||||||
eventSource.on(event_types.CHAT_CHANGED, () => {
|
eventSource.on(event_types.CHAT_CHANGED, () => {
|
||||||
console.log('[世界书编辑器] 检测到聊天变更 (CHAT_CHANGED),将自动刷新。');
|
console.log('[世界书编辑器] 检测到聊天变更,将自动刷新。');
|
||||||
this.loadAvailableWorldBooks();
|
if (this.currentWorldBook) {
|
||||||
|
this.loadWorldBookEntries(this.currentWorldBook);
|
||||||
|
} else {
|
||||||
|
this.loadAvailableWorldBooks();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('[世界书编辑器] 已成功绑定外部事件监听器。');
|
console.log('[世界书编辑器] 已成功绑定外部事件监听器。');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function initializeWorldEditorWhenVisible() {
|
function initializeWorldEditor() {
|
||||||
const panel = document.getElementById('amily2_world_editor_panel');
|
// 确保面板存在
|
||||||
if (!panel) { console.error('[WorldEditor] Panel not found!'); return; }
|
if (!document.getElementById('amily2_world_editor_panel')) {
|
||||||
const observer = new MutationObserver(() => {
|
console.error('[WorldEditor] Panel not found, initialization aborted.');
|
||||||
if (panel.style.display !== 'none' && !window.worldEditorInstance) {
|
return;
|
||||||
window.worldEditorInstance = new WorldEditor();
|
}
|
||||||
observer.disconnect();
|
// 防止重复初始化
|
||||||
}
|
if (!window.worldEditorInstance) {
|
||||||
});
|
console.log('[WorldEditor] Initializing WorldEditor instance.');
|
||||||
observer.observe(panel, { attributes: true, attributeFilter: ['style'] });
|
window.worldEditorInstance = new WorldEditor();
|
||||||
if (panel.style.display !== 'none') { // Check initial state
|
|
||||||
if (!window.worldEditorInstance) window.worldEditorInstance = new WorldEditor();
|
|
||||||
observer.disconnect();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 确保在DOM加载完毕后执行
|
||||||
if (document.readyState === 'loading') {
|
if (document.readyState === 'loading') {
|
||||||
document.addEventListener('DOMContentLoaded', initializeWorldEditorWhenVisible);
|
document.addEventListener('DOMContentLoaded', initializeWorldEditor);
|
||||||
} else {
|
} else {
|
||||||
initializeWorldEditorWhenVisible();
|
initializeWorldEditor();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user