refactor: Agents页面重构为独立模块

- 新增 web/src/views/agents/useAgents.ts: Agents页面的组合式函数
- 新增 web/src/views/agents/agents.css: Agents页面样式文件
- 精简 web/src/views/Agents.vue: 保留主入口,引用新的模块

将Agents页面的逻辑拆分为独立的TS文件和CSS文件,提升代码可维护性

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 08:32:08 +08:00
parent 2a9326ef5f
commit 4045dad903
3 changed files with 1015 additions and 966 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,370 @@
/* Skills Selector Styles */
.skills-selector {
position: relative;
}
/* Skills Mode Options */
.skills-mode-options {
padding: 8px;
border-bottom: 1px solid #2a2c36;
}
.skills-mode-item {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 12px;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
}
.skills-mode-item:hover {
background: #1a1c25;
}
.skills-mode-item.active {
background: rgba(249, 115, 22, 0.1);
}
.mode-radio {
width: 18px;
height: 18px;
border: 2px solid #6b7280;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
margin-top: 2px;
}
.skills-mode-item.active .mode-radio {
border-color: #f97316;
}
.radio-dot {
width: 10px;
height: 10px;
background: #f97316;
border-radius: 50%;
}
.mode-content {
display: flex;
flex-direction: column;
gap: 2px;
}
.mode-label {
font-size: 14px;
font-weight: 500;
color: #f3f4f6;
}
.mode-desc {
font-size: 12px;
color: #9ca3af;
}
/* 子下拉框箭头 */
.sub-arrow {
margin-left: auto;
color: #9ca3af;
transition: transform 0.2s ease;
}
.skills-mode-item:hover .sub-arrow {
color: #f97316;
}
/* 子下拉面板 */
.sub-dropdown-panel {
position: absolute;
top: 0;
left: 100%;
margin-left: 8px;
width: 280px;
background: #171922;
border: 1px solid #2a2c36;
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.4);
z-index: 100;
overflow: hidden;
}
.sub-dropdown-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
border-bottom: 1px solid #2a2c36;
background: #1a1c25;
}
.sub-dropdown-title {
font-size: 13px;
font-weight: 500;
color: #f3f4f6;
}
.sub-dropdown-close {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: 6px;
color: #9ca3af;
transition: all 0.2s ease;
}
.sub-dropdown-close:hover {
background: #374151;
color: #f3f4f6;
}
/* 无结果样式 */
.no-results {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 24px;
color: #6b7280;
gap: 8px;
}
.no-results-icon {
opacity: 0.5;
}
.selected-tags {
display: flex;
align-items: center;
justify-content: space-between;
min-height: 44px;
padding: 0 12px;
background: #171922;
border: 1px solid #2a2c36;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
}
.selected-tags:hover {
border-color: #f97316;
}
.selected-tags .placeholder {
color: #9ca3af;
font-size: 14px;
}
.tags-container {
display: flex;
flex-wrap: wrap;
gap: 6px;
flex: 1;
overflow: hidden;
}
.selected-tag {
display: inline-flex;
align-items: center;
padding: 2px 8px;
background: linear-gradient(135deg, #f97316 0%, #ea580c 100%);
color: white;
font-size: 12px;
font-weight: 500;
border-radius: 4px;
white-space: nowrap;
}
.more-tag {
display: inline-flex;
align-items: center;
padding: 2px 8px;
background: #374151;
color: #d1d5db;
font-size: 12px;
border-radius: 4px;
}
.dropdown-icon {
color: #9ca3af;
transition: transform 0.2s ease;
flex-shrink: 0;
}
.dropdown-icon.rotate-180 {
transform: rotate(180deg);
}
/* Dropdown Panel */
.dropdown-panel {
position: absolute;
top: calc(100% + 8px);
left: 0;
right: 0;
background: #171922;
border: 1px solid #2a2c36;
border-radius: 12px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4);
z-index: 100;
overflow: hidden;
}
/* Search Box */
.search-box {
position: relative;
padding: 12px;
border-bottom: 1px solid #2a2c36;
}
.search-icon {
position: absolute;
left: 24px;
top: 50%;
transform: translateY(-50%);
color: #9ca3af;
}
.search-input {
width: 100%;
padding: 8px 12px 8px 36px;
background: #111827;
border: 1px solid #374151;
border-radius: 6px;
color: white;
font-size: 14px;
outline: none;
transition: border-color 0.2s ease;
}
.search-input:focus {
border-color: #f97316;
}
.search-input::placeholder {
color: #6b7280;
}
/* Action Bar */
.action-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 12px;
background: #111827;
border-bottom: 1px solid #374151;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
}
.checkbox {
width: 16px;
height: 16px;
accent-color: #f97316;
cursor: pointer;
}
.checkbox-text {
font-size: 13px;
font-weight: 500;
color: #f97316;
}
.clear-btn {
padding: 4px 10px;
background: transparent;
border: 1px solid #4b5563;
border-radius: 4px;
color: #9ca3af;
font-size: 12px;
cursor: pointer;
transition: all 0.2s ease;
}
.clear-btn:hover {
background: #374151;
color: white;
}
/* Options List */
.options-list {
max-height: 240px;
overflow-y: auto;
padding: 8px;
}
.options-list::-webkit-scrollbar {
width: 6px;
}
.options-list::-webkit-scrollbar-track {
background: #171922;
}
.options-list::-webkit-scrollbar-thumb {
background: #4b5563;
border-radius: 3px;
}
.option-item {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 10px 12px;
border-radius: 8px;
cursor: pointer;
transition: background 0.15s ease;
}
.option-item:hover {
background: #374151;
}
.option-item .checkbox {
margin-top: 2px;
flex-shrink: 0;
}
.option-content {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
flex: 1;
}
.option-label {
font-size: 14px;
font-weight: 500;
color: white;
}
.option-desc {
font-size: 12px;
color: #9ca3af;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Dropdown Transition */
.dropdown-enter-active,
.dropdown-leave-active {
transition: all 0.2s ease;
}
.dropdown-enter-from,
.dropdown-leave-to {
opacity: 0;
transform: translateY(-8px);
}

View File

@@ -0,0 +1,591 @@
// Agent API 调用和状态管理
import { ref, computed } from 'vue'
import { ElMessage } from 'element-plus'
import { formatDate } from '@/utils/format'
const API_BASE = 'http://localhost:8082'
// 类型定义
export interface Skill {
id: string
skill_name: string
skill_type: string
skill_desc: string
path: string
status: string
}
export interface ModelOption {
id: string
name: string
provider: string
model: string
}
export interface Agent {
id: string
name: string
avatar: string
description: string
accentColor: string
gradient: string
status: string
skills: string
model: string
createdAt: string
}
export interface AgentFormData {
name: string
description: string
avatar: string
skillsMode: 'all' | 'include' | 'exclude'
selectedSkills: string[]
knowledge: string
prompt: string
modelId: string
}
// 状态
const agents = ref<Agent[]>([])
const skillsList = ref<Skill[]>([])
const modelsList = ref<ModelOption[]>([])
const searchQuery = ref('')
const filterStatus = ref('all')
const isLoading = ref(false)
const skillsLoading = ref(false)
const modelsLoading = ref(false)
// 创建弹窗
const showCreateModal = ref(false)
const isCreating = ref(false)
const newAgent = ref<AgentFormData>({
name: '',
description: '',
avatar: '🤖',
skillsMode: 'all',
selectedSkills: [],
knowledge: '',
prompt: '',
modelId: ''
})
// 编辑弹窗
const showEditModal = ref(false)
const isEditing = ref(false)
const editingAgent = ref<AgentFormData & { id: string }>({
id: '',
name: '',
description: '',
avatar: '🤖',
skillsMode: 'all',
selectedSkills: [],
knowledge: '',
prompt: '',
modelId: ''
})
// Skills 选择器
const showSkillsDropdown = ref(false)
const showSubSkillsDropdown = ref(false)
const skillsSearch = ref('')
// 选项
const skillsModeOptions = [
{ value: 'all', label: 'All Skills', desc: 'Use all available skills' },
{ value: 'include', label: 'Include Skills', desc: 'Select specific skills to use' },
{ value: 'exclude', label: 'Exclude Skills', desc: 'Select skills to exclude' },
]
const avatarOptions = [
'🤖', '🧠', '💻', '📊', '🔬', '🎧', '✨', '💬', '🔮', '🌙',
'🐉', '☁️', '🎨', '🎯', '🚀', '⚡', '🔥', '💡', '🎭', '🎪'
]
const knowledgeOptions = [
{ value: 'general', label: 'General Knowledge' },
{ value: 'codebase', label: 'Codebase' },
{ value: 'docs', label: 'Documentation' },
{ value: 'api', label: 'API Reference' },
]
// 计算属性
const skillsOptions = computed(() => {
return skillsList.value.map(skill => ({
value: skill.id,
label: skill.skill_name,
desc: skill.skill_desc
}))
})
const filteredSkills = computed(() => {
if (!skillsSearch.value) return skillsOptions.value
const search = skillsSearch.value.toLowerCase()
return skillsOptions.value.filter(s =>
s.label.toLowerCase().includes(search) ||
(s.desc && s.desc.toLowerCase().includes(search))
)
})
const filteredAgents = computed(() => {
return agents.value.filter(agent => {
const matchSearch = agent.name.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
agent.skills.toLowerCase().includes(searchQuery.value.toLowerCase())
const matchStatus = filterStatus.value === 'all' || agent.status === filterStatus.value
return matchSearch && matchStatus
})
})
const stats = computed(() => ({
total: agents.value.length,
active: agents.value.filter(a => a.status === 'active').length,
inactive: agents.value.filter(a => a.status === 'inactive').length,
}))
const isAllSelected = computed(() => {
return skillsOptions.value.length > 0 && newAgent.value.selectedSkills.length === skillsOptions.value.length
})
const isIndeterminate = computed(() => {
return newAgent.value.selectedSkills.length > 0 && newAgent.value.selectedSkills.length < skillsOptions.value.length
})
// 方法
async function fetchAgents() {
try {
const response = await fetch(`${API_BASE}/api/agent/list`)
if (!response.ok) throw new Error('Failed to fetch agents')
const data = await response.json()
agents.value = (data.agents || []).map((agent: any) => ({
id: agent.id,
name: agent.name,
avatar: agent.avatar || '🤖',
description: agent.description || '',
accentColor: '#f97316',
gradient: 'from-orange-500/20 to-amber-500/20',
status: agent.is_active ? 'active' : 'inactive',
skills: agent.skills?.length > 0 ? agent.skills.join(', ') : 'None',
model: agent.model_name || 'None',
createdAt: agent.created_at ? formatDate(agent.created_at, 'YYYY/MM/DD HH:mm') : formatDate(new Date(), 'YYYY/MM/DD HH:mm'),
}))
} catch (error) {
console.error('Failed to fetch agents:', error)
}
}
async function fetchSkills() {
skillsLoading.value = true
try {
const response = await fetch(`${API_BASE}/skill/list`)
if (!response.ok) {
console.error('Failed to fetch skills:', response.status, response.statusText)
return
}
const result = await response.json()
if (result.list) {
skillsList.value = result.list
}
} catch (error) {
console.error('Failed to fetch skills:', error)
} finally {
skillsLoading.value = false
}
}
async function fetchModels() {
modelsLoading.value = true
try {
const response = await fetch(`${API_BASE}/model/list`)
if (!response.ok) {
console.error('Failed to fetch models:', response.status, response.statusText)
return
}
const result = await response.json()
if (result.list) {
modelsList.value = result.list.map((m: any) => ({
id: m.id,
name: m.name,
provider: m.provider,
model: m.model
}))
}
} catch (error) {
console.error('Failed to fetch models:', error)
} finally {
modelsLoading.value = false
}
}
function getSkillLabel(id: string) {
return skillsOptions.value.find(s => s.value === id)?.label || id
}
function openCreateModal() {
newAgent.value = {
name: '',
description: '',
avatar: '🤖',
skillsMode: 'all',
selectedSkills: [],
knowledge: '',
prompt: '',
modelId: ''
}
showCreateModal.value = true
}
async function createAgent() {
if (!newAgent.value.name || (newAgent.value.skillsMode !== 'all' && newAgent.value.selectedSkills.length === 0)) {
return
}
const selectedModel = modelsList.value.find(m => m.id === newAgent.value.modelId)
isCreating.value = true
try {
const skills = newAgent.value.skillsMode === 'all' ? ['*'] : newAgent.value.selectedSkills
const response = await fetch(`${API_BASE}/api/agent/create`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: newAgent.value.name,
description: newAgent.value.description,
avatar: newAgent.value.avatar,
skillsMode: newAgent.value.skillsMode,
skills: skills,
knowledge: newAgent.value.knowledge,
prompt: newAgent.value.prompt,
model_provider: selectedModel?.provider,
model_name: selectedModel?.name
})
})
if (!response.ok) throw new Error('Failed to create agent')
const result = await response.json()
const skillsLabels = newAgent.value.selectedSkills.map(id => getSkillLabel(id)).join(', ')
agents.value.unshift({
id: result.agent_id,
name: newAgent.value.name,
avatar: newAgent.value.avatar,
description: newAgent.value.description,
accentColor: '#f97316',
gradient: 'from-orange-500/20 to-amber-500/20',
status: 'inactive',
skills: skillsLabels || 'None',
model: selectedModel?.name || 'None',
createdAt: formatDate(new Date(), 'YYYY/MM/DD HH:mm')
})
showCreateModal.value = false
ElMessage.success('Agent created successfully')
} catch (error) {
console.error('Failed to create agent:', error)
ElMessage.error('Failed to create agent')
} finally {
isCreating.value = false
}
}
function openEdit(agent: Agent) {
let selectedSkills: string[] = []
let skillsMode: 'all' | 'include' | 'exclude' = 'all'
if (agent.skills === '*') {
skillsMode = 'all'
} else if (agent.skills && agent.skills !== 'None') {
skillsMode = 'include'
selectedSkills = agent.skills.split(',').map((s: string) => s.trim())
}
const model = modelsList.value.find(m => m.name === agent.model)
editingAgent.value = {
id: agent.id,
name: agent.name,
description: agent.description || '',
avatar: agent.avatar || '🤖',
skillsMode,
selectedSkills,
modelId: model?.id || '',
prompt: '',
knowledge: ''
}
showEditModal.value = true
}
async function saveEdit() {
if (!editingAgent.value.name) return
const skills = editingAgent.value.skillsMode === 'all' ? ['*'] : editingAgent.value.selectedSkills
const selectedModel = modelsList.value.find(m => m.id === editingAgent.value.modelId)
isEditing.value = true
try {
const response = await fetch(`${API_BASE}/api/agent/${editingAgent.value.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: editingAgent.value.name,
description: editingAgent.value.description,
skills: skills,
role_description: editingAgent.value.prompt,
model_provider: selectedModel?.provider || '',
model_name: selectedModel?.name || ''
})
})
if (response.ok) {
const agent = agents.value.find(a => a.id === editingAgent.value.id)
if (agent) {
agent.name = editingAgent.value.name
agent.description = editingAgent.value.description
agent.skills = editingAgent.value.skillsMode === 'all' ? '*' : editingAgent.value.selectedSkills.join(', ')
agent.model = selectedModel?.name || ''
}
showEditModal.value = false
ElMessage.success('Agent updated successfully')
} else {
ElMessage.error('Failed to update agent')
}
} catch (error) {
console.error('Failed to update agent:', error)
ElMessage.error('Failed to update agent')
} finally {
isEditing.value = false
}
}
async function toggleStatus(agent: Agent) {
const newStatus = agent.status === 'active' ? false : true
try {
const response = await fetch(`${API_BASE}/api/agent/${agent.id}/status`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ is_active: newStatus })
})
if (response.ok) {
agent.status = newStatus ? 'active' : 'inactive'
} else {
ElMessage.error('Failed to update status')
}
} catch (error) {
console.error('Failed to update status:', error)
ElMessage.error('Failed to update status')
}
}
async function deleteAgent(id: string) {
try {
const response = await fetch(`${API_BASE}/api/agent/${id}`, { method: 'DELETE' })
if (response.ok) {
agents.value = agents.value.filter(a => a.id !== id)
ElMessage.success('Agent deleted successfully')
} else {
ElMessage.error('Failed to delete agent')
}
} catch (error) {
console.error('Failed to delete agent:', error)
ElMessage.error('Failed to delete agent')
}
}
function toggleSkillsDropdown() {
showSkillsDropdown.value = !showSkillsDropdown.value
if (!showSkillsDropdown.value) {
skillsSearch.value = ''
}
}
function closeSkillsDropdown() {
showSkillsDropdown.value = false
skillsSearch.value = ''
}
function handleSkillsModeClick(mode: string) {
if (mode === 'all' || mode === 'include' || mode === 'exclude') {
newAgent.value.skillsMode = mode
if (mode === 'all') {
newAgent.value.selectedSkills = []
}
showSkillsDropdown.value = false
if (mode === 'include' || mode === 'exclude') {
showSubSkillsDropdown.value = true
}
}
}
function handleSkillsModeClickEdit(mode: string) {
if (mode === 'all' || mode === 'include' || mode === 'exclude') {
editingAgent.value.skillsMode = mode
if (mode === 'all') {
editingAgent.value.selectedSkills = []
}
showSkillsDropdown.value = false
if (mode === 'include' || mode === 'exclude') {
showSubSkillsDropdown.value = true
}
}
}
function toggleSelectAll() {
if (isAllSelected.value) {
newAgent.value.selectedSkills = []
} else {
newAgent.value.selectedSkills = skillsOptions.value.map(s => s.value)
}
}
function clearSkills() {
newAgent.value.selectedSkills = []
}
// 切换技能模式下拉框
function toggleSkillsMode() {
showSkillsDropdown.value = !showSkillsDropdown.value
showSubSkillsDropdown.value = false
}
// 选择技能模式
function selectSkillsMode(mode: 'all' | 'include' | 'exclude') {
newAgent.value.skillsMode = mode
if (mode === 'all') {
newAgent.value.selectedSkills = []
}
showSkillsDropdown.value = false
if (mode === 'include' || mode === 'exclude') {
showSubSkillsDropdown.value = true
}
}
// 切换子下拉框
function toggleSubSkillsDropdown() {
showSubSkillsDropdown.value = !showSubSkillsDropdown.value
}
// 关闭所有下拉框
function closeAllDropdowns() {
showSkillsDropdown.value = false
showSubSkillsDropdown.value = false
}
// 获取显示文本
function getSkillsDisplayText() {
if (newAgent.value.skillsMode === 'all') {
return 'All Skills'
}
const count = newAgent.value.selectedSkills.length
if (count === 0) {
return newAgent.value.skillsMode === 'include' ? 'Select skills to include...' : 'Select skills to exclude...'
}
return `${count} skill${count > 1 ? 's' : ''} ${newAgent.value.skillsMode === 'include' ? 'included' : 'excluded'}`
}
// 切换子下拉框中的技能选择
function toggleSkillSelection(skillId: string) {
const index = newAgent.value.selectedSkills.indexOf(skillId)
if (index > -1) {
newAgent.value.selectedSkills.splice(index, 1)
} else {
newAgent.value.selectedSkills.push(skillId)
}
}
// 全选 skills
function selectAllSkills() {
newAgent.value.selectedSkills = skillsOptions.value.map(s => s.value)
}
// 状态颜色
function statusClass(status: string) {
switch (status) {
case 'active': return 'bg-green-500'
case 'inactive': return 'bg-gray-500'
default: return 'bg-gray-500'
}
}
function handleClickOutside(e: MouseEvent) {
const target = e.target as HTMLElement
if (!target.closest('.skills-selector')) {
closeSkillsDropdown()
}
}
// 初始化 - 在 useAgents 函数中导出初始化函数
function init() {
fetchSkills()
fetchModels()
fetchAgents()
document.addEventListener('click', handleClickOutside)
}
function cleanup() {
document.removeEventListener('click', handleClickOutside)
}
// 导出
export function useAgents() {
// 在 useAgents 被调用时初始化(组件挂载时)
init()
return {
// 状态
agents,
skillsList,
modelsList,
searchQuery,
filterStatus,
isLoading,
skillsLoading,
modelsLoading,
showCreateModal,
isCreating,
newAgent,
showEditModal,
isEditing,
editingAgent,
showSkillsDropdown,
showSubSkillsDropdown,
skillsSearch,
skillsModeOptions,
avatarOptions,
knowledgeOptions,
skillsOptions,
filteredSkills,
filteredAgents,
stats,
isAllSelected,
isIndeterminate,
// 方法
fetchAgents,
fetchSkills,
fetchModels,
getSkillLabel,
openCreateModal,
createAgent,
openEdit,
saveEdit,
toggleStatus,
deleteAgent,
toggleSkillsDropdown,
closeSkillsDropdown,
handleSkillsModeClick,
handleSkillsModeClickEdit,
toggleSelectAll,
clearSkills,
handleClickOutside,
toggleSkillsMode,
selectSkillsMode,
toggleSubSkillsDropdown,
closeAllDropdowns,
getSkillsDisplayText,
toggleSkillSelection,
selectAllSkills,
statusClass,
cleanup
}
}