- 微调多个页面的样式和布局 - 保持功能不变 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1258 lines
57 KiB
Vue
1258 lines
57 KiB
Vue
<script setup lang="ts">
|
||
import { ref, onMounted, computed } from 'vue'
|
||
import { useModelSettings } from './settings/useModelSettings'
|
||
import { useSkillServers } from './skill/useSkillServers'
|
||
import { useFileTree } from './skill/useFileTree'
|
||
import { useSkillChat } from './skill/useSkillChat'
|
||
import { useWorkflow } from './skill/useWorkflow'
|
||
import type { FileItem } from './skill/types'
|
||
|
||
// MCP Server management
|
||
const {
|
||
mcpServers,
|
||
editingServer,
|
||
isEditing,
|
||
searchQuery,
|
||
filterStatus,
|
||
editForm,
|
||
openEdit,
|
||
saveEdit,
|
||
cancelEdit,
|
||
toggleStatus,
|
||
deleteServer,
|
||
filteredServers,
|
||
statusClass,
|
||
} = useSkillServers()
|
||
|
||
// File tree management
|
||
const {
|
||
fileTree,
|
||
rootFiles,
|
||
inlineNewFolderName,
|
||
inlineCreatingFolderId,
|
||
inlineNewFileName,
|
||
inlineCreatingFileId,
|
||
isEditingFile,
|
||
isEditingFileClosing,
|
||
editingFile,
|
||
editingFileName,
|
||
editingFileContent,
|
||
editingFileType,
|
||
hoveringItem,
|
||
selectedFolder,
|
||
toggleFolder,
|
||
showInlineNewFolderInput,
|
||
confirmInlineCreateFolder,
|
||
cancelInlineCreateFolder,
|
||
findFolderById,
|
||
showInlineNewFileInput,
|
||
confirmInlineCreateFile,
|
||
cancelInlineCreateFile,
|
||
getFileIconByName,
|
||
handleFileUpload,
|
||
findFile,
|
||
selectFolder,
|
||
deleteFile,
|
||
deleteFolder,
|
||
openFile,
|
||
saveFileEdit,
|
||
cancelFileEdit,
|
||
} = useFileTree()
|
||
|
||
// Chat functionality
|
||
const {
|
||
isChatOpen,
|
||
chatInput,
|
||
chatMessages,
|
||
toggleChat,
|
||
sendMessage,
|
||
} = useSkillChat()
|
||
|
||
// Workflow visualization
|
||
const {
|
||
workflowData,
|
||
isGeneratingGraph,
|
||
isGraphGenerated,
|
||
toggleNode,
|
||
renderWorkflow,
|
||
generateGraph,
|
||
} = useWorkflow()
|
||
|
||
// 新建 Skill
|
||
const isCreating = ref(false)
|
||
const createStep = ref(1)
|
||
const newSkillForm = ref({
|
||
name: '',
|
||
type: 'API',
|
||
provider: 'Custom',
|
||
port: 3000,
|
||
description: '',
|
||
enabled: true,
|
||
model: 'gpt-4o',
|
||
})
|
||
|
||
// 从 Model Settings 获取模型
|
||
const { models, fetchModels } = useModelSettings()
|
||
|
||
const availableModels = computed(() => {
|
||
return models.value
|
||
.filter((m: any) => m.model_type === 'chat')
|
||
.map((m: any) => ({
|
||
name: m.model,
|
||
provider: m.provider,
|
||
icon: 'fa-brain'
|
||
}))
|
||
})
|
||
|
||
onMounted(() => {
|
||
fetchModels()
|
||
})
|
||
|
||
const goToStep2 = () => {
|
||
if (!newSkillForm.value.name.trim()) return
|
||
createStep.value = 2
|
||
isGraphGenerated.value = false
|
||
isGeneratingGraph.value = false
|
||
}
|
||
|
||
const goBackToStep1 = () => {
|
||
createStep.value = 1
|
||
}
|
||
|
||
const openCreate = () => {
|
||
newSkillForm.value = {
|
||
name: '',
|
||
type: 'API',
|
||
provider: 'Custom',
|
||
port: 3000,
|
||
description: '',
|
||
enabled: true,
|
||
model: 'gpt-4o',
|
||
}
|
||
createStep.value = 1
|
||
isCreating.value = true
|
||
}
|
||
|
||
const closeCreate = () => {
|
||
isCreating.value = false
|
||
createStep.value = 1
|
||
}
|
||
|
||
const saveNewSkill = () => {
|
||
const newId = Math.max(...mcpServers.value.map(s => s.id)) + 1
|
||
mcpServers.value.push({
|
||
id: newId,
|
||
name: newSkillForm.value.name || 'Untitled Skill',
|
||
status: 'stopped',
|
||
type: newSkillForm.value.type,
|
||
port: newSkillForm.value.port,
|
||
createdAt: new Date().toISOString().split('T')[0],
|
||
description: newSkillForm.value.description,
|
||
})
|
||
isCreating.value = false
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
/* 模态框进入动画 */
|
||
@keyframes modal-in {
|
||
0% {
|
||
opacity: 0;
|
||
transform: scale(0.95) translateY(20px);
|
||
}
|
||
100% {
|
||
opacity: 1;
|
||
transform: scale(1) translateY(0);
|
||
}
|
||
}
|
||
|
||
@keyframes fade-in {
|
||
0% { opacity: 0; transform: translateY(-5px); }
|
||
100% { opacity: 1; transform: translateY(0); }
|
||
}
|
||
|
||
@keyframes float {
|
||
0%, 100% { transform: translateY(0); }
|
||
50% { transform: translateY(-8px); }
|
||
}
|
||
|
||
@keyframes scale-in {
|
||
0% { opacity: 0; transform: scale(0.9); }
|
||
100% { opacity: 1; transform: scale(1); }
|
||
}
|
||
|
||
.animate-modal-in {
|
||
animation: modal-in 0.4s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
||
}
|
||
|
||
@keyframes modal-out {
|
||
0% { opacity: 1; transform: scale(1); }
|
||
100% { opacity: 0; transform: scale(0.95); }
|
||
}
|
||
|
||
.animate-modal-out {
|
||
animation: modal-out 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
||
}
|
||
|
||
@keyframes slide-in {
|
||
0% { transform: translateX(100%); opacity: 0; }
|
||
100% { transform: translateX(0); opacity: 1; }
|
||
}
|
||
|
||
@keyframes slide-out {
|
||
0% { transform: translateX(0); opacity: 1; }
|
||
100% { transform: translateX(100%); opacity: 0; }
|
||
}
|
||
|
||
.animate-slide-in {
|
||
animation: slide-in 0.3s ease-out forwards;
|
||
}
|
||
|
||
@keyframes progress-width {
|
||
0% { width: 0%; }
|
||
100% { width: 100%; }
|
||
}
|
||
|
||
.animate-progress-width {
|
||
animation: progress-width 2.5s ease-in-out forwards;
|
||
}
|
||
|
||
@keyframes spin-slow {
|
||
0% { transform: rotate(0deg); }
|
||
100% { transform: rotate(360deg); }
|
||
}
|
||
|
||
.animate-spin-slow {
|
||
animation: spin-slow 20s linear infinite;
|
||
}
|
||
|
||
@keyframes pulse-slow {
|
||
0%, 100% { transform: scale(1); opacity: 1; }
|
||
50% { transform: scale(1.1); opacity: 0.8; }
|
||
}
|
||
|
||
.animate-pulse-slow {
|
||
animation: pulse-slow 2s ease-in-out infinite;
|
||
}
|
||
|
||
@keyframes float-particle {
|
||
0%, 100% { transform: translateY(0) scale(1); opacity: 0.8; }
|
||
50% { transform: translateY(-10px) scale(1.2); opacity: 1; }
|
||
}
|
||
|
||
.animate-float-particle {
|
||
animation: float-particle 3s ease-in-out infinite;
|
||
}
|
||
|
||
@keyframes scan {
|
||
0% { transform: translateX(-100%); }
|
||
100% { transform: translateX(200%); }
|
||
}
|
||
|
||
.animate-scan {
|
||
animation: scan 1.5s ease-in-out infinite;
|
||
}
|
||
|
||
.animate-fade-in {
|
||
animation: fade-in 0.3s ease-out forwards;
|
||
}
|
||
|
||
.animate-float {
|
||
animation: float 3s ease-in-out infinite;
|
||
}
|
||
|
||
.animate-scale-in {
|
||
animation: scale-in 0.5s ease-out forwards;
|
||
}
|
||
|
||
/* 流程节点动画 */
|
||
.graph-node {
|
||
transition: transform 0.3s ease;
|
||
}
|
||
|
||
.graph-node:hover {
|
||
transform: scale(1.1);
|
||
}
|
||
|
||
.center-node {
|
||
transition: transform 0.3s ease;
|
||
}
|
||
|
||
.center-node:hover {
|
||
transform: scale(1.05);
|
||
}
|
||
|
||
.animate-pulse-slow {
|
||
animation: pulse-slow 3s ease-in-out infinite;
|
||
}
|
||
|
||
@keyframes pulse-slow {
|
||
0%, 100% { box-shadow: 0 0 20px rgba(30, 107, 249, 0.3); }
|
||
50% { box-shadow: 0 0 40px rgba(30, 107, 249, 0.5); }
|
||
}
|
||
|
||
@keyframes draw-line {
|
||
0% { stroke-dashoffset: 100; }
|
||
100% { stroke-dashoffset: 0; }
|
||
}
|
||
|
||
.animate-draw-line {
|
||
stroke-dasharray: 100;
|
||
stroke-dashoffset: 100;
|
||
animation: draw-line 1.5s ease forwards;
|
||
}
|
||
|
||
</style>
|
||
|
||
<template>
|
||
<div class="p-6 min-h-screen">
|
||
<div class="flex justify-between items-center mb-6">
|
||
<div class="flex items-center gap-2">
|
||
<i class="fa-solid fa-wand-magic-sparkles text-orange-500"></i>
|
||
<span class="font-medium">Skills</span>
|
||
</div>
|
||
<button @click="openCreate" class="bg-gradient-to-r from-primary-orange to-red-500 hover:from-orange-500 hover:to-red-600 text-white px-4 py-2 rounded-lg font-medium flex items-center gap-2 transition-all">
|
||
<i class="fa-solid fa-plus"></i>
|
||
New Skill
|
||
</button>
|
||
</div>
|
||
|
||
<div class="flex gap-4 mb-6">
|
||
<div class="flex-1 relative">
|
||
<i class="fa-solid fa-search absolute left-3 top-1/2 -translate-y-1/2 text-gray-400"></i>
|
||
<input
|
||
v-model="searchQuery"
|
||
type="text"
|
||
placeholder="Search skills..."
|
||
class="w-full bg-dark-600 border border-dark-500 rounded-lg py-2 pl-10 pr-4 text-white placeholder-gray-500 focus:outline-none focus:border-primary-orange"
|
||
>
|
||
</div>
|
||
<el-select v-model="filterStatus" placeholder="Select" class="w-40" size="large">
|
||
<el-option label="All Status" value="all" />
|
||
<el-option label="Running" value="running" />
|
||
<el-option label="Stopped" value="stopped" />
|
||
<el-option label="Error" value="error" />
|
||
</el-select>
|
||
</div>
|
||
|
||
<div class="bg-dark-700 rounded-xl overflow-hidden">
|
||
<table class="w-full">
|
||
<thead class="bg-dark-600">
|
||
<tr>
|
||
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Skill Name</th>
|
||
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Type</th>
|
||
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Port</th>
|
||
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Status</th>
|
||
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Created</th>
|
||
<th class="text-right px-5 py-3 text-sm font-medium text-gray-400">Actions</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="server in filteredServers()" :key="server.id" class="border-t border-dark-600 hover:bg-dark-600/50 transition-colors">
|
||
<td class="px-5 py-4">
|
||
<div class="font-medium">{{ server.name }}</div>
|
||
<div class="text-sm text-gray-500">{{ server.description }}</div>
|
||
</td>
|
||
<td class="px-5 py-4">
|
||
<span class="bg-dark-500 px-2 py-1 rounded text-sm">{{ server.type }}</span>
|
||
</td>
|
||
<td class="px-5 py-4 text-gray-300">{{ server.port }}</td>
|
||
<td class="px-5 py-4">
|
||
<div class="flex items-center gap-2">
|
||
<span class="w-2 h-2 rounded-full" :class="statusClass(server.status)"></span>
|
||
<span class="capitalize text-sm">{{ server.status }}</span>
|
||
</div>
|
||
</td>
|
||
<td class="px-5 py-4 text-gray-400 text-sm">{{ server.createdAt }}</td>
|
||
<td class="px-5 py-4">
|
||
<div class="flex items-center justify-end gap-2">
|
||
<button
|
||
@click="toggleStatus(server)"
|
||
class="p-2 rounded-lg hover:bg-dark-500 transition-colors"
|
||
:title="server.status === 'running' ? 'Stop' : 'Start'"
|
||
>
|
||
<i :class="['fa-solid', server.status === 'running' ? 'fa-stop' : 'fa-play', 'text-gray-400 hover:text-white']"></i>
|
||
</button>
|
||
<button
|
||
@click="openEdit(server)"
|
||
class="p-2 rounded-lg hover:bg-dark-500 transition-colors"
|
||
title="Edit"
|
||
>
|
||
<i class="fa-solid fa-pen text-gray-400 hover:text-white"></i>
|
||
</button>
|
||
<button
|
||
@click="deleteServer(server.id)"
|
||
class="p-2 rounded-lg hover:bg-dark-500 transition-colors"
|
||
title="Delete"
|
||
>
|
||
<i class="fa-solid fa-trash text-gray-400 hover:text-primary-danger"></i>
|
||
</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
|
||
<div v-if="filteredServers().length === 0" class="py-12 text-center text-gray-500">
|
||
<i class="fa-solid fa-code text-4xl mb-3"></i>
|
||
<p>No skills found</p>
|
||
</div>
|
||
</div>
|
||
|
||
<Teleport to="body">
|
||
<div v-if="isEditing" class="fixed inset-0 bg-black/60 flex items-center justify-center z-50">
|
||
<div class="bg-dark-700 rounded-2xl w-full max-w-lg border border-dark-500 shadow-2xl">
|
||
<div class="flex items-center justify-between p-5 border-b border-dark-500">
|
||
<h3 class="text-lg font-semibold">Edit Skill</h3>
|
||
<button @click="cancelEdit" class="text-gray-400 hover:text-white transition-colors">
|
||
<i class="fa-solid fa-xmark text-xl"></i>
|
||
</button>
|
||
</div>
|
||
|
||
<div class="p-5 space-y-4">
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-300 mb-2">Skill Name</label>
|
||
<input
|
||
v-model="editForm.name"
|
||
type="text"
|
||
class="w-full bg-dark-600 border border-dark-500 rounded-lg px-4 py-2.5 text-white focus:outline-none focus:border-primary-orange"
|
||
>
|
||
</div>
|
||
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-300 mb-2">Type</label>
|
||
<el-select v-model="editForm.type" placeholder="Select" class="w-full" size="large">
|
||
<el-option label="Linear" value="Linear" />
|
||
<el-option label="Google Maps" value="Google Maps" />
|
||
<el-option label="File System" value="File System" />
|
||
<el-option label="PostgreSQL" value="PostgreSQL" />
|
||
<el-option label="GitHub" value="GitHub" />
|
||
</el-select>
|
||
</div>
|
||
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-300 mb-2">Port</label>
|
||
<input
|
||
v-model="editForm.port"
|
||
type="number"
|
||
class="w-full bg-dark-600 border border-dark-500 rounded-lg px-4 py-2.5 text-white focus:outline-none focus:border-primary-orange"
|
||
>
|
||
</div>
|
||
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-300 mb-2">Description</label>
|
||
<textarea
|
||
v-model="editForm.description"
|
||
rows="3"
|
||
class="w-full bg-dark-600 border border-dark-500 rounded-lg px-4 py-2.5 text-white focus:outline-none focus:border-primary-orange resize-none"
|
||
></textarea>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="flex items-center justify-end gap-3 p-5 border-t border-dark-500">
|
||
<button
|
||
@click="cancelEdit"
|
||
class="px-4 py-2 rounded-lg bg-dark-600 text-gray-300 hover:bg-dark-500 transition-colors"
|
||
>
|
||
Cancel
|
||
</button>
|
||
<button
|
||
@click="saveEdit"
|
||
class="px-4 py-2 rounded-lg bg-gradient-to-r from-primary-orange to-red-500 text-white hover:from-orange-500 hover:to-red-600 transition-all"
|
||
>
|
||
Save Changes
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Teleport>
|
||
|
||
<!-- 新建 Skill 模态框 -->
|
||
<Teleport to="body">
|
||
<div v-if="isCreating" class="fixed inset-0 bg-black/80 flex items-center justify-center z-50 p-6">
|
||
<!-- 步骤1:基本信息 -->
|
||
<div v-if="createStep === 1" class="bg-dark-800 rounded-2xl w-full max-w-lg border border-dark-600 shadow-2xl overflow-hidden animate-modal-in">
|
||
<!-- 头部 -->
|
||
<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-primary-orange to-red-500 flex items-center justify-center">
|
||
<i class="fa-solid fa-wand-magic-sparkles text-white"></i>
|
||
</div>
|
||
<div>
|
||
<h3 class="text-xl font-semibold text-white">Create New Skill</h3>
|
||
<p class="text-sm text-gray-400">Step 1 of 2 - Basic Info</p>
|
||
</div>
|
||
</div>
|
||
<button @click="closeCreate" class="text-gray-400 hover:text-white transition-all p-2 hover:bg-dark-600 rounded-lg">
|
||
<i class="fa-solid fa-xmark text-xl"></i>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 表单内容 -->
|
||
<div class="p-6 space-y-5">
|
||
<!-- Skill Name -->
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-300 mb-2">
|
||
<i class="fa-solid fa-tag mr-2 text-primary-orange"></i>Skill Name
|
||
</label>
|
||
<input
|
||
v-model="newSkillForm.name"
|
||
type="text"
|
||
placeholder="Enter skill name..."
|
||
class="w-full bg-dark-600 border border-dark-500 rounded-xl px-4 py-3 text-white placeholder-gray-500 focus:outline-none focus:border-primary-orange transition-colors"
|
||
>
|
||
</div>
|
||
|
||
<!-- Description -->
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-300 mb-2">
|
||
<i class="fa-solid fa-align-left mr-2 text-gray-400"></i>Description
|
||
</label>
|
||
<textarea
|
||
v-model="newSkillForm.description"
|
||
rows="3"
|
||
placeholder="Describe your skill's purpose..."
|
||
class="w-full bg-dark-600 border border-dark-500 rounded-xl px-4 py-3 text-white placeholder-gray-500 focus:outline-none focus:border-primary-orange transition-colors resize-none"
|
||
></textarea>
|
||
</div>
|
||
|
||
<!-- Model Selection -->
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-300 mb-2">
|
||
<i class="fa-solid fa-brain mr-2 text-primary-cyan"></i>Select Model
|
||
</label>
|
||
<el-select
|
||
v-model="newSkillForm.model"
|
||
placeholder="Select a model"
|
||
class="w-full"
|
||
size="large"
|
||
popper-class="dark-select-dropdown"
|
||
>
|
||
<el-option
|
||
v-for="model in availableModels"
|
||
:key="model.name"
|
||
:label="`${model.name} (${model.provider})`"
|
||
:value="model.name"
|
||
>
|
||
<div class="flex items-center gap-2">
|
||
<i :class="['fa-solid', model.icon, 'text-primary-cyan']"></i>
|
||
<span class="text-white">{{ model.name }}</span>
|
||
<span class="text-gray-500 text-sm">({{ model.provider }})</span>
|
||
</div>
|
||
</el-option>
|
||
</el-select>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 底部按钮 -->
|
||
<div class="flex items-center justify-end gap-3 p-5 border-t border-dark-600 bg-dark-700/50">
|
||
<button
|
||
@click="closeCreate"
|
||
class="px-5 py-2.5 rounded-xl bg-dark-600 text-gray-300 hover:bg-dark-500 border border-dark-500 transition-all"
|
||
>
|
||
Cancel
|
||
</button>
|
||
<button
|
||
@click="goToStep2"
|
||
:disabled="!newSkillForm.name.trim()"
|
||
class="px-6 py-2.5 rounded-xl bg-gradient-to-r from-primary-orange to-red-500 text-white hover:from-orange-500 hover:to-red-600 transition-all disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
|
||
>
|
||
Next Step
|
||
<i class="fa-solid fa-arrow-right"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 步骤2:三栏配置界面 -->
|
||
<div v-else class="bg-dark-800 rounded-2xl w-full max-w-[95vw] h-[90vh] border border-dark-600 shadow-2xl overflow-hidden flex animate-modal-in relative">
|
||
<!-- 顶部进度指示 -->
|
||
<div class="absolute top-0 left-0 right-0 h-1 bg-dark-700">
|
||
<div class="h-full bg-gradient-to-r from-primary-orange to-red-500 w-full"></div>
|
||
</div>
|
||
|
||
<!-- 左侧边栏:VS Code 风格文件管理器 -->
|
||
<div class="w-64 bg-dark-900 border-r border-dark-600 flex flex-col">
|
||
<!-- 头部 -->
|
||
<div class="p-3 border-b border-dark-600 flex items-center justify-between">
|
||
<button
|
||
@click="goBackToStep1"
|
||
class="flex items-center gap-2 text-gray-400 hover:text-white transition-colors"
|
||
>
|
||
<i class="fa-solid fa-arrow-left text-sm"></i>
|
||
<span class="font-semibold text-white text-sm">Explorer</span>
|
||
</button>
|
||
<div class="flex items-center gap-1">
|
||
<button @click="showInlineNewFolderInput(null)" class="p-1.5 text-gray-500 hover:text-white hover:bg-dark-700 rounded transition-colors" title="New Folder">
|
||
<i class="fa-solid fa-folder-plus text-sm"></i>
|
||
</button>
|
||
<button @click="showInlineNewFileInput(null)" class="p-1.5 text-gray-500 hover:text-white hover:bg-dark-700 rounded transition-colors" title="New File">
|
||
<i class="fa-solid fa-file-circle-plus text-sm"></i>
|
||
</button>
|
||
<label class="p-1.5 text-gray-500 hover:text-white hover:bg-dark-700 rounded transition-colors cursor-pointer" title="Upload File">
|
||
<i class="fa-solid fa-upload text-sm"></i>
|
||
<input type="file" class="hidden" multiple @change="handleFileUpload($event, null)">
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- VS Code 风格文件树 -->
|
||
<div class="flex-1 overflow-y-auto py-2">
|
||
<!-- 根目录文件 -->
|
||
<div class="px-2">
|
||
<div class="text-xs font-semibold text-gray-500 uppercase tracking-wider px-2 py-1">Workspace</div>
|
||
<div
|
||
v-for="file in rootFiles"
|
||
:key="file.id"
|
||
@click="openFile(file)"
|
||
@mouseenter="hoveringItem = file.id"
|
||
@mouseleave="hoveringItem = null"
|
||
class="flex items-center gap-2 px-2 py-1.5 rounded cursor-pointer hover:bg-dark-700 group transition-colors"
|
||
:class="{ 'bg-dark-700': hoveringItem === file.id }"
|
||
>
|
||
<i :class="['fa-solid', file.icon, 'text-primary-cyan text-sm']"></i>
|
||
<span class="text-sm text-gray-300 group-hover:text-white truncate flex-1">{{ file.name }}</span>
|
||
<button
|
||
@click.stop="deleteFile(file, null)"
|
||
class="text-red-400 hover:text-red-300 p-1 rounded hover:bg-dark-600 transition-colors opacity-0 group-hover:opacity-100"
|
||
title="Delete File"
|
||
>
|
||
<i class="fa-solid fa-trash text-xs"></i>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 根目录新建文件夹/文件按钮 -->
|
||
<div v-if="inlineCreatingFolderId === 'root'" class="flex items-center gap-2 px-2 py-1">
|
||
<i class="fa-solid fa-folder text-yellow-400 text-sm"></i>
|
||
<input
|
||
v-model="inlineNewFolderName"
|
||
type="text"
|
||
placeholder="Folder name"
|
||
class="flex-1 bg-dark-700 border border-dark-500 rounded px-2 py-0.5 text-sm text-white placeholder-gray-500 focus:outline-none focus:border-primary-cyan"
|
||
@keyup.enter="confirmInlineCreateFolder"
|
||
@blur="confirmInlineCreateFolder"
|
||
@keyup.escape="cancelInlineCreateFolder"
|
||
autofocus
|
||
>
|
||
</div>
|
||
<div v-else-if="inlineCreatingFileId === 'root'" class="flex items-center gap-2 px-2 py-1">
|
||
<i class="fa-solid fa-file text-gray-400 text-sm"></i>
|
||
<input
|
||
v-model="inlineNewFileName"
|
||
type="text"
|
||
placeholder="File name"
|
||
class="flex-1 bg-dark-700 border border-dark-500 rounded px-2 py-0.5 text-sm text-white placeholder-gray-500 focus:outline-none focus:border-primary-cyan"
|
||
@keyup.enter="confirmInlineCreateFile"
|
||
@blur="confirmInlineCreateFile"
|
||
@keyup.escape="cancelInlineCreateFile"
|
||
autofocus
|
||
>
|
||
</div>
|
||
<div v-else class="flex items-center gap-2 px-2 py-1">
|
||
<button
|
||
@click="showInlineNewFolderInput(null)"
|
||
class="flex items-center gap-1 text-xs text-gray-500 hover:text-white transition-colors"
|
||
>
|
||
<i class="fa-solid fa-folder-plus"></i>
|
||
<span>New Folder</span>
|
||
</button>
|
||
<button
|
||
@click="showInlineNewFileInput(null)"
|
||
class="flex items-center gap-1 text-xs text-gray-500 hover:text-white transition-colors"
|
||
>
|
||
<i class="fa-solid fa-file-circle-plus"></i>
|
||
<span>New File</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 文件夹列表 -->
|
||
<div class="mt-2">
|
||
<div
|
||
v-for="folder in fileTree"
|
||
:key="folder.id"
|
||
class=""
|
||
>
|
||
<!-- 文件夹标题 -->
|
||
<div
|
||
@click.stop="selectFolder(folder.id)"
|
||
@mouseenter="hoveringItem = folder.id"
|
||
@mouseleave="hoveringItem = null"
|
||
class="flex items-center gap-2 px-2 py-1.5 cursor-pointer hover:bg-dark-700 group transition-colors"
|
||
:class="{ 'bg-dark-700': hoveringItem === folder.id || selectedFolder === folder.id }"
|
||
>
|
||
<i
|
||
@click.stop="toggleFolder(folder)"
|
||
:class="['fa-solid', folder.expanded ? 'fa-chevron-down' : 'fa-chevron-right', 'text-gray-500 text-xs']"
|
||
></i>
|
||
<i
|
||
@click.stop="toggleFolder(folder)"
|
||
:class="['fa-solid', folder.expanded ? 'fa-folder-open' : 'fa-folder', 'text-yellow-400 text-sm']"
|
||
></i>
|
||
<span
|
||
class="text-sm text-gray-300 group-hover:text-white font-medium flex-1"
|
||
>{{ folder.name }}</span>
|
||
<!-- 选中状态指示器 -->
|
||
<span v-if="selectedFolder === folder.id" class="text-xs text-primary-cyan">Selected</span>
|
||
<button
|
||
@click.stop="deleteFolder(folder)"
|
||
class="text-red-400 hover:text-red-300 p-1 rounded hover:bg-dark-600 transition-colors opacity-0 group-hover:opacity-100"
|
||
title="Delete Folder"
|
||
>
|
||
<i class="fa-solid fa-trash text-xs"></i>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 文件夹内的文件 -->
|
||
<div v-if="folder.expanded || selectedFolder === folder.id" class="ml-4">
|
||
<div
|
||
v-for="child in folder.children"
|
||
:key="child.id"
|
||
@click="openFile(child as FileItem)"
|
||
@mouseenter="hoveringItem = child.id"
|
||
@mouseleave="hoveringItem = null"
|
||
class="flex items-center gap-2 px-2 py-1.5 rounded cursor-pointer hover:bg-dark-700 group transition-colors"
|
||
:class="{ 'bg-dark-700': hoveringItem === child.id }"
|
||
>
|
||
<i :class="['fa-solid', (child as FileItem).icon, 'text-gray-400 text-sm']"></i>
|
||
<span class="text-sm text-gray-300 group-hover:text-white truncate flex-1">{{ child.name }}</span>
|
||
<button
|
||
@click.stop="deleteFile(child as FileItem, folder.id)"
|
||
class="text-red-400 hover:text-red-300 p-1 rounded hover:bg-dark-600 transition-colors opacity-0 group-hover:opacity-100"
|
||
title="Delete File"
|
||
>
|
||
<i class="fa-solid fa-trash text-xs"></i>
|
||
</button>
|
||
</div>
|
||
<!-- 文件夹内新建文件夹/文件按钮或内联输入框 -->
|
||
<div v-if="inlineCreatingFolderId === folder.id" class="flex items-center gap-2 px-2 py-1">
|
||
<i class="fa-solid fa-folder text-yellow-400 text-xs"></i>
|
||
<input
|
||
v-model="inlineNewFolderName"
|
||
type="text"
|
||
placeholder="Folder name"
|
||
class="flex-1 bg-dark-700 border border-dark-500 rounded px-2 py-0.5 text-xs text-white placeholder-gray-500 focus:outline-none focus:border-primary-cyan"
|
||
@keyup.enter="confirmInlineCreateFolder"
|
||
@blur="confirmInlineCreateFolder"
|
||
@keyup.escape="cancelInlineCreateFolder"
|
||
autofocus
|
||
>
|
||
</div>
|
||
<div v-else-if="inlineCreatingFileId === folder.id" class="flex items-center gap-2 px-2 py-1">
|
||
<i class="fa-solid fa-file text-gray-400 text-xs"></i>
|
||
<input
|
||
v-model="inlineNewFileName"
|
||
type="text"
|
||
placeholder="File name"
|
||
class="flex-1 bg-dark-700 border border-dark-500 rounded px-2 py-0.5 text-xs text-white placeholder-gray-500 focus:outline-none focus:border-primary-cyan"
|
||
@keyup.enter="confirmInlineCreateFile"
|
||
@blur="confirmInlineCreateFile"
|
||
@keyup.escape="cancelInlineCreateFile"
|
||
autofocus
|
||
>
|
||
</div>
|
||
<div v-else class="flex items-center gap-2 px-2 py-1">
|
||
<button
|
||
@click.stop="showInlineNewFolderInput(folder.id)"
|
||
class="flex items-center gap-1 text-xs text-gray-500 hover:text-white transition-colors"
|
||
:class="selectedFolder === folder.id ? '' : 'opacity-0 group-hover:opacity-100'"
|
||
title="New Folder"
|
||
>
|
||
<i class="fa-solid fa-folder-plus"></i>
|
||
<span>New Folder</span>
|
||
</button>
|
||
<button
|
||
@click.stop="showInlineNewFileInput(folder.id)"
|
||
class="flex items-center gap-1 text-xs text-gray-500 hover:text-white transition-colors"
|
||
:class="selectedFolder === folder.id ? '' : 'opacity-0 group-hover:opacity-100'"
|
||
>
|
||
<i class="fa-solid fa-plus"></i>
|
||
<span>New File</span>
|
||
</button>
|
||
<label
|
||
class="flex items-center gap-1 text-xs text-gray-500 hover:text-white transition-colors cursor-pointer"
|
||
:class="selectedFolder === folder.id ? '' : 'opacity-0 group-hover:opacity-100'"
|
||
title="Upload File"
|
||
>
|
||
<i class="fa-solid fa-upload"></i>
|
||
<input type="file" class="hidden" multiple @change="handleFileUpload($event, folder.id)">
|
||
</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 底部操作 -->
|
||
<div class="p-3 border-t border-dark-600">
|
||
<button
|
||
@click="generateGraph"
|
||
:disabled="isGeneratingGraph || isGraphGenerated"
|
||
class="w-full bg-gradient-to-r from-primary-orange to-red-500 hover:from-orange-500 hover:to-red-600 text-white font-medium py-2 px-3 rounded-lg transition-all flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
|
||
>
|
||
<i :class="['fa-solid fa-sitemap', isGraphGenerated ? 'text-green-400' : '']"></i>
|
||
{{ isGraphGenerated ? '流程已生成' : (isGeneratingGraph ? '生成中...' : '生成流程') }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 中间:流程视图 -->
|
||
<div class="flex-1 relative overflow-hidden" style="background: linear-gradient(135deg, #0a0e1a 0%, #0f172a 50%, #0a0e1a 100%);">
|
||
<!-- 未生成流程时的提示 -->
|
||
<div v-if="!isGraphGenerated && !isGeneratingGraph" class="absolute inset-0 flex flex-col items-center justify-center z-10">
|
||
<div class="w-24 h-24 rounded-2xl bg-dark-700 flex items-center justify-center mb-4">
|
||
<i class="fa-solid fa-sitemap text-4xl text-gray-600"></i>
|
||
</div>
|
||
<p class="text-gray-500 text-sm">点击左侧按钮生成流程</p>
|
||
</div>
|
||
|
||
<!-- 生成中的加载动画 - 现代化设计 -->
|
||
<div v-else-if="isGeneratingGraph" class="absolute inset-0 flex flex-col items-center justify-center z-10">
|
||
<!-- 现代化加载动画 -->
|
||
<div class="flex flex-col items-center">
|
||
<!-- 多层旋转环 + 脉冲点 -->
|
||
<div class="relative w-24 h-24 mb-8">
|
||
<!-- 外环 -->
|
||
<div class="absolute inset-0 border-2 border-primary-cyan/20 rounded-full"></div>
|
||
<div class="absolute inset-1 border-2 border-t-primary-cyan rounded-full loading-spin" style="animation-duration: 1.5s;"></div>
|
||
<!-- 中环 - 反向 -->
|
||
<div class="absolute inset-2 border-2 border-purple-500/30 rounded-full" style="animation: loading-spin 2s linear infinite reverse;"></div>
|
||
<!-- 内环 -->
|
||
<div class="absolute inset-4 border-2 border-primary-cyan/40 rounded-full" style="animation: loading-spin 1s linear infinite;"></div>
|
||
<!-- 中心脉冲 -->
|
||
<div class="absolute inset-0 flex items-center justify-center">
|
||
<div class="w-3 h-3 bg-primary-cyan rounded-full animate-pulse"></div>
|
||
</div>
|
||
<!-- 周围脉冲点 -->
|
||
<div class="absolute inset-0 flex items-center justify-center" style="animation: loading-dots 1.4s ease-in-out infinite both;">
|
||
<span class="absolute w-2 h-2 bg-primary-cyan rounded-full" style="top: 10%;"></span>
|
||
<span class="absolute w-2 h-2 bg-purple-500 rounded-full" style="right: 10%;"></span>
|
||
<span class="absolute w-2 h-2 bg-primary-orange rounded-full" style="bottom: 10%;"></span>
|
||
<span class="absolute w-2 h-2 bg-green-500 rounded-full" style="left: 10%;"></span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 文字 -->
|
||
<div class="text-center mb-6">
|
||
<p class="text-white font-medium text-lg mb-2">正在生成执行流程</p>
|
||
<p class="text-gray-400 text-sm">Building your workflow...</p>
|
||
</div>
|
||
|
||
<!-- 步骤指示器 -->
|
||
<div class="flex items-center gap-2 mb-6">
|
||
<div class="flex items-center gap-1.5">
|
||
<div class="w-2 h-2 rounded-full bg-primary-cyan loading-pulse"></div>
|
||
<span class="text-xs text-gray-500">分析</span>
|
||
</div>
|
||
<div class="w-8 h-px bg-dark-600"></div>
|
||
<div class="flex items-center gap-1.5">
|
||
<div class="w-2 h-2 rounded-full bg-gray-600"></div>
|
||
<span class="text-xs text-gray-500">规划</span>
|
||
</div>
|
||
<div class="w-8 h-px bg-dark-600"></div>
|
||
<div class="flex items-center gap-1.5">
|
||
<div class="w-2 h-2 rounded-full bg-gray-600"></div>
|
||
<span class="text-xs text-gray-500">执行</span>
|
||
</div>
|
||
<div class="w-8 h-px bg-dark-600"></div>
|
||
<div class="flex items-center gap-1.5">
|
||
<div class="w-2 h-2 rounded-full bg-gray-600"></div>
|
||
<span class="text-xs text-gray-500">完成</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 进度条 -->
|
||
<div class="w-64">
|
||
<div class="h-1 bg-dark-700 rounded-full overflow-hidden">
|
||
<div class="h-full bg-gradient-to-r from-primary-cyan via-purple-500 to-primary-orange loading-progress-bar rounded-full"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 背景光效层 -->
|
||
<div v-show="isGraphGenerated" class="absolute inset-0 overflow-hidden">
|
||
<!-- 青色光晕 - 更亮更柔和 -->
|
||
<div class="absolute top-0 left-0 w-[500px] h-[500px] rounded-full opacity-30" style="background: radial-gradient(circle, rgba(0, 217, 255, 0.4) 0%, transparent 70%); filter: blur(80px);"></div>
|
||
<!-- 紫色光晕 - 更亮更柔和 -->
|
||
<div class="absolute bottom-0 right-0 w-[400px] h-[400px] rounded-full opacity-25" style="background: radial-gradient(circle, rgba(139, 92, 246, 0.4) 0%, transparent 70%); filter: blur(80px);"></div>
|
||
<!-- 中心补充光效 -->
|
||
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] h-[600px] rounded-full opacity-15" style="background: radial-gradient(circle, rgba(255, 255, 255, 0.1) 0%, transparent 70%); filter: blur(100px);"></div>
|
||
</div>
|
||
|
||
<!-- 金字塔/树形流程图展示 -->
|
||
<div v-show="isGraphGenerated" class="w-full h-full relative z-10 overflow-auto p-6">
|
||
<!-- 流程标题 -->
|
||
<div class="sticky top-0 bg-slate-900/80 backdrop-blur-sm px-4 py-3 border-b border-slate-700/50 mb-6 z-20">
|
||
<div class="flex items-center justify-between">
|
||
<div class="flex items-center gap-2">
|
||
<i class="fa-solid fa-sitemap text-primary-cyan"></i>
|
||
<span class="text-sm font-medium text-white">执行流程</span>
|
||
<span class="px-2 py-0.5 rounded-full bg-slate-700 text-xs text-slate-300">4层</span>
|
||
</div>
|
||
<span class="text-xs text-slate-400">点击节点展开详情</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 树形结构 - 深邃版本 -->
|
||
<div class="flex flex-col items-center">
|
||
<!-- 递归渲染树节点 -->
|
||
<template v-for="node in workflowData" :key="node.id">
|
||
<div class="w-full flex flex-col items-center">
|
||
<!-- 根节点 - 深邃设计 -->
|
||
<div
|
||
@click="toggleNode(node)"
|
||
class="w-full max-w-lg cursor-pointer group"
|
||
>
|
||
<!-- 外层发光效果 -->
|
||
<div
|
||
class="relative p-1 rounded-2xl transition-all duration-500"
|
||
:class="node.expanded ? 'opacity-100' : 'opacity-80 hover:opacity-100'"
|
||
>
|
||
<!-- 内层背景 -->
|
||
<div
|
||
class="relative bg-slate-800/70 backdrop-blur-xl border rounded-2xl p-5 overflow-hidden"
|
||
:class="node.expanded ? `border-2 ${node.borderColor} shadow-lg shadow-${node.color.includes('green') ? 'green' : node.color.includes('orange') ? 'orange' : node.color.includes('purple') ? 'purple' : 'cyan'}-500/30` : 'border-slate-600/50'"
|
||
>
|
||
<!-- 背景光效 -->
|
||
<div
|
||
class="absolute inset-0 opacity-30 transition-opacity duration-500"
|
||
:class="node.expanded ? 'opacity-100' : 'opacity-0'"
|
||
:style="`background: linear-gradient(135deg, ${node.color.includes('green') ? 'rgba(34, 197, 94, 0.15)' : node.color.includes('orange') ? 'rgba(249, 115, 22, 0.15)' : node.color.includes('purple') ? 'rgba(168, 85, 247, 0.15)' : 'rgba(6, 182, 212, 0.15)'} 0%, transparent 100%);`"
|
||
></div>
|
||
|
||
<!-- 网格纹理 -->
|
||
<div class="absolute inset-0 opacity-10" style="background-image: linear-gradient(rgba(255,255,255,0.1) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,0.1) 1px, transparent 1px); background-size: 20px 20px;"></div>
|
||
|
||
<!-- 主内容 -->
|
||
<div class="relative flex items-center gap-4">
|
||
<!-- 图标容器 - 多层设计 -->
|
||
<div class="relative">
|
||
<!-- 外层光晕 -->
|
||
<div
|
||
class="absolute inset-0 rounded-2xl blur-xl opacity-40 transition-all duration-500"
|
||
:class="node.expanded ? 'scale-150 opacity-40' : 'scale-100 opacity-0'"
|
||
:style="`background: ${node.color.includes('green') ? '#22c55e' : node.color.includes('orange') ? '#f97316' : node.color.includes('purple') ? '#a855f7' : '#06b6d4'};`"
|
||
></div>
|
||
<!-- 图标框 -->
|
||
<div
|
||
class="relative w-16 h-16 rounded-2xl bg-gradient-to-br flex items-center justify-center border-2 backdrop-blur-sm"
|
||
:class="[node.bgColor, node.borderColor]"
|
||
>
|
||
<i :class="['fa-solid', node.icon, node.color, 'text-2xl']"></i>
|
||
</div>
|
||
<!-- 状态指示点 -->
|
||
<div
|
||
class="absolute -bottom-1 -right-1 w-4 h-4 rounded-full border-2 border-slate-700 flex items-center justify-center"
|
||
:class="node.status === 'completed' ? 'bg-green-500' : node.status === 'processing' ? 'bg-yellow-500' : 'bg-gray-500'"
|
||
>
|
||
<i v-if="node.status === 'completed'" class="fa-solid fa-check text-[8px] text-white"></i>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 文字内容 -->
|
||
<div class="flex-1">
|
||
<div class="flex items-center gap-2 mb-1">
|
||
<h4 class="text-white font-bold text-lg tracking-wide">{{ node.name }}</h4>
|
||
<span
|
||
class="px-2.5 py-1 rounded-full text-xs font-semibold uppercase tracking-wider backdrop-blur-sm"
|
||
:class="[
|
||
node.status === 'completed' ? 'bg-green-500/30 text-green-400 border border-green-500/50' :
|
||
node.status === 'processing' ? 'bg-yellow-500/30 text-yellow-400 border border-yellow-500/50' :
|
||
'bg-slate-500/30 text-slate-300 border border-slate-500/50'
|
||
]"
|
||
>
|
||
{{ node.status === 'completed' ? '✓ 完成' : node.status === 'processing' ? '◐ 进行中' : '○ 待处理' }}
|
||
</span>
|
||
</div>
|
||
<p class="text-slate-300 text-sm">{{ node.description }}</p>
|
||
<!-- 底部信息 -->
|
||
<div class="flex items-center gap-4 mt-2 text-xs text-slate-400">
|
||
<span class="flex items-center gap-1">
|
||
<i class="fa-solid fa-layer-group"></i>
|
||
Level 1
|
||
</span>
|
||
<span class="flex items-center gap-1">
|
||
<i class="fa-solid fa-code-branch"></i>
|
||
{{ node.children?.length || 0 }} 个子任务
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 展开箭头 -->
|
||
<div
|
||
v-if="node.children && node.children.length > 0"
|
||
class="flex items-center justify-center w-8 h-8 rounded-lg bg-slate-700/50 border border-slate-600"
|
||
>
|
||
<i
|
||
:class="['fa-solid fa-chevron-down text-slate-300 transition-transform duration-300', node.expanded ? 'rotate-180' : '']"
|
||
></i>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 连接线 + 子节点 -->
|
||
<div v-if="node.expanded && node.children" class="w-full flex flex-col items-center mt-1">
|
||
<!-- 垂直连接线 - 复杂设计 -->
|
||
<div class="relative flex flex-col items-center">
|
||
<!-- 主线 -->
|
||
<div class="w-0.5 h-8 bg-gradient-to-b from-green-500/40 to-purple-500/40"></div>
|
||
<!-- 发光点 -->
|
||
<div class="absolute top-1/2 w-2 h-2 rounded-full bg-cyan-400 shadow-lg shadow-cyan-400/40"></div>
|
||
</div>
|
||
|
||
<!-- 子节点容器 -->
|
||
<div class="w-full flex justify-center gap-6">
|
||
<div v-for="(child, idx) in node.children" :key="child.id" class="flex-1 flex flex-col items-center max-w-md">
|
||
<!-- 水平连接线 -->
|
||
<div class="w-full h-0.5 bg-gradient-to-r from-transparent via-purple-500/40 to-transparent mb-3"></div>
|
||
|
||
<!-- 子节点 - 深邃设计 -->
|
||
<div
|
||
@click.stop="toggleNode(child)"
|
||
class="w-full cursor-pointer group"
|
||
>
|
||
<div
|
||
class="relative p-0.5 rounded-xl transition-all duration-300"
|
||
:class="child.expanded ? 'opacity-100' : 'opacity-80 hover:opacity-100'"
|
||
>
|
||
<div
|
||
class="relative bg-slate-800/60 backdrop-blur-xl border rounded-xl p-4"
|
||
:class="child.expanded ? `border-2 ${child.borderColor}` : 'border-slate-600/50'"
|
||
>
|
||
<!-- 背景微光 -->
|
||
<div
|
||
class="absolute inset-0 opacity-20 rounded-xl"
|
||
:style="`background: linear-gradient(135deg, ${child.color.includes('green') ? 'rgba(34, 197, 94, 0.2)' : child.color.includes('orange') ? 'rgba(249, 115, 22, 0.2)' : child.color.includes('yellow') ? 'rgba(234, 179, 8, 0.2)' : 'rgba(6, 182, 212, 0.2)'} 0%, transparent 100%);`"
|
||
></div>
|
||
|
||
<div class="relative flex items-center gap-3">
|
||
<!-- 图标 -->
|
||
<div class="relative">
|
||
<div
|
||
class="w-12 h-12 rounded-xl bg-gradient-to-br flex items-center justify-center border"
|
||
:class="[child.bgColor, child.borderColor]"
|
||
>
|
||
<i :class="['fa-solid', child.icon, child.color, 'text-xl']"></i>
|
||
</div>
|
||
<!-- 序号标签 -->
|
||
<div class="absolute -top-2 -left-2 w-5 h-5 rounded-full bg-slate-700 border border-slate-500 flex items-center justify-center text-[10px] text-slate-300 font-bold">
|
||
{{ idx + 1 }}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 文字 -->
|
||
<div class="flex-1 min-w-0">
|
||
<div class="flex items-center gap-2 mb-0.5">
|
||
<h5 class="text-white font-semibold">{{ child.name }}</h5>
|
||
<span
|
||
v-if="child.children && child.children.length > 0"
|
||
class="px-1.5 py-0.5 rounded bg-slate-700 text-slate-300 text-xs"
|
||
>
|
||
{{ child.children.length }}
|
||
</span>
|
||
</div>
|
||
<p class="text-slate-400 text-xs truncate">{{ child.description }}</p>
|
||
</div>
|
||
|
||
<!-- 箭头 -->
|
||
<i
|
||
v-if="child.children && child.children.length > 0"
|
||
:class="['fa-solid fa-chevron-down text-slate-400 text-xs transition-transform duration-300', child.expanded ? 'rotate-180' : '']"
|
||
></i>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 孙子节点 -->
|
||
<div v-if="child.expanded && child.children" class="mt-2 ml-4 space-y-2">
|
||
<div
|
||
v-for="(grandchild, gidx) in child.children"
|
||
:key="grandchild.id"
|
||
class="relative pl-4 border-l-2 border-slate-600/50"
|
||
>
|
||
<!-- 连接点 -->
|
||
<div class="absolute -left-1 top-1/2 -translate-y-1/2 w-2 h-2 rounded-full bg-slate-500"></div>
|
||
|
||
<div class="flex items-center gap-3 p-2 rounded-lg bg-slate-700/30 hover:bg-slate-700/60 transition-colors cursor-pointer border border-transparent hover:border-slate-500">
|
||
<div
|
||
class="w-8 h-8 rounded-lg bg-gradient-to-br flex items-center justify-center border"
|
||
:class="[grandchild.bgColor, grandchild.borderColor]"
|
||
>
|
||
<i :class="['fa-solid', grandchild.icon, grandchild.color, 'text-sm']"></i>
|
||
</div>
|
||
<div class="flex-1 min-w-0">
|
||
<h6 class="text-white font-medium text-sm">{{ grandchild.name }}</h6>
|
||
<p class="text-slate-400 text-xs truncate">{{ grandchild.description }}</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 底部提示 -->
|
||
<div v-show="isGraphGenerated" class="absolute bottom-4 left-1/2 -translate-x-1/2 bg-slate-800/80 backdrop-blur-sm border border-slate-600/50 rounded-lg px-4 py-2 text-xs text-slate-300 shadow-lg">
|
||
<i class="fa-solid fa-sitemap mr-1 text-primary-cyan"></i>
|
||
金字塔结构 · 4层执行流程 · 点击展开
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 右侧:对话面板 -->
|
||
<!-- 悬浮图标按钮 -->
|
||
<button
|
||
v-if="!isChatOpen"
|
||
@click="toggleChat"
|
||
class="absolute right-4 bottom-4 w-14 h-14 rounded-full bg-gradient-to-br from-primary-cyan to-purple-500 flex items-center justify-center shadow-lg shadow-purple-500/30 hover:scale-110 transition-transform z-20"
|
||
>
|
||
<i class="fa-solid fa-robot text-white text-xl animate-bounce"></i>
|
||
</button>
|
||
|
||
<!-- 展开的对话面板 -->
|
||
<div
|
||
v-else
|
||
class="w-80 bg-dark-800 border-l border-dark-600 flex flex-col animate-slide-in"
|
||
>
|
||
<!-- 头部 -->
|
||
<div class="p-4 border-b border-dark-600 flex items-center justify-between">
|
||
<div class="flex items-center gap-2">
|
||
<div class="w-8 h-8 rounded-lg bg-gradient-to-br from-primary-cyan to-purple-500 flex items-center justify-center">
|
||
<i class="fa-solid fa-robot text-white text-sm"></i>
|
||
</div>
|
||
<div>
|
||
<div class="font-medium text-white text-sm">AI Assistant</div>
|
||
<div class="text-xs text-gray-500">Configure your skill</div>
|
||
</div>
|
||
</div>
|
||
<button @click="toggleChat" class="text-gray-400 hover:text-white transition-colors">
|
||
<i class="fa-solid fa-chevron-right"></i>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 对话内容 -->
|
||
<div class="flex-1 overflow-y-auto p-4 space-y-4">
|
||
<!-- AI 消息 -->
|
||
<div class="flex gap-3">
|
||
<div class="w-7 h-7 rounded-lg bg-gradient-to-br from-primary-cyan to-purple-500 flex items-center justify-center flex-shrink-0">
|
||
<i class="fa-solid fa-robot text-white text-xs"></i>
|
||
</div>
|
||
<div class="bg-dark-700 rounded-2xl rounded-tl-none px-4 py-3 text-sm text-gray-300">
|
||
<p>Hello! I'm your AI assistant. I can help you configure your skill by:</p>
|
||
<ul class="mt-2 space-y-1 text-gray-400">
|
||
<li>• Managing data files</li>
|
||
<li>• Adding reference documents</li>
|
||
<li>• Writing scripts</li>
|
||
</ul>
|
||
<p class="mt-2">What would you like to do?</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 用户消息 -->
|
||
<div v-for="msg in chatMessages" :key="msg.id" class="flex gap-3 flex-row-reverse">
|
||
<div class="w-7 h-7 rounded-lg bg-dark-600 flex items-center justify-center flex-shrink-0">
|
||
<i class="fa-solid fa-user text-gray-400 text-xs"></i>
|
||
</div>
|
||
<div class="bg-dark-600 rounded-2xl rounded-tr-none px-4 py-3 text-sm text-white max-w-[80%]">
|
||
{{ msg.text }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 输入框 -->
|
||
<div class="p-4 border-t border-dark-600">
|
||
<div class="relative">
|
||
<input
|
||
v-model="chatInput"
|
||
type="text"
|
||
placeholder="Ask AI to help..."
|
||
class="w-full bg-dark-700 border border-dark-500 rounded-xl px-4 py-3 pr-12 text-sm text-white placeholder-gray-500 focus:outline-none focus:border-primary-orange"
|
||
@keyup.enter="sendMessage"
|
||
>
|
||
<button
|
||
@click="sendMessage"
|
||
class="absolute right-2 top-1/2 -translate-y-1/2 w-8 h-8 rounded-lg bg-primary-orange text-white flex items-center justify-center hover:from-orange-500 hover:to-red-600 transition-all"
|
||
>
|
||
<i class="fa-solid fa-paper-plane text-xs"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 底部按钮 -->
|
||
<div class="p-3 border-t border-dark-600 flex gap-2">
|
||
<button
|
||
@click="closeCreate"
|
||
class="flex-1 py-2 rounded-lg bg-dark-700 text-gray-300 hover:bg-dark-600 text-sm transition-colors"
|
||
>
|
||
Cancel
|
||
</button>
|
||
<button
|
||
@click="saveNewSkill"
|
||
class="flex-1 py-2 rounded-lg bg-gradient-to-r from-primary-orange to-red-500 text-white hover:from-orange-500 hover:to-red-600 text-sm transition-all flex items-center justify-center gap-2"
|
||
>
|
||
<i class="fa-solid fa-check"></i>
|
||
Create
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Teleport>
|
||
|
||
<!-- 文件编辑弹窗 -->
|
||
<Teleport to="body">
|
||
<div v-if="isEditingFile" class="fixed inset-0 bg-black/80 flex items-center justify-center z-[60]">
|
||
<div :class="['bg-dark-800 rounded-2xl w-full max-w-5xl h-[85vh] border border-dark-600 shadow-2xl overflow-hidden flex flex-col', isEditingFileClosing ? 'animate-modal-out' : 'animate-modal-in']">
|
||
<!-- 头部 -->
|
||
<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-lg bg-gradient-to-br from-primary-cyan to-purple-500 flex items-center justify-center">
|
||
<i class="fa-solid fa-file-edit text-white"></i>
|
||
</div>
|
||
<div>
|
||
<h3 class="text-lg font-semibold text-white">Edit File</h3>
|
||
<p class="text-sm text-gray-400">{{ editingFileName }}</p>
|
||
</div>
|
||
</div>
|
||
<button @click="cancelFileEdit" class="text-gray-400 hover:text-white transition-all p-2 hover:bg-dark-600 rounded-lg">
|
||
<i class="fa-solid fa-xmark text-xl"></i>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 编辑器内容 -->
|
||
<div class="flex-1 p-5 overflow-hidden">
|
||
<textarea
|
||
v-model="editingFileContent"
|
||
class="w-full h-full bg-dark-900 border border-dark-500 rounded-xl p-4 text-sm text-gray-300 font-mono focus:outline-none focus:border-primary-cyan resize-none"
|
||
placeholder="File content..."
|
||
></textarea>
|
||
</div>
|
||
|
||
<!-- 底部按钮 -->
|
||
<div class="flex items-center justify-end gap-3 p-5 border-t border-dark-600 bg-dark-700/50">
|
||
<button
|
||
@click="cancelFileEdit"
|
||
class="px-5 py-2.5 rounded-xl bg-dark-600 text-gray-300 hover:bg-dark-500 border border-dark-500 transition-all"
|
||
>
|
||
Cancel
|
||
</button>
|
||
<button
|
||
@click="saveFileEdit"
|
||
class="px-6 py-2.5 rounded-xl bg-gradient-to-r from-primary-cyan to-purple-500 text-white hover:from-cyan-500 hover:to-purple-600 transition-all flex items-center gap-2"
|
||
>
|
||
<i class="fa-solid fa-save"></i>
|
||
Save Changes
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Teleport>
|
||
</div>
|
||
</template>
|