feat:1.历史记录重构UI重构2.置顶功能实现

This commit is contained in:
liailing1026
2026-02-27 11:39:20 +08:00
parent dad1ac2e51
commit c009db12a6
6 changed files with 376 additions and 149 deletions

View File

@@ -0,0 +1,157 @@
<template>
<el-dialog
v-model="dialogVisible"
:width="width"
:modal="false"
:close-on-click-modal="false"
:center="true"
:show-close="false"
top="20vh"
@closed="handleClosed"
>
<template #header>
<div class="dialog-header">
<SvgIcon icon-class="JingGao" size="20px" color="#ff6712" />
<span>{{ title }}</span>
<button class="dialog-close-btn" @click="handleCancel">
<SvgIcon icon-class="close" size="18px" />
</button>
</div>
</template>
<slot>
<span>{{ content }}</span>
</slot>
<template #footer>
<span class="dialog-footer">
<el-button @click="handleCancel">{{ cancelText }}</el-button>
<el-button class="confirm-btn" type="danger" :loading="loading" @click="handleConfirm">
{{ confirmText }}
</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import SvgIcon from '@/components/SvgIcon/index.vue'
interface Props {
modelValue: boolean
title?: string
content?: string
width?: string | number
confirmText?: string
cancelText?: string
}
const props = withDefaults(defineProps<Props>(), {
title: '确认删除',
content: '删除后,该操作无法恢复!',
width: '400px',
confirmText: '删除',
cancelText: '取消'
})
const emit = defineEmits<{
(e: 'update:modelValue', value: boolean): void
(e: 'confirm'): void
(e: 'cancel'): void
}>()
const loading = ref(false)
const dialogVisible = ref(props.modelValue)
// 监听 v-model 变化
watch(
() => props.modelValue,
val => {
dialogVisible.value = val
}
)
// 监听对话框显示状态变化
watch(dialogVisible, val => {
emit('update:modelValue', val)
})
// 确认删除
const handleConfirm = async () => {
loading.value = true
emit('confirm')
}
// 取消
const handleCancel = () => {
emit('cancel')
dialogVisible.value = false
}
// 对话框关闭后重置 loading 状态
const handleClosed = () => {
loading.value = false
}
// 暴露方法供外部调用
defineExpose({
setLoading: (val: boolean) => {
loading.value = val
},
close: () => {
dialogVisible.value = false
}
})
</script>
<style scoped lang="scss">
.dialog-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
font-size: 16px;
font-weight: 600;
width: 100%;
.dialog-close-btn {
background: none;
border: none;
cursor: pointer;
padding: 4px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
margin-left: auto;
transition: transform 0.3s ease;
&:hover {
transform: rotate(360deg);
}
}
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
.el-button:not(.confirm-btn) {
background: #ffffff;
border-color: #dcdfe6;
color: #000000;
&:hover {
background: #f5f5f5;
border-color: #c0c0c0;
}
}
}
.confirm-btn {
--el-button-bg-color: #ff6712;
--el-button-border-color: #ff6712;
--el-button-hover-bg-color: #e55a0f;
--el-button-hover-border-color: #e55a0f;
}
</style>

View File

@@ -18,7 +18,7 @@ const symbolId = computed(() => `#${props.prefix}-${props.iconClass}`)
</script>
<template>
<svg aria-hidden="true" class="svg-icon" :style="`color:${props.color}`">
<svg aria-hidden="true" class="svg-icon" :style="props.color ? `color:${props.color}` : ''">
<use :xlink:href="symbolId" />
</svg>
</template>

View File

