feat: 更新skill相关前端页面

- 新增 web/src/views/skill/skill.ts skill视图组件
- 更新 Agents.vue 页面
- 更新 Skill.vue 页面
- 移除旧的 useSkills.ts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-12 15:23:13 +08:00
parent 465fdf2e6c
commit e2c9bbd0d1
3 changed files with 1401 additions and 119 deletions

View File

@@ -0,0 +1,496 @@
import { ref, computed } from 'vue'
import { ElMessageBox, ElMessage } from 'element-plus'
const API_BASE = 'http://localhost:8082'
export interface Skill {
id: string
skill_name: string
skill_type: string
skill_desc: string
path: string
status: string
created_at?: string
updated_at?: string
}
export function useSkills() {
// ============ 导入相关状态 ============
const fileInputRef = ref<HTMLInputElement | null>(null)
const isImporting = ref(false)
const isImportingDialog = ref(false)
const importFile = ref<File | null>(null)
const importFileName = ref('')
const importSkillName = ref('')
const importSkillDesc = ref('')
const importSkillContent = ref('')
const isImportStep2 = ref(false)
// 下拉菜单状态
const showDropdown = ref(false)
const dropdownTimeout = ref<number | null>(null)
// ============ 技能列表状态 ============
const skills = ref<Skill[]>([])
const skillsLoading = ref(false)
// 搜索和筛选
const searchQuery = ref('')
const filterStatus = ref('all')
// 编辑状态
const isEditing = ref(false)
const isCreating = ref(false)
const isEditingContent = ref(false)
const editingSkill = ref<Skill | null>(null)
// 表单
const editForm = ref({
skill_name: '',
skill_desc: '',
skill_type: 'user',
})
const newSkillForm = ref({
skill_name: '',
skill_desc: '',
skill_type: 'user',
})
const newSkillContent = ref('')
// ============ 方法 ============
// 获取技能列表
const fetchSkills = async (type?: string) => {
skillsLoading.value = true
try {
let url = `${API_BASE}/skill/list`
if (type) {
url += `?type=${type}`
}
const response = await fetch(url)
const result = await response.json()
if (result.list) {
skills.value = result.list.map((skill: any) => ({
id: skill.id,
skill_name: skill.skill_name,
skill_type: skill.skill_type,
skill_desc: skill.skill_desc || '',
path: skill.path || '',
status: skill.status || 'active',
created_at: skill.created_at,
updated_at: skill.updated_at,
}))
}
return result.list || []
} catch (error) {
console.error('Failed to fetch skills:', error)
return []
} finally {
skillsLoading.value = false
}
}
// 更新技能
const updateSkill = async (id: string, skill: Partial<Skill>) => {
try {
const response = await fetch(`${API_BASE}/skill/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(skill),
})
const data = await response.json()
await fetchSkills()
return data
} catch (error) {
console.error('Failed to update skill:', error)
throw error
}
}
// 删除技能
const deleteSkill = async (id: string) => {
try {
const response = await fetch(`${API_BASE}/skill/${id}`, {
method: 'DELETE',
})
const data = await response.json()
await fetchSkills()
return data
} catch (error) {
console.error('Failed to delete skill:', error)
throw error
}
}
// 筛选
const filteredSkills = computed(() => {
return skills.value.filter(skill => {
const matchSearch = skill.skill_name.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
(skill.skill_desc && skill.skill_desc.toLowerCase().includes(searchQuery.value.toLowerCase()))
const matchStatus = filterStatus.value === 'all' || skill.status === filterStatus.value
return matchSearch && matchStatus
})
})
// 打开创建弹窗
const openCreate = () => {
newSkillForm.value = { skill_name: '', skill_desc: '', skill_type: 'user' }
newSkillContent.value = ''
isCreating.value = true
}
// 关闭创建弹窗
const closeCreate = () => {
isCreating.value = false
newSkillContent.value = ''
}
// 第一步:点击 Next跳转到编辑内容
const goToEditContent = () => {
if (!newSkillForm.value.skill_name || !newSkillForm.value.skill_desc) {
ElMessage.warning('Please fill in skill name and description')
return
}
newSkillContent.value = `---
name: ${newSkillForm.value.skill_name}
description: ${newSkillForm.value.skill_desc}
---
# ${newSkillForm.value.skill_name}
${newSkillForm.value.skill_desc}
`
isCreating.value = false
isEditingContent.value = true
}
// 关闭编辑内容弹窗
const closeEditContent = () => {
isEditingContent.value = false
newSkillContent.value = ''
}
// 第二步:保存技能内容
const saveNewSkill = async () => {
try {
const blob = new Blob([newSkillContent.value], { type: 'text/markdown' })
const file = new File([blob], 'SKILL.md', { type: 'text/markdown' })
const formData = new FormData()
formData.append('skill_name', newSkillForm.value.skill_name)
formData.append('skill_desc', newSkillForm.value.skill_desc)
formData.append('skill_type', newSkillForm.value.skill_type)
formData.append('file', file)
const response = await fetch(`${API_BASE}/skill/add`, {
method: 'POST',
body: formData,
})
const data = await response.json()
if (response.ok) {
ElMessage.success('Skill created successfully')
isEditingContent.value = false
newSkillContent.value = ''
await fetchSkills()
} else {
ElMessage.error(data.error || 'Failed to create skill')
}
} catch (error) {
console.error('Failed to create skill:', error)
ElMessage.error('Failed to create skill')
}
}
// 打开编辑弹窗
const openEdit = (skill: Skill) => {
editingSkill.value = skill
editForm.value = {
skill_name: skill.skill_name,
skill_desc: skill.skill_desc,
skill_type: skill.skill_type,
}
isEditing.value = true
}
// 关闭编辑弹窗
const closeEdit = () => {
isEditing.value = false
editingSkill.value = null
}
// 保存编辑
const saveEdit = async () => {
try {
await updateSkill(editingSkill.value!.id, {
skill_name: editForm.value.skill_name,
skill_desc: editForm.value.skill_desc,
skill_type: editForm.value.skill_type,
})
ElMessage.success('Skill updated successfully')
isEditing.value = false
} catch (error) {
ElMessage.error('Failed to update skill')
}
}
// 切换状态
const toggleStatus = async (skill: Skill) => {
const newStatus = skill.status === 'active' ? 'inactive' : 'active'
try {
await updateSkill(skill.id, { status: newStatus })
skill.status = newStatus
} catch (error) {
ElMessage.error('Failed to update status')
}
}
// 删除技能(带确认)
const handleDeleteSkill = async (id: string) => {
try {
await ElMessageBox.confirm('Are you sure you want to delete this skill?', 'Confirm Delete', {
confirmButtonText: 'Delete',
cancelButtonText: 'Cancel',
type: 'warning',
})
await deleteSkill(id)
ElMessage.success('Skill deleted successfully')
} catch (error: any) {
if (error !== 'cancel') {
console.error('Failed to delete skill:', error)
ElMessage.error('Failed to delete skill')
}
}
}
// ============ 导入相关方法 ============
// 打开导入弹窗
const openImportDialog = () => {
showDropdown.value = false
isImportingDialog.value = true
importFile.value = null
importFileName.value = ''
importSkillName.value = ''
importSkillDesc.value = ''
importSkillContent.value = ''
isImportStep2.value = false
}
// 关闭导入弹窗
const closeImportDialog = () => {
isImportingDialog.value = false
importFile.value = null
importFileName.value = ''
isImportStep2.value = false
}
// 解析 SKILL.md 内容
const parseSkillContent = (content: string, fallbackName: string) => {
let skillName = ''
let skillDesc = ''
const trimmedContent = content.trim()
if (trimmedContent.startsWith('---')) {
const endIndex = trimmedContent.indexOf('---', 3)
if (endIndex > 3) {
const yamlContent = trimmedContent.substring(3, endIndex).trim()
const nameMatch = yamlContent.match(/name:\s*(.+)/)
const descMatch = yamlContent.match(/description:\s*(.+)/)
skillName = nameMatch ? nameMatch[1].trim() : fallbackName
skillDesc = descMatch ? descMatch[1].trim() : ''
const afterYaml = trimmedContent.substring(endIndex + 3).trim()
if (afterYaml && !skillDesc) {
skillDesc = afterYaml
}
}
}
if (!skillName) {
const lines = trimmedContent.split('\n')
skillName = lines[0]?.replace(/^#\s*/, '').trim() || fallbackName
skillDesc = lines.slice(1).join('\n').trim()
}
return { skillName, skillDesc }
}
// 处理文件选择
const handleFileChange = async (event: Event) => {
const input = event.target as HTMLInputElement
if (!input.files || input.files.length === 0) return
const file = input.files[0]
importFile.value = file
importFileName.value = file.name
try {
const content = await file.text()
const { skillName, skillDesc } = parseSkillContent(content, file.name.replace('.md', ''))
importSkillName.value = skillName
importSkillDesc.value = skillDesc
importSkillContent.value = content
isImportStep2.value = true
} catch (error) {
console.error('Failed to read file:', error)
ElMessage.error('读取文件失败')
}
input.value = ''
}
// 提交导入
const submitImport = async () => {
if (!importSkillName.value || !importSkillContent.value) {
ElMessage.warning('请选择有效的技能文件')
return
}
isImporting.value = true
try {
const blob = new Blob([importSkillContent.value], { type: 'text/markdown' })
const file = new File([blob], 'SKILL.md', { type: 'text/markdown' })
const formData = new FormData()
formData.append('skill_name', importSkillName.value)
formData.append('skill_desc', importSkillDesc.value)
formData.append('file', file)
const response = await fetch(`${API_BASE}/skill/add`, {
method: 'POST',
body: formData
})
if (response.ok) {
ElMessage.success(`Skill imported: ${importSkillName.value}`)
await fetchSkills()
closeImportDialog()
} else {
const err = await response.json()
ElMessage.error(err.message || '导入失败')
}
} catch (error) {
console.error('Import failed:', error)
ElMessage.error('导入失败,请检查文件格式是否正确')
} finally {
isImporting.value = false
}
}
// 处理文件夹选择
const handleFolderSelect = async (event: Event) => {
const input = event.target as HTMLInputElement
const files = input.files
if (!files || files.length === 0) return
isImporting.value = true
try {
const folder = files[0].webkitRelativePath?.split('/')[0] || files[0].name
// 查找 SKILL.md 文件
let skillMdFile: File | null = null
for (let i = 0; i < files.length; i++) {
const file = files[i]
if (file.name === 'SKILL.md' || file.webkitRelativePath?.endsWith('/SKILL.md')) {
skillMdFile = file
break
}
}
if (!skillMdFile) {
ElMessage.error('导入失败:所选文件夹中未找到 SKILL.md 文件')
return
}
const content = await skillMdFile.text()
const { skillName, skillDesc } = parseSkillContent(content, folder)
// 使用导入弹窗显示内容
importSkillName.value = skillName
importSkillDesc.value = skillDesc
importSkillContent.value = content
isImportingDialog.value = true
isImportStep2.value = true
} catch (error) {
console.error('Import failed:', error)
ElMessage.error('导入失败,请检查文件夹格式是否正确')
} finally {
isImporting.value = false
input.value = ''
}
}
// 鼠标进入时显示
const onDropdownEnter = () => {
if (dropdownTimeout.value) {
clearTimeout(dropdownTimeout.value)
dropdownTimeout.value = null
}
showDropdown.value = true
}
// 鼠标离开时延迟隐藏
const onDropdownLeave = () => {
dropdownTimeout.value = window.setTimeout(() => {
showDropdown.value = false
}, 300)
}
// ============ 返回 ============
return {
// State
skills,
skillsLoading,
searchQuery,
filterStatus,
isEditing,
isCreating,
isEditingContent,
editingSkill,
editForm,
newSkillForm,
newSkillContent,
// Computed
filteredSkills,
// Methods
fetchSkills,
openCreate,
closeCreate,
goToEditContent,
closeEditContent,
saveNewSkill,
openEdit,
closeEdit,
saveEdit,
toggleStatus,
deleteSkill: handleDeleteSkill,
// 导入相关
fileInputRef,
isImporting,
isImportingDialog,
importFileName,
importSkillName,
importSkillDesc,
importSkillContent,
isImportStep2,
openImportDialog,
closeImportDialog,
handleFileChange,
submitImport,
handleFolderSelect,
// 下拉菜单
showDropdown,
dropdownTimeout,
onDropdownEnter,
onDropdownLeave,
}
}