| <template> |
| <BaseFolderItemSelector |
| :model-value="modelValue" |
| @update:model-value="handleUpdate" |
| :folder-tree="folderTree" |
| :items="currentPersonas as any" |
| :tree-loading="treeLoading" |
| :items-loading="itemsLoading" |
| :labels="labels" |
| :show-create-button="true" |
| :show-edit-button="true" |
| :default-item="defaultPersona" |
| item-id-field="persona_id" |
| item-name-field="persona_id" |
| item-description-field="system_prompt" |
| :display-value-formatter="formatDisplayValue" |
| @navigate="handleNavigate" |
| @create="openCreatePersona" |
| @edit="openEditPersona" |
| /> |
|
|
| <!-- 创建/编辑人格对话框 --> |
| <PersonaForm |
| v-model="showPersonaDialog" |
| :editing-persona="editingPersona ?? undefined" |
| :current-folder-id="currentFolderId ?? undefined" |
| :current-folder-name="currentFolderName ?? undefined" |
| @saved="handlePersonaSaved" |
| @error="handleError" /> |
| </template> |
|
|
| <script setup lang="ts"> |
| import { ref, computed, onMounted } from 'vue' |
| import axios from 'axios' |
| import BaseFolderItemSelector from '@/components/folder/BaseFolderItemSelector.vue' |
| import PersonaForm from './PersonaForm.vue' |
| import { useI18n, useModuleI18n } from '@/i18n/composables' |
| import type { FolderTreeNode, SelectableItem } from '@/components/folder/types' |
|
|
| interface Persona { |
| persona_id: string |
| system_prompt: string |
| custom_error_message?: string | null |
| folder_id?: string | null |
| [key: string]: any |
| } |
|
|
| const props = defineProps({ |
| modelValue: { |
| type: String, |
| default: '' |
| }, |
| buttonText: { |
| type: String, |
| default: '' |
| } |
| }) |
|
|
| const emit = defineEmits(['update:modelValue']) |
| const { t } = useI18n() |
| const { tm } = useModuleI18n('core.shared') |
|
|
| // 状态 |
| const folderTree = ref<FolderTreeNode[]>([]) |
| const currentPersonas = ref<Persona[]>([]) |
| const treeLoading = ref(false) |
| const itemsLoading = ref(false) |
| const showPersonaDialog = ref(false) |
| const editingPersona = ref<Persona | null>(null) |
| const currentFolderId = ref<string | null>(null) |
|
|
| // 默认人格 |
| const defaultPersona: SelectableItem = { |
| id: 'default', |
| persona_id: 'default', |
| name: tm('personaSelector.defaultPersona'), |
| system_prompt: 'You are a helpful and friendly assistant.' |
| } |
|
|
| // 递归查找文件夹名称 |
| function findFolderName(nodes: FolderTreeNode[], folderId: string): string | null { |
| for (const node of nodes) { |
| if (node.folder_id === folderId) { |
| return node.name |
| } |
| if (node.children && node.children.length > 0) { |
| const found = findFolderName(node.children, folderId) |
| if (found) return found |
| } |
| } |
| return null |
| } |
|
|
| // 当前文件夹名称 |
| const currentFolderName = computed(() => { |
| if (!currentFolderId.value) { |
| return null // 根目录,PersonaForm 会使用 tm('form.rootFolder') |
| } |
| return findFolderName(folderTree.value, currentFolderId.value) |
| }) |
|
|
| // 标签配置 |
| const labels = computed(() => ({ |
| dialogTitle: tm('personaSelector.dialogTitle'), |
| notSelected: tm('personaSelector.notSelected'), |
| buttonText: props.buttonText || tm('personaSelector.buttonText'), |
| noItems: tm('personaSelector.noPersonas'), |
| defaultItem: tm('personaSelector.defaultPersona'), |
| noDescription: tm('personaSelector.noDescription'), |
| createButton: tm('personaSelector.createPersona'), |
| editButton: tm('personaSelector.editPersona') || 'Edit', |
| confirmButton: t('core.common.confirm'), |
| cancelButton: t('core.common.cancel'), |
| rootFolder: tm('personaSelector.rootFolder') || '全部人格', |
| emptyFolder: tm('personaSelector.emptyFolder') || '此文件夹为空' |
| })) |
|
|
| // 格式化显示值 |
| function formatDisplayValue(value: string): string { |
| if (value === 'default') { |
| return tm('personaSelector.defaultPersona') |
| } |
| return value |
| } |
|
|
| // 处理值更新 |
| function handleUpdate(value: string) { |
| emit('update:modelValue', value) |
| } |
|
|
| // 加载文件夹树 |
| async function loadFolderTree() { |
| treeLoading.value = true |
| try { |
| const response = await axios.get('/api/persona/folder/tree') |
| if (response.data.status === 'ok') { |
| folderTree.value = response.data.data || [] |
| } |
| } catch (error) { |
| console.error('加载文件夹树失败:', error) |
| folderTree.value = [] |
| } finally { |
| treeLoading.value = false |
| } |
| } |
|
|
| // 加载指定文件夹的人格 |
| async function loadPersonasInFolder(folderId: string | null) { |
| itemsLoading.value = true |
| try { |
| // 使用 /api/persona/list 端点,通过 folder_id 参数筛选 |
| const params = new URLSearchParams() |
| if (folderId !== null) { |
| params.set('folder_id', folderId) |
| } else { |
| // 根目录:folder_id 为空字符串表示获取根目录下的人格 |
| params.set('folder_id', '') |
| } |
| const response = await axios.get(`/api/persona/list?${params.toString()}`) |
| if (response.data.status === 'ok') { |
| currentPersonas.value = response.data.data || [] |
| } |
| } catch (error) { |
| console.error('加载人格列表失败:', error) |
| currentPersonas.value = [] |
| } finally { |
| itemsLoading.value = false |
| } |
| } |
|
|
| // 处理文件夹导航 |
| async function handleNavigate(folderId: string | null) { |
| currentFolderId.value = folderId |
| await loadPersonasInFolder(folderId) |
| } |
|
|
| // 打开创建人格对话框 |
| function openCreatePersona() { |
| editingPersona.value = null |
| showPersonaDialog.value = true |
| } |
|
|
| // 打开编辑人格对话框 |
| function openEditPersona(persona: Persona) { |
| editingPersona.value = persona |
| showPersonaDialog.value = true |
| } |
|
|
| // 人格保存成功(创建或编辑) |
| async function handlePersonaSaved(message: string) { |
| console.log('人格保存成功:', message) |
| const savedPersonaId = editingPersona.value?.persona_id || '' |
| showPersonaDialog.value = false |
| editingPersona.value = null |
| // 刷新当前文件夹的人格列表 |
| await loadPersonasInFolder(currentFolderId.value) |
| window.dispatchEvent( |
| new CustomEvent('astrbot:persona-saved', { |
| detail: { persona_id: savedPersonaId } |
| }) |
| ) |
| } |
|
|
| // 错误处理 |
| function handleError(error: string) { |
| console.error('创建人格失败:', error) |
| } |
|
|
| // 初始化加载文件夹树 |
| onMounted(() => { |
| loadFolderTree() |
| }) |
| </script> |
|
|
| <style scoped> |
| /* 样式继承自 BaseFolderItemSelector */ |
| </style> |
|
|