365 lines
8.1 KiB
Vue
365 lines
8.1 KiB
Vue
<script setup lang="ts">
|
|
import { ref, computed, watch, nextTick } from 'vue'
|
|
import { useAgentsStore } from '@/stores'
|
|
import SvgIcon from '@/components/SvgIcon/index.vue'
|
|
import PlanModification from './TaskSyllabus/Branch/PlanModification.vue'
|
|
import AgentAllocation from './TaskSyllabus/components/AgentAllocation.vue'
|
|
import PlanTask from './TaskProcess/components/PlanTask.vue'
|
|
|
|
const agentsStore = useAgentsStore()
|
|
|
|
// 面板显示状态
|
|
const visible = ref(false)
|
|
const activeTab = ref('outline')
|
|
const isMaximized = ref(false)
|
|
|
|
// PlanTask 组件引用
|
|
const planTaskRef = ref<InstanceType<typeof PlanTask> | null>(null)
|
|
|
|
// 判断是否有流程数据
|
|
const planReady = computed(() => {
|
|
return agentsStore.agentRawPlan.data !== undefined
|
|
})
|
|
|
|
// 判断是否选中了任务
|
|
const hasSelectedTask = computed(() => {
|
|
return !!agentsStore.currentTask
|
|
})
|
|
|
|
// 标签页配置
|
|
const tabs = [
|
|
{ key: 'outline', label: '任务大纲编排', icon: 'One', required: '' },
|
|
{ key: 'agent', label: '专家智能体评选', icon: 'Two', required: 'selectedTask' },
|
|
{ key: 'process', label: '执行过程编排', icon: 'Three', required: 'selectedTask' }
|
|
]
|
|
|
|
// 判断标签是否可用
|
|
const isTabDisabled = (tab: typeof tabs[0]) => {
|
|
if (tab.required === 'selectedTask') {
|
|
return !hasSelectedTask.value
|
|
}
|
|
return false
|
|
}
|
|
|
|
// 获取禁用原因的提示文本
|
|
const getDisabledReason = (tab: typeof tabs[0]) => {
|
|
if (tab.required === 'selectedTask' && !hasSelectedTask.value) {
|
|
return '请先在任务大纲中选中一个任务'
|
|
}
|
|
return ''
|
|
}
|
|
|
|
// 打开面板
|
|
const open = () => {
|
|
visible.value = true
|
|
// 默认选中可用的第一个标签
|
|
const firstAvailableTab = tabs.find(t => !isTabDisabled(t))
|
|
if (firstAvailableTab) {
|
|
activeTab.value = firstAvailableTab.key
|
|
}
|
|
}
|
|
|
|
// 关闭面板
|
|
const close = () => {
|
|
visible.value = false
|
|
}
|
|
|
|
// 切换标签
|
|
const handleTabChange = (tabKey: string) => {
|
|
activeTab.value = tabKey
|
|
}
|
|
|
|
// 切换到执行过程编排时,延迟触发自适应视图
|
|
watch(activeTab, (newTab) => {
|
|
if (newTab === 'process') {
|
|
nextTick(() => {
|
|
setTimeout(() => {
|
|
// 通过 expose 的方法触发自适应视图(如果有的话)
|
|
;(planTaskRef.value as any)?.triggerFitView?.()
|
|
}, 400)
|
|
})
|
|
}
|
|
})
|
|
|
|
// 切换最大化/还原
|
|
const toggleMaximize = () => {
|
|
isMaximized.value = !isMaximized.value
|
|
}
|
|
|
|
// 暴露给父组件
|
|
defineExpose({
|
|
open,
|
|
close
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<Teleport to="body">
|
|
<!-- 面板主体 -->
|
|
<Transition name="slide-up">
|
|
<div v-if="visible" :class="['settings-panel', { 'is-maximized': isMaximized }]">
|
|
<!-- 标签栏 -->
|
|
<div class="panel-header">
|
|
<div class="tabs-container">
|
|
<div
|
|
v-for="tab in tabs"
|
|
:key="tab.key"
|
|
:class="[
|
|
'tab-item',
|
|
{ 'is-active': activeTab === tab.key },
|
|
{ 'is-disabled': isTabDisabled(tab) }
|
|
]"
|
|
:title="getDisabledReason(tab)"
|
|
@click="!isTabDisabled(tab) && handleTabChange(tab.key)"
|
|
>
|
|
<SvgIcon :icon-class="tab.icon" size="18px" />
|
|
<span class="tab-label">{{ tab.label }}</span>
|
|
<span v-if="isTabDisabled(tab)" class="tab-disabled-hint">
|
|
{{ getDisabledReason(tab) }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<button class="maximize-btn" @click="toggleMaximize">
|
|
<SvgIcon :icon-class="isMaximized ? 'SuoXiao' : 'FangDa'" size="20px" />
|
|
</button>
|
|
<button class="close-btn" @click="close">
|
|
<SvgIcon icon-class="close" size="20px" />
|
|
</button>
|
|
</div>
|
|
|
|
<!-- 内容区域 -->
|
|
<div class="panel-content">
|
|
<!-- 任务大纲 -->
|
|
<div v-show="activeTab === 'outline'" class="tab-content first-tab">
|
|
<PlanModification />
|
|
</div>
|
|
|
|
<!-- 智能体 -->
|
|
<div v-show="activeTab === 'agent'" class="tab-content">
|
|
<AgentAllocation />
|
|
</div>
|
|
|
|
<!-- 任务过程 -->
|
|
<div v-show="activeTab === 'process'" class="tab-content">
|
|
<PlanTask ref="planTaskRef" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Transition>
|
|
</Teleport>
|
|
</template>
|
|
|
|
<style scoped lang="scss">
|
|
// 面板主体
|
|
.settings-panel {
|
|
position: fixed;
|
|
bottom: 6%;
|
|
left: 0;
|
|
right: 0;
|
|
width: 80%;
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
height: 70vh;
|
|
background: var(--color-bg-three);
|
|
border-radius: 20px;
|
|
box-shadow: 0 -4px 30px rgba(0, 0, 0.4);
|
|
z-index: 3000;
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
transition: width 0.4s ease, height 0.4s ease, bottom 0.4s ease, border-radius 0.4s ease;
|
|
|
|
&.is-maximized {
|
|
max-width: none;
|
|
left: 24px;
|
|
right: 24px;
|
|
width: auto;
|
|
height: calc(100% - 194px);
|
|
bottom: 24px;
|
|
border-radius: 20px;
|
|
}
|
|
}
|
|
|
|
// 标题栏(包含标签栏)
|
|
.panel-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 10px 10px 0 30px;
|
|
flex-shrink: 0;
|
|
position: relative;
|
|
}
|
|
|
|
.tabs-container {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.maximize-btn {
|
|
background: none;
|
|
border: none;
|
|
cursor: pointer;
|
|
width: 32px;
|
|
height: 32px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border-radius: 6px;
|
|
color: var(--color-text-secondary);
|
|
transition: all 0.3s;
|
|
position: absolute;
|
|
right: 60px;
|
|
|
|
&:hover {
|
|
transform: rotate(180deg);
|
|
}
|
|
}
|
|
|
|
.close-btn {
|
|
background: none;
|
|
border: none;
|
|
cursor: pointer;
|
|
width: 32px;
|
|
height: 32px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border-radius: 6px;
|
|
color: var(--color-text-secondary);
|
|
transition: all 0.3s;
|
|
position: absolute;
|
|
right: 20px;
|
|
|
|
&:hover {
|
|
transform: rotate(180deg);
|
|
}
|
|
}
|
|
|
|
.tab-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 12px 20px;
|
|
cursor: pointer;
|
|
color: var(--color-text-secondary);
|
|
border-bottom: 2px solid transparent;
|
|
transition: all 0.2s;
|
|
position: relative;
|
|
margin: 0 4px;
|
|
|
|
&:hover:not(.is-disabled) {
|
|
color: var(--color-text-primary);
|
|
}
|
|
|
|
&.is-active {
|
|
color: var(--color-primary);
|
|
background: var(--color-settings-panel-content-bg);
|
|
border-radius: 5px 5px 0 0;
|
|
}
|
|
|
|
&:hover:not(.is-disabled)::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
width: calc(100% - 8px);
|
|
height: calc(100% - 8px);
|
|
background: var(--color-bg-detail);
|
|
border-radius: 40px;
|
|
z-index: -1;
|
|
}
|
|
|
|
&.is-disabled {
|
|
cursor: not-allowed;
|
|
opacity: 0.5;
|
|
}
|
|
|
|
:deep(.svg-icon) {
|
|
color: var(--color-text-secondary);
|
|
}
|
|
|
|
&:hover:not(.is-disabled) :deep(.svg-icon) {
|
|
color: var(--color-text-primary);
|
|
}
|
|
|
|
&.is-active :deep(.svg-icon) {
|
|
color: var(--color-primary);
|
|
}
|
|
}
|
|
|
|
.tab-label {
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.tab-disabled-hint {
|
|
position: absolute;
|
|
bottom: -30px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
background: var(--color-bg-detail);
|
|
color: var(--color-text-secondary);
|
|
padding: 4px 12px;
|
|
border-radius: 4px;
|
|
font-size: 12px;
|
|
white-space: nowrap;
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
transition: opacity 0.2s;
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
|
}
|
|
|
|
.tab-item.is-disabled:hover .tab-disabled-hint {
|
|
opacity: 1;
|
|
}
|
|
|
|
// 内容区域
|
|
.panel-content {
|
|
flex: 1;
|
|
overflow: hidden;
|
|
position: relative;
|
|
background: var(--color-settings-panel-content-bg);
|
|
margin: 0 10px 10px;
|
|
border-radius: 5px;
|
|
}
|
|
|
|
.tab-content {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
overflow: auto;
|
|
}
|
|
|
|
// 空状态
|
|
.empty-state {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
height: 100%;
|
|
color: var(--color-text-secondary);
|
|
gap: 16px;
|
|
|
|
p {
|
|
font-size: 14px;
|
|
}
|
|
}
|
|
|
|
// 动画效果
|
|
.slide-up-enter-active,
|
|
.slide-up-leave-active {
|
|
transition: opacity 0.5s ease, transform 0.5s ease;
|
|
}
|
|
|
|
.slide-up-enter-from,
|
|
.slide-up-leave-to {
|
|
opacity: 0;
|
|
transform: translateY(100%);
|
|
}
|
|
</style>
|