@@ -10,6 +10,7 @@ import ForgotPasswordPage from './pages/ForgotPasswordPage'
|
||||
import ProfilePage from './pages/ProfilePage'
|
||||
import CharacterManagePage from './pages/CharacterManagePage'
|
||||
import PresetManagePage from './pages/PresetManagePage'
|
||||
import WorldbookManagePage from './pages/WorldbookManagePage'
|
||||
import AdminPage from './pages/AdminPage'
|
||||
|
||||
function App() {
|
||||
@@ -27,6 +28,7 @@ function App() {
|
||||
<Route path="/profile" element={<ProfilePage />} />
|
||||
<Route path="/characters" element={<CharacterManagePage />} />
|
||||
<Route path="/presets" element={<PresetManagePage />} />
|
||||
<Route path="/worldbooks" element={<WorldbookManagePage />} />
|
||||
<Route path="/admin" element={<AdminPage />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
|
||||
164
web-app/src/api/worldbook.ts
Normal file
164
web-app/src/api/worldbook.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
import client from './client';
|
||||
|
||||
export interface Worldbook {
|
||||
id: number;
|
||||
userId: number;
|
||||
name: string;
|
||||
description: string;
|
||||
isPublic: boolean;
|
||||
entryCount: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface WorldbookEntry {
|
||||
id: number;
|
||||
worldbookId: number;
|
||||
comment: string;
|
||||
content: string;
|
||||
keys: string[];
|
||||
secondaryKeys: string[];
|
||||
constant: boolean;
|
||||
enabled: boolean;
|
||||
useRegex: boolean;
|
||||
caseSensitive: boolean;
|
||||
matchWholeWords: boolean;
|
||||
selective: boolean;
|
||||
selectiveLogic: number;
|
||||
position: number;
|
||||
depth: number;
|
||||
order: number;
|
||||
probability: number;
|
||||
scanDepth: number;
|
||||
groupId: string;
|
||||
extensions?: Record<string, any>;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface CreateWorldbookRequest {
|
||||
name: string;
|
||||
description?: string;
|
||||
isPublic?: boolean;
|
||||
}
|
||||
|
||||
export interface UpdateWorldbookRequest {
|
||||
name?: string;
|
||||
description?: string;
|
||||
isPublic?: boolean;
|
||||
}
|
||||
|
||||
export interface CreateEntryRequest {
|
||||
comment?: string;
|
||||
content: string;
|
||||
keys?: string[];
|
||||
secondaryKeys?: string[];
|
||||
constant?: boolean;
|
||||
enabled?: boolean;
|
||||
useRegex?: boolean;
|
||||
caseSensitive?: boolean;
|
||||
matchWholeWords?: boolean;
|
||||
selective?: boolean;
|
||||
selectiveLogic?: number;
|
||||
position?: number;
|
||||
depth?: number;
|
||||
order?: number;
|
||||
probability?: number;
|
||||
scanDepth?: number;
|
||||
groupId?: string;
|
||||
}
|
||||
|
||||
export interface UpdateEntryRequest {
|
||||
comment?: string;
|
||||
content?: string;
|
||||
keys?: string[];
|
||||
secondaryKeys?: string[];
|
||||
constant?: boolean;
|
||||
enabled?: boolean;
|
||||
useRegex?: boolean;
|
||||
caseSensitive?: boolean;
|
||||
matchWholeWords?: boolean;
|
||||
selective?: boolean;
|
||||
selectiveLogic?: number;
|
||||
position?: number;
|
||||
depth?: number;
|
||||
order?: number;
|
||||
probability?: number;
|
||||
scanDepth?: number;
|
||||
groupId?: string;
|
||||
}
|
||||
|
||||
// 创建世界书
|
||||
export const createWorldbook = (data: CreateWorldbookRequest) => {
|
||||
return client.post<Worldbook>('/app/worldbook', data);
|
||||
};
|
||||
|
||||
// 获取世界书列表
|
||||
export const getWorldbookList = (params: {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
keyword?: string;
|
||||
}) => {
|
||||
return client.get<{
|
||||
list: Worldbook[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}>('/app/worldbook', { params });
|
||||
};
|
||||
|
||||
// 获取世界书详情
|
||||
export const getWorldbookById = (id: number) => {
|
||||
return client.get<Worldbook>(`/app/worldbook/${id}`);
|
||||
};
|
||||
|
||||
// 更新世界书
|
||||
export const updateWorldbook = (id: number, data: UpdateWorldbookRequest) => {
|
||||
return client.put(`/app/worldbook/${id}`, data);
|
||||
};
|
||||
|
||||
// 删除世界书
|
||||
export const deleteWorldbook = (id: number) => {
|
||||
return client.delete(`/app/worldbook/${id}`);
|
||||
};
|
||||
|
||||
// 导入世界书
|
||||
export const importWorldbook = (file: File) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
return client.post<Worldbook>('/app/worldbook/import', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 导出世界书
|
||||
export const exportWorldbook = (id: number) => {
|
||||
return client.get(`/app/worldbook/${id}/export`, {
|
||||
responseType: 'blob',
|
||||
});
|
||||
};
|
||||
|
||||
// 创建条目
|
||||
export const createEntry = (worldbookId: number, data: CreateEntryRequest) => {
|
||||
return client.post<WorldbookEntry>(`/app/worldbook/${worldbookId}/entry`, data);
|
||||
};
|
||||
|
||||
// 获取条目列表
|
||||
export const getEntryList = (worldbookId: number) => {
|
||||
return client.get<{
|
||||
list: WorldbookEntry[];
|
||||
total: number;
|
||||
}>(`/app/worldbook/${worldbookId}/entries`);
|
||||
};
|
||||
|
||||
// 更新条目
|
||||
export const updateEntry = (worldbookId: number, entryId: number, data: UpdateEntryRequest) => {
|
||||
return client.put(`/app/worldbook/${worldbookId}/entry/${entryId}`, data);
|
||||
};
|
||||
|
||||
// 删除条目
|
||||
export const deleteEntry = (worldbookId: number, entryId: number) => {
|
||||
return client.delete(`/app/worldbook/${worldbookId}/entry/${entryId}`);
|
||||
};
|
||||
613
web-app/src/pages/WorldbookManagePage.tsx
Normal file
613
web-app/src/pages/WorldbookManagePage.tsx
Normal file
@@ -0,0 +1,613 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
getWorldbookList,
|
||||
createWorldbook,
|
||||
updateWorldbook,
|
||||
deleteWorldbook,
|
||||
importWorldbook,
|
||||
exportWorldbook,
|
||||
getEntryList,
|
||||
createEntry,
|
||||
updateEntry,
|
||||
deleteEntry,
|
||||
type Worldbook,
|
||||
type WorldbookEntry,
|
||||
type CreateWorldbookRequest,
|
||||
type CreateEntryRequest,
|
||||
} from '../api/worldbook';
|
||||
|
||||
const WorldbookManagePage: React.FC = () => {
|
||||
const [worldbooks, setWorldbooks] = useState<Worldbook[]>([]);
|
||||
const [selectedWorldbook, setSelectedWorldbook] = useState<Worldbook | null>(null);
|
||||
const [entries, setEntries] = useState<WorldbookEntry[]>([]);
|
||||
const [selectedEntry, setSelectedEntry] = useState<WorldbookEntry | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||
const [showEntryModal, setShowEntryModal] = useState(false);
|
||||
const [searchKeyword, setSearchKeyword] = useState('');
|
||||
|
||||
// 表单状态
|
||||
const [worldbookForm, setWorldbookForm] = useState<CreateWorldbookRequest>({
|
||||
name: '',
|
||||
description: '',
|
||||
isPublic: false,
|
||||
});
|
||||
|
||||
const [entryForm, setEntryForm] = useState<CreateEntryRequest>({
|
||||
comment: '',
|
||||
content: '',
|
||||
keys: [],
|
||||
secondaryKeys: [],
|
||||
constant: false,
|
||||
enabled: true,
|
||||
useRegex: false,
|
||||
caseSensitive: false,
|
||||
matchWholeWords: false,
|
||||
selective: false,
|
||||
selectiveLogic: 0,
|
||||
position: 1,
|
||||
depth: 4,
|
||||
order: 100,
|
||||
probability: 100,
|
||||
scanDepth: 2,
|
||||
groupId: '',
|
||||
});
|
||||
|
||||
const [keyInput, setKeyInput] = useState('');
|
||||
const [secondaryKeyInput, setSecondaryKeyInput] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
loadWorldbooks();
|
||||
}, [searchKeyword]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedWorldbook) {
|
||||
loadEntries(selectedWorldbook.id);
|
||||
}
|
||||
}, [selectedWorldbook]);
|
||||
|
||||
const loadWorldbooks = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await getWorldbookList({
|
||||
page: 1,
|
||||
pageSize: 100,
|
||||
keyword: searchKeyword,
|
||||
});
|
||||
setWorldbooks(response.data.list || []);
|
||||
} catch (error: any) {
|
||||
alert('加载世界书列表失败: ' + (error.response?.data?.msg || error.message));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const loadEntries = async (worldbookId: number) => {
|
||||
try {
|
||||
const response = await getEntryList(worldbookId);
|
||||
setEntries(response.data.list || []);
|
||||
} catch (error: any) {
|
||||
alert('加载条目列表失败: ' + (error.response?.data?.msg || error.message));
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateWorldbook = async () => {
|
||||
if (!worldbookForm.name.trim()) {
|
||||
alert('请输入世界书名称');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await createWorldbook(worldbookForm);
|
||||
alert('创建成功');
|
||||
setShowCreateModal(false);
|
||||
setWorldbookForm({ name: '', description: '', isPublic: false });
|
||||
loadWorldbooks();
|
||||
} catch (error: any) {
|
||||
alert('创建失败: ' + (error.response?.data?.msg || error.message));
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteWorldbook = async (id: number) => {
|
||||
if (!confirm('确定要删除这个世界书吗?所有条目也会被删除。')) return;
|
||||
try {
|
||||
await deleteWorldbook(id);
|
||||
alert('删除成功');
|
||||
if (selectedWorldbook?.id === id) {
|
||||
setSelectedWorldbook(null);
|
||||
setEntries([]);
|
||||
}
|
||||
loadWorldbooks();
|
||||
} catch (error: any) {
|
||||
alert('删除失败: ' + (error.response?.data?.msg || error.message));
|
||||
}
|
||||
};
|
||||
|
||||
const handleImport = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
try {
|
||||
setLoading(true);
|
||||
await importWorldbook(file);
|
||||
alert('导入成功');
|
||||
loadWorldbooks();
|
||||
} catch (error: any) {
|
||||
alert('导入失败: ' + (error.response?.data?.msg || error.message));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
e.target.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
const handleExport = async (id: number) => {
|
||||
try {
|
||||
const response = await exportWorldbook(id);
|
||||
const blob = new Blob([response.data], { type: 'application/json' });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `worldbook_${id}.json`;
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
} catch (error: any) {
|
||||
alert('导出失败: ' + (error.response?.data?.msg || error.message));
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveEntry = async () => {
|
||||
if (!selectedWorldbook) return;
|
||||
if (!entryForm.content.trim()) {
|
||||
alert('请输入条目内容');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (selectedEntry) {
|
||||
await updateEntry(selectedWorldbook.id, selectedEntry.id, entryForm);
|
||||
alert('更新成功');
|
||||
} else {
|
||||
await createEntry(selectedWorldbook.id, entryForm);
|
||||
alert('创建成功');
|
||||
}
|
||||
setShowEntryModal(false);
|
||||
resetEntryForm();
|
||||
loadEntries(selectedWorldbook.id);
|
||||
} catch (error: any) {
|
||||
alert('保存失败: ' + (error.response?.data?.msg || error.message));
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteEntry = async (entryId: number) => {
|
||||
if (!selectedWorldbook) return;
|
||||
if (!confirm('确定要删除这个条目吗?')) return;
|
||||
try {
|
||||
await deleteEntry(selectedWorldbook.id, entryId);
|
||||
alert('删除成功');
|
||||
loadEntries(selectedWorldbook.id);
|
||||
} catch (error: any) {
|
||||
alert('删除失败: ' + (error.response?.data?.msg || error.message));
|
||||
}
|
||||
};
|
||||
|
||||
const openEntryModal = (entry?: WorldbookEntry) => {
|
||||
if (entry) {
|
||||
setSelectedEntry(entry);
|
||||
setEntryForm({
|
||||
comment: entry.comment,
|
||||
content: entry.content,
|
||||
keys: entry.keys,
|
||||
secondaryKeys: entry.secondaryKeys,
|
||||
constant: entry.constant,
|
||||
enabled: entry.enabled,
|
||||
useRegex: entry.useRegex,
|
||||
caseSensitive: entry.caseSensitive,
|
||||
matchWholeWords: entry.matchWholeWords,
|
||||
selective: entry.selective,
|
||||
selectiveLogic: entry.selectiveLogic,
|
||||
position: entry.position,
|
||||
depth: entry.depth,
|
||||
order: entry.order,
|
||||
probability: entry.probability,
|
||||
scanDepth: entry.scanDepth,
|
||||
groupId: entry.groupId,
|
||||
});
|
||||
} else {
|
||||
resetEntryForm();
|
||||
}
|
||||
setShowEntryModal(true);
|
||||
};
|
||||
|
||||
const resetEntryForm = () => {
|
||||
setSelectedEntry(null);
|
||||
setEntryForm({
|
||||
comment: '',
|
||||
content: '',
|
||||
keys: [],
|
||||
secondaryKeys: [],
|
||||
constant: false,
|
||||
enabled: true,
|
||||
useRegex: false,
|
||||
caseSensitive: false,
|
||||
matchWholeWords: false,
|
||||
selective: false,
|
||||
selectiveLogic: 0,
|
||||
position: 1,
|
||||
depth: 4,
|
||||
order: 100,
|
||||
probability: 100,
|
||||
scanDepth: 2,
|
||||
groupId: '',
|
||||
});
|
||||
setKeyInput('');
|
||||
setSecondaryKeyInput('');
|
||||
};
|
||||
|
||||
const addKey = () => {
|
||||
if (keyInput.trim()) {
|
||||
setEntryForm({
|
||||
...entryForm,
|
||||
keys: [...(entryForm.keys || []), keyInput.trim()],
|
||||
});
|
||||
setKeyInput('');
|
||||
}
|
||||
};
|
||||
|
||||
const removeKey = (index: number) => {
|
||||
setEntryForm({
|
||||
...entryForm,
|
||||
keys: entryForm.keys?.filter((_, i) => i !== index),
|
||||
});
|
||||
};
|
||||
|
||||
const addSecondaryKey = () => {
|
||||
if (secondaryKeyInput.trim()) {
|
||||
setEntryForm({
|
||||
...entryForm,
|
||||
secondaryKeys: [...(entryForm.secondaryKeys || []), secondaryKeyInput.trim()],
|
||||
});
|
||||
setSecondaryKeyInput('');
|
||||
}
|
||||
};
|
||||
|
||||
const removeSecondaryKey = (index: number) => {
|
||||
setEntryForm({
|
||||
...entryForm,
|
||||
secondaryKeys: entryForm.secondaryKeys?.filter((_, i) => i !== index),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-purple-900 to-slate-900 p-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h1 className="text-3xl font-bold text-white">世界书管理</h1>
|
||||
<div className="flex gap-3">
|
||||
<label className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg cursor-pointer transition-colors">
|
||||
导入世界书
|
||||
<input type="file" accept=".json" onChange={handleImport} className="hidden" />
|
||||
</label>
|
||||
<button
|
||||
onClick={() => setShowCreateModal(true)}
|
||||
className="px-4 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-lg transition-colors"
|
||||
>
|
||||
创建世界书
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* 世界书列表 */}
|
||||
<div className="lg:col-span-1 bg-white/10 backdrop-blur-md rounded-xl p-4">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="搜索世界书..."
|
||||
value={searchKeyword}
|
||||
onChange={(e) => setSearchKeyword(e.target.value)}
|
||||
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/50 mb-4"
|
||||
/>
|
||||
<div className="space-y-2 max-h-[calc(100vh-250px)] overflow-y-auto">
|
||||
{worldbooks.map((wb) => (
|
||||
<div
|
||||
key={wb.id}
|
||||
onClick={() => setSelectedWorldbook(wb)}
|
||||
className={`p-3 rounded-lg cursor-pointer transition-colors ${
|
||||
selectedWorldbook?.id === wb.id
|
||||
? 'bg-purple-600 text-white'
|
||||
: 'bg-white/5 hover:bg-white/10 text-white/80'
|
||||
}`}
|
||||
>
|
||||
<div className="font-medium">{wb.name}</div>
|
||||
<div className="text-sm opacity-70">{wb.entryCount} 条目</div>
|
||||
<div className="flex gap-2 mt-2">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleExport(wb.id);
|
||||
}}
|
||||
className="text-xs px-2 py-1 bg-blue-500 hover:bg-blue-600 rounded"
|
||||
>
|
||||
导出
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDeleteWorldbook(wb.id);
|
||||
}}
|
||||
className="text-xs px-2 py-1 bg-red-500 hover:bg-red-600 rounded"
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 条目列表 */}
|
||||
<div className="lg:col-span-2 bg-white/10 backdrop-blur-md rounded-xl p-4">
|
||||
{selectedWorldbook ? (
|
||||
<>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-xl font-bold text-white">{selectedWorldbook.name}</h2>
|
||||
<button
|
||||
onClick={() => openEntryModal()}
|
||||
className="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg transition-colors"
|
||||
>
|
||||
添加条目
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-3 max-h-[calc(100vh-250px)] overflow-y-auto">
|
||||
{entries.map((entry) => (
|
||||
<div key={entry.id} className="bg-white/5 rounded-lg p-4">
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-white">
|
||||
{entry.comment || '未命名条目'}
|
||||
</div>
|
||||
<div className="text-sm text-white/60 mt-1">
|
||||
关键词: {entry.keys.join(', ') || '无'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => openEntryModal(entry)}
|
||||
className="text-xs px-3 py-1 bg-blue-500 hover:bg-blue-600 text-white rounded"
|
||||
>
|
||||
编辑
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDeleteEntry(entry.id)}
|
||||
className="text-xs px-3 py-1 bg-red-500 hover:bg-red-600 text-white rounded"
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm text-white/70 line-clamp-2">{entry.content}</div>
|
||||
<div className="flex gap-2 mt-2 text-xs text-white/50">
|
||||
{entry.constant && <span className="px-2 py-1 bg-yellow-500/20 rounded">常驻</span>}
|
||||
{!entry.enabled && <span className="px-2 py-1 bg-red-500/20 rounded">已禁用</span>}
|
||||
{entry.useRegex && <span className="px-2 py-1 bg-blue-500/20 rounded">正则</span>}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-full text-white/50">
|
||||
请选择一个世界书
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 创建世界书模态框 */}
|
||||
{showCreateModal && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||
<div className="bg-slate-800 rounded-xl p-6 w-full max-w-md">
|
||||
<h2 className="text-xl font-bold text-white mb-4">创建世界书</h2>
|
||||
<div className="space-y-4">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="世界书名称"
|
||||
value={worldbookForm.name}
|
||||
onChange={(e) => setWorldbookForm({ ...worldbookForm, name: e.target.value })}
|
||||
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white"
|
||||
/>
|
||||
<textarea
|
||||
placeholder="描述(可选)"
|
||||
value={worldbookForm.description}
|
||||
onChange={(e) => setWorldbookForm({ ...worldbookForm, description: e.target.value })}
|
||||
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white h-24"
|
||||
/>
|
||||
<label className="flex items-center text-white">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={worldbookForm.isPublic}
|
||||
onChange={(e) => setWorldbookForm({ ...worldbookForm, isPublic: e.target.checked })}
|
||||
className="mr-2"
|
||||
/>
|
||||
公开世界书
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex gap-3 mt-6">
|
||||
<button
|
||||
onClick={handleCreateWorldbook}
|
||||
className="flex-1 px-4 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-lg"
|
||||
>
|
||||
创建
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowCreateModal(false)}
|
||||
className="flex-1 px-4 py-2 bg-gray-600 hover:bg-gray-700 text-white rounded-lg"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 条目编辑模态框 */}
|
||||
{showEntryModal && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-slate-800 rounded-xl p-6 w-full max-w-3xl max-h-[90vh] overflow-y-auto">
|
||||
<h2 className="text-xl font-bold text-white mb-4">
|
||||
{selectedEntry ? '编辑条目' : '创建条目'}
|
||||
</h2>
|
||||
<div className="space-y-4">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="条目标题/备注"
|
||||
value={entryForm.comment}
|
||||
onChange={(e) => setEntryForm({ ...entryForm, comment: e.target.value })}
|
||||
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white"
|
||||
/>
|
||||
<textarea
|
||||
placeholder="条目内容 *"
|
||||
value={entryForm.content}
|
||||
onChange={(e) => setEntryForm({ ...entryForm, content: e.target.value })}
|
||||
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white h-32"
|
||||
/>
|
||||
|
||||
{/* 关键词 */}
|
||||
<div>
|
||||
<label className="text-white text-sm mb-2 block">主关键词</label>
|
||||
<div className="flex gap-2 mb-2">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="输入关键词后按回车"
|
||||
value={keyInput}
|
||||
onChange={(e) => setKeyInput(e.target.value)}
|
||||
onKeyPress={(e) => e.key === 'Enter' && addKey()}
|
||||
className="flex-1 px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white"
|
||||
/>
|
||||
<button onClick={addKey} className="px-4 py-2 bg-blue-600 rounded-lg text-white">
|
||||
添加
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{entryForm.keys?.map((key, index) => (
|
||||
<span key={index} className="px-3 py-1 bg-blue-500/20 text-white rounded-full text-sm flex items-center gap-2">
|
||||
{key}
|
||||
<button onClick={() => removeKey(index)} className="text-red-400 hover:text-red-300">×</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 次要关键词 */}
|
||||
<div>
|
||||
<label className="text-white text-sm mb-2 block">次要关键词(可选)</label>
|
||||
<div className="flex gap-2 mb-2">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="输入次要关键词后按回车"
|
||||
value={secondaryKeyInput}
|
||||
onChange={(e) => setSecondaryKeyInput(e.target.value)}
|
||||
onKeyPress={(e) => e.key === 'Enter' && addSecondaryKey()}
|
||||
className="flex-1 px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white"
|
||||
/>
|
||||
<button onClick={addSecondaryKey} className="px-4 py-2 bg-blue-600 rounded-lg text-white">
|
||||
添加
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{entryForm.secondaryKeys?.map((key, index) => (
|
||||
<span key={index} className="px-3 py-1 bg-purple-500/20 text-white rounded-full text-sm flex items-center gap-2">
|
||||
{key}
|
||||
<button onClick={() => removeSecondaryKey(index)} className="text-red-400 hover:text-red-300">×</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 选项 */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<label className="flex items-center text-white">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={entryForm.constant}
|
||||
onChange={(e) => setEntryForm({ ...entryForm, constant: e.target.checked })}
|
||||
className="mr-2"
|
||||
/>
|
||||
常驻注入
|
||||
</label>
|
||||
<label className="flex items-center text-white">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={entryForm.enabled}
|
||||
onChange={(e) => setEntryForm({ ...entryForm, enabled: e.target.checked })}
|
||||
className="mr-2"
|
||||
/>
|
||||
启用
|
||||
</label>
|
||||
<label className="flex items-center text-white">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={entryForm.useRegex}
|
||||
onChange={(e) => setEntryForm({ ...entryForm, useRegex: e.target.checked })}
|
||||
className="mr-2"
|
||||
/>
|
||||
使用正则
|
||||
</label>
|
||||
<label className="flex items-center text-white">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={entryForm.caseSensitive}
|
||||
onChange={(e) => setEntryForm({ ...entryForm, caseSensitive: e.target.checked })}
|
||||
className="mr-2"
|
||||
/>
|
||||
区分大小写
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* 高级选项 */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="text-white text-sm mb-1 block">触发概率 (%)</label>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
max="100"
|
||||
value={entryForm.probability}
|
||||
onChange={(e) => setEntryForm({ ...entryForm, probability: parseInt(e.target.value) })}
|
||||
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-white text-sm mb-1 block">扫描深度</label>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
value={entryForm.scanDepth}
|
||||
onChange={(e) => setEntryForm({ ...entryForm, scanDepth: parseInt(e.target.value) })}
|
||||
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-3 mt-6">
|
||||
<button
|
||||
onClick={handleSaveEntry}
|
||||
className="flex-1 px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg"
|
||||
>
|
||||
保存
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowEntryModal(false);
|
||||
resetEntryForm();
|
||||
}}
|
||||
className="flex-1 px-4 py-2 bg-gray-600 hover:bg-gray-700 text-white rounded-lg"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WorldbookManagePage;
|
||||
Reference in New Issue
Block a user