feat(file-tree): 优化文件加载反馈,添加加载状态指示器

This commit is contained in:
Wenjie Zhang 2026-04-07 12:51:15 +08:00
parent 39a0fba402
commit ced97d5636
3 changed files with 53 additions and 6 deletions

View File

@ -41,6 +41,7 @@
- 收紧文件系统安全边界viewer/chat 下载与删除路径统一基于解析后的真实路径做允许目录校验,阻止通过软链接逃逸工作区/线程目录;同时将密码哈希默认实现升级为 Argon2并移除 skill frontmatter 解析中的正则回溯风险。
- 调整 Skills 导入能力:`/api/system/skills/import` 现在除 ZIP 外也支持直接上传单个 `SKILL.md`,前端上传入口与后端导入服务同步兼容,便于快速导入单文件技能
- 扩展 viewer 工作区文件操作:`/home/gem/user-data/workspace` 支持从文件系统面板新建文件夹和上传文件,后端限制写入范围并保持同名冲突直接报错。
- 优化文件系统面板目录展开反馈:异步加载子文件时在对应文件夹图标位置显示 loading 状态,避免点击后无视觉响应。
- 新增 Skills 远程安装能力Skills 管理页支持填写 `owner/repo` 或 GitHub URL后端通过隔离的临时 `HOME` 调用 `npx skills add` 下载指定 skill再复用现有导入链路写入 `saves/skills` 和数据库,避免将 `~/.agents/skills` 直接作为系统主存储;前端远程安装弹窗补充多选串行安装与批量进度展示,复用现有单 skill 安装接口逐个提交请求
- 调整部门删除语义:删除部门时不再要求用户数为 0而是将部门下用户迁移到默认部门同时清理部门级配置和部门 API Key保证测试部门、撤换部门等场景可直接删除并补充对应集成测试覆盖该链路
- 重构 MCP 运行时配置加载模型:移除 `MCP_SERVERS` 作为运行正确性前提的设计,改为每次直接从数据库读取最新 MCP 配置,并用 `server_name:config_hash` 作为本地工具缓存 key同时将内置 MCP 初始化职责收敛为仅同步数据库默认项,前端 MCP 选项改为直接使用实时资源列表,解决 `api`/`worker` 分进程下的配置不一致与缓存失效问题

View File

@ -870,7 +870,7 @@ watch(useInlinePreview, (isInline) => {
justify-content: space-between;
gap: 8px;
padding: 4px 16px;
min-height: 56px;
min-height: 44px;
background: var(--gray-25);
flex-shrink: 0;
@ -1250,7 +1250,6 @@ watch(useInlinePreview, (isInline) => {
/* File Tree Styles - VS Code Style Refined */
.file-tree-container {
padding: 4px;
margin: 0 -4px;
min-height: 0;
}

View File

@ -4,7 +4,7 @@
:selected-keys="selectedKeys"
:expanded-keys="expandedKeys"
:tree-data="treeData"
:load-data="loadData"
:load-data="loadData ? internalLoadData : undefined"
:show-icon="showIcon"
:block-node="blockNode"
:show-line="showLine"
@ -25,7 +25,12 @@
<FileText v-else :size="16" class="file-icon" />
</template>
<template v-else>
<FolderOpen v-if="expanded" :size="18" class="folder-icon open" />
<span
v-if="isNodeLoading(data.key)"
class="folder-loading-icon"
aria-label="正在加载"
></span>
<FolderOpen v-else-if="expanded" :size="18" class="folder-icon open" />
<Folder v-else :size="18" class="folder-icon" />
</template>
</slot>
@ -49,6 +54,7 @@
</template>
<script setup>
import { ref } from 'vue'
import { Folder, FolderOpen, FileText } from 'lucide-vue-next'
import { getFileIcon, getFileIconColor } from '@/utils/file_utils'
@ -100,6 +106,32 @@ const emit = defineEmits([
'toggleFolder'
])
const loadingKeys = ref(new Set())
const setNodeLoading = (key, isLoading) => {
const nextLoadingKeys = new Set(loadingKeys.value)
if (isLoading) {
nextLoadingKeys.add(key)
} else {
nextLoadingKeys.delete(key)
}
loadingKeys.value = nextLoadingKeys
}
const isNodeLoading = (key) => loadingKeys.value.has(key)
const internalLoadData = async (treeNode) => {
if (!props.loadData) return
const key = treeNode?.key
if (key) setNodeLoading(key, true)
try {
await props.loadData(treeNode)
} finally {
if (key) setNodeLoading(key, false)
}
}
const handleSelectedUpdate = (keys) => {
emit('update:selectedKeys', keys)
}
@ -233,11 +265,26 @@ const handleNodeClick = (data) => {
}
.folder-icon {
color: #dcb67a;
fill: #dcb67a;
color: var(--main-500);
fill: var(--main-500);
fill-opacity: 0.2;
}
.folder-loading-icon {
width: 15px;
height: 15px;
border: 2px solid var(--gray-200);
border-top-color: var(--main-500);
border-radius: 50%;
animation: file-tree-folder-loading 0.8s linear infinite;
}
@keyframes file-tree-folder-loading {
to {
transform: rotate(360deg);
}
}
.node-actions {
display: none;
align-items: center;