✨ 新增世界书模块
This commit is contained in:
158
web-app-vue/src/api/worldInfo.ts
Normal file
158
web-app-vue/src/api/worldInfo.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
import request from '@/utils/request'
|
||||
import type {
|
||||
WorldBook,
|
||||
WorldBookListResponse,
|
||||
WorldBookListParams,
|
||||
CreateWorldBookRequest,
|
||||
UpdateWorldBookRequest,
|
||||
WorldInfoEntry,
|
||||
CreateWorldEntryRequest,
|
||||
UpdateWorldEntryRequest,
|
||||
DeleteWorldEntryRequest,
|
||||
LinkCharacterRequest,
|
||||
WorldBookExportData,
|
||||
MatchWorldInfoRequest,
|
||||
MatchWorldInfoResponse
|
||||
} from '@/types/worldInfo'
|
||||
import type { ApiResponse } from '@/types/api'
|
||||
|
||||
// ========== 世界书管理 ==========
|
||||
|
||||
/**
|
||||
* 创建世界书
|
||||
*/
|
||||
export const createWorldBook = (data: CreateWorldBookRequest) => {
|
||||
return request.post<ApiResponse<WorldBook>>('/app/worldbook', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新世界书
|
||||
*/
|
||||
export const updateWorldBook = (id: number, data: UpdateWorldBookRequest) => {
|
||||
return request.put<ApiResponse<void>>(`/app/worldbook/${id}`, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除世界书
|
||||
*/
|
||||
export const deleteWorldBook = (id: number) => {
|
||||
return request.delete<ApiResponse<void>>(`/app/worldbook/${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取世界书详情
|
||||
*/
|
||||
export const getWorldBook = (id: number) => {
|
||||
return request.get<ApiResponse<WorldBook>>(`/app/worldbook/${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取世界书列表
|
||||
*/
|
||||
export const getWorldBookList = (params: WorldBookListParams) => {
|
||||
return request.get<ApiResponse<WorldBookListResponse>>('/app/worldbook/list', { params })
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制世界书
|
||||
*/
|
||||
export const duplicateWorldBook = (id: number) => {
|
||||
return request.post<ApiResponse<WorldBook>>(`/app/worldbook/${id}/duplicate`)
|
||||
}
|
||||
|
||||
// ========== 条目管理 ==========
|
||||
|
||||
/**
|
||||
* 创建世界书条目
|
||||
*/
|
||||
export const createWorldEntry = (data: CreateWorldEntryRequest) => {
|
||||
return request.post<ApiResponse<void>>('/app/worldbook/entry', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新世界书条目
|
||||
*/
|
||||
export const updateWorldEntry = (data: UpdateWorldEntryRequest) => {
|
||||
return request.put<ApiResponse<void>>('/app/worldbook/entry', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除世界书条目
|
||||
*/
|
||||
export const deleteWorldEntry = (data: DeleteWorldEntryRequest) => {
|
||||
return request.delete<ApiResponse<void>>('/app/worldbook/entry', { data })
|
||||
}
|
||||
|
||||
// ========== 关联管理 ==========
|
||||
|
||||
/**
|
||||
* 关联角色到世界书
|
||||
*/
|
||||
export const linkCharactersToWorldBook = (data: LinkCharacterRequest) => {
|
||||
return request.post<ApiResponse<void>>('/app/worldbook/link', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取角色关联的世界书列表
|
||||
*/
|
||||
export const getCharacterWorldBooks = (characterId: number) => {
|
||||
return request.get<ApiResponse<WorldBook[]>>(`/app/worldbook/character/${characterId}`)
|
||||
}
|
||||
|
||||
// ========== 导入导出 ==========
|
||||
|
||||
/**
|
||||
* 导入世界书
|
||||
*/
|
||||
export const importWorldBook = (file: File, bookName?: string) => {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
if (bookName) {
|
||||
formData.append('bookName', bookName)
|
||||
}
|
||||
|
||||
return request.post<ApiResponse<WorldBook>>('/app/worldbook/import', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出世界书(JSON)
|
||||
*/
|
||||
export const exportWorldBook = (id: number) => {
|
||||
return request.get<WorldBookExportData>(`/app/worldbook/${id}/export`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载世界书 JSON 文件
|
||||
*/
|
||||
export const downloadWorldBookJSON = async (id: number, bookName: string) => {
|
||||
try {
|
||||
const response = await exportWorldBook(id)
|
||||
|
||||
// 创建 Blob 并下载
|
||||
const blob = new Blob([JSON.stringify(response, null, 2)], { type: 'application/json' })
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = `${bookName}.json`
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
window.URL.revokeObjectURL(url)
|
||||
} catch (error) {
|
||||
console.error('导出世界书失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 匹配引擎 ==========
|
||||
|
||||
/**
|
||||
* 匹配世界书条目(用于聊天)
|
||||
*/
|
||||
export const matchWorldInfo = (data: MatchWorldInfoRequest) => {
|
||||
return request.post<ApiResponse<MatchWorldInfoResponse>>('/app/worldbook/match', data)
|
||||
}
|
||||
298
web-app-vue/src/stores/worldInfo.ts
Normal file
298
web-app-vue/src/stores/worldInfo.ts
Normal file
@@ -0,0 +1,298 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import type {
|
||||
WorldBook,
|
||||
WorldBookListParams,
|
||||
CreateWorldBookRequest,
|
||||
UpdateWorldBookRequest,
|
||||
WorldInfoEntry,
|
||||
MatchWorldInfoRequest,
|
||||
MatchedWorldInfoEntry
|
||||
} from '@/types/worldInfo'
|
||||
import * as worldInfoApi from '@/api/worldInfo'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
export const useWorldInfoStore = defineStore('worldInfo', () => {
|
||||
// 状态
|
||||
const worldBooks = ref<WorldBook[]>([])
|
||||
const currentWorldBook = ref<WorldBook | null>(null)
|
||||
const characterWorldBooks = ref<WorldBook[]>([])
|
||||
const loading = ref(false)
|
||||
const total = ref(0)
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(10)
|
||||
|
||||
// 获取世界书列表
|
||||
const fetchWorldBookList = async (params?: Partial<WorldBookListParams>) => {
|
||||
try {
|
||||
loading.value = true
|
||||
const requestParams: WorldBookListParams = {
|
||||
page: params?.page || currentPage.value,
|
||||
pageSize: params?.pageSize || pageSize.value,
|
||||
bookName: params?.bookName,
|
||||
isGlobal: params?.isGlobal,
|
||||
characterId: params?.characterId
|
||||
}
|
||||
|
||||
const response = await worldInfoApi.getWorldBookList(requestParams)
|
||||
worldBooks.value = response.data.data.list
|
||||
total.value = response.data.data.total
|
||||
currentPage.value = response.data.data.page
|
||||
pageSize.value = response.data.data.pageSize
|
||||
} catch (error: any) {
|
||||
console.error('获取世界书列表失败:', error)
|
||||
ElMessage.error(error.response?.data?.msg || '获取世界书列表失败')
|
||||
throw error
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取世界书详情
|
||||
const fetchWorldBookDetail = async (id: number) => {
|
||||
try {
|
||||
loading.value = true
|
||||
const response = await worldInfoApi.getWorldBook(id)
|
||||
currentWorldBook.value = response.data.data
|
||||
return response.data.data
|
||||
} catch (error: any) {
|
||||
console.error('获取世界书详情失败:', error)
|
||||
ElMessage.error(error.response?.data?.msg || '获取世界书详情失败')
|
||||
throw error
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 创建世界书
|
||||
const createWorldBook = async (data: CreateWorldBookRequest) => {
|
||||
try {
|
||||
loading.value = true
|
||||
const response = await worldInfoApi.createWorldBook(data)
|
||||
ElMessage.success('创建成功')
|
||||
await fetchWorldBookList()
|
||||
return response.data.data
|
||||
} catch (error: any) {
|
||||
console.error('创建世界书失败:', error)
|
||||
ElMessage.error(error.response?.data?.msg || '创建世界书失败')
|
||||
throw error
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 更新世界书
|
||||
const updateWorldBook = async (id: number, data: UpdateWorldBookRequest) => {
|
||||
try {
|
||||
loading.value = true
|
||||
await worldInfoApi.updateWorldBook(id, data)
|
||||
ElMessage.success('更新成功')
|
||||
await fetchWorldBookList()
|
||||
} catch (error: any) {
|
||||
console.error('更新世界书失败:', error)
|
||||
ElMessage.error(error.response?.data?.msg || '更新世界书失败')
|
||||
throw error
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 删除世界书
|
||||
const deleteWorldBook = async (id: number) => {
|
||||
try {
|
||||
loading.value = true
|
||||
await worldInfoApi.deleteWorldBook(id)
|
||||
ElMessage.success('删除成功')
|
||||
await fetchWorldBookList()
|
||||
} catch (error: any) {
|
||||
console.error('删除世界书失败:', error)
|
||||
ElMessage.error(error.response?.data?.msg || '删除世界书失败')
|
||||
throw error
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 复制世界书
|
||||
const duplicateWorldBook = async (id: number) => {
|
||||
try {
|
||||
loading.value = true
|
||||
const response = await worldInfoApi.duplicateWorldBook(id)
|
||||
ElMessage.success('复制成功')
|
||||
await fetchWorldBookList()
|
||||
return response.data.data
|
||||
} catch (error: any) {
|
||||
console.error('复制世界书失败:', error)
|
||||
ElMessage.error(error.response?.data?.msg || '复制世界书失败')
|
||||
throw error
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 添加条目
|
||||
const addEntry = async (bookId: number, entry: WorldInfoEntry) => {
|
||||
try {
|
||||
loading.value = true
|
||||
await worldInfoApi.createWorldEntry({ bookId, entry })
|
||||
ElMessage.success('添加条目成功')
|
||||
if (currentWorldBook.value?.id === bookId) {
|
||||
await fetchWorldBookDetail(bookId)
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('添加条目失败:', error)
|
||||
ElMessage.error(error.response?.data?.msg || '添加条目失败')
|
||||
throw error
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 更新条目
|
||||
const updateEntry = async (bookId: number, entry: WorldInfoEntry) => {
|
||||
try {
|
||||
loading.value = true
|
||||
await worldInfoApi.updateWorldEntry({ bookId, entry })
|
||||
ElMessage.success('更新条目成功')
|
||||
if (currentWorldBook.value?.id === bookId) {
|
||||
await fetchWorldBookDetail(bookId)
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('更新条目失败:', error)
|
||||
ElMessage.error(error.response?.data?.msg || '更新条目失败')
|
||||
throw error
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 删除条目
|
||||
const deleteEntry = async (bookId: number, entryId: string) => {
|
||||
try {
|
||||
loading.value = true
|
||||
await worldInfoApi.deleteWorldEntry({ bookId, entryId })
|
||||
ElMessage.success('删除条目成功')
|
||||
if (currentWorldBook.value?.id === bookId) {
|
||||
await fetchWorldBookDetail(bookId)
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('删除条目失败:', error)
|
||||
ElMessage.error(error.response?.data?.msg || '删除条目失败')
|
||||
throw error
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 关联角色
|
||||
const linkCharacters = async (bookId: number, characterIds: number[]) => {
|
||||
try {
|
||||
loading.value = true
|
||||
await worldInfoApi.linkCharactersToWorldBook({ bookId, characterIds })
|
||||
ElMessage.success('关联角色成功')
|
||||
if (currentWorldBook.value?.id === bookId) {
|
||||
await fetchWorldBookDetail(bookId)
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('关联角色失败:', error)
|
||||
ElMessage.error(error.response?.data?.msg || '关联角色失败')
|
||||
throw error
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取角色关联的世界书
|
||||
const fetchCharacterWorldBooks = async (characterId: number) => {
|
||||
try {
|
||||
loading.value = true
|
||||
const response = await worldInfoApi.getCharacterWorldBooks(characterId)
|
||||
characterWorldBooks.value = response.data.data
|
||||
return response.data.data
|
||||
} catch (error: any) {
|
||||
console.error('获取角色世界书失败:', error)
|
||||
ElMessage.error(error.response?.data?.msg || '获取角色世界书失败')
|
||||
throw error
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 导入世界书
|
||||
const importWorldBook = async (file: File, bookName?: string) => {
|
||||
try {
|
||||
loading.value = true
|
||||
const response = await worldInfoApi.importWorldBook(file, bookName)
|
||||
ElMessage.success('导入成功')
|
||||
await fetchWorldBookList()
|
||||
return response.data.data
|
||||
} catch (error: any) {
|
||||
console.error('导入世界书失败:', error)
|
||||
ElMessage.error(error.response?.data?.msg || '导入世界书失败')
|
||||
throw error
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 导出世界书
|
||||
const exportWorldBook = async (id: number, bookName: string) => {
|
||||
try {
|
||||
loading.value = true
|
||||
await worldInfoApi.downloadWorldBookJSON(id, bookName)
|
||||
ElMessage.success('导出成功')
|
||||
} catch (error: any) {
|
||||
console.error('导出世界书失败:', error)
|
||||
ElMessage.error(error.response?.data?.msg || '导出世界书失败')
|
||||
throw error
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 匹配世界书条目(用于聊天)
|
||||
const matchWorldInfo = async (params: MatchWorldInfoRequest): Promise<MatchedWorldInfoEntry[]> => {
|
||||
try {
|
||||
const response = await worldInfoApi.matchWorldInfo(params)
|
||||
return response.data.data.entries
|
||||
} catch (error: any) {
|
||||
console.error('匹配世界书失败:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
// 重置分页
|
||||
const resetPagination = () => {
|
||||
currentPage.value = 1
|
||||
pageSize.value = 10
|
||||
total.value = 0
|
||||
}
|
||||
|
||||
return {
|
||||
// 状态
|
||||
worldBooks,
|
||||
currentWorldBook,
|
||||
characterWorldBooks,
|
||||
loading,
|
||||
total,
|
||||
currentPage,
|
||||
pageSize,
|
||||
|
||||
// 方法
|
||||
fetchWorldBookList,
|
||||
fetchWorldBookDetail,
|
||||
createWorldBook,
|
||||
updateWorldBook,
|
||||
deleteWorldBook,
|
||||
duplicateWorldBook,
|
||||
addEntry,
|
||||
updateEntry,
|
||||
deleteEntry,
|
||||
linkCharacters,
|
||||
fetchCharacterWorldBooks,
|
||||
importWorldBook,
|
||||
exportWorldBook,
|
||||
matchWorldInfo,
|
||||
resetPagination
|
||||
}
|
||||
})
|
||||
127
web-app-vue/src/types/worldInfo.d.ts
vendored
Normal file
127
web-app-vue/src/types/worldInfo.d.ts
vendored
Normal file
@@ -0,0 +1,127 @@
|
||||
// 世界书条目
|
||||
export interface WorldInfoEntry {
|
||||
uid: string; // 条目唯一ID
|
||||
keys: string[]; // 主要关键词
|
||||
secondary_keys?: string[]; // 次要关键词
|
||||
content: string; // 条目内容
|
||||
comment?: string; // 备注
|
||||
enabled: boolean; // 是否启用
|
||||
constant?: boolean; // 永远激活
|
||||
selective?: boolean; // 选择性激活
|
||||
order: number; // 插入顺序
|
||||
position?: string; // 插入位置:before_char, after_char
|
||||
depth?: number; // 扫描深度
|
||||
probability?: number; // 激活概率(0-100)
|
||||
use_probability?: boolean; // 是否使用概率
|
||||
group?: string; // 分组
|
||||
group_override?: boolean; // 分组覆盖
|
||||
group_weight?: number; // 分组权重
|
||||
prevent_recursion?: boolean; // 防止递归激活
|
||||
delay_until_recursion?: boolean;// 延迟到递归时激活
|
||||
scan_depth?: number | null; // 扫描深度(null=使用全局设置)
|
||||
case_sensitive?: boolean | null;// 大小写敏感
|
||||
match_whole_words?: boolean | null; // 匹配整词
|
||||
use_regex?: boolean | null; // 使用正则表达式
|
||||
automation_id?: string; // 自动化ID
|
||||
role?: string; // 角色(system/user/assistant)
|
||||
vectorized?: string; // 向量化的内容ID
|
||||
extensions?: Record<string, any>; // 扩展数据
|
||||
}
|
||||
|
||||
// 世界书
|
||||
export interface WorldBook {
|
||||
id: number;
|
||||
userId: number;
|
||||
bookName: string;
|
||||
isGlobal: boolean;
|
||||
entries: WorldInfoEntry[];
|
||||
linkedChars: string[];
|
||||
entryCount: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
// 世界书列表响应
|
||||
export interface WorldBookListResponse {
|
||||
list: WorldBook[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
// 世界书列表查询参数
|
||||
export interface WorldBookListParams {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
bookName?: string;
|
||||
isGlobal?: boolean;
|
||||
characterId?: number;
|
||||
}
|
||||
|
||||
// 创建世界书请求
|
||||
export interface CreateWorldBookRequest {
|
||||
bookName: string;
|
||||
isGlobal: boolean;
|
||||
entries: WorldInfoEntry[];
|
||||
linkedChars?: string[];
|
||||
}
|
||||
|
||||
// 更新世界书请求
|
||||
export interface UpdateWorldBookRequest {
|
||||
bookName: string;
|
||||
isGlobal: boolean;
|
||||
entries: WorldInfoEntry[];
|
||||
linkedChars?: string[];
|
||||
}
|
||||
|
||||
// 创建条目请求
|
||||
export interface CreateWorldEntryRequest {
|
||||
bookId: number;
|
||||
entry: WorldInfoEntry;
|
||||
}
|
||||
|
||||
// 更新条目请求
|
||||
export interface UpdateWorldEntryRequest {
|
||||
bookId: number;
|
||||
entry: WorldInfoEntry;
|
||||
}
|
||||
|
||||
// 删除条目请求
|
||||
export interface DeleteWorldEntryRequest {
|
||||
bookId: number;
|
||||
entryId: string;
|
||||
}
|
||||
|
||||
// 关联角色请求
|
||||
export interface LinkCharacterRequest {
|
||||
bookId: number;
|
||||
characterIds: number[];
|
||||
}
|
||||
|
||||
// 世界书导出数据
|
||||
export interface WorldBookExportData {
|
||||
name: string;
|
||||
entries: WorldInfoEntry[];
|
||||
}
|
||||
|
||||
// 匹配的世界书条目
|
||||
export interface MatchedWorldInfoEntry {
|
||||
content: string;
|
||||
position: string;
|
||||
order: number;
|
||||
source: string;
|
||||
}
|
||||
|
||||
// 匹配世界书请求
|
||||
export interface MatchWorldInfoRequest {
|
||||
characterId: number;
|
||||
messages: string[];
|
||||
scanDepth?: number;
|
||||
maxTokens?: number;
|
||||
}
|
||||
|
||||
// 匹配世界书响应
|
||||
export interface MatchWorldInfoResponse {
|
||||
entries: MatchedWorldInfoEntry[];
|
||||
totalTokens: number;
|
||||
}
|
||||
305
web-app-vue/src/views/worldbook/WorldBookList.vue
Normal file
305
web-app-vue/src/views/worldbook/WorldBookList.vue
Normal file
@@ -0,0 +1,305 @@
|
||||
<template>
|
||||
<div class="world-book-list">
|
||||
<!-- 页面标题 -->
|
||||
<div class="page-header">
|
||||
<h2>世界书管理</h2>
|
||||
<div class="actions">
|
||||
<el-upload
|
||||
:show-file-list="false"
|
||||
:before-upload="handleImport"
|
||||
accept=".json"
|
||||
>
|
||||
<el-button type="success" :icon="Upload">导入世界书</el-button>
|
||||
</el-upload>
|
||||
<el-button type="primary" :icon="Plus" @click="handleCreate">
|
||||
创建世界书
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<el-card class="search-card">
|
||||
<el-form :inline="true" :model="searchForm" @submit.prevent="handleSearch">
|
||||
<el-form-item label="世界书名称">
|
||||
<el-input
|
||||
v-model="searchForm.bookName"
|
||||
placeholder="搜索世界书"
|
||||
clearable
|
||||
@clear="handleSearch"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="类型">
|
||||
<el-select
|
||||
v-model="searchForm.isGlobal"
|
||||
placeholder="全部"
|
||||
clearable
|
||||
@change="handleSearch"
|
||||
>
|
||||
<el-option label="全局" :value="true" />
|
||||
<el-option label="非全局" :value="false" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<!-- 世界书列表 -->
|
||||
<el-card class="list-card">
|
||||
<el-table
|
||||
v-loading="worldInfoStore.loading"
|
||||
:data="worldInfoStore.worldBooks"
|
||||
stripe
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="bookName" label="世界书名称" min-width="200" />
|
||||
<el-table-column label="类型" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.isGlobal ? 'success' : 'info'">
|
||||
{{ row.isGlobal ? '全局' : '非全局' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="条目数" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-badge :value="row.entryCount" :max="999">
|
||||
<el-icon><Document /></el-icon>
|
||||
</el-badge>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="关联角色" width="120">
|
||||
<template #default="{ row }">
|
||||
{{ row.linkedChars?.length || 0 }} 个
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="updatedAt" label="更新时间" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.updatedAt) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="280" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
:icon="Edit"
|
||||
@click="handleEdit(row)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
type="success"
|
||||
size="small"
|
||||
:icon="DocumentCopy"
|
||||
@click="handleDuplicate(row)"
|
||||
>
|
||||
复制
|
||||
</el-button>
|
||||
<el-button
|
||||
type="info"
|
||||
size="small"
|
||||
:icon="Download"
|
||||
@click="handleExport(row)"
|
||||
>
|
||||
导出
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
size="small"
|
||||
:icon="Delete"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="worldInfoStore.currentPage"
|
||||
v-model:page-size="worldInfoStore.pageSize"
|
||||
:total="worldInfoStore.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSearch"
|
||||
@current-change="handleSearch"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessageBox, ElMessage } from 'element-plus'
|
||||
import {
|
||||
Plus,
|
||||
Edit,
|
||||
Delete,
|
||||
Upload,
|
||||
Download,
|
||||
DocumentCopy,
|
||||
Document
|
||||
} from '@element-plus/icons-vue'
|
||||
import { useWorldInfoStore } from '@/stores/worldInfo'
|
||||
import type { WorldBook } from '@/types/worldInfo'
|
||||
|
||||
const router = useRouter()
|
||||
const worldInfoStore = useWorldInfoStore()
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = ref({
|
||||
bookName: '',
|
||||
isGlobal: undefined as boolean | undefined
|
||||
})
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (date: string) => {
|
||||
return new Date(date).toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
worldInfoStore.fetchWorldBookList({
|
||||
page: worldInfoStore.currentPage,
|
||||
pageSize: worldInfoStore.pageSize,
|
||||
bookName: searchForm.value.bookName,
|
||||
isGlobal: searchForm.value.isGlobal
|
||||
})
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
const handleReset = () => {
|
||||
searchForm.value = {
|
||||
bookName: '',
|
||||
isGlobal: undefined
|
||||
}
|
||||
worldInfoStore.resetPagination()
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
// 创建世界书
|
||||
const handleCreate = () => {
|
||||
router.push('/worldbook/create')
|
||||
}
|
||||
|
||||
// 编辑世界书
|
||||
const handleEdit = (row: WorldBook) => {
|
||||
router.push(`/worldbook/edit/${row.id}`)
|
||||
}
|
||||
|
||||
// 复制世界书
|
||||
const handleDuplicate = async (row: WorldBook) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要复制世界书"${row.bookName}"吗?`,
|
||||
'提示',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'info'
|
||||
}
|
||||
)
|
||||
await worldInfoStore.duplicateWorldBook(row.id)
|
||||
} catch (error) {
|
||||
// 用户取消操作
|
||||
}
|
||||
}
|
||||
|
||||
// 导出世界书
|
||||
const handleExport = async (row: WorldBook) => {
|
||||
try {
|
||||
await worldInfoStore.exportWorldBook(row.id, row.bookName)
|
||||
} catch (error) {
|
||||
console.error('导出失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 删除世界书
|
||||
const handleDelete = async (row: WorldBook) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除世界书"${row.bookName}"吗?此操作不可撤销!`,
|
||||
'警告',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
await worldInfoStore.deleteWorldBook(row.id)
|
||||
} catch (error) {
|
||||
// 用户取消操作
|
||||
}
|
||||
}
|
||||
|
||||
// 导入世界书
|
||||
const handleImport = async (file: File) => {
|
||||
// 验证文件类型
|
||||
if (!file.name.endsWith('.json')) {
|
||||
ElMessage.error('只支持 JSON 格式文件')
|
||||
return false
|
||||
}
|
||||
|
||||
// 验证文件大小(10MB)
|
||||
if (file.size > 10 * 1024 * 1024) {
|
||||
ElMessage.error('文件大小不能超过 10MB')
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
await worldInfoStore.importWorldBook(file)
|
||||
} catch (error) {
|
||||
console.error('导入失败:', error)
|
||||
}
|
||||
|
||||
return false // 阻止自动上传
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
handleSearch()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.world-book-list {
|
||||
padding: 20px;
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.search-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.list-card {
|
||||
.pagination {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user