243 lines
7.1 KiB
JavaScript
243 lines
7.1 KiB
JavaScript
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;
|