api_get_script.md 15 KB

DataFlow 脚本获取接口 - 前端开发指南

接口概述

该接口用于根据 DataFlow ID 获取关联的 Python 脚本内容,支持前端代码预览和展示功能。


接口信息

项目 说明
接口路径 /api/dataflow/get-script/<dataflow_id>
请求方法 GET
Content-Type application/json
认证方式 根据项目配置(如有)

请求参数

URL 路径参数

参数名 类型 必填 说明
dataflow_id Integer DataFlow 节点的 ID(Neo4j 节点 ID)

请求示例

GET /api/dataflow/get-script/12345

返回数据格式

成功响应

{
  "code": 200,
  "message": "获取脚本成功",
  "data": {
    "script_path": "app/core/data_flow/scripts/sync_talent_data.py",
    "script_content": "#!/usr/bin/env python3\n\"\"\"人才数据同步脚本\"\"\"\n\nimport pandas as pd\nfrom app.config.config import get_config_by_env\n\ndef main():\n    config = get_config_by_env()\n    # 脚本主逻辑...\n    print('执行完成')\n\nif __name__ == '__main__':\n    main()\n",
    "script_type": "python",
    "dataflow_id": 12345,
    "dataflow_name": "人才数据同步",
    "dataflow_name_en": "sync_talent_data"
  }
}

返回字段说明

字段名 类型 说明
code Integer 状态码,200 表示成功
message String 响应消息
data.script_path String 脚本文件的相对路径
data.script_content String 脚本文件的完整内容
data.script_type String 脚本类型,可选值:pythonjavascripttypescriptsqlshelltext
data.dataflow_id Integer DataFlow 节点 ID
data.dataflow_name String DataFlow 中文名称
data.dataflow_name_en String DataFlow 英文名称

错误响应

1. DataFlow 不存在或脚本路径为空(400)

{
  "code": 400,
  "message": "未找到 ID 为 12345 的 DataFlow 节点",
  "data": {}
}

{
  "code": 400,
  "message": "DataFlow (ID: 12345) 的 script_path 属性为空",
  "data": {}
}

2. 脚本文件不存在(404)

{
  "code": 404,
  "message": "脚本文件不存在: /opt/dataops-platform/app/core/data_flow/scripts/missing_script.py",
  "data": {}
}

3. 服务器内部错误(500)

{
  "code": 500,
  "message": "获取脚本失败: 数据库连接异常",
  "data": {}
}

错误码汇总

错误码 说明 处理建议
400 参数错误或 DataFlow 脚本路径为空 检查 DataFlow ID 是否正确,确认该 DataFlow 已关联脚本
404 脚本文件不存在 检查服务器上脚本文件是否存在,可能需要重新生成脚本
500 服务器内部错误 联系后端开发人员检查日志

前端集成指南

1. Axios 请求封装

// api/dataflow.js
import axios from 'axios'

const BASE_URL = process.env.VUE_APP_API_BASE_URL || ''

/**
 * 获取 DataFlow 关联的脚本内容
 * @param {number} dataflowId - DataFlow 节点 ID
 * @returns {Promise} 返回脚本信息
 */
export function getDataFlowScript(dataflowId) {
  return axios.get(`${BASE_URL}/api/dataflow/get-script/${dataflowId}`)
}

2. Vue 3 组件示例(使用 Composition API)

<template>
  <div class="script-viewer">
    <!-- 头部信息 -->
    <div class="script-header" v-if="scriptData">
      <div class="script-info">
        <h3>{{ scriptData.dataflow_name }}</h3>
        <span class="script-path">{{ scriptData.script_path }}</span>
      </div>
      <div class="script-actions">
        <el-button type="primary" size="small" @click="copyScript">
          <el-icon><CopyDocument /></el-icon>
          复制代码
        </el-button>
        <el-button size="small" @click="downloadScript">
          <el-icon><Download /></el-icon>
          下载
        </el-button>
      </div>
    </div>

    <!-- 加载状态 -->
    <div class="loading-container" v-if="loading">
      <el-skeleton :rows="15" animated />
    </div>

    <!-- 错误提示 -->
    <el-alert
      v-if="error"
      :title="error.title"
      :description="error.message"
      type="error"
      show-icon
      :closable="false"
    />

    <!-- 代码展示区域 -->
    <div class="code-container" v-if="scriptData && !loading">
      <prism-editor
        v-model="scriptData.script_content"
        :highlight="highlighter"
        :readonly="true"
        line-numbers
        class="code-editor"
      />
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, computed } from 'vue'
import { useRoute } from 'vue-router'
import { ElMessage } from 'element-plus'
import { CopyDocument, Download } from '@element-plus/icons-vue'
import { PrismEditor } from 'vue-prism-editor'
import 'vue-prism-editor/dist/prismeditor.min.css'
import { highlight, languages } from 'prismjs'
import 'prismjs/components/prism-python'
import 'prismjs/themes/prism-tomorrow.css'
import { getDataFlowScript } from '@/api/dataflow'

