diff --git a/server/routes/company.js b/server/routes/company.js index 2c1b993..068ab20 100644 --- a/server/routes/company.js +++ b/server/routes/company.js @@ -47,12 +47,12 @@ router.get('/templates', (req, res) => { res.json(companyService.listTemplates()); }); -// PATCH /api/company/templates/:templateId — 修改模板元信息 (如名称) +// PATCH /api/company/templates/:templateId — 修改模板元信息 (名称, 类型, 描述) router.patch('/templates/:templateId', (req, res) => { try { const { templateId } = req.params; - const { name } = req.body; - if (!name || !name.trim()) return res.status(400).json({ error: 'name 为必填项' }); + const { name, type, description } = req.body; + if (name !== undefined && !name.trim()) return res.status(400).json({ error: 'name 为必填项' }); const path = require('path'); const fs = require('fs'); @@ -63,29 +63,66 @@ router.patch('/templates/:templateId', (req, res) => { return res.status(404).json({ error: `模板 "${templateId}" 不存在` }); } - // 1. 更新 config.json 中的 name + // 1. 更新 config.json 中的字段 const configPath = path.join(templateDir, 'config.json'); if (fs.existsSync(configPath)) { const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); - config.name = name.trim(); + if (name !== undefined) config.name = name.trim(); + if (type !== undefined) config.type = type.trim(); + if (description !== undefined) config.description = description.trim(); fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8'); } - // 2. 更新 _registry.json 中的 name + // 2. 更新 _registry.json 中的字段 const registryPath = path.join(TEMPLATES_DIR, '_registry.json'); if (fs.existsSync(registryPath)) { try { const registry = JSON.parse(fs.readFileSync(registryPath, 'utf-8')); const entry = registry.find(t => t.id === templateId); if (entry) { - entry.name = name.trim(); + if (name !== undefined) entry.name = name.trim(); + if (type !== undefined) entry.type = type.trim(); + if (description !== undefined) entry.description = description.trim(); fs.writeFileSync(registryPath, JSON.stringify(registry, null, 2), 'utf-8'); } } catch {} } - console.log(`[Template] ✏️ 模板 ${templateId} 名称已更新为: ${name.trim()}`); - res.json({ success: true, id: templateId, name: name.trim() }); + console.log(`[Template] ✏️ 模板 ${templateId} 元信息已更新`); + res.json({ success: true, id: templateId, name: name?.trim(), type: type?.trim(), description: description?.trim() }); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +// DELETE /api/company/templates/:templateId — 删除模板 +router.delete('/templates/:templateId', (req, res) => { + try { + const { templateId } = req.params; + const path = require('path'); + const fs = require('fs'); + const TEMPLATES_DIR = path.resolve(__dirname, '../../system/templates'); + const templateDir = path.join(TEMPLATES_DIR, templateId); + + if (!fs.existsSync(templateDir)) { + return res.status(404).json({ error: `模板 "${templateId}" 不存在` }); + } + + // 1. 删除模版文件夹 + fs.rmSync(templateDir, { recursive: true, force: true }); + + // 2. 更新 registry + const registryPath = path.join(TEMPLATES_DIR, '_registry.json'); + if (fs.existsSync(registryPath)) { + try { + let registry = JSON.parse(fs.readFileSync(registryPath, 'utf-8')); + registry = registry.filter(t => t.id !== templateId); + fs.writeFileSync(registryPath, JSON.stringify(registry, null, 2), 'utf-8'); + } catch {} + } + + console.log(`[Template] 🗑️ 模板 ${templateId} 已被删除`); + res.json({ success: true, message: `模板 ${templateId} 已删除` }); } catch (err) { res.status(500).json({ error: err.message }); } diff --git a/web/src/views/TemplateAgents.vue b/web/src/views/TemplateAgents.vue index c2325b0..97c6f2c 100644 --- a/web/src/views/TemplateAgents.vue +++ b/web/src/views/TemplateAgents.vue @@ -96,8 +96,33 @@ #{{ selectedTemplate.id }} +
+ + +
- {{ selectedTemplate.type }} + + 被使用: {{ selectedTemplate.usedCount }} 次 评分: {{ selectedTemplate.benchmarkScore }} 训练于: {{ selectedTemplate.trainedAt }} @@ -111,6 +136,9 @@
+ @@ -241,6 +269,13 @@ const editingName = ref(false) const editNameValue = ref('') const nameInputRef = ref(null) +// Inline type/desc editing +const editingType = ref(false) +const editTypeValue = ref('') +const typeSelectRef = ref(null) +const editingDesc = ref(false) +const editDescValue = ref('') + const fileDirty = computed(() => editFileContent.value !== null && filePreviewContent.value !== null && editFileContent.value !== filePreviewContent.value) function toggleSortDir() { @@ -426,6 +461,79 @@ async function saveTemplateName() { } } +// ─── Inline Type / Desc Editing ─── + +function startEditType() { + if (!selectedTemplate.value) return; + editTypeValue.value = selectedTemplate.value.type; + editingType.value = true; + nextTick(() => { typeSelectRef.value?.focus() }); +} + +function cancelEditType() { + editingType.value = false; +} + +async function saveTemplateType() { + if (!editingType.value) return; + const newType = editTypeValue.value.trim(); + if (!newType || newType === selectedTemplate.value?.type) { + cancelEditType(); + return; + } + try { + await axios.patch(`/api/company/templates/${selectedTemplateId.value}`, { type: newType }); + const tpl = templates.value.find(t => t.id === selectedTemplateId.value); + if (tpl) tpl.type = newType; + editingType.value = false; + } catch (e) { + alert(`修改类型失败: ${e.response?.data?.error || e.message}`); + } +} + +function startEditDesc() { + if (!selectedTemplate.value) return; + editDescValue.value = selectedTemplate.value.description || ''; + editingDesc.value = true; +} + +function cancelEditDesc() { + editingDesc.value = false; +} + +async function saveTemplateDesc() { + if (!editingDesc.value) return; + const newDesc = editDescValue.value.trim(); + if (newDesc === selectedTemplate.value?.description) { + cancelEditDesc(); + return; + } + try { + await axios.patch(`/api/company/templates/${selectedTemplateId.value}`, { description: newDesc }); + const tpl = templates.value.find(t => t.id === selectedTemplateId.value); + if (tpl) tpl.description = newDesc; + editingDesc.value = false; + } catch (e) { + alert(`修改描述失败: ${e.response?.data?.error || e.message}`); + } +} + +async function deleteTemplate() { + if (!selectedTemplate.value) return; + if (!confirm(`⚠️ 确定要彻底删除 Agent 骨架 "${selectedTemplate.value.name || selectedTemplate.value.id}" 吗?此操作将删除所有关单文件,并且无法恢复!`)) return; + try { + actionStatus.value = 'uploading'; // reuse this state to show loading + actionMessage.value = '正在删除模板...'; + await axios.delete(`/api/company/templates/${selectedTemplateId.value}`); + actionStatus.value = null; + selectedTemplateId.value = null; + await loadTemplates(); + } catch (e) { + actionStatus.value = 'error'; + actionMessage.value = `删除失败: ${e.response?.data?.error || e.message}`; + } +} + // ─── Agent Creator & Innovator ─── async function triggerGenerate() { @@ -785,6 +893,19 @@ onMounted(() => { .action-upload:hover:not(:disabled) { background: rgba(245,158,11,0.2); box-shadow: 0 3px 10px rgba(245,158,11,0.15); } .action-provision { background: rgba(20,184,166,0.1); color: #14b8a6; border-color: rgba(20,184,166,0.2); } .action-provision:hover:not(:disabled) { background: rgba(20,184,166,0.2); box-shadow: 0 3px 10px rgba(20,184,166,0.15); } +.action-delete { background: rgba(239, 68, 68, 0.1); color: var(--red-text, #ef4444); border-color: rgba(239, 68, 68, 0.2); } +.action-delete:hover:not(:disabled) { background: rgba(239, 68, 68, 0.2); box-shadow: 0 3px 10px rgba(239, 68, 68, 0.15); } + +/* Field Editing overrides */ +.template-desc-edit { margin-bottom: 12px; margin-top: -2px; } +.desc-display { font-size: 13px; color: var(--text-secondary); cursor: pointer; display: inline-flex; align-items: flex-start; gap: 6px; } +.desc-display:hover .name-edit-hint { opacity: 0.6; } +.desc-display:hover { color: var(--text-primary); } +.desc-edit-input { width: 100%; max-width: 500px; display: block; margin-bottom: 8px; min-height: 60px; font-size: 13px; } +.desc-edit-actions { display: flex; gap: 6px; } +.editable-badge { cursor: pointer; transition: all 0.2s; display: inline-flex; align-items: center; gap: 4px; } +.editable-badge:hover { background: var(--bg-input-focus); border-color: var(--accent); color: var(--accent); } +.type-edit-select { width: 140px; } /* Status Banner */ .status-banner {