CanvasView.vue 10.1 KB
<script setup lang="ts">
import { computed, shallowRef } from 'vue'
import ModeTabs from '@/components/layout/ModeTabs.vue'
import { useTaskViewModel, type TaskStage, type TaskWorkspaceContext } from '@/composables/useTaskViewModel'
import ObserveView from '@/views/ObserveView.vue'
import AnalyzeView from '@/views/AnalyzeView.vue'
import DeliverView from '@/views/DeliverView.vue'
import SettingsView from '@/views/SettingsView.vue'
import TasksView from '@/views/TasksView.vue'

type WorkspaceMode = 'observe' | 'analyze' | 'deliver' | 'tasks' | 'settings'
type WorkflowStage = 'draft' | 'crawler' | 'analysis' | 'report'
type TaskReportContext = TaskWorkspaceContext

const taskViewModel = useTaskViewModel()

const activeMode = shallowRef<WorkspaceMode>('tasks')
const selectedTask = shallowRef<TaskReportContext | null>(null)

function isReportReady(task?: TaskReportContext | null): boolean {
  if (!task) {
    return false
  }

  return task.hasReport || task.reportStatus === 'completed' || task.reportProgress >= 100
}

function resolveTaskStage(task?: TaskReportContext | null): TaskStage {
  if (!task) {
    return 'draft'
  }

  if (task.reportStatus === 'error' || task.reportStatus === 'failed' || task.reportStatus === 'cancelled') {
    return 'failed'
  }

  if (task.status === 'failed' || task.status === 'error') {
    return 'failed'
  }

  if (isReportReady(task)) {
    return 'completed'
  }

  if (task.reportStatus === 'running' || task.reportStatus === 'queued' || task.reportStatus === 'pending') {
    return 'report'
  }

  if (task.status === 'processing' || task.status === 'running' || task.progress >= 45) {
    return 'analysis'
  }

  if (task.status === 'crawled') {
    return 'analysis'
  }

  if (task.status === 'queued' || task.status === 'crawling' || task.progress >= 20) {
    return 'crawler'
  }

  return 'draft'
}

function normalizeWorkflowStage(stage: TaskStage): WorkflowStage {
  switch (stage) {
    case 'crawler':
      return 'crawler'
    case 'analysis':
      return 'analysis'
    case 'report':
    case 'completed':
      return 'report'
    default:
      return 'draft'
  }
}

const currentTask = computed<TaskReportContext | null>(() => {
  const fallback = selectedTask.value

  if (activeMode.value !== 'tasks' && activeMode.value !== 'settings') {
    return taskViewModel.resolveTaskContext(fallback?.id, fallback)
  }

  return fallback
})

const taskStage = computed<TaskStage>(() => {
  const task = currentTask.value
  const taskView = taskViewModel.resolveTaskView(task?.id)

  if (taskView && task?.id && taskView.id === task.id) {
    return taskView.stage
  }

  return resolveTaskStage(task)
})

const workflowStage = computed<WorkflowStage>(() => {
  switch (activeMode.value) {
    case 'observe':
      return taskStage.value === 'draft' ? 'draft' : 'crawler'
    case 'analyze':
      return 'analysis'
    case 'deliver':
      return 'report'
    default:
      return normalizeWorkflowStage(taskStage.value)
  }
})

const workflowTips = computed(() => {
  const task = currentTask.value
  if (!task) {
    switch (activeMode.value) {
      case 'observe':
        return '已进入新的分析入口,请先填写任务 Brief,再把采集、分析和交付串起来。'
      case 'analyze':
        return '请先从任务中心选择任务,或先在 Observe 完成 Brief 与采集同步。'
      case 'deliver':
        return '请先选择任务并生成报告,再进入交付页面。'
      default:
        return '先创建一个分析任务,再把采集、分析和报告主线串起来。'
    }
  }

  switch (workflowStage.value) {
    case 'crawler':
      return `正在为 ${task.venueName} 准备采集上下文,下一步应继续同步或启动爬虫。`
    case 'analysis':
      return '任务上下文已就绪,可以继续推进 Query / Media / Insight / Forum 的协作分析。'
    case 'report':
      return `分析主线已经进入交付阶段,可以生成、刷新或查看 ${task.venueName} 的报告。`
    default:
      return `已选中 ${task.venueName},可以继续补全任务 Brief 并建立后续主线。`
  }
})

const stageNotice = computed(() => {
  const task = currentTask.value

  if (!task) {
    switch (activeMode.value) {
      case 'observe':
        return '已创建新的分析入口,请先填写任务 Brief。'
      case 'analyze':
        return '当前还没有稳定的任务上下文,请先从任务中心选择任务。'
      case 'deliver':
        return '当前还没有可交付的任务,请先选择任务或生成报告。'
      default:
        return ''
    }
  }

  if (activeMode.value === 'deliver') {
    return isReportReady(task)
      ? `报告已就绪,可直接进入 ${task.venueName} 的交付页面。`
      : `${task.venueName} 已切入交付流程,可继续生成或刷新报告。`
  }

  if (activeMode.value === 'analyze') {
    return `${task.venueName} 已进入 Agent 协作分析。`
  }

  if (activeMode.value === 'observe') {
    return workflowStage.value === 'crawler'
      ? `${task.venueName} 已同步到采集流程,建议继续启动爬虫。`
      : `${task.venueName || '当前任务'} 仍处于任务准备阶段。`
  }

  switch (taskStage.value) {
    case 'completed':
      return `报告已就绪,可直接进入 ${task.venueName} 的交付页面。`
    case 'report':
      return `${task.venueName} 已准备好进入报告交付阶段。`
    case 'analysis':
      return `${task.venueName} 已进入 Agent 分析阶段。`
    case 'crawler':
      return `${task.venueName} 已同步到采集流程,建议继续启动爬虫。`
    case 'failed':
      return `${task.venueName} 当前存在待处理异常,建议先回到主线排查。`
    default:
      return `${task.venueName || '当前任务'} 仍处于任务准备阶段。`
  }
})

