该接口用于根据 DataFlow ID 获取关联的 Python 脚本内容,支持前端代码预览和展示功能。
| 项目 | 说明 |
|---|---|
| 接口路径 | /api/dataflow/get-script/<dataflow_id> |
| 请求方法 | GET |
| Content-Type | application/json |
| 认证方式 | 根据项目配置(如有) |
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
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 | 脚本类型,可选值:python、javascript、typescript、sql、shell、text |
data.dataflow_id |
Integer | DataFlow 节点 ID |
data.dataflow_name |
String | DataFlow 中文名称 |
data.dataflow_name_en |
String | DataFlow 英文名称 |
{
"code": 400,
"message": "未找到 ID 为 12345 的 DataFlow 节点",
"data": {}
}
或
{
"code": 400,
"message": "DataFlow (ID: 12345) 的 script_path 属性为空",
"data": {}
}
{
"code": 404,
"message": "脚本文件不存在: /opt/dataops-platform/app/core/data_flow/scripts/missing_script.py",
"data": {}
}
{
"code": 500,
"message": "获取脚本失败: 数据库连接异常",
"data": {}
}
| 错误码 | 说明 | 处理建议 |
|---|---|---|
| 400 | 参数错误或 DataFlow 脚本路径为空 | 检查 DataFlow ID 是否正确,确认该 DataFlow 已关联脚本 |
| 404 | 脚本文件不存在 | 检查服务器上脚本文件是否存在,可能需要重新生成脚本 |
| 500 | 服务器内部错误 | 联系后端开发人员检查日志 |
// 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}`)
}
<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>
如果需要更强大的代码编辑器功能,可以使用 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>
npm install vue-prism-editor prismjs
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({})
]
}
| 版本 | 日期 | 更新内容 |
|---|---|---|
| v1.0.0 | 2025-01-05 | 初始版本,支持 Python 脚本获取和展示 |