@@ -1,13 +1,5 @@
<template>
<div class="history-list">
<div class="header">
<h3>📋 历史任务</h3>
<el-button type="primary" link @click="fetchPlans">
<el-icon><Refresh /></el-icon>
刷新
</el-button>
</div>
<!-- 加载状态 -->
<div v-if="loading" class="loading">
<el-icon class="is-loading"><Loading /></el-icon>
@@ -24,66 +16,61 @@
:key="plan.id"
class="plan-item"
:class="{ active: selectedPlanId === plan.id }"
@click="selectPlan(plan)"
@click="restorePlan(plan)"
>
<div class="plan-info">
<div class="plan-goal">{{ plan.general_goal || '未知任务' }}</div>
<div class="plan-meta">
<el-tag size="small" :type="getStatusType(plan.status)">
{{ getStatusText(plan.status) }}
</el-tag>
<span class="plan-time">{{ formatTime(plan.created_at) }}</span>
</div>
<div class="plan-stats">
<span>执行次数: {{ plan.execution_count }}</span>
<div class="plan-goal">
<SvgIcon icon-class="XiaoXi" size="20px" />
{{ plan.general_goal || '未知任务' }}
</div>
</div>
<div class="plan-actions">
<el-button
type="primary"
size="small"
@click.stop="restorePlan(plan)"
:disabled="restoring"
<el-popover
placement="bottom-end"
:show-arrow="false"
trigger="click"
popper-class="action-popover"
>
恢复
</el-button>
<el-button
type="danger"
size="small"
link
@click.stop="deletePlan(plan)"
>
<el-icon><Delete /></el-icon>
</el-button>
<template #reference>
<button class="more-btn" @click.stop>
<SvgIcon icon-class="more" class="more-icon" size="16px" />
</button>
</template>
<div class="action-menu">
<div class="action-item" @click="pinPlan(plan)">
<SvgIcon icon-class="ZhiDing" size="14px" />
<span>{{ plan.is_pinned ? '取消置顶' : '置顶' }}</span>
</div>
<div class="action-item">
<SvgIcon icon-class="FenXiang" size="14px" />
<span>分享</span>
</div>
<div class="action-item" @click="deletePlan(plan)">
<SvgIcon icon-class="ShanChu" size="14px" />
<span>删除</span>
</div>
</div>
</el-popover>
</div>
</div>
</div>
<!-- 分页 -->
<div v-if="plans.length > 0" class="pagination">
<span> {{ total }} 个任务</span>
</div>
<!-- 删除确认对话框 -->
<el-dialog
<DeleteConfirmDialog
v-model="dialogVisible"
title="删除确认"
width="400px"
:close-on-click-modal="false"
>
<span>确定要删除任务 "{{ planToDelete?.general_goal }}" 此操作不可恢复</span>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="danger" @click="confirmDelete" :loading="deleting">确定删除</el-button>
</template>
</el-dialog>
title="确认删除该任务 ?"
content="删除后,该任务无法恢复 !"
@confirm="confirmDelete"
/>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { ElMessage } from 'element-plus'
import { Refresh, Loading, Delete } from '@element-plus/icons-vue'
import { Loading } from '@element-plus/icons-vue'
import SvgIcon from '@/components/SvgIcon/index.vue'
import DeleteConfirmDialog from '@/components/DeleteConfirmDialog/index.vue'
import websocket from '@/utils/websocket'
// 事件定义
@@ -94,12 +81,9 @@ const emit = defineEmits<{
// 数据类型
interface PlanInfo {
id: string // 对应数据库 task_id
general_goal: string // 对应数据库 query
status: string
execution_count: number
created_at: string
// 完整恢复数据
id: string
general_goal: string
is_pinned?: boolean
task_outline?: any
assigned_agents?: any
agent_scores?: any
@@ -111,8 +95,6 @@ const plans = ref<PlanInfo[]>([])
const loading = ref(false)
const restoring = ref(false)
const selectedPlanId = ref<string | null>(null)
const total = ref(0)
const isConnected = ref(false)
// 删除对话框相关
const dialogVisible = ref(false)
@@ -137,22 +119,14 @@ const fetchPlans = async () => {
try {
const result = await websocket.send('get_plans', { id: reqId })
plans.value = (result.data || []) as PlanInfo[]
total.value = plans.value.length
isConnected.value = true
} catch (error) {
console.error('获取任务列表失败:', error)
ElMessage.error('获取任务列表失败')
isConnected.value = false
} finally {
loading.value = false
}
}
// 选择任务
const selectPlan = (plan: PlanInfo) => {
selectedPlanId.value = plan.id
}
// 恢复任务
const restorePlan = async (plan: PlanInfo) => {
if (restoring.value) return
@@ -192,6 +166,25 @@ const restorePlan = async (plan: PlanInfo) => {
}
}
// 置顶/取消置顶任务
const pinPlan = async (plan: PlanInfo) => {
const newPinnedState = !plan.is_pinned
const reqId = generateRequestId()
try {
await websocket.send('pin_plan', {
id: reqId,
data: { plan_id: plan.id, is_pinned: newPinnedState }
})
ElMessage.success(newPinnedState ? '置顶成功' : '取消置顶成功')
// 成功后会自动通过 history_updated 事件刷新列表
} catch (error) {
console.error('置顶任务失败:', error)
ElMessage.error('置顶失败')
}
}
// 删除任务
const deletePlan = (plan: PlanInfo) => {
planToDelete.value = plan
@@ -222,44 +215,6 @@ const confirmDelete = async () => {
}
}
// 格式化时间
const formatTime = (timeStr: string | undefined) => {
if (!timeStr) return ''
try {
const date = new Date(timeStr)
return date.toLocaleString('zh-CN', {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
})
} catch {
return timeStr
}
}
// 获取状态类型
const getStatusType = (status: string): string => {
const statusMap: Record<string, string> = {
'generating': 'warning',
'executing': 'warning',
'completed': 'success',
'stopped': 'danger'
}
return statusMap[status] || 'info'
}
// 获取状态文本
const getStatusText = (status: string): string => {
const statusMap: Record<string, string> = {
'generating': '生成中',
'executing': '执行中',
'completed': '已完成',
'stopped': '已停止'
}
return statusMap[status] || status
}
// 生命周期
onMounted(() => {
fetchPlans()
@@ -281,19 +236,6 @@ onUnmounted(() => {
flex-direction: column;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
}
}
.loading {
display: flex;
align-items: center;
@@ -315,17 +257,21 @@ onUnmounted(() => {
padding: 12px;
margin-bottom: 8px;
border-radius: 8px;
background: #f5f7fa;
background: var(--color-bg-three);
cursor: pointer;
transition: all 0.2s;
&:hover {
background: #ecf5ff;
background: var(--color-bg-hover);
.plan-goal {
color: var(--color-text-plan-item-hover);
}
}
&.active {
background: #ecf5ff;
border-left: 3px solid #409eff;
background: var(--color-bg-hover);
border-left: 3px solid var(--color-primary);
}
}
@@ -335,29 +281,17 @@ onUnmounted(() => {
}
.plan-goal {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
font-weight: 500;
margin-bottom: 6px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.plan-meta {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
}
.plan-time {
font-size: 12px;
color: #909399;
}
.plan-stats {
font-size: 12px;
color: #606266;
color: var(--color-text-plan-item);
transition: color 0.2s;
}
.plan-actions {
@@ -366,11 +300,83 @@ onUnmounted(() => {
margin-left: 12px;
}
.pagination {
// 更多按钮
.more-btn {
background: none;
border: none;
cursor: pointer;
padding: 4px;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
margin-top: 16px;
color: #909399;
border-radius: 50%;
transition: background-color 0.2s;
.more-icon {
color: var(--color-text-plan-item);
transition: color 0.2s;
}
&:hover {
background-color: var(--color-card-border-three);
border-radius: 50%;
.more-icon {
color: var(--color-text-plan-item-hover);
}
}
}
// 操作菜单
.action-menu {
display: flex;
flex-direction: column;
:deep(.el-popover__content) {
padding: 0;
}
}
.action-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
cursor: pointer;
font-size: 13px;
color: var(--color-text-plan-item);
transition: background-color 0.2s, color 0.2s;
.svg-icon {
color: var(--color-text-plan-item);
transition: color 0.2s;
}
&:first-child {
border-radius: 8px 8px 0 0;
}
&:last-child {
border-radius: 0 0 8px 8px;
}
&:hover {
background-color: var(--color-card-border-three);
color: var(--color-text-plan-item-hover);
.svg-icon {
color: var(--color-text-plan-item-hover);
}
}
}
</style>
<style lang="scss">
.action-popover {
padding: 0 !important;
border-radius: 8px !important;
width: 120px !important;
min-width: 120px !important;
}
</style>