feat(todo-tools): 添加待办事项结果渲染组件并优化状态图标显示

添加 TodoListResult 组件用于渲染待办事项结果
使用 ant-design 图标替换原有 SVG 图标以保持一致性
优化待办事项状态显示样式
This commit is contained in:
Wenjie Zhang 2025-12-24 20:37:46 +08:00
parent 012e46a761
commit e8a5844af7
3 changed files with 201 additions and 59 deletions

View File

@ -39,17 +39,13 @@
:key="index"
class="todo-item"
>
<span class="todo-status" :class="todo.status">
<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>
<div class="todo-status">
<CheckCircleOutlined v-if="todo.status === 'completed'" class="icon completed" />
<SyncOutlined v-else-if="todo.status === 'in_progress'" class="icon in-progress" spin />
<ClockCircleOutlined v-else-if="todo.status === 'pending'" class="icon pending" />
<CloseCircleOutlined v-else-if="todo.status === 'cancelled'" class="icon cancelled" />
<QuestionCircleOutlined v-else class="icon unknown" />
</div>
<span class="todo-text">{{ todo.content }}</span>
</div>
</div>
@ -110,6 +106,13 @@
<script setup>
import { computed, ref, watch } from 'vue';
import { Download } from 'lucide-vue-next';
import {
CheckCircleOutlined,
SyncOutlined,
ClockCircleOutlined,
CloseCircleOutlined,
QuestionCircleOutlined
} from '@ant-design/icons-vue'
const props = defineProps({
visible: {
@ -367,59 +370,19 @@ const emitRefresh = () => {
.todo-status {
flex-shrink: 0;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
margin-top: 2px;
border-radius: 50%;
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 {
background: var(--color-success-50);
color: var(--color-success-700);
.icon {
transform: scale(1.1);
}
}
&.in_progress {
background: var(--color-warning-50);
color: var(--color-warning-700);
}
&.pending {
background: var(--gray-100);
color: var(--gray-500);
}
}
@keyframes spin {
0% {
transform: rotate(0deg);
stroke-dashoffset: 0;
}
50% {
stroke-dashoffset: 15.708;
}
100% {
transform: rotate(360deg);
stroke-dashoffset: 0;
font-size: 16px;
&.completed { color: #52c41a; }
&.in-progress { color: #1890ff; }
&.pending { color: #faad14; }
&.cancelled { color: #ff4d4f; }
&.unknown { color: var(--gray-400); }
}
}

View File

@ -0,0 +1,118 @@
<template>
<div class="todo-list-result">
<!-- <div class="todo-header">
<h4><UnorderedListOutlined /> 待办事项</h4>
</div> -->
<div class="todo-content">
<div v-for="(item, index) in data" :key="index" class="todo-item">
<div class="todo-status">
<CheckCircleOutlined v-if="item.status === 'completed'" class="icon completed" />
<SyncOutlined v-else-if="item.status === 'in_progress'" class="icon in-progress" spin />
<ClockCircleOutlined v-else-if="item.status === 'pending'" class="icon pending" />
<CloseCircleOutlined v-else-if="item.status === 'cancelled'" class="icon cancelled" />
<QuestionCircleOutlined v-else class="icon unknown" />
</div>
<div class="todo-text" :class="{ completed: item.status === 'completed', cancelled: item.status === 'cancelled' }">
{{ item.content }}
</div>
</div>
</div>
</div>
</template>
<script setup>
import {
UnorderedListOutlined,
CheckCircleOutlined,
SyncOutlined,
ClockCircleOutlined,
CloseCircleOutlined,
QuestionCircleOutlined
} from '@ant-design/icons-vue'
defineProps({
data: {
type: Array,
required: true
}
})
</script>
<style lang="less" scoped>
.todo-list-result {
background: var(--gray-0);
border-radius: 8px;
// border: 1px solid var(--gray-200);
overflow: hidden;
.todo-header {
padding: 12px 16px;
border-bottom: 1px solid var(--gray-100);
background: var(--gray-25);
h4 {
margin: 0;
font-size: 14px;
font-weight: 500;
color: var(--main-color);
display: flex;
align-items: center;
gap: 6px;
}
}
.todo-content {
padding: 12px;
display: flex;
flex-direction: column;
gap: 8px;
}
.todo-item {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 8px 12px;
border-radius: 6px;
background: var(--gray-50);
border: 1px solid var(--gray-100);
transition: background-color 0.2s;
&:hover {
background: var(--gray-100);
}
.todo-status {
padding-top: 2px;
flex-shrink: 0;
.icon {
font-size: 16px;
&.completed { color: #52c41a; }
&.in-progress { color: #1890ff; }
&.pending { color: #faad14; }
&.cancelled { color: #ff4d4f; }
&.unknown { color: var(--gray-400); }
}
}
.todo-text {
font-size: 14px;
line-height: 1.5;
color: var(--gray-800);
word-break: break-word;
&.completed {
text-decoration: line-through;
color: var(--gray-500);
}
&.cancelled {
text-decoration: line-through;
color: var(--gray-400);
}
}
}
}
</style>

View File

@ -19,6 +19,12 @@
ref="graphResultRef"
/>
<!-- 待办事项结果 -->
<TodoListResult
v-else-if="isTodoListResult"
:data="todoListData"
/>
<!-- 计算器结果 -->
<CalculatorResult
v-else-if="isCalculatorResult"
@ -49,6 +55,7 @@ import WebSearchResult from './WebSearchResult.vue'
import KnowledgeBaseResult from './KnowledgeBaseResult.vue'
import KnowledgeGraphResult from './KnowledgeGraphResult.vue'
import CalculatorResult from './CalculatorResult.vue'
import TodoListResult from './TodoListResult.vue'
import { useAgentStore } from '@/stores/agent';
const agentStore = useAgentStore()
@ -81,6 +88,60 @@ const parsedData = computed(() => {
return props.resultContent
})
const todoListData = computed(() => {
if (props.toolName !== 'write_todos') return []
const raw = props.resultContent
// 1. Try from parsedData (JSON object)
const data = parsedData.value
if (data && typeof data === 'object') {
if (Array.isArray(data)) return data
if (data.todos && Array.isArray(data.todos)) return data.todos
}
// 2. Try parsing string if it matches specific pattern
if (typeof raw === 'string') {
let str = raw
if (str.startsWith('Updated todo list to ')) {
str = str.replace('Updated todo list to ', '')
}
// Try regex parsing for Python-like string
const items = []
// Matches {'content': '...', 'status': '...'} with escaped quotes support
// content might contain escaped quotes
const contentRegex = /'content':\s*'((?:[^'\\]|\\.)*)'/
const statusRegex = /'status':\s*'((?:[^'\\]|\\.)*)'/
// Split by "}, {" roughly, or just look for objects
// Since it is a list of dicts, we can match individual dicts
const dictRegex = /\{.*?\}/g
const dictMatches = str.match(dictRegex)
if (dictMatches) {
for (const dictStr of dictMatches) {
const contentMatch = dictStr.match(contentRegex)
const statusMatch = dictStr.match(statusRegex)
if (contentMatch && statusMatch) {
items.push({
content: contentMatch[1].replace(/\\'/g, "'").replace(/\\\\/g, "\\"),
status: statusMatch[1]
})
}
}
}
if (items.length > 0) return items
}
return []
})
const isTodoListResult = computed(() => {
return props.toolName === 'write_todos' && todoListData.value.length > 0
})
//
const isWebSearchResult = computed(() => {
const toolNameLower = props.toolName.toLowerCase()