2026-03-12 15:23:13 +08:00
|
|
|
|
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)
|
2026-03-12 16:42:48 +08:00
|
|
|
|
const isEditingContent = ref(false) // 创建弹窗的第二步
|
|
|
|
|
|
const isEditingStep2 = ref(false) // 编辑弹窗的第二步
|
2026-03-12 15:23:13 +08:00
|
|
|
|
const isCreating = 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('')
|
2026-03-12 16:42:48 +08:00
|
|
|
|
const editSkillContent = ref('') // 编辑弹窗第二步的 SKILL.md 内容
|
2026-03-12 15:23:13 +08:00
|
|
|
|
|
|
|
|
|
|
// ============ 方法 ============
|
|
|
|
|
|
|
|
|
|
|
|
// 获取技能列表
|
|
|
|
|
|
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')
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 16:42:48 +08:00
|
|
|
|
// 打开编辑弹窗(第一步)
|
|
|
|
|
|
const openEdit = async (skill: Skill) => {
|
2026-03-12 15:23:13 +08:00
|
|
|
|
editingSkill.value = skill
|
|
|
|
|
|
editForm.value = {
|
|
|
|
|
|
skill_name: skill.skill_name,
|
|
|
|
|
|
skill_desc: skill.skill_desc,
|
|
|
|
|
|
skill_type: skill.skill_type,
|
|
|
|
|
|
}
|
2026-03-12 16:42:48 +08:00
|
|
|
|
|
|
|
|
|
|
// 读取现有的 SKILL.md 内容
|
|
|
|
|
|
console.log('Opening edit for skill:', skill)
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch(`${API_BASE}/skill/content?id=${skill.id}`)
|
|
|
|
|
|
console.log('Content API response:', response.status, response.statusText)
|
|
|
|
|
|
if (response.ok) {
|
|
|
|
|
|
const text = await response.text()
|
|
|
|
|
|
console.log('Skill content:', text.substring(0, 100))
|
|
|
|
|
|
editSkillContent.value = text
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// API 返回 404 说明文件不存在,生成默认内容
|
|
|
|
|
|
console.log('File not found, using default content')
|
|
|
|
|
|
editSkillContent.value = `---
|
|
|
|
|
|
name: ${skill.skill_name}
|
|
|
|
|
|
description: ${skill.skill_desc || 'A custom skill'}
|
|
|
|
|
|
category: ${skill.skill_type || 'user'}
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
# ${skill.skill_name}
|
|
|
|
|
|
|
|
|
|
|
|
${skill.skill_desc || 'A custom skill'}
|
|
|
|
|
|
|
|
|
|
|
|
## Instructions
|
|
|
|
|
|
|
|
|
|
|
|
Describe what this skill does and how it works.
|
|
|
|
|
|
|
|
|
|
|
|
## Tools
|
|
|
|
|
|
|
|
|
|
|
|
List the tools this skill can use:
|
|
|
|
|
|
|
|
|
|
|
|
-
|
|
|
|
|
|
|
|
|
|
|
|
## Examples
|
|
|
|
|
|
|
|
|
|
|
|
Provide some examples of how to use this skill:
|
|
|
|
|
|
|
|
|
|
|
|
\`\`\`
|
|
|
|
|
|
Example 1:
|
|
|
|
|
|
\`\`\`
|
|
|
|
|
|
`
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Error getting skill content:', error)
|
|
|
|
|
|
// 网络错误也使用默认内容
|
|
|
|
|
|
editSkillContent.value = `---
|
|
|
|
|
|
name: ${skill.skill_name}
|
|
|
|
|
|
description: ${skill.skill_desc || 'A custom skill'}
|
|
|
|
|
|
category: ${skill.skill_type || 'user'}
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
# ${skill.skill_name}
|
|
|
|
|
|
|
|
|
|
|
|
${skill.skill_desc || 'A custom skill'}
|
|
|
|
|
|
|
|
|
|
|
|
## Instructions
|
|
|
|
|
|
|
|
|
|
|
|
Describe what this skill does and how it works.
|
|
|
|
|
|
|
|
|
|
|
|
## Tools
|
|
|
|
|
|
|
|
|
|
|
|
List the tools this skill can use:
|
|
|
|
|
|
|
|
|
|
|
|
-
|
|
|
|
|
|
|
|
|
|
|
|
## Examples
|
|
|
|
|
|
|
|
|
|
|
|
Provide some examples of how to use this skill:
|
|
|
|
|
|
|
|
|
|
|
|
\`\`\`
|
|
|
|
|
|
Example 1:
|
|
|
|
|
|
\`\`\`
|
|
|
|
|
|
`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 15:23:13 +08:00
|
|
|
|
isEditing.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 关闭编辑弹窗
|
|
|
|
|
|
const closeEdit = () => {
|
|
|
|
|
|
isEditing.value = false
|
2026-03-12 16:42:48 +08:00
|
|
|
|
isEditingStep2.value = false
|
2026-03-12 15:23:13 +08:00
|
|
|
|
editingSkill.value = null
|
2026-03-12 16:42:48 +08:00
|
|
|
|
editSkillContent.value = ''
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 编辑第一步:点击 Next,跳转到编辑内容
|
|
|
|
|
|
const goToEditStep2 = () => {
|
|
|
|
|
|
if (!editForm.value.skill_name || !editForm.value.skill_desc) {
|
|
|
|
|
|
ElMessage.warning('Please fill in skill name and description')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
isEditing.value = false
|
|
|
|
|
|
isEditingStep2.value = true
|
2026-03-12 15:23:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 16:42:48 +08:00
|
|
|
|
// 编辑第二步:保存
|
|
|
|
|
|
const saveEditStep2 = async () => {
|
|
|
|
|
|
if (!editingSkill.value) return
|
|
|
|
|
|
|
2026-03-12 15:23:13 +08:00
|
|
|
|
try {
|
2026-03-12 16:42:48 +08:00
|
|
|
|
// 将内容转换为文件上传
|
|
|
|
|
|
const blob = new Blob([editSkillContent.value], { type: 'text/markdown' })
|
|
|
|
|
|
const file = new File([blob], 'SKILL.md', { type: 'text/markdown' })
|
|
|
|
|
|
|
|
|
|
|
|
const formData = new FormData()
|
|
|
|
|
|
formData.append('skill_name', editForm.value.skill_name)
|
|
|
|
|
|
formData.append('skill_desc', editForm.value.skill_desc)
|
|
|
|
|
|
formData.append('skill_type', editForm.value.skill_type)
|
|
|
|
|
|
formData.append('file', file)
|
|
|
|
|
|
|
|
|
|
|
|
const response = await fetch(`${API_BASE}/skill/${editingSkill.value.id}`, {
|
|
|
|
|
|
method: 'PUT',
|
|
|
|
|
|
body: formData,
|
2026-03-12 15:23:13 +08:00
|
|
|
|
})
|
2026-03-12 16:42:48 +08:00
|
|
|
|
|
|
|
|
|
|
if (response.ok) {
|
|
|
|
|
|
ElMessage.success('Skill updated successfully')
|
|
|
|
|
|
await fetchSkills()
|
|
|
|
|
|
isEditingStep2.value = false
|
|
|
|
|
|
editingSkill.value = null
|
|
|
|
|
|
editSkillContent.value = ''
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const data = await response.json()
|
|
|
|
|
|
ElMessage.error(data.error || 'Failed to update skill')
|
|
|
|
|
|
}
|
2026-03-12 15:23:13 +08:00
|
|
|
|
} catch (error) {
|
2026-03-12 16:42:48 +08:00
|
|
|
|
console.error('Failed to update skill:', error)
|
2026-03-12 15:23:13 +08:00
|
|
|
|
ElMessage.error('Failed to update skill')
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 17:17:08 +08:00
|
|
|
|
// 返回编辑第一步
|
|
|
|
|
|
const goBackToEditStep1 = () => {
|
2026-03-12 16:42:48 +08:00
|
|
|
|
isEditingStep2.value = false
|
|
|
|
|
|
isEditing.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 17:17:08 +08:00
|
|
|
|
// 关闭编辑第二步弹窗(完全关闭)
|
|
|
|
|
|
const closeEditStep2 = () => {
|
|
|
|
|
|
isEditingStep2.value = false
|
|
|
|
|
|
isEditing.value = false
|
|
|
|
|
|
editingSkill.value = null
|
|
|
|
|
|
editSkillContent.value = ''
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 15:23:13 +08:00
|
|
|
|
// 切换状态
|
|
|
|
|
|
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
|
2026-03-12 17:17:08 +08:00
|
|
|
|
|
|
|
|
|
|
// 获取纯文件名,去除路径
|
|
|
|
|
|
const fileName = file.name.split(/[/\\]/).pop()?.replace('.md', '') || 'untitled'
|
|
|
|
|
|
importFileName.value = fileName
|
2026-03-12 15:23:13 +08:00
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const content = await file.text()
|
2026-03-12 17:17:08 +08:00
|
|
|
|
const { skillName, skillDesc } = parseSkillContent(content, fileName)
|
2026-03-12 15:23:13 +08:00
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 16:42:48 +08:00
|
|
|
|
// 处理文件选择(单个 SKILL.md 文件)
|
2026-03-12 15:23:13 +08:00
|
|
|
|
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 {
|
2026-03-12 16:42:48 +08:00
|
|
|
|
const file = files[0]
|
2026-03-12 15:23:13 +08:00
|
|
|
|
|
2026-03-12 16:42:48 +08:00
|
|
|
|
// 读取文件内容
|
|
|
|
|
|
const content = await file.text()
|
|
|
|
|
|
const fileName = file.name.replace('.md', '')
|
2026-03-12 15:23:13 +08:00
|
|
|
|
|
2026-03-12 16:42:48 +08:00
|
|
|
|
const { skillName, skillDesc } = parseSkillContent(content, fileName)
|
2026-03-12 15:23:13 +08:00
|
|
|
|
|
|
|
|
|
|
// 使用导入弹窗显示内容
|
|
|
|
|
|
importSkillName.value = skillName
|
|
|
|
|
|
importSkillDesc.value = skillDesc
|
|
|
|
|
|
importSkillContent.value = content
|
|
|
|
|
|
isImportingDialog.value = true
|
|
|
|
|
|
isImportStep2.value = true
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Import failed:', error)
|
2026-03-12 16:42:48 +08:00
|
|
|
|
ElMessage.error('导入失败,请检查文件格式是否正确')
|
2026-03-12 15:23:13 +08:00
|
|
|
|
} 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,
|
2026-03-12 16:42:48 +08:00
|
|
|
|
isEditingStep2,
|
2026-03-12 15:23:13 +08:00
|
|
|
|
isCreating,
|
|
|
|
|
|
isEditingContent,
|
|
|
|
|
|
editingSkill,
|
|
|
|
|
|
editForm,
|
|
|
|
|
|
newSkillForm,
|
|
|
|
|
|
newSkillContent,
|
2026-03-12 16:42:48 +08:00
|
|
|
|
editSkillContent,
|
2026-03-12 15:23:13 +08:00
|
|
|
|
// Computed
|
|
|
|
|
|
filteredSkills,
|
|
|
|
|
|
// Methods
|
|
|
|
|
|
fetchSkills,
|
|
|
|
|
|
openCreate,
|
|
|
|
|
|
closeCreate,
|
|
|
|
|
|
goToEditContent,
|
|
|
|
|
|
closeEditContent,
|
|
|
|
|
|
saveNewSkill,
|
|
|
|
|
|
openEdit,
|
|
|
|
|
|
closeEdit,
|
2026-03-12 16:42:48 +08:00
|
|
|
|
goToEditStep2,
|
2026-03-12 17:17:08 +08:00
|
|
|
|
goBackToEditStep1,
|
2026-03-12 16:42:48 +08:00
|
|
|
|
saveEditStep2,
|
|
|
|
|
|
closeEditStep2,
|
2026-03-12 15:23:13 +08:00
|
|
|
|
toggleStatus,
|
|
|
|
|
|
deleteSkill: handleDeleteSkill,
|
|
|
|
|
|
// 导入相关
|
|
|
|
|
|
fileInputRef,
|
|
|
|
|
|
isImporting,
|
|
|
|
|
|
isImportingDialog,
|
|
|
|
|
|
importFileName,
|
|
|
|
|
|
importSkillName,
|
|
|
|
|
|
importSkillDesc,
|
|
|
|
|
|
importSkillContent,
|
|
|
|
|
|
isImportStep2,
|
|
|
|
|
|
openImportDialog,
|
|
|
|
|
|
closeImportDialog,
|
|
|
|
|
|
handleFileChange,
|
|
|
|
|
|
submitImport,
|
|
|
|
|
|
handleFolderSelect,
|
|
|
|
|
|
// 下拉菜单
|
|
|
|
|
|
showDropdown,
|
|
|
|
|
|
dropdownTimeout,
|
|
|
|
|
|
onDropdownEnter,
|
|
|
|
|
|
onDropdownLeave,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|