App.vue 6.41 KB
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { ElMessage } from 'element-plus'
import { FlowEditor, normalizeGraphData } from '@flow-editor'
import type {
  FlowEditorRef,
  FlowEditorInputGraphData,
  ToolbarButtonDefinition,
} from '@flow-editor'

const editorRef = ref<FlowEditorRef | null>(null)
const currentVersionLabel = ref('基线版本')
const latestAction = ref('等待初始化')
const flowData = ref<FlowEditorInputGraphData>({
  nodes: [],
  edges: [],
})

const toolbarButtonHandler = (buttons: ToolbarButtonDefinition[]) => {
  const hiddenKeys = new Set(['grid', 'miniMap', 'delete', 'undo', 'redo'])
  return buttons.filter((button) => !hiddenKeys.has(button.key))
}

const hasLoadedRemoteBootstrap = computed(() => {
  const normalizedGraph = normalizeGraphData(flowData.value)
  return normalizedGraph.nodes.length > 0 || normalizedGraph.edges.length > 0
})

function onRef(instance: FlowEditorRef) {
  editorRef.value = instance
}

function centerCanvas() {
  editorRef.value?.commander.fitView()
  latestAction.value = '已执行居中/适应画布'
}

function saveFlowDraft() {
  latestAction.value = '当前为迁移阶段演示版,已模拟保存成功'
  ElMessage.success('当前为迁移阶段演示版,已模拟保存成功')
}

function openPreviewPanel() {
  editorRef.value?.openPreview()
  latestAction.value = '已打开流程预览'
}

async function loadBootstrapData() {
  // 这里保留旧项目的接口语义,方便把 Playwright 拦截能力平移到新项目。
  const requestJson = async <T,>(url: string, fallback: T): Promise<T> => {
    try {
      const response = await fetch(url)
      if (!response.ok) {
        return fallback
      }

      const payload = (await response.json()) as { data?: T }
      return payload.data ?? fallback
    } catch {
      return fallback
    }
  }

  const [versionList, graph] = await Promise.all([
    requestJson<Array<{ note?: string }>>('/admin/index.php?m=mod&a=flow_version', [
      { note: '基线版本' },
    ]),
    requestJson<FlowEditorInputGraphData>('/admin/index.php?m=srv&a=flow_nodes', {
      nodes: [],
      edges: [],
    }),
  ])

  currentVersionLabel.value = versionList[0]?.note || '基线版本'
  flowData.value = graph
  editorRef.value?.read(graph)
}

function bindTestContract() {
  window.__FLOW_EDITOR_TEST_API__ = {
    getEditorContract: () => {
      const instance = editorRef.value

      return {
        hasEditor: Boolean(instance),
        hasCommander: Boolean(instance?.commander),
        hasDelete: typeof instance?.commander?.delete === 'function',
        hasUndo: typeof instance?.commander?.undo === 'function',
        hasRedo: typeof instance?.commander?.redo === 'function',
        hasOpenModel: typeof instance?.openModel === 'function',
        hasCloseModel: typeof instance?.closeModel === 'function',
        hasAddNode: typeof instance?.addNode === 'function',
        hasUpdateModel: typeof instance?.updateModel === 'function',
        hasGraphRead: typeof instance?.read === 'function',
      }
    },
  }
}

onMounted(async () => {
  bindTestContract()
  await loadBootstrapData()
  bindTestContract()
})
</script>

<template>
  <div class="app">
    <header class="page-header">
      <div>
        <p class="page-eyebrow">流程编辑器重构迁移</p>
        <h1>新项目基线页</h1>
      </div>
      <div class="version-card">
        <span>当前启用版本</span>
        <strong>{{ currentVersionLabel }}</strong>
      </div>
    </header>

    <main class="page-main">
      <FlowEditor
        :data="flowData"
        :grid="false"
        :mini-map="false"
        :on-ref="onRef"
        :toolbar-button-handler="toolbarButtonHandler"
      >
        <template #toolbar-extra>
          <button
            type="button"
            class="toolbar-button toolbar-button--accent"
            @click="centerCanvas"
          >
            居中
          </button>
          <button
            type="button"
            class="toolbar-button toolbar-button--accent"
            @click="saveFlowDraft"
          >
            保存
          </button>
          <button
            type="button"
            class="toolbar-button toolbar-button--accent"
            data-testid="open-preview-panel"
            @click="openPreviewPanel"
          >
            预览测试
          </button>
        </template>
      </FlowEditor>

      <aside class="status-panel">
        <h2>迁移状态</h2>
        <p>当前目标:先建立新项目最小壳层与测试护栏,再逐步补齐 LogicFlow 实现。</p>
        <p>接口基线:{{ hasLoadedRemoteBootstrap ? '已接入初始化 mock 语义' : '使用本地兜底空图数据' }}</p>
        <p>最近动作:{{ latestAction }}</p>
      </aside>
    </main>
  </div>
</template>

<style scoped>
.app {
  min-height: 100vh;
  padding: 24px;
  background:
    radial-gradient(circle at top left, rgba(43, 104, 168, 0.14), transparent 32%),
    linear-gradient(180deg, #f5f7fb 0%, #eef2f8 100%);
  color: #18314f;
  font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
}

.page-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
  margin-bottom: 20px;
}

.page-header h1 {
  margin: 6px 0 0;
  font-size: 28px;
}

.page-eyebrow {
  margin: 0;
  font-size: 12px;
  letter-spacing: 0.14em;
  color: #52749b;
}

.version-card {
  min-width: 180px;
  padding: 14px 16px;
  border: 1px solid rgba(24, 49, 79, 0.1);
  border-radius: 16px;
  background: rgba(255, 255, 255, 0.9);
  box-shadow: 0 18px 40px rgba(34, 61, 96, 0.08);
}

.version-card span {
  display: block;
  margin-bottom: 6px;
  font-size: 12px;
  color: #52749b;
}

.version-card strong {
  font-size: 18px;
}

.page-main {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 320px;
  gap: 20px;
}

.status-panel {
  padding: 18px;
  border: 1px solid rgba(24, 49, 79, 0.08);
  border-radius: 20px;
  background: rgba(255, 255, 255, 0.88);
  box-shadow: 0 18px 32px rgba(34, 61, 96, 0.08);
}

.status-panel h2 {
  margin: 0 0 12px;
  font-size: 18px;
}

.status-panel p {
  margin: 0 0 10px;
  line-height: 1.6;
}

.toolbar-button {
  border: none;
  border-radius: 999px;
  background: #ffffff;
  color: #18314f;
}

.toolbar-button--accent {
  background: linear-gradient(135deg, #2b68a8 0%, #3f8ec6 100%);
  color: #ffffff;
}

@media (max-width: 960px) {
  .page-main {
    grid-template-columns: 1fr;
  }

  .page-header {
    flex-direction: column;
  }
}
</style>