feat: 更新skill前端页面 - 新增编辑功能
- skill.ts: 新增编辑弹窗第二步、获取skill内容功能 - Skill.vue: 更新skill编辑界面 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -9,11 +9,13 @@ const {
|
||||
searchQuery,
|
||||
filterStatus,
|
||||
isEditing,
|
||||
isEditingStep2,
|
||||
isCreating,
|
||||
isEditingContent,
|
||||
editForm,
|
||||
newSkillForm,
|
||||
newSkillContent,
|
||||
editSkillContent,
|
||||
filteredSkills,
|
||||
fetchSkills,
|
||||
openCreate,
|
||||
@@ -23,21 +25,20 @@ const {
|
||||
saveNewSkill,
|
||||
openEdit,
|
||||
closeEdit,
|
||||
saveEdit,
|
||||
goToEditStep2,
|
||||
saveEditStep2,
|
||||
closeEditStep2,
|
||||
toggleStatus,
|
||||
deleteSkill,
|
||||
// 导入相关 - 从 useSkills 引入
|
||||
fileInputRef,
|
||||
// 导入相关
|
||||
isImporting,
|
||||
isImportingDialog,
|
||||
importFileName,
|
||||
importSkillName,
|
||||
importSkillDesc,
|
||||
importSkillContent,
|
||||
isImportStep2,
|
||||
openImportDialog,
|
||||
closeImportDialog,
|
||||
handleFileChange,
|
||||
submitImport,
|
||||
handleFolderSelect,
|
||||
// 下拉菜单
|
||||
@@ -215,7 +216,58 @@ onMounted(() => {
|
||||
|
||||
<div class="flex items-center justify-end gap-3 p-5 border-t border-dark-500">
|
||||
<button @click="closeEdit" class="btn-secondary">Cancel</button>
|
||||
<button @click="saveEdit" class="btn-primary">Save Changes</button>
|
||||
<button @click="goToEditStep2" class="btn-primary">Next</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
|
||||
<!-- 编辑弹窗第二步:编辑内容 -->
|
||||
<Teleport to="body">
|
||||
<div v-if="isEditingStep2" class="fixed inset-0 bg-black/80 flex items-center justify-center z-50 p-4 modal-overlay">
|
||||
<div class="bg-dark-800 rounded-2xl w-full max-w-4xl border border-dark-600 shadow-2xl overflow-hidden modal-content" style="max-height: 90vh;">
|
||||
<div class="flex items-center justify-between p-5 border-b border-dark-600 bg-dark-700/50">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 rounded-xl bg-gradient-to-br from-blue-500 to-indigo-500 flex items-center justify-center">
|
||||
<Edit class="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-xl font-semibold text-white">Edit Skill Content</h3>
|
||||
<p class="text-sm text-gray-400">Configure skill details in SKILL.md format</p>
|
||||
</div>
|
||||
</div>
|
||||
<button @click="closeEditStep2" class="btn-icon">
|
||||
<X class="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="p-5 space-y-4" style="max-height: calc(90vh - 180px); overflow-y: auto;">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Skill Name</label>
|
||||
<input v-model="editForm.skill_name" type="text" class="input-field" readonly>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Category</label>
|
||||
<select v-model="editForm.skill_type" class="input-field" disabled>
|
||||
<option value="user">User</option>
|
||||
<option value="system">System</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">SKILL.md Content</label>
|
||||
<textarea v-model="editSkillContent" rows="20" placeholder="Write your skill content in markdown format..." class="input-field resize-none font-mono text-sm" style="min-height: 400px;"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="text-xs text-gray-500">
|
||||
<p>Tip: SKILL.md should contain YAML front matter with name and description, followed by the skill implementation in markdown.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between gap-3 p-5 border-t border-dark-600 bg-dark-700/50">
|
||||
<button @click="closeEditStep2" class="btn-secondary">Back</button>
|
||||
<button @click="saveEditStep2" class="btn-primary">Save Changes</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -319,7 +371,7 @@ onMounted(() => {
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-xl font-semibold text-white">Import Skill</h3>
|
||||
<p class="text-sm text-gray-400">Select a file or folder to import</p>
|
||||
<p class="text-sm text-gray-400">Select a folder containing SKILL.md</p>
|
||||
</div>
|
||||
</div>
|
||||
<button @click="closeImportDialog" class="btn-icon">
|
||||
@@ -327,43 +379,24 @@ onMounted(() => {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="p-5 space-y-4">
|
||||
<!-- 导入文件选项 -->
|
||||
<div
|
||||
class="border-2 border-dashed border-dark-500 rounded-xl p-6 text-center cursor-pointer hover:border-blue-500 transition-colors"
|
||||
@click="fileInputRef?.click?.()"
|
||||
>
|
||||
<FolderInput class="w-10 h-10 mx-auto text-gray-400 mb-2" />
|
||||
<p class="text-white mb-1">Import File</p>
|
||||
<p class="text-sm text-gray-500">Select a single SKILL.md file</p>
|
||||
</div>
|
||||
<div class="p-5">
|
||||
<!-- 导入单个文件选项 -->
|
||||
<input
|
||||
ref="fileInputRef"
|
||||
type="file"
|
||||
accept=".md"
|
||||
style="display: none"
|
||||
@change="handleFileChange"
|
||||
/>
|
||||
|
||||
<!-- 导入文件夹选项 -->
|
||||
<input
|
||||
type="file"
|
||||
webkitdirectory=""
|
||||
directory=""
|
||||
multiple
|
||||
style="display: none"
|
||||
@change="handleFolderSelect"
|
||||
id="folderInput"
|
||||
/>
|
||||
<label for="folderInput" class="block">
|
||||
<div
|
||||
class="border-2 border-dashed border-dark-500 rounded-xl p-6 text-center cursor-pointer hover:border-green-500 transition-colors"
|
||||
class="border-2 border-dashed border-dark-500 rounded-xl p-10 text-center cursor-pointer hover:border-green-500 transition-colors"
|
||||
>
|
||||
<svg class="w-10 h-10 mx-auto text-gray-400 mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"></path>
|
||||
<svg class="w-14 h-14 mx-auto text-gray-400 mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
||||
</svg>
|
||||
<p class="text-white mb-1">Import Folder</p>
|
||||
<p class="text-sm text-gray-500">Select a folder containing SKILL.md</p>
|
||||
<p class="text-white text-lg mb-1">Import SKILL.md</p>
|
||||
<p class="text-sm text-gray-500">Select a SKILL.md file</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -40,8 +40,9 @@ export function useSkills() {
|
||||
|
||||
// 编辑状态
|
||||
const isEditing = ref(false)
|
||||
const isEditingContent = ref(false) // 创建弹窗的第二步
|
||||
const isEditingStep2 = ref(false) // 编辑弹窗的第二步
|
||||
const isCreating = ref(false)
|
||||
const isEditingContent = ref(false)
|
||||
const editingSkill = ref<Skill | null>(null)
|
||||
|
||||
// 表单
|
||||
@@ -58,6 +59,7 @@ export function useSkills() {
|
||||
})
|
||||
|
||||
const newSkillContent = ref('')
|
||||
const editSkillContent = ref('') // 编辑弹窗第二步的 SKILL.md 内容
|
||||
|
||||
// ============ 方法 ============
|
||||
|
||||
@@ -206,38 +208,152 @@ ${newSkillForm.value.skill_desc}
|
||||
}
|
||||
}
|
||||
|
||||
// 打开编辑弹窗
|
||||
const openEdit = (skill: Skill) => {
|
||||
// 打开编辑弹窗(第一步)
|
||||
const openEdit = async (skill: Skill) => {
|
||||
editingSkill.value = skill
|
||||
editForm.value = {
|
||||
skill_name: skill.skill_name,
|
||||
skill_desc: skill.skill_desc,
|
||||
skill_type: skill.skill_type,
|
||||
}
|
||||
|
||||
// 读取现有的 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:
|
||||
\`\`\`
|
||||
`
|
||||
}
|
||||
|
||||
isEditing.value = true
|
||||
}
|
||||
|
||||
// 关闭编辑弹窗
|
||||
const closeEdit = () => {
|
||||
isEditing.value = false
|
||||
isEditingStep2.value = false
|
||||
editingSkill.value = null
|
||||
editSkillContent.value = ''
|
||||
}
|
||||
|
||||
// 保存编辑
|
||||
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')
|
||||
// 编辑第一步:点击 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
|
||||
}
|
||||
|
||||
// 编辑第二步:保存
|
||||
const saveEditStep2 = async () => {
|
||||
if (!editingSkill.value) return
|
||||
|
||||
try {
|
||||
// 将内容转换为文件上传
|
||||
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,
|
||||
})
|
||||
|
||||
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')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to update skill:', error)
|
||||
ElMessage.error('Failed to update skill')
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭编辑第二步弹窗
|
||||
const closeEditStep2 = () => {
|
||||
isEditingStep2.value = false
|
||||
isEditing.value = true
|
||||
}
|
||||
|
||||
// 切换状态
|
||||
const toggleStatus = async (skill: Skill) => {
|
||||
const newStatus = skill.status === 'active' ? 'inactive' : 'active'
|
||||
@@ -384,7 +500,7 @@ ${newSkillForm.value.skill_desc}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理文件夹选择
|
||||
// 处理文件选择(单个 SKILL.md 文件)
|
||||
const handleFolderSelect = async (event: Event) => {
|
||||
const input = event.target as HTMLInputElement
|
||||
const files = input.files
|
||||
@@ -394,25 +510,13 @@ ${newSkillForm.value.skill_desc}
|
||||
isImporting.value = true
|
||||
|
||||
try {
|
||||
const folder = files[0].webkitRelativePath?.split('/')[0] || files[0].name
|
||||
const file = files[0]
|
||||
|
||||
// 查找 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
|
||||
}
|
||||
}
|
||||
// 读取文件内容
|
||||
const content = await file.text()
|
||||
const fileName = file.name.replace('.md', '')
|
||||
|
||||
if (!skillMdFile) {
|
||||
ElMessage.error('导入失败:所选文件夹中未找到 SKILL.md 文件')
|
||||
return
|
||||
}
|
||||
|
||||
const content = await skillMdFile.text()
|
||||
const { skillName, skillDesc } = parseSkillContent(content, folder)
|
||||
const { skillName, skillDesc } = parseSkillContent(content, fileName)
|
||||
|
||||
// 使用导入弹窗显示内容
|
||||
importSkillName.value = skillName
|
||||
@@ -422,7 +526,7 @@ ${newSkillForm.value.skill_desc}
|
||||
isImportStep2.value = true
|
||||
} catch (error) {
|
||||
console.error('Import failed:', error)
|
||||
ElMessage.error('导入失败,请检查文件夹格式是否正确')
|
||||
ElMessage.error('导入失败,请检查文件格式是否正确')
|
||||
} finally {
|
||||
isImporting.value = false
|
||||
input.value = ''
|
||||
@@ -453,12 +557,14 @@ ${newSkillForm.value.skill_desc}
|
||||
searchQuery,
|
||||
filterStatus,
|
||||
isEditing,
|
||||
isEditingStep2,
|
||||
isCreating,
|
||||
isEditingContent,
|
||||
editingSkill,
|
||||
editForm,
|
||||
newSkillForm,
|
||||
newSkillContent,
|
||||
editSkillContent,
|
||||
// Computed
|
||||
filteredSkills,
|
||||
// Methods
|
||||
@@ -470,7 +576,9 @@ ${newSkillForm.value.skill_desc}
|
||||
saveNewSkill,
|
||||
openEdit,
|
||||
closeEdit,
|
||||
saveEdit,
|
||||
goToEditStep2,
|
||||
saveEditStep2,
|
||||
closeEditStep2,
|
||||
toggleStatus,
|
||||
deleteSkill: handleDeleteSkill,
|
||||
// 导入相关
|
||||
|
||||
Reference in New Issue
Block a user