function navigateTo(mode: WorkspaceMode, task?: TaskReportContext) {
  if (task) {
    selectedTask.value = task
  }

  activeMode.value = mode
}

function setMode(mode: WorkspaceMode) {
  activeMode.value = mode
}

function handleOpenReport(task: TaskReportContext) {
  navigateTo('deliver', task)
}

function handleOpenTaskDetail(task: TaskReportContext) {
  navigateTo(task.status === 'draft' ? 'observe' : 'analyze', task)
}

function handleCreateAnalysis() {
  navigateTo('observe')
}

function handleTaskPrepared(task: TaskReportContext) {
  navigateTo('observe', task)
}

function handleCrawlerSynced(task: TaskReportContext) {
  navigateTo('observe', task)
}

function handleStartAnalysis(task: TaskReportContext) {
  navigateTo('analyze', task)
}

function handleGoDeliver(task: TaskReportContext) {
  navigateTo('deliver', task)
}
</script>

<template>
  <section class="canvas">
    <ModeTabs v-model:active-mode="activeMode" />

    <div class="workflow-banner">
      <div class="workflow-banner__copy">
        <span class="workflow-banner__label">分析流程 Workflow</span>
        <strong>{{ workflowTips }}</strong>
        <span v-if="stageNotice" class="workflow-banner__notice">{{ stageNotice }}</span>
      </div>
      <div class="workflow-banner__steps">
        <span class="workflow-step" :class="{ 'workflow-step--active': workflowStage === 'draft' }">1. 新建分析</span>
        <span class="workflow-step" :class="{ 'workflow-step--active': workflowStage === 'crawler' }">2. 同步爬虫</span>
        <span class="workflow-step" :class="{ 'workflow-step--active': workflowStage === 'analysis' }">3. Agent 分析</span>
        <span class="workflow-step" :class="{ 'workflow-step--active': workflowStage === 'report' }">4. 报告交付</span>
      </div>
    </div>

    <div class="canvas__content">
      <TasksView
        v-if="activeMode === 'tasks'"
        @create-analysis="handleCreateAnalysis"
        @open-report="handleOpenReport"
        @open-task-detail="handleOpenTaskDetail"
      />
      <ObserveView
        v-else-if="activeMode === 'observe'"
        :selected-task="selectedTask"
        @task-prepared="handleTaskPrepared"
        @crawler-synced="handleCrawlerSynced"
        @start-analysis="handleStartAnalysis"
      />
      <AnalyzeView
        v-else-if="activeMode === 'analyze'"
        :selected-task="selectedTask"
        @go-deliver="handleGoDeliver"
      />
      <DeliverView
        v-else-if="activeMode === 'deliver'"
        :selected-task="selectedTask"
        @back-to-tasks="setMode('tasks')"
      />
      <SettingsView v-else-if="activeMode === 'settings'" />
    </div>
  </section>
</template>

<style scoped>
.canvas {
  display: flex;
  flex-direction: column;
  height: 100%;
  padding: 24px;
  background: var(--bg-base);
}

.workflow-banner {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 20px;
  margin: 12px 0 20px;
  padding: 16px 18px;
  border: 1px solid rgba(196, 149, 106, 0.22);
  border-radius: var(--radius-xl);
  background: linear-gradient(135deg, rgba(196, 149, 106, 0.08), rgba(13, 33, 28, 0.04));
}

.workflow-banner__copy {
  display: grid;
  gap: 6px;
}

.workflow-banner__label {
  font-family: var(--font-mono);
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-muted);
}

.workflow-banner__copy strong {
  color: var(--text-primary);
  font-size: 14px;
}

.workflow-banner__notice {
  font-size: 13px;
  color: var(--accent-dark, var(--accent));
}

.workflow-banner__steps {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 8px;
}

.workflow-step {
  padding: 8px 12px;
  border-radius: var(--radius-full);
  background: rgba(255, 255, 255, 0.68);
  border: 1px solid var(--border-subtle);
  color: var(--text-muted);
  font-size: 12px;
  white-space: nowrap;
}

.workflow-step--active {
  background: var(--primary);
  border-color: var(--primary);
  color: var(--text-inverse);
}

.canvas__content {
  flex: 1;
  animation: fadeIn 0.3s ease;
}

@keyframes fadeIn {
  from {
    opacity: 0;
    transform: translateY(8px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@media (max-width: 1080px) {
  .workflow-banner {
    flex-direction: column;
    align-items: stretch;
  }

  .workflow-banner__steps {
    justify-content: flex-start;
  }
}

@media (max-width: 768px) {
  .canvas {
    padding: 16px;
  }
}
</style>