style(ui): 优化状态面板 UI 和交互体验

- 调整聊天面板与状态面板的布局比例 (4:3)
- 优化动画过渡效果,缩短时长
- 统一图标风格 (FolderDot -> FolderCode)
- 移除 tab 条件显示,始终显示所有标签
- 调整进度显示格式 (completed/total)
- 缩小状态面板元素尺寸
- 知识库工具支持纯文本结果展示
This commit is contained in:
Wenjie Zhang 2026-01-31 14:16:53 +08:00
parent ff38ac9e26
commit f31705d58c
5 changed files with 81 additions and 74 deletions

View File

@ -1143,16 +1143,17 @@ watch(
} }
.chat-main { .chat-main {
flex: 1; flex: 4;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow-y: auto; /* Scroll is here now */ overflow-y: auto; /* Scroll is here now */
position: relative; position: relative;
transition: flex 0.4s ease; transition: flex 0.4s ease;
min-width: 0; /* Prevent flex item from overflowing */
} }
.agent-panel-wrapper { .agent-panel-wrapper {
flex: 1; /* 1:1 ratio with chat-main */ flex: 3; /* 4:3 ratio with chat-main */
height: calc(100% - 32px); height: calc(100% - 32px);
overflow: hidden; overflow: hidden;
z-index: 20; z-index: 20;
@ -1162,20 +1163,21 @@ watch(
border-radius: 12px; border-radius: 12px;
box-shadow: 0 4px 20px var(--shadow-1); box-shadow: 0 4px 20px var(--shadow-1);
border: 1px solid var(--gray-200); border: 1px solid var(--gray-200);
min-width: 0; /* Prevent flex item from overflowing */
} }
/* Workbench transition animations */ /* Workbench transition animations */
.panel-slide-enter-active, .panel-slide-enter-active,
.panel-slide-leave-active { .panel-slide-leave-active {
transition: transition:
transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1), transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1),
opacity 0.3s ease, opacity 0.15s ease,
flex 0.4s ease; flex 0.2s ease;
} }
.panel-slide-enter-from, .panel-slide-enter-from,
.panel-slide-leave-to { .panel-slide-leave-to {
transform: translateX(30px) scale(0.98); transform: translateX(20px) scale(0.98);
opacity: 0; opacity: 0;
flex: 0 0 0; /* Shrink to zero width during transition */ flex: 0 0 0; /* Shrink to zero width during transition */
margin-left: -16px; /* Compensate for margin during close */ margin-left: -16px; /* Compensate for margin during close */

View File

@ -38,7 +38,7 @@
@click="$emit('toggle-panel')" @click="$emit('toggle-panel')"
title="查看工作状态" title="查看工作状态"
> >
<FolderDot :size="16" /> <FolderCode :size="14" />
<span>状态</span> <span>状态</span>
</div> </div>
</div> </div>
@ -55,7 +55,7 @@ import AttachmentOptionsComponent from '@/components/AttachmentOptionsComponent.
import { threadApi } from '@/apis' import { threadApi } from '@/apis'
import { AgentValidator } from '@/utils/agentValidator' import { AgentValidator } from '@/utils/agentValidator'
import { handleChatError, handleValidationError } from '@/utils/errorHandler' import { handleChatError, handleValidationError } from '@/utils/errorHandler'
import { FolderDot } from 'lucide-vue-next' import { FolderCode } from 'lucide-vue-next'
const props = defineProps({ const props = defineProps({
modelValue: { type: String, default: '' }, modelValue: { type: String, default: '' },

View File

@ -1,7 +1,10 @@
<template> <template>
<div class="agent-panel"> <div class="agent-panel">
<div class="panel-header"> <div class="panel-header">
<div class="panel-title">状态工作台</div> <div class="panel-title">
<FolderCode :size="16" class="header-icon" />
<span><strong>状态工作台</strong></span>
</div>
<div class="header-actions"> <div class="header-actions">
<a-button type="text" class="refresh-btn" @click="emitRefresh"> <a-button type="text" class="refresh-btn" @click="emitRefresh">
<template #icon><RefreshCw :size="16" /></template> <template #icon><RefreshCw :size="16" /></template>
@ -18,15 +21,13 @@
class="tab" class="tab"
:class="{ active: activeTab === 'todos' }" :class="{ active: activeTab === 'todos' }"
@click="activeTab = 'todos'" @click="activeTab = 'todos'"
v-if="hasTodos"
> >
任务 ({{ todoCount }}) 任务 ({{ completedCount }}/{{ todos.length }})
</button> </button>
<button <button
class="tab" class="tab"
:class="{ active: activeTab === 'files' }" :class="{ active: activeTab === 'files' }"
@click="activeTab = 'files'" @click="activeTab = 'files'"
v-if="hasFiles"
> >
文件 ({{ fileCount }}) 文件 ({{ fileCount }})
</button> </button>
@ -34,7 +35,6 @@
class="tab" class="tab"
:class="{ active: activeTab === 'attachments' }" :class="{ active: activeTab === 'attachments' }"
@click="activeTab = 'attachments'" @click="activeTab = 'attachments'"
v-if="hasAttachments"
> >
附件 ({{ attachmentCount }}) 附件 ({{ attachmentCount }})
</button> </button>
@ -211,15 +211,14 @@
</template> </template>
<script setup> <script setup>
import { computed, ref, watch } from 'vue' import { computed, ref } from 'vue'
import { Download, X, Plus, Info } from 'lucide-vue-next' import { Download, X, Plus, Info, FolderCode } from 'lucide-vue-next'
import { import {
CheckCircleOutlined, CheckCircleOutlined,
SyncOutlined, SyncOutlined,
ClockCircleOutlined, ClockCircleOutlined,
CloseCircleOutlined, CloseCircleOutlined,
QuestionCircleOutlined, QuestionCircleOutlined
DownloadOutlined
} from '@ant-design/icons-vue' } from '@ant-design/icons-vue'
import { MdPreview } from 'md-editor-v3' import { MdPreview } from 'md-editor-v3'
import 'md-editor-v3/lib/preview.css' import 'md-editor-v3/lib/preview.css'
@ -267,20 +266,8 @@ const attachments = computed(() => {
return props.agentState?.attachments || [] return props.agentState?.attachments || []
}) })
const hasTodos = computed(() => { const completedCount = computed(() => {
return todos.value.length > 0 return todos.value.filter(t => t.status === 'completed').length
})
const hasFiles = computed(() => {
return files.value.length > 0
})
const hasAttachments = computed(() => {
return attachments.value.length > 0
})
const todoCount = computed(() => {
return todos.value.length
}) })
// //
@ -321,23 +308,6 @@ const attachmentCount = computed(() => {
return normalizedAttachments.value.length return normalizedAttachments.value.length
}) })
// agentState
watch(
() => props.agentState,
(newState) => {
if (newState) {
if (hasAttachments.value && !hasFiles.value && !hasTodos.value) {
activeTab.value = 'attachments'
} else if (hasFiles.value && !hasTodos.value) {
activeTab.value = 'files'
} else if (hasTodos.value) {
activeTab.value = 'todos'
}
}
},
{ immediate: true }
)
// //
const getFileName = (fileItem) => { const getFileName = (fileItem) => {
if (fileItem.path) { if (fileItem.path) {
@ -440,16 +410,23 @@ const emitRefresh = () => {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 0 16px; padding: 0 12px;
height: 48px; height: 40px;
border-bottom: 1px solid var(--gray-200); border-bottom: 1px solid var(--gray-200);
flex-shrink: 0; flex-shrink: 0;
} }
.panel-title { .panel-title {
display: flex;
align-items: center;
gap: 6px;
font-weight: 600; font-weight: 600;
font-size: 15px; font-size: 13px;
color: var(--gray-900); color: var(--gray-900);
.header-icon {
color: var(--gray-700);
}
} }
.header-actions { .header-actions {
@ -487,41 +464,33 @@ const emitRefresh = () => {
.tabs { .tabs {
display: flex; display: flex;
border-bottom: 1px solid var(--gray-200); border-bottom: 1px solid var(--gray-100);
position: relative; position: relative;
align-items: center; align-items: center;
padding: 0 16px; padding: 4px 6px;
gap: 4px;
flex-shrink: 0; flex-shrink: 0;
} }
.tab { .tab {
padding: 12px 16px; padding: 4px 12px;
border: none; border: none;
background: none; background: none;
color: var(--gray-600); color: var(--gray-600);
cursor: pointer; cursor: pointer;
font-size: 14px; font-size: 12px;
font-weight: 500; font-weight: 500;
transition: all 0.15s ease; transition: all 0.15s ease;
position: relative; border-radius: 999px;
&:hover { &:hover {
color: var(--main-700); background: var(--gray-100);
color: var(--gray-900);
} }
&.active { &.active {
color: var(--main-700); background: var(--gray-100);
color: var(--gray-900);
&::after {
content: '';
position: absolute;
bottom: -1px;
left: 0;
right: 0;
height: 2px;
background: var(--main-500);
border-radius: 1px;
}
} }
} }

View File

@ -13,7 +13,7 @@
/> />
<a-tooltip title="支持 txt/md/docx/html 格式 ≤ 5 MB" placement="right"> <a-tooltip title="支持 txt/md/docx/html 格式 ≤ 5 MB" placement="right">
<div class="option-content"> <div class="option-content">
<FileText :size="18" class="option-icon" /> <FileText :size="14" class="option-icon" />
<span class="option-text">添加附件</span> <span class="option-text">添加附件</span>
</div> </div>
</a-tooltip> </a-tooltip>
@ -23,7 +23,7 @@
<div class="option-item" @click="handleImageUpload"> <div class="option-item" @click="handleImageUpload">
<a-tooltip title="支持 jpg/jpeg/png/gif ≤ 5 MB" placement="right"> <a-tooltip title="支持 jpg/jpeg/png/gif ≤ 5 MB" placement="right">
<div class="option-content"> <div class="option-content">
<Image :size="18" class="option-icon" /> <Image :size="14" class="option-icon" />
<span class="option-text">上传图片</span> <span class="option-text">上传图片</span>
</div> </div>
</a-tooltip> </a-tooltip>
@ -171,9 +171,9 @@ const processImageUpload = async (file) => {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px; gap: 10px;
padding: 8px 12px; padding: 6px 10px;
color: var(--gray-700); color: var(--gray-700);
font-size: 14px; font-size: 12px;
border-radius: 6px; border-radius: 6px;
transition: all 0.15s ease; transition: all 0.15s ease;

View File

@ -18,7 +18,7 @@
</div> </div>
<!-- search 操作原有的文件分组显示 --> <!-- search 操作原有的文件分组显示 -->
<div v-else class="knowledge-base-result"> <div v-else-if="isArrayResult(resultContent)" class="knowledge-base-result">
<div class="result-summary"> <div class="result-summary">
找到 {{ parsedData(resultContent).length }} 个相关文档片段来自 找到 {{ parsedData(resultContent).length }} 个相关文档片段来自
{{ fileGroups(parsedData(resultContent)).length }} 个文件 {{ fileGroups(parsedData(resultContent)).length }} 个文件
@ -131,6 +131,13 @@
</div> </div>
</a-modal> </a-modal>
</div> </div>
<!-- 纯文本结果显示 -->
<div v-else class="knowledge-base-result">
<div class="plain-text-result">
<pre class="plain-text-content">{{ resultContent }}</pre>
</div>
</div>
</template> </template>
</BaseToolCall> </BaseToolCall>
</template> </template>
@ -200,6 +207,20 @@ const parseData = (content) => {
const parsedData = (content) => parseData(content) const parsedData = (content) => parseData(content)
//
const isArrayResult = (content) => {
if (Array.isArray(content)) return true
if (typeof content === 'string') {
try {
const parsed = JSON.parse(content)
return Array.isArray(parsed)
} catch {
return false
}
}
return false
}
// //
const expandedFiles = ref(new Set()) const expandedFiles = ref(new Set())
@ -297,6 +318,21 @@ const formatMindmapResult = (content) => {
} }
} }
.plain-text-result {
padding: 12px 16px;
max-height: 300px;
overflow-y: auto;
.plain-text-content {
margin: 0;
font-size: 13px;
line-height: 1.6;
color: var(--gray-700);
white-space: pre-wrap;
word-break: break-word;
}
}
.result-summary { .result-summary {
padding: 12px 16px; padding: 12px 16px;
background: var(--gray-25); background: var(--gray-25);