feat:1.历史记录重构UI重构2.置顶功能实现
This commit is contained in:
157
frontend/src/components/DeleteConfirmDialog/index.vue
Normal file
157
frontend/src/components/DeleteConfirmDialog/index.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user