feat:导出板块的静态实现

This commit is contained in:
liailing1026
2026-03-03 15:49:39 +08:00
parent 2be5a5a454
commit 00500c9bd6
12 changed files with 572 additions and 7 deletions

View File

@@ -0,0 +1,418 @@
<template>
<div class="export-list">
<!-- 导出样式选择 -->
<div class="export-style-grid">
<div
v-for="item in exportStyles"
:key="item.type"
class="export-style-item"
@click="handleSelect(item)"
>
<div class="style-icon" :style="{ color: item.color }">
<SvgIcon :icon-class="item.icon" size="24px" />
</div>
<div class="style-name">{{ item.name }}</div>
</div>
</div>
<!-- 导出结果列表 -->
<div class="export-result-section">
<div v-if="exportResults.length > 0" class="result-list">
<div v-for="(result, index) in exportResults" :key="index" class="result-item">
<div class="result-icon" :style="{ color: result.color }">
<SvgIcon :icon-class="result.icon" size="30px" />
</div>
<div class="result-info">
<div class="result-name">{{ result.name }}</div>
<div class="result-time">{{ formatTime(result.exportTime || Date.now()) }}</div>
</div>
<div class="result-actions">
<el-popover
placement="bottom-end"
:width="120"
:show-arrow="false"
trigger="click"
popper-class="action-popover"
>
<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="previewResult(result)">
<SvgIcon icon-class="YuLan" size="14px" />
<span>预览文件</span>
</div>
<div class="action-item" @click="downloadResult(result)">
<SvgIcon icon-class="XiaZai" size="14px" />
<span>下载文件</span>
</div>
<div class="action-item" @click="shareResult(result)">
<SvgIcon icon-class="FenXiang" size="14px" />
<span>分享文件</span>
</div>
<div class="action-item" @click="deleteResult(result)">
<SvgIcon icon-class="ShanChu" size="14px" />
<span>删除文件</span>
</div>
</div>
</el-popover>
</div>
</div>
</div>
<div v-else class="result-empty">暂无导出记录</div>
</div>
<!-- 删除确认对话框 -->
<DeleteConfirmDialog
v-model="dialogVisible"
title="确认删除该导出记录 ?"
content="删除后,该记录无法恢复 !"
@confirm="confirmDelete"
/>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
import SvgIcon from '@/components/SvgIcon/index.vue'
import DeleteConfirmDialog from '@/components/DeleteConfirmDialog/index.vue'
import { useAgentsStore } from '@/stores'
const agentsStore = useAgentsStore()
// 事件定义
const emit = defineEmits<{
(e: 'close'): void
}>()
// 导出样式类型
interface ExportStyle {
type: string
name: string
icon: string
color: string
}
// 导出结果类型
interface ExportResult {
name: string
icon: string
type: string
color: string
exportTime?: number // 时间戳
}
// 导出样式数据
const exportStyles = ref<ExportStyle[]>([
{ type: 'doc', name: '文本报告', icon: 'DOCX', color: '#00A2D2' },
{ type: 'markdown', name: 'Markdown', icon: 'Markdown', color: '#FF5B5B' },
{ type: 'mindmap', name: '思维导图', icon: 'SiWeiDaoTu', color: '#FF5BEE' },
{ type: 'infographic', name: '信息图', icon: 'XinXiTu', color: '#FCE460' },
{ type: 'excel', name: '数据表格', icon: 'XLSX', color: '#65AE00' },
{ type: 'ppt', name: '演示文稿', icon: 'PPT', color: '#FF7914' }
])
// 导出结果列表
const exportResults = ref<ExportResult[]>([])
// 删除对话框相关
const dialogVisible = ref(false)
const resultToDelete = ref<ExportResult | null>(null)
// 格式化时间显示
const formatTime = (timestamp: number): string => {
const now = Date.now()
const diff = now - timestamp
const minutes = Math.floor(diff / 60000)
const hours = Math.floor(diff / 3600000)
const days = Math.floor(diff / 86400000)
if (minutes < 1) return '刚刚'
if (minutes < 60) return `${minutes}分钟前`
if (hours < 24) return `${hours}小时前`
if (days < 7) return `${days}天前`
const date = new Date(timestamp)
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(
date.getDate()
).padStart(2, '0')}`
}
// 预览文件
const previewResult = (_result: ExportResult) => {
ElMessage.success('预览功能开发中')
}
// 下载文件
const downloadResult = (_result: ExportResult) => {
ElMessage.success('下载功能开发中')
}
// 分享
const shareResult = (_result: ExportResult) => {
ElMessage.success('分享功能开发中')
}
// 删除
const deleteResult = (result: ExportResult) => {
resultToDelete.value = result
dialogVisible.value = true
}
// 确认删除
const confirmDelete = () => {
if (!resultToDelete.value) return
const index = exportResults.value.indexOf(resultToDelete.value)
if (index > -1) {
exportResults.value.splice(index, 1)
ElMessage.success('删除成功')
}
dialogVisible.value = false
resultToDelete.value = null
}
// 选择导出样式
const handleSelect = (item: ExportStyle) => {
const currentTask = agentsStore.currentTask
if (!currentTask) {
ElMessage.warning('请先选择任务')
return
}
// 生成导出名称:任务名称 + 样式名称
const taskName = currentTask.StepName || '未知任务'
const exportName = `${taskName}${item.name}`
// 添加到导出结果列表(添加到最前面)
const newItem = {
name: exportName,
icon: item.icon,
type: item.type,
color: item.color,
exportTime: Date.now()
}
exportResults.value.unshift(newItem)
console.log('导出任务:', { task: currentTask, style: item })
ElMessage.success(`已开始导出: ${item.name}`)
}
</script>
<style scoped lang="scss">
.export-list {
padding: 16px;
height: 100%;
display: flex;
flex-direction: column;
}
.export-style-grid {
display: grid;
grid-template-columns: repeat(3, 152px);
gap: 12px;
justify-content: center;
margin: 0 -16px 16px -16px;
padding: 0 16px 16px 16px;
border-bottom: 1px solid var(--color-border-separate);
}
.export-style-item {
width: 152px;
height: 50px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
padding: 0 12px;
gap: 8px;
border-radius: 12px;
background: var(--color-bg-detail);
cursor: pointer;
transition: all 0.2s;
&:hover {
background: var(--color-bg-content-hover);
}
.style-icon {
flex-shrink: 0;
color: var(--color-text-plan-item);
}
.style-name {
font-size: 14px;
font-weight: 500;
color: var(--color-text-plan-item);
white-space: nowrap;
}
}
.export-result-section {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
.result-title {
font-size: 14px;
font-weight: 500;
color: var(--color-text-primary);
margin-bottom: 12px;
}
.result-list {
flex: 1;
overflow-y: auto;
max-height: calc(100vh - 430px);
}
.result-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
margin-bottom: 8px;
border-radius: 8px;
background: var(--color-bg-three);
transition: background 0.2s;
&:hover {
background: var(--color-bg-hover);
.result-name {
color: var(--color-text-plan-item-hover);
}
}
.result-icon {
color: var(--color-text-plan-item);
}
.result-info {
flex: 1;
min-width: 0;
.result-name {
font-size: 14px;
color: var(--color-text-placeholder);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
transition: color 0.2s;
}
.result-time {
font-size: 12px;
color: var(--color-text-plan-item);
margin-top: 4px;
}
}
.result-actions {
display: flex;
gap: 8px;
margin-left: 12px;
}
}
// 更多按钮
.more-btn {
background: none;
border: none;
cursor: pointer;
padding: 4px;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
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);
.more-icon {
color: var(--color-text-plan-item-hover);
}
}
}
// 操作菜单
.action-menu {
display: flex;
flex-direction: column;
}
.result-empty {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
color: var(--color-text-placeholder);
font-size: 14px;
}
}
</style>
<style lang="scss">
.action-popover {
padding: 0 !important;
border-radius: 8px !important;
width: 120px !important;
min-width: 120px !important;
background: var(--color-bg-three) !important;
border: 1px solid var(--color-card-border-three) !important;
}
.action-menu {
display: flex;
flex-direction: column;
.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;
}
&:hover {
background-color: var(--color-card-border-three);
color: var(--color-text-plan-item-hover);
.svg-icon {
color: var(--color-text-plan-item-hover);
}
}
&:first-child {
border-radius: 8px 8px 0 0;
}
&:last-child {
border-radius: 0 0 8px 8px;
}
}
</style>

View File

@@ -23,6 +23,7 @@ const props = defineProps<{
const emit = defineEmits<{
(e: 'refreshLine'): void
(e: 'setCurrentTask', task: IRawStepTask): void
(e: 'openExport'): void
}>()
const agentsStore = useAgentsStore()
@@ -1148,7 +1149,7 @@ onMounted(() => {
})
// 按钮交互状态管理
type ButtonState = 'process' | 'execute' | 'refresh' | null
type ButtonState = 'process' | 'execute' | 'refresh' | 'export' | null
const buttonHoverState = ref<ButtonState>(null)
let buttonHoverTimer: ReturnType<typeof setTimeout> | null = null
@@ -1173,6 +1174,8 @@ const handleExecuteMouseEnter = () =>
setButtonHoverState('execute', !!agentsStore.agentRawPlan.data)
const handleRefreshMouseEnter = () =>
setButtonHoverState('refresh', agentsStore.executePlan.length > 0)
const handleExportMouseEnter = () =>
setButtonHoverState('export', agentsStore.executePlan.length > 0)
const handleButtonMouseLeave = () => {
clearButtonHoverTimer()
@@ -1189,21 +1192,44 @@ onUnmounted(() => {
})
// 计算按钮类名
const processBtnClass = computed(() => {
if (buttonHoverState.value === 'refresh' || buttonHoverState.value === 'execute') {
if (
buttonHoverState.value === 'refresh' ||
buttonHoverState.value === 'execute' ||
buttonHoverState.value === 'export'
) {
return 'circle'
}
return buttonHoverState.value === 'process' ? 'ellipse' : 'circle'
})
const executeBtnClass = computed(() => {
if (buttonHoverState.value === 'process' || buttonHoverState.value === 'refresh') {
if (
buttonHoverState.value === 'process' ||
buttonHoverState.value === 'refresh' ||
buttonHoverState.value === 'export'
) {
return 'circle task-execute-btn'
}
return agentsStore.agentRawPlan.data ? 'ellipse task-execute-btn' : 'circle task-execute-btn'
})
const refreshBtnClass = computed(() => {
if (buttonHoverState.value === 'process' || buttonHoverState.value === 'execute') {
if (
buttonHoverState.value === 'process' ||
buttonHoverState.value === 'execute' ||
buttonHoverState.value === 'export'
) {
return 'circle'
}
return agentsStore.executePlan.length > 0 ? 'ellipse' : 'circle'
})
const exportBtnClass = computed(() => {
if (
buttonHoverState.value === 'process' ||
buttonHoverState.value === 'execute' ||
buttonHoverState.value === 'refresh'
) {
return 'circle'
}
return agentsStore.executePlan.length > 0 ? 'ellipse' : 'circle'
@@ -1223,6 +1249,10 @@ const showRefreshText = computed(() => {
return buttonHoverState.value === 'refresh'
})
const showExportText = computed(() => {
return buttonHoverState.value === 'export'
})
// 计算按钮标题
const processBtnTitle = computed(() => {
return buttonHoverState.value === 'process' ? '查看任务流程' : '点击查看任务流程'
@@ -1255,7 +1285,7 @@ defineExpose({
<div class="text-[18px] font-bold mb-[7px] flex justify-between items-center px-[20px]">
<span class="text-[var(--color-text-title-header)]">执行结果</span>
<div
class="flex items-center justify-end gap-[10px] task-button-group w-[230px]"
class="flex items-center justify-end gap-[10px] task-button-group w-[280px]"
@mouseleave="handleButtonMouseLeave"
>
<!-- 刷新按钮 -->
@@ -1266,11 +1296,24 @@ defineExpose({
:disabled="agentsStore.executePlan.length === 0"
@mouseenter="handleRefreshMouseEnter"
@click="handleRefresh"
style="order: 0"
style="order: 1"
>
<svg-icon icon-class="refresh" />
<span v-if="showRefreshText" class="btn-text">重置</span>
</el-button>
<!-- 导出按钮 -->
<el-button
:class="exportBtnClass"
:color="variables.tertiary"
title="导出"
:disabled="agentsStore.executePlan.length === 0"
@mouseenter="handleExportMouseEnter"
@click="emit('openExport')"
style="order: 0"
>
<svg-icon icon-class="DaoChu" />
<span v-if="showExportText" class="btn-text">导出</span>
</el-button>
<!-- Task Process按钮 -->
<el-button
:class="processBtnClass"
@@ -1278,7 +1321,7 @@ defineExpose({
:title="processBtnTitle"
@mouseenter="handleProcessMouseEnter"
@click="handleTaskProcess"
style="order: 1"
style="order: 3"
>
<svg-icon icon-class="process" />
<span v-if="showProcessText" class="btn-text">任务过程</span>

View File

@@ -3,6 +3,7 @@ import AgentRepo from './AgentRepo/index.vue'
import TaskSyllabus from './TaskSyllabus/index.vue'
import TaskResult from './TaskResult/index.vue'
import HistoryList from './HistoryList/index.vue'
import ExportList from './ExportList/index.vue'
import SvgIcon from '@/components/SvgIcon/index.vue'
import { Jsplumb } from './utils.ts'
import { type IRawStepTask, useAgentsStore } from '@/stores'
@@ -24,11 +25,19 @@ const agentsStore = useAgentsStore()
// 历史记录弹窗控制
const historyDialogVisible = ref(false)
// 导出弹窗控制
const exportDialogVisible = ref(false)
// 打开历史记录弹窗
const openHistoryDialog = () => {
historyDialogVisible.value = true
}
// 打开导出弹窗
const openExportDialog = () => {
exportDialogVisible.value = true
}
// 处理历史任务恢复
const handleRestorePlan = (plan: any) => {
// 关闭弹窗
@@ -102,6 +111,7 @@ function clear() {
defineExpose({
openHistoryDialog,
openExportDialog,
changeTask,
resetAgentRepoLine,
clear
@@ -133,6 +143,7 @@ defineExpose({
:TaskID="props.TaskID"
@refresh-line="taskResultJsplumb.repaintEverything"
@set-current-task="handleTaskResultCurrentTask"
@open-export="openExportDialog"
/>
</div>
@@ -151,6 +162,22 @@ defineExpose({
</div>
</div>
</div>
<!-- 导出弹窗 -->
<div v-if="exportDialogVisible" class="export-drawer">
<div class="export-drawer-content">
<div class="export-drawer-header">
<span class="export-drawer-title">导出</span>
<button class="export-drawer-close" @click="exportDialogVisible = false">
<SvgIcon icon-class="close" size="20px" />
</button>
</div>
<div class="h-[1px] w-full bg-[var(--color-border-separate)] my-[8px]"></div>
<div class="export-drawer-body">
<ExportList @close="exportDialogVisible = false" />
</div>
</div>
</div>
</div>
</template>
@@ -247,4 +274,72 @@ defineExpose({
opacity: 1;
}
}
// 导出 Drawer 样式
:deep(.export-drawer) {
// 强制使用绝对定位,相对于父容器
position: absolute !important;
top: 0 !important;
bottom: 0 !important;
right: 0 !important;
z-index: 1000;
// 高度由 top/bottom 控制
height: auto !important;
width: 500px !important;
background-color: var(--color-bg-three);
border: 1px solid var(--color-card-border-three);
border-radius: 24px;
box-shadow: var(--color-card-shadow-three);
padding-top: 20px;
padding-bottom: 20px;
// 动画:从右向左滑入
animation: export-drawer-slide-in 0.5s ease-out;
// 头部
.export-drawer-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 16px;
background-color: var(--color-bg-three);
}
// 标题
.export-drawer-title {
color: var(--color-text-primary);
font-size: 16px;
font-weight: 600;
padding-left: 8px;
}
// 关闭按钮
.export-drawer-close {
background: none;
border: none;
cursor: pointer;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
color: var(--color-text-regular);
transition: transform 0.3s ease;
&:hover {
transform: rotate(360deg);
}
}
}
@keyframes export-drawer-slide-in {
from {
transform: translateX(100%);
}
to {
transform: translateX(0);
}
}
</style>