const route = useRoute()
const loading = ref(false)
const error = ref(null)
const scriptData = ref(null)

// Prism 代码高亮
const highlighter = (code) => {
  const lang = scriptData.value?.script_type || 'python'
  const grammar = languages[lang] || languages.plain
  return highlight(code, grammar, lang)
}

// 获取脚本内容
const fetchScript = async (dataflowId) => {
  loading.value = true
  error.value = null
  
  try {
    const response = await getDataFlowScript(dataflowId)
    
    if (response.data.code === 200) {
      scriptData.value = response.data.data
    } else {
      error.value = {
        title: '获取脚本失败',
        message: response.data.message
      }
    }
  } catch (err) {
    console.error('获取脚本失败:', err)
    
    if (err.response) {
      const { code, message } = err.response.data
      error.value = {
        title: getErrorTitle(code),
        message: message
      }
    } else {
      error.value = {
        title: '网络错误',
        message: '无法连接到服务器,请检查网络连接'
      }
    }
  } finally {
    loading.value = false
  }
}

// 根据错误码获取标题
const getErrorTitle = (code) => {
  const titles = {
    400: '参数错误',
    404: '文件不存在',
    500: '服务器错误'
  }
  return titles[code] || '未知错误'
}

// 复制代码到剪贴板
const copyScript = async () => {
  if (!scriptData.value?.script_content) return
  
  try {
    await navigator.clipboard.writeText(scriptData.value.script_content)
    ElMessage.success('代码已复制到剪贴板')
  } catch (err) {
    ElMessage.error('复制失败,请手动选择复制')
  }
}

// 下载脚本文件
const downloadScript = () => {
  if (!scriptData.value) return
  
  const { script_content, dataflow_name_en, script_type } = scriptData.value
  const extension = script_type === 'python' ? 'py' : script_type
  const filename = `${dataflow_name_en || 'script'}.${extension}`
  
  const blob = new Blob([script_content], { type: 'text/plain;charset=utf-8' })
  const url = URL.createObjectURL(blob)
  const link = document.createElement('a')
  link.href = url
  link.download = filename
  link.click()
  URL.revokeObjectURL(url)
  
  ElMessage.success(`已下载: ${filename}`)
}

// 组件挂载时获取脚本
onMounted(() => {
  const dataflowId = route.params.id || route.query.dataflowId
  if (dataflowId) {
    fetchScript(Number(dataflowId))
  }
})

// 暴露方法供父组件调用
defineExpose({
  fetchScript
})
</script>

<style scoped>
.script-viewer {
  padding: 16px;
  background: #fff;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

.script-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 16px;
  padding-bottom: 12px;
  border-bottom: 1px solid #e8e8e8;
}

.script-info h3 {
  margin: 0 0 4px 0;
  font-size: 16px;
  color: #333;
}

.script-path {
  font-size: 12px;
  color: #999;
  font-family: 'Courier New', monospace;
}

.script-actions {
  display: flex;
  gap: 8px;
}

.loading-container {
  padding: 20px 0;
}

.code-container {
  border: 1px solid #e8e8e8;
  border-radius: 4px;
  overflow: hidden;
}

.code-editor {
  font-family: 'Fira Code', 'Consolas', 'Monaco', monospace;
  font-size: 14px;
  line-height: 1.6;
  padding: 16px;
  background: #1e1e1e;
  min-height: 400px;
  max-height: 600px;
  overflow: auto;
}

/* Prism Editor 行号样式 */
:deep(.prism-editor__line-numbers) {
  background: #252526;
  color: #858585;
  padding: 16px 8px;
  border-right: 1px solid #404040;
}
</style>

3. 使用 Monaco Editor 的高级示例

如果需要更强大的代码编辑器功能,可以使用 Monaco Editor:

<template>
  <div class="monaco-script-viewer">
    <div class="viewer-header" v-if="scriptData">
      <div class="file-info">
        <el-icon><Document /></el-icon>
        <span class="filename">{{ getFilename }}</span>
        <el-tag size="small" :type="getLanguageTagType">
          {{ scriptData.script_type }}
        </el-tag>
      </div>
      <div class="toolbar">
        <el-tooltip content="复制代码">
          <el-button :icon="CopyDocument" circle size="small" @click="copyCode" />
        </el-tooltip>
        <el-tooltip content="下载文件">
          <el-button :icon="Download" circle size="small" @click="downloadFile" />
        </el-tooltip>
        <el-tooltip content="全屏查看">
          <el-button :icon="FullScreen" circle size="small" @click="toggleFullscreen" />
        </el-tooltip>
      </div>
    </div>

    <div 
      ref="editorContainer" 
      class="editor-container"
      :class="{ 'fullscreen': isFullscreen }"
    />
  </div>
</template>

<script setup>
import { ref, onMounted, onBeforeUnmount, watch, computed } from 'vue'
import * as monaco from 'monaco-editor'
import { Document, CopyDocument, Download, FullScreen } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { getDataFlowScript } from '@/api/dataflow'

