完成大量美化

This commit is contained in:
2026-05-01 15:44:14 +08:00
parent 6b65b24b0f
commit f0e7e75ffb
36 changed files with 4393 additions and 1506 deletions

View File

@@ -0,0 +1,242 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
const useCharacterStore = create(
persist(
(set, get) => ({
// 状态
characters: [], // 所有角色卡列表
selectedCharacter: null, // 当前选中的角色
isLoading: false,
error: null,
currentPage: 1, // 当前页码
pageSize: 12, // 每页显示数量
characterChats: {}, // 存储每个角色的聊天记录 {characterName: [chats]}
// 获取所有角色卡
fetchCharacters: async () => {
set({ isLoading: true, error: null });
try {
const response = await fetch('/api/characters/');
if (!response.ok) throw new Error('Failed to fetch characters');
const data = await response.json();
set({
characters: data,
isLoading: false,
currentPage: 1 // 重置到第一页
});
} catch (error) {
set({
error: error.message,
isLoading: false
});
}
},
// 设置当前页码
setCurrentPage: (page) => {
set({ currentPage: page });
},
// 设置每页显示数量
setPageSize: (size) => {
set({ pageSize: size, currentPage: 1 }); // 重置到第一页
},
// 获取当前页的角色数据
getCurrentPageCharacters: () => {
const { characters, currentPage, pageSize } = get();
const startIndex = (currentPage - 1) * pageSize;
const endIndex = startIndex + pageSize;
return characters.slice(startIndex, endIndex);
},
// 获取总页数
getTotalPages: () => {
const { characters, pageSize } = get();
return Math.ceil(characters.length / pageSize);
},
// 获取角色的聊天记录
fetchCharacterChats: async (characterName) => {
const { characterChats } = get();
// 如果已经缓存了,直接返回
if (characterChats[characterName]) {
return characterChats[characterName];
}
try {
const response = await fetch(`/api/characters/${encodeURIComponent(characterName)}/chats`);
if (!response.ok) throw new Error('Failed to fetch chats');
const data = await response.json();
// 更新缓存
set((state) => ({
characterChats: {
...state.characterChats,
[characterName]: data.chats || []
}
}));
return data.chats || [];
} catch (error) {
console.error('获取聊天记录失败:', error);
return [];
}
},
// 选择角色
selectCharacter: (character) => {
// 使用函数式更新避免不必要的重渲染
set((state) => {
// 如果选中的是同一个角色,不更新状态
if (state.selectedCharacter?.id === character.id) {
return state;
}
return { selectedCharacter: character };
});
},
// 创建角色
createCharacter: async (characterData) => {
try {
const response = await fetch('/api/characters/', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(characterData)
});
if (!response.ok) throw new Error('Failed to create character');
const data = await response.json();
// 刷新列表
await get().fetchCharacters();
return data.character;
} catch (error) {
set({ error: error.message });
throw error;
}
},
// 更新角色
updateCharacter: async (name, updates) => {
try {
const response = await fetch(`/api/characters/${encodeURIComponent(name)}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updates)
});
if (!response.ok) throw new Error('Failed to update character');
// 刷新列表
await get().fetchCharacters();
} catch (error) {
set({ error: error.message });
throw error;
}
},
// 删除角色
deleteCharacter: async (name) => {
try {
const response = await fetch(`/api/characters/${encodeURIComponent(name)}`, {
method: 'DELETE'
});
if (!response.ok) throw new Error('Failed to delete character');
// 刷新列表
await get().fetchCharacters();
} catch (error) {
set({ error: error.message });
throw error;
}
},
// 上传头像
uploadAvatar: async (name, file) => {
try {
const formData = new FormData();
formData.append('file', file);
const response = await fetch(`/api/characters/${encodeURIComponent(name)}/avatar`, {
method: 'POST',
body: formData
});
if (!response.ok) throw new Error('Failed to upload avatar');
// 刷新列表以更新头像路径
await get().fetchCharacters();
} catch (error) {
set({ error: error.message });
throw error;
}
},
// 导入角色卡
importCharacter: async (file) => {
try {
const formData = new FormData();
formData.append('file', file);
const response = await fetch('/api/characters/import', {
method: 'POST',
body: formData
});
if (!response.ok) throw new Error('Failed to import character');
const data = await response.json();
// 刷新列表
await get().fetchCharacters();
return data.character;
} catch (error) {
set({ error: error.message });
throw error;
}
},
// 导出角色卡为 PNG
exportCharacterAsPng: async (name) => {
try {
const response = await fetch(`/api/characters/${encodeURIComponent(name)}/export/png`, {
method: 'POST'
});
if (!response.ok) throw new Error('Failed to export character');
// 下载文件
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${name}.png`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
} catch (error) {
set({ error: error.message });
throw error;
}
}
}),
{
name: 'character-storage',
partialize: (state) => ({
selectedCharacter: state.selectedCharacter
})
}
)
);
export default useCharacterStore;

View File

@@ -53,6 +53,12 @@ const useWorldBookStore = create((set, get) => ({
currentWorldBook: null, // 当前选中的世界书
currentEntries: [], // 当前世界书的条目列表
currentEntry: null, // 当前选中的条目
entriesPagination: { // 条目分页信息
total: 0,
page: 1,
page_size: 20,
total_pages: 0
},
loading: false, // 加载状态
error: null, // 错误信息
success: false, // 操作成功状态
@@ -116,8 +122,7 @@ const useWorldBookStore = create((set, get) => ({
return {
loading: false,
globalWorldBooks: updatedGlobalBooks,
success: true,
message: isGlobal ? `已将 "${name}" 设为全局世界书` : `已取消 "${name}" 的全局状态`
error: null
};
});
},
@@ -130,7 +135,18 @@ const useWorldBookStore = create((set, get) => ({
const data = await handleResponse(response);
// 从 LocalStorage 获取全局世界书列表
const globalBooks = loadGlobalWorldBooks();
let globalBooks = loadGlobalWorldBooks();
// 清理 LocalStorage 中已不存在的世界书
const existingWorldBookNames = new Set(data.map(wb => wb.name));
const cleanedGlobalBooks = globalBooks.filter(wb => existingWorldBookNames.has(wb.name));
// 如果有被清理的项,更新 LocalStorage
if (cleanedGlobalBooks.length !== globalBooks.length) {
console.log(`清理了 ${globalBooks.length - cleanedGlobalBooks.length} 个不存在的全局世界书`);
saveGlobalWorldBooks(cleanedGlobalBooks);
globalBooks = cleanedGlobalBooks;
}
set({
loading: false,
@@ -333,18 +349,24 @@ const useWorldBookStore = create((set, get) => ({
}
},
// 异步操作:获取世界书的所有条目
fetchWorldBookEntries: async (name) => {
// 异步操作:获取世界书的所有条目(支持分页)
fetchWorldBookEntries: async (name, page = 1, page_size = 20) => {
set({ loading: true, error: null });
try {
const response = await fetch(`/api/worldbooks/${name}/entries`);
const response = await fetch(`/api/worldbooks/${name}/entries?page=${page}&page_size=${page_size}`);
const data = await handleResponse(response);
set(state => {
if (state.currentWorldBook?.name === name) {
return {
loading: false,
currentEntries: data,
currentEntries: data.entries || [],
entriesPagination: {
total: data.total || 0,
page: data.page || 1,
page_size: data.page_size || 20,
total_pages: data.total_pages || 0
},
error: null
};
}