feat(AgentPopover): 增强待办事项图标样式并添加文件下载功能

- 将简单的文本图标替换为SVG图标,提升视觉体验
- 为文件列表项添加下载按钮功能
- 优化文件区域的样式和布局
- 移除已完成TODO注释和相关代码
This commit is contained in:
Wenjie Zhang 2025-12-02 21:36:12 +08:00
parent 10ad84a679
commit 5a66ff80c2
5 changed files with 110 additions and 13 deletions

View File

@ -12,6 +12,10 @@
- 集成 LangFuse (观望) 添加用户日志与用户反馈模块,可以在 AgentView 中查看信息 - 集成 LangFuse (观望) 添加用户日志与用户反馈模块,可以在 AgentView 中查看信息
- 集成 neo4j mcp (或者自己构建工具) - 集成 neo4j mcp (或者自己构建工具)
- 文档解析部分的 markdown 中的图片替换为内部可访问的链接 (2/4) - 文档解析部分的 markdown 中的图片替换为内部可访问的链接 (2/4)
- 同名文件处理逻辑:遇到同名文件则在上传区域提示,是否删除旧文件
- conversation 待修改为异步的版本
- DBManager 需要将数据库修改为异步的aiosqlite或者异步mysql缓存使用Redis存储
- agent 状态中的文件区域,新增可以下载
### Bugs ### Bugs
- 部分异常状态下,智能体的模型名称出现重叠[#279](https://github.com/xerrors/Yuxi-Know/issues/279) - 部分异常状态下,智能体的模型名称出现重叠[#279](https://github.com/xerrors/Yuxi-Know/issues/279)

View File

@ -1065,8 +1065,6 @@ async def upload_file(
upload_dir = os.path.join(config.save_dir, "database", "uploads") upload_dir = os.path.join(config.save_dir, "database", "uploads")
basename, ext = os.path.splitext(file.filename) basename, ext = os.path.splitext(file.filename)
# TODO:
# 后续修改为遇到同名文件则在上传区域提示,是否删除旧文件,同时 filename name 也就不用添加 hash 了
filename = f"{basename}_{hashstr(basename, 4, with_salt=True, salt='fixed_salt')}{ext}".lower() filename = f"{basename}_{hashstr(basename, 4, with_salt=True, salt='fixed_salt')}{ext}".lower()
file_path = os.path.join(upload_dir, filename) file_path = os.path.join(upload_dir, filename)

View File

@ -4,8 +4,6 @@ from fastapi import FastAPI
from server.services import tasker from server.services import tasker
# TODO:[已完成]使用lifespan进行统一生命周期管理
@asynccontextmanager @asynccontextmanager
async def lifespan(app: FastAPI): async def lifespan(app: FastAPI):

View File

@ -40,7 +40,15 @@
class="todo-item" class="todo-item"
> >
<span class="todo-status" :class="todo.status"> <span class="todo-status" :class="todo.status">
{{ todo.status === 'completed' ? '✓' : todo.status === 'in_progress' ? '⟳' : '○' }} <svg v-if="todo.status === 'completed'" viewBox="0 0 24 24" fill="currentColor" class="icon">
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/>
</svg>
<svg v-else-if="todo.status === 'in_progress'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="icon spinning">
<circle cx="12" cy="12" r="10" stroke-dasharray="31.416" stroke-dashoffset="31.416" stroke-linecap="round"/>
</svg>
<svg v-else viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="icon">
<circle cx="12" cy="12" r="10"/>
</svg>
</span> </span>
<span class="todo-text">{{ todo.content }}</span> <span class="todo-text">{{ todo.content }}</span>
</div> </div>
@ -60,10 +68,19 @@
@click="showFileContent(fileItem.path, fileItem)" @click="showFileContent(fileItem.path, fileItem)"
> >
<div class="file-info"> <div class="file-info">
<div class="file-name">{{ getFileName(fileItem) }}</div> <div class="file-content-wrapper">
<div class="file-time" v-if="fileItem.modified_at"> <div class="file-name">{{ getFileName(fileItem) }}</div>
{{ formatDate(fileItem.modified_at) }} <div class="file-time" v-if="fileItem.modified_at">
{{ formatDate(fileItem.modified_at) }}
</div>
</div> </div>
<button
class="download-btn"
@click.stop="downloadFile(fileItem)"
title="下载文件"
>
<Download :size="18" />
</button>
</div> </div>
</div> </div>
</div> </div>
@ -92,6 +109,7 @@
<script setup> <script setup>
import { computed, ref, watch } from 'vue'; import { computed, ref, watch } from 'vue';
import { Download } from 'lucide-vue-next';
const props = defineProps({ const props = defineProps({
visible: { visible: {
@ -211,6 +229,24 @@ const closeModal = () => {
currentFilePath.value = ''; currentFilePath.value = '';
}; };
const downloadFile = (fileItem) => {
try {
const content = formatContent(fileItem.content);
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = getFileName(fileItem);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
} catch (error) {
console.error('下载文件失败:', error);
}
};
const emitRefresh = () => { const emitRefresh = () => {
emit('refresh'); emit('refresh');
}; };
@ -330,19 +366,35 @@ const emitRefresh = () => {
.todo-status { .todo-status {
flex-shrink: 0; flex-shrink: 0;
width: 16px; width: 20px;
height: 16px; height: 20px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: 10px;
margin-top: 2px; margin-top: 2px;
border-radius: 50%; border-radius: 50%;
transition: all 0.15s ease; transition: all 0.15s ease;
.icon {
width: 14px;
height: 14px;
transition: all 0.15s ease;
}
.spinning {
animation: spin 1.5s linear infinite;
stroke-dasharray: 31.416;
stroke-dashoffset: 0;
animation: spin 1.5s linear infinite;
}
&.completed { &.completed {
background: var(--color-success-50); background: var(--color-success-50);
color: var(--color-success-700); color: var(--color-success-700);
.icon {
transform: scale(1.1);
}
} }
&.in_progress { &.in_progress {
@ -356,6 +408,20 @@ const emitRefresh = () => {
} }
} }
@keyframes spin {
0% {
transform: rotate(0deg);
stroke-dashoffset: 0;
}
50% {
stroke-dashoffset: 15.708;
}
100% {
transform: rotate(360deg);
stroke-dashoffset: 0;
}
}
.todo-text { .todo-text {
flex: 1; flex: 1;
font-size: 14px; font-size: 14px;
@ -377,7 +443,7 @@ const emitRefresh = () => {
.file-item { .file-item {
padding: 12px 14px; padding: 12px 14px;
background: var(--main-5); background: var(--gray-0);
border: 1px solid var(--gray-150); border: 1px solid var(--gray-150);
border-radius: 6px; border-radius: 6px;
cursor: pointer; cursor: pointer;
@ -395,6 +461,15 @@ const emitRefresh = () => {
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
gap: 10px; gap: 10px;
width: 100%;
}
.file-content-wrapper {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
} }
.file-name { .file-name {
@ -415,6 +490,29 @@ const emitRefresh = () => {
white-space: nowrap; white-space: nowrap;
} }
.download-btn {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
background: transparent;
border: none;
color: var(--gray-600);
cursor: pointer;
transition: all 0.15s ease;
padding: 0;
flex-shrink: 0;
&:hover {
color: var(--main-600);
}
&:active {
color: var(--main-400);
}
}
.file-content { .file-content {
max-height: 60vh; max-height: 60vh;
overflow-y: auto; overflow-y: auto;

View File

@ -162,7 +162,6 @@
<ModelProvidersComponent /> <ModelProvidersComponent />
</div> </div>
<!-- TODO 用户管理优化添加姓名默认使用用户名配置项 -->
<div class="setting" v-if="(state.windowWidth <= 520 || state.section === 'user') && userStore.isAdmin"> <div class="setting" v-if="(state.windowWidth <= 520 || state.section === 'user') && userStore.isAdmin">
<UserManagementComponent /> <UserManagementComponent />
</div> </div>