diff --git a/web/src/components/AgentPanel.vue b/web/src/components/AgentPanel.vue
index 0c2bc0b1..7d1385b7 100644
--- a/web/src/components/AgentPanel.vue
+++ b/web/src/components/AgentPanel.vue
@@ -55,44 +55,29 @@
暂无文件
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ data.nameStart || data.title }}
- {{ data.nameEnd }}
-
-
-
-
+
+
+ {{ node.nameStart || node.title }}
+ {{ node.nameEnd }}
-
+
+
+
+
+
+
@@ -163,6 +148,7 @@ import { MdPreview } from 'md-editor-v3'
import 'md-editor-v3/lib/preview.css'
import { useThemeStore } from '@/stores/theme'
import { getFileIcon, getFileIconColor, formatFileSize } from '@/utils/file_utils'
+import FileTreeComponent from '@/components/FileTreeComponent.vue'
const props = defineProps({
agentState: {
@@ -384,17 +370,6 @@ const truncateFilename = (name) => {
const fileTreeData = computed(() => buildTreeData(normalizedFiles.value))
-const toggleFolder = (data) => {
- if (data.isLeaf) return
- const key = data.key
- const index = expandedKeys.value.indexOf(key)
- if (index > -1) {
- expandedKeys.value = expandedKeys.value.filter((k) => k !== key)
- } else {
- expandedKeys.value = [...expandedKeys.value, key]
- }
-}
-
const onFileSelect = (selectedKeys, { node }) => {
if (node.isLeaf) {
if (node.fileData) {
@@ -945,15 +920,6 @@ const stopResize = () => {
margin: 0 -4px;
}
-.tree-node-wrapper {
- display: flex;
- align-items: center;
- width: 100%;
- height: 28px;
- padding-right: 8px;
- position: relative;
-}
-
.tree-node-name {
display: flex;
align-items: center;
@@ -963,7 +929,6 @@ const stopResize = () => {
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
font-size: 14px;
color: var(--gray-800);
- line-height: 28px;
}
.name-start {
@@ -977,26 +942,14 @@ const stopResize = () => {
white-space: nowrap;
}
-.folder-icon {
- color: #dcb67a;
- fill: #dcb67a;
- fill-opacity: 0.2;
-
- &.open {
- color: #dcb67a;
- }
-}
-
-.node-actions {
+.node-actions-container {
display: flex;
align-items: center;
- justify-content: flex-end;
- padding-left: 4px;
- gap: 2px;
+ gap: 4px;
}
.tree-action-btn {
- display: none;
+ display: flex;
align-items: center;
justify-content: center;
width: 24px;
@@ -1012,89 +965,6 @@ const stopResize = () => {
color: var(--main-600);
}
-.tree-delete-btn:hover {
- color: var(--color-error-500);
-}
-
-/* Specific Ant Design Tree Overrides */
-.file-tree-container :deep(.ant-tree) {
- font-family: inherit;
- font-size: 14px;
- overflow: hidden;
-
- .ant-tree-treenode {
- width: 100%;
- padding: 0;
- }
-
- .ant-tree-node-content-wrapper {
- display: flex;
- align-items: center;
- transition: none;
- border-radius: 0;
- padding: 0 8px 0 0;
- line-height: 28px;
- flex: 1;
- position: relative;
- padding: 0 6px;
- border-radius: 6px;
- gap: 6px;
-
- &:hover {
- background-color: var(--gray-50);
-
- .tree-action-btn {
- display: flex;
- }
- }
-
- &.ant-tree-node-selected {
- background-color: var(--gray-100);
- }
- }
-
- /* Icon Vertical Alignment */
- .ant-tree-iconEle {
- line-height: 30px;
- height: 30px;
- display: flex;
- align-items: center;
- justify-content: center;
- width: 22px;
- }
-
- /* Ensure Title Fills Space */
- .ant-tree-title {
- flex: 1;
- overflow: hidden;
- min-width: 0;
- }
-
- /* Switcher (Arrow) */
- .ant-tree-switcher {
- width: 24px;
- height: 30px;
- line-height: 30px;
- display: flex;
- align-items: center;
- justify-content: center;
-
- .ant-tree-switcher-icon {
- font-size: 10px;
- color: var(--gray-500);
- }
- }
-
- .ant-tree-indent-unit {
- width: 18px;
- }
-
- /* 隐藏文件夹展开/折叠箭头 */
- .ant-tree-switcher {
- display: none !important;
- }
-}
-
/* 附件列表专用样式 */
.attachment-tree :deep(.ant-tree-node-content-wrapper) {
border: 1px solid var(--gray-200);
diff --git a/web/src/components/FileTreeComponent.vue b/web/src/components/FileTreeComponent.vue
new file mode 100644
index 00000000..5e3af939
--- /dev/null
+++ b/web/src/components/FileTreeComponent.vue
@@ -0,0 +1,240 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ data.title }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/components/SkillsManagerComponent.vue b/web/src/components/SkillsManagerComponent.vue
index 5361c9f1..84e6786b 100644
--- a/web/src/components/SkillsManagerComponent.vue
+++ b/web/src/components/SkillsManagerComponent.vue
@@ -104,12 +104,11 @@
@@ -216,6 +215,7 @@ import {
} from 'lucide-vue-next'
import { skillApi } from '@/apis/skill_api'
import HeaderComponent from '@/components/HeaderComponent.vue'
+import FileTreeComponent from '@/components/FileTreeComponent.vue'
dayjs.extend(relativeTime)
dayjs.locale('zh-cn')
@@ -232,6 +232,7 @@ const skills = ref([])
const currentSkill = ref(null)
const treeData = ref([])
const selectedTreeKeys = ref([])
+const expandedKeys = ref([])
const selectedPath = ref('')
const selectedIsDir = ref(false)
const fileContent = ref('')
@@ -268,16 +269,34 @@ const resetFileState = () => {
selectedPath.value = ''
selectedIsDir.value = false
selectedTreeKeys.value = []
+ expandedKeys.value = []
fileContent.value = ''
originalFileContent.value = ''
}
+const expandAllKeys = (nodes) => {
+ let keys = []
+ nodes.forEach(node => {
+ if (node.is_dir) {
+ keys.push(node.key)
+ if (node.children) {
+ keys = keys.concat(expandAllKeys(node.children))
+ }
+ }
+ })
+ return keys
+}
+
const fetchSkills = async () => {
loading.value = true
try {
const result = await skillApi.listSkills()
skills.value = result?.data || []
- if (currentSkill.value) {
+
+ // 默认选中第一个技能并加载 SKILL.md
+ if (!currentSkill.value && skills.value.length > 0) {
+ await selectSkill(skills.value[0])
+ } else if (currentSkill.value) {
const latest = skills.value.find(i => i.slug === currentSkill.value.slug)
if (latest) {
currentSkill.value = latest
@@ -317,7 +336,9 @@ const reloadTree = async () => {
loading.value = true
try {
const result = await skillApi.getSkillTree(currentSkill.value.slug)
- treeData.value = normalizeTree(result?.data || [])
+ const normalized = normalizeTree(result?.data || [])
+ treeData.value = normalized
+ expandedKeys.value = expandAllKeys(normalized)
} catch {
message.error('加载目录树失败')
} finally {
@@ -325,11 +346,30 @@ const reloadTree = async () => {
}
}
+const loadSkillFile = async (slug, path = 'SKILL.md') => {
+ try {
+ const fileResult = await skillApi.getSkillFile(slug, path)
+ const content = fileResult?.data?.content || ''
+ fileContent.value = content
+ originalFileContent.value = content
+ selectedPath.value = path
+ selectedIsDir.value = false
+ selectedTreeKeys.value = [path]
+ } catch {
+ // 文件不存在时忽略
+ }
+}
+
const selectSkill = async (record) => {
currentSkill.value = record
syncDependencyFormFromSkill(record)
resetFileState()
- await reloadTree()
+
+ // 并行执行:加载树结构和获取 SKILL.md
+ const [treeResult] = await Promise.all([
+ reloadTree(),
+ loadSkillFile(record.slug)
+ ])
}
const handleTreeSelect = async (keys, info) => {
@@ -541,7 +581,7 @@ onMounted(fetchSkills)
.search-input {
:deep(.ant-input) {
- height: 32px;
+ height: 24px;
}
}
}
@@ -617,9 +657,9 @@ onMounted(fetchSkills)
}
.panel-top-bar {
- padding: 12px 24px;
+ padding: 10px 16px 0 16px;
box-sizing: border-box;
- border-bottom: 1px solid @border-color;
+ // border-bottom: 1px solid @border-color;
display: flex;
justify-content: space-between;
align-items: center;
@@ -651,7 +691,7 @@ onMounted(fetchSkills)
:deep(.ant-tabs-nav) {
margin: 0;
- padding: 0 24px;
+ padding: 0 16px;
border-bottom: 1px solid @border-color;
flex-shrink: 0;
&::before { border-bottom: none; }
@@ -686,11 +726,11 @@ onMounted(fetchSkills)
align-items: center;
border-bottom: 1px solid @border-color;
background-color: var(--gray-50);
- .label { font-size: 11px; font-weight: 600; color: var(--gray-400); text-transform: uppercase; letter-spacing: 0.5px; }
+ .label { font-size: 11px; font-weight: 600; color: var(--gray-500); text-transform: uppercase; letter-spacing: 0.5px; }
.tree-actions {
display: flex; gap: 4px;
button {
- background: none; border: none; padding: 2px; cursor: pointer; color: var(--gray-400);
+ background: none; border: none; padding: 2px; cursor: pointer; color: var(--gray-500);
display: flex; align-items: center;
&:hover { color: var(--gray-900); }
}
@@ -699,11 +739,6 @@ onMounted(fetchSkills)
.tree-content {
flex: 1; overflow-y: auto; padding: 8px;
- :deep(.ant-tree) {
- background: transparent;
- .ant-tree-node-content-wrapper { border-radius: 4px; font-size: 13px; padding: 4px 8px; }
- .ant-tree-node-selected { background-color: var(--gray-200) !important; font-weight: 500; }
- }
}
}
@@ -731,24 +766,25 @@ onMounted(fetchSkills)
}
.editor-main {
- flex: 1;
- min-height: 0;
- background-color: var(--gray-0);
- display: flex;
- flex-direction: column;
-}
+ flex: 1;
+ min-height: 0;
+ background-color: var(--gray-0);
+ display: flex;
+ flex-direction: column;
+ }
-.editor-main :deep(.ant-empty) {
- flex: 1;
- display: flex;
- align-items: center;
- justify-content: center;
-}
+ .editor-main :deep(.ant-empty) {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ .editor-main :deep(textarea) {
+ flex: 1;
+ min-height: 0;
+ }
-.editor-main :deep(textarea) {
- flex: 1;
- min-height: 0;
-}
.pure-editor {
width: 100%; height: 100%; border: none; resize: none; padding: 20px;
font-family: 'Fira Code', 'Monaco', monospace; font-size: 13px; line-height: 1.6;
diff --git a/web/src/layouts/AppLayout.vue b/web/src/layouts/AppLayout.vue
index cef52630..1a3637b4 100644
--- a/web/src/layouts/AppLayout.vue
+++ b/web/src/layouts/AppLayout.vue
@@ -107,12 +107,6 @@ const mainList = computed(() => {
path: '/database',
icon: LibraryBig,
activeIcon: LibraryBig
- },
- {
- name: 'Dashboard',
- path: '/dashboard',
- icon: BarChart3,
- activeIcon: BarChart3
}
]
@@ -125,6 +119,13 @@ const mainList = computed(() => {
})
}
+ items.push({
+ name: 'Dashboard',
+ path: '/dashboard',
+ icon: BarChart3,
+ activeIcon: BarChart3
+ })
+
return items
})