const props = defineProps({
  dataflowId: {
    type: Number,
    required: true
  }
})

const editorContainer = ref(null)
const scriptData = ref(null)
const loading = ref(false)
const isFullscreen = ref(false)

let editor = null

// 计算文件名
const getFilename = computed(() => {
  if (!scriptData.value?.script_path) return ''
  return scriptData.value.script_path.split('/').pop()
})

// 语言标签类型
const getLanguageTagType = computed(() => {
  const types = {
    python: 'success',
    javascript: 'warning',
    sql: 'info',
    shell: ''
  }
  return types[scriptData.value?.script_type] || ''
})

// 获取 Monaco 语言标识
const getMonacoLanguage = (scriptType) => {
  const languageMap = {
    python: 'python',
    javascript: 'javascript',
    typescript: 'typescript',
    sql: 'sql',
    shell: 'shell'
  }
  return languageMap[scriptType] || 'plaintext'
}

// 初始化编辑器
const initEditor = () => {
  if (!editorContainer.value) return
  
  editor = monaco.editor.create(editorContainer.value, {
    value: '',
    language: 'python',
    theme: 'vs-dark',
    readOnly: true,
    automaticLayout: true,
    minimap: { enabled: true },
    scrollBeyondLastLine: false,
    fontSize: 14,
    lineNumbers: 'on',
    renderLineHighlight: 'all',
    folding: true,
    wordWrap: 'on'
  })
}

// 加载脚本
const loadScript = async () => {
  if (!props.dataflowId) return
  
  loading.value = true
  try {
    const response = await getDataFlowScript(props.dataflowId)
    
    if (response.data.code === 200) {
      scriptData.value = response.data.data
      
      if (editor) {
        const language = getMonacoLanguage(scriptData.value.script_type)
        monaco.editor.setModelLanguage(editor.getModel(), language)
        editor.setValue(scriptData.value.script_content)
      }
    } else {
      ElMessage.error(response.data.message)
    }
  } catch (err) {
    ElMessage.error('加载脚本失败')
    console.error(err)
  } finally {
    loading.value = false
  }
}

// 复制代码
const copyCode = async () => {
  const content = editor?.getValue()
  if (!content) return
  
  try {
    await navigator.clipboard.writeText(content)
    ElMessage.success('代码已复制')
  } catch (err) {
    ElMessage.error('复制失败')
  }
}

// 下载文件
const downloadFile = () => {
  if (!scriptData.value) return
  
  const blob = new Blob([scriptData.value.script_content], { type: 'text/plain' })
  const url = URL.createObjectURL(blob)
  const a = document.createElement('a')
  a.href = url
  a.download = getFilename.value
  a.click()
  URL.revokeObjectURL(url)
}

// 切换全屏
const toggleFullscreen = () => {
  isFullscreen.value = !isFullscreen.value
  setTimeout(() => editor?.layout(), 100)
}

// 监听 dataflowId 变化
watch(() => props.dataflowId, (newId) => {
  if (newId) loadScript()
})

onMounted(() => {
  initEditor()
  loadScript()
})

onBeforeUnmount(() => {
  editor?.dispose()
})
</script>

<style scoped>
.monaco-script-viewer {
  display: flex;
  flex-direction: column;
  height: 100%;
  background: #1e1e1e;
  border-radius: 8px;
  overflow: hidden;
}

.viewer-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 8px 16px;
  background: #252526;
  border-bottom: 1px solid #3c3c3c;
}

.file-info {
  display: flex;
  align-items: center;
  gap: 8px;
  color: #cccccc;
}

.filename {
  font-family: 'Consolas', monospace;
  font-size: 13px;
}

.toolbar {
  display: flex;
  gap: 4px;
}

.editor-container {
  flex: 1;
  min-height: 400px;
}

.editor-container.fullscreen {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 9999;
  min-height: 100vh;
}
</style>

依赖安装

Vue Prism Editor

npm install vue-prism-editor prismjs

Monaco Editor

npm install monaco-editor

对于 Vite 项目,还需要安装 worker 插件:

npm install vite-plugin-monaco-editor

vite.config.js 中配置:

import monacoEditorPlugin from 'vite-plugin-monaco-editor'

export default {
  plugins: [
    monacoEditorPlugin({})
  ]
}

使用场景

  1. DataFlow 详情页面 - 展示关联的处理脚本
  2. 任务执行历史 - 查看执行时使用的脚本版本
  3. 脚本审核流程 - 代码评审和确认
  4. 调试和排错 - 快速查看脚本内容定位问题

注意事项

  1. 脚本内容可能较大:建议添加加载动画和懒加载机制
  2. 特殊字符转义:脚本内容中可能包含特殊字符,前端展示时注意 XSS 防护
  3. 编码格式:脚本文件统一使用 UTF-8 编码
  4. 缓存策略:可根据业务需求添加前端缓存,减少重复请求

更新日志

版本 日期 更新内容
v1.0.0 2025-01-05 初始版本,支持 Python 脚本获取和展示