|
|
@@ -10,7 +10,7 @@ import {
|
|
|
ArrowBackOutline, SaveOutline, ChatbubbleEllipsesOutline,
|
|
|
SparklesOutline, RocketOutline, FlaskOutline, GitBranchOutline, ExitOutline,
|
|
|
PlayOutline, CloseOutline, ColorWandOutline, LinkOutline, TrashOutline, OpenOutline,
|
|
|
- TimeOutline, LibraryOutline
|
|
|
+ TimeOutline, LibraryOutline, DownloadOutline, CloudUploadOutline
|
|
|
} from '@vicons/ionicons5'
|
|
|
import { useWorkflowStore } from '../../stores/workflow'
|
|
|
import { useSkillStore } from '../../stores/skill'
|
|
|
@@ -103,7 +103,7 @@ const inputFields = computed(() => {
|
|
|
for (const node of getNodes.value) {
|
|
|
if (node.type === 'userInput' && node.data?.variables) {
|
|
|
for (const v of node.data.variables) {
|
|
|
- if (v.name) fields.push(v)
|
|
|
+ if (v.name) fields.push({ ...v, nodeName: node.data?.nodeName || '输入节点' })
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
@@ -361,6 +361,8 @@ function deleteNodeById(nodeId) {
|
|
|
}
|
|
|
}
|
|
|
removeNodes(nodeId)
|
|
|
+ // 节点删除后,其后继节点中指向该节点的显式关联回退为自动推断
|
|
|
+ revertSuccessorMappings(nodeId)
|
|
|
if (selectedNode.value?.id === nodeId) {
|
|
|
selectedNode.value = null
|
|
|
}
|
|
|
@@ -512,14 +514,17 @@ function confirmImportField(candidate) {
|
|
|
if (importDialog.mode === 'predecessor') {
|
|
|
// 输出节点:引入到 fields(接收字段仍作为业务输出);其他节点:引入到 inputs(含 mapping)
|
|
|
const targetField = nodeType === 'output' ? 'fields' : 'inputs'
|
|
|
+ const exists = (selectedData.value?.[targetField] || []).some(f => f.name === candidate.fieldName)
|
|
|
+ if (exists) {
|
|
|
+ message.warning(`字段「${candidate.fieldName}」已在${FIELD_LIST_LABELS[targetField]}中存在`)
|
|
|
+ return
|
|
|
+ }
|
|
|
const newField = {
|
|
|
name: candidate.fieldName,
|
|
|
label: candidate.fieldLabel || candidate.fieldName,
|
|
|
type: candidate.fieldType || 'string',
|
|
|
- description: candidate.fieldDescription || ''
|
|
|
- }
|
|
|
- if (targetField === 'inputs') {
|
|
|
- newField.mapping = {
|
|
|
+ description: candidate.fieldDescription || '',
|
|
|
+ mapping: {
|
|
|
sourceNodeId: candidate.nodeId,
|
|
|
sourceField: candidate.fieldName,
|
|
|
sourcePath: null,
|
|
|
@@ -531,6 +536,11 @@ function confirmImportField(candidate) {
|
|
|
} else if (importDialog.mode === 'successor') {
|
|
|
// 输入节点:引入到 variables(输入变量仍对业务暴露);其他节点:引入到 outputs
|
|
|
const targetField = nodeType === 'userInput' ? 'variables' : 'outputs'
|
|
|
+ const exists = (selectedData.value?.[targetField] || []).some(f => f.name === candidate.fieldName)
|
|
|
+ if (exists) {
|
|
|
+ message.warning(`字段「${candidate.fieldName}」已在${FIELD_LIST_LABELS[targetField]}中存在`)
|
|
|
+ return
|
|
|
+ }
|
|
|
const newField = {
|
|
|
name: candidate.fieldName,
|
|
|
label: candidate.fieldLabel || candidate.fieldName,
|
|
|
@@ -582,6 +592,24 @@ function updateNodeName(value) {
|
|
|
onFieldChange('nodeName', trimmed)
|
|
|
}
|
|
|
|
|
|
+// 节点内字段列表名称唯一性校验
|
|
|
+const FIELD_LIST_LABELS = {
|
|
|
+ variables: '输入变量',
|
|
|
+ inputs: '前置数据',
|
|
|
+ outputs: '输出变量',
|
|
|
+ fields: '接收字段'
|
|
|
+}
|
|
|
+
|
|
|
+function updateFieldName(item, index, listKey, newName) {
|
|
|
+ const list = selectedData.value?.[listKey] || []
|
|
|
+ const trimmed = (newName || '').trim()
|
|
|
+ if (trimmed && list.some((f, i) => i !== index && f.name === trimmed)) {
|
|
|
+ message.warning(`变量名「${trimmed}」在${FIELD_LIST_LABELS[listKey] || listKey}中已存在`)
|
|
|
+ }
|
|
|
+ item.name = newName
|
|
|
+ onFieldChange(listKey, [...list])
|
|
|
+}
|
|
|
+
|
|
|
// ========== 智能体选项 ==========
|
|
|
const agentOptions = computed(() =>
|
|
|
(skillStore.skills || []).map(s => ({
|
|
|
@@ -701,7 +729,8 @@ const {
|
|
|
addListField, removeListField,
|
|
|
addVariable, removeVariable,
|
|
|
addOutputField, removeOutputField,
|
|
|
- addElif, removeElif, onConditionChange
|
|
|
+ addElif, removeElif, onConditionChange,
|
|
|
+ revertSuccessorMappings
|
|
|
} = useNodeFields({
|
|
|
selectedNode,
|
|
|
selectedData,
|
|
|
@@ -965,6 +994,39 @@ function validateGraphData() {
|
|
|
return { valid: true }
|
|
|
}
|
|
|
|
|
|
+// 校验各节点内部变量/字段名是否重复
|
|
|
+function validateDuplicateFieldNames() {
|
|
|
+ const listConfigs = [
|
|
|
+ { key: 'variables', label: '输入变量' },
|
|
|
+ { key: 'inputs', label: '前置数据' },
|
|
|
+ { key: 'outputs', label: '输出变量' },
|
|
|
+ { key: 'fields', label: '接收字段' }
|
|
|
+ ]
|
|
|
+ const duplicates = []
|
|
|
+ for (const node of getNodes.value.filter(n => !n.data?.virtual)) {
|
|
|
+ const d = node.data || {}
|
|
|
+ const nodeLabel = d.nodeName || d.label || node.id
|
|
|
+ for (const { key, label } of listConfigs) {
|
|
|
+ const items = d[key] || []
|
|
|
+ const seen = new Map()
|
|
|
+ const dupNames = new Set()
|
|
|
+ for (const item of items) {
|
|
|
+ const name = (item.name || '').trim()
|
|
|
+ if (!name) continue
|
|
|
+ if (seen.has(name)) {
|
|
|
+ dupNames.add(name)
|
|
|
+ } else {
|
|
|
+ seen.set(name, true)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (dupNames.size) {
|
|
|
+ duplicates.push({ nodeLabel, listLabel: label, names: [...dupNames] })
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return duplicates
|
|
|
+}
|
|
|
+
|
|
|
// ========== 保存 ==========
|
|
|
async function handleSave() {
|
|
|
// 校验 displayName(必填)
|
|
|
@@ -988,6 +1050,12 @@ async function handleSave() {
|
|
|
message.warning(validation.message)
|
|
|
return
|
|
|
}
|
|
|
+ const duplicates = validateDuplicateFieldNames()
|
|
|
+ if (duplicates.length) {
|
|
|
+ const lines = duplicates.map(d => `${d.nodeLabel}(${d.listLabel}):${d.names.join('、')}`)
|
|
|
+ message.warning('以下节点存在重名变量,请修改后再保存:\n' + lines.join('\n'))
|
|
|
+ return
|
|
|
+ }
|
|
|
saving.value = true
|
|
|
try {
|
|
|
const graphData = JSON.stringify({
|
|
|
@@ -1034,6 +1102,90 @@ function goBack() {
|
|
|
router.push('/orchestration')
|
|
|
}
|
|
|
|
|
|
+// ========== 运行对话框变量导出/导入 ==========
|
|
|
+async function exportRunVariables() {
|
|
|
+ const data = {}
|
|
|
+ for (const f of inputFields.value) {
|
|
|
+ const value = formData[f.name]
|
|
|
+ if (value !== undefined && value !== null && String(value).trim() !== '') {
|
|
|
+ data[f.name] = value
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (!Object.keys(data).length) {
|
|
|
+ message.warning('当前没有已输入的变量可导出')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ const workflowName = form.value.displayName || form.value.name || 'workflow'
|
|
|
+ const nodeName = inputFields.value[0]?.nodeName || 'input'
|
|
|
+ const fileName = `${workflowName}-${nodeName}-输入变量.json`
|
|
|
+ const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' })
|
|
|
+
|
|
|
+ // 优先使用 File System Access API,强制弹出保存位置选择框
|
|
|
+ if (typeof window !== 'undefined' && window.showSaveFilePicker) {
|
|
|
+ try {
|
|
|
+ const handle = await window.showSaveFilePicker({
|
|
|
+ suggestedName: fileName,
|
|
|
+ types: [{
|
|
|
+ description: 'JSON 文件',
|
|
|
+ accept: { 'application/json': ['.json'] }
|
|
|
+ }]
|
|
|
+ })
|
|
|
+ const writable = await handle.createWritable()
|
|
|
+ await writable.write(blob)
|
|
|
+ await writable.close()
|
|
|
+ message.success('变量已导出')
|
|
|
+ } catch (error) {
|
|
|
+ if (error.name === 'AbortError') {
|
|
|
+ // 用户取消选择,静默处理
|
|
|
+ return
|
|
|
+ }
|
|
|
+ message.error(`导出失败:${error.message}`)
|
|
|
+ }
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // 不支持 File System Access API 的浏览器回退到默认下载
|
|
|
+ const url = URL.createObjectURL(blob)
|
|
|
+ const a = document.createElement('a')
|
|
|
+ a.href = url
|
|
|
+ a.download = fileName
|
|
|
+ document.body.appendChild(a)
|
|
|
+ a.click()
|
|
|
+ document.body.removeChild(a)
|
|
|
+ URL.revokeObjectURL(url)
|
|
|
+ message.success('变量已导出(当前浏览器不支持选择保存位置)')
|
|
|
+}
|
|
|
+
|
|
|
+function onImportVariablesClick() {
|
|
|
+ const input = document.createElement('input')
|
|
|
+ input.type = 'file'
|
|
|
+ input.accept = 'application/json,.json'
|
|
|
+ input.onchange = e => importRunVariables(e.target.files?.[0])
|
|
|
+ input.click()
|
|
|
+}
|
|
|
+
|
|
|
+async function importRunVariables(file) {
|
|
|
+ if (!file) return
|
|
|
+ try {
|
|
|
+ const text = await file.text()
|
|
|
+ const data = JSON.parse(text)
|
|
|
+ if (!data || typeof data !== 'object') {
|
|
|
+ message.error('导入文件格式错误,应为 JSON 对象')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ let count = 0
|
|
|
+ for (const f of inputFields.value) {
|
|
|
+ if (Object.prototype.hasOwnProperty.call(data, f.name)) {
|
|
|
+ formData[f.name] = data[f.name]
|
|
|
+ count++
|
|
|
+ }
|
|
|
+ }
|
|
|
+ message.success(`已导入 ${count} 个变量`)
|
|
|
+ } catch (error) {
|
|
|
+ message.error(`导入失败:${error.message}`)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
// ========== 文件选择(运行时上传) ==========
|
|
|
function onFileSelect(e, isDir = false) {
|
|
|
const files = Array.from(e.target.files || [])
|
|
|
@@ -1047,6 +1199,11 @@ function onFileSelect(e, isDir = false) {
|
|
|
// 文件上传列表(运行对话框使用)
|
|
|
const uploadFiles = ref([])
|
|
|
|
|
|
+function handleOpenRunDialog() {
|
|
|
+ openRunDialog(inputFields, formData)
|
|
|
+ uploadFiles.value = []
|
|
|
+}
|
|
|
+
|
|
|
// 启动运行(包装 composable,处理上传文件 + 输入字段)
|
|
|
async function handleStartRun() {
|
|
|
const inputs = {}
|
|
|
@@ -1057,11 +1214,6 @@ async function handleStartRun() {
|
|
|
uploadFiles.value = []
|
|
|
}
|
|
|
|
|
|
-function handleOpenRunDialog() {
|
|
|
- openRunDialog(inputFields, formData)
|
|
|
- uploadFiles.value = []
|
|
|
-}
|
|
|
-
|
|
|
// 回滚应用到画布(供 useVersionHistory 调用)
|
|
|
async function applyVersionToCanvas(wf) {
|
|
|
if (!wf) return
|
|
|
@@ -1338,7 +1490,7 @@ function applyGraphData(graphData) {
|
|
|
<div v-for="(v, i) in selectedData?.variables || []" :key="i" class="var-form">
|
|
|
<div class="var-form-row">
|
|
|
<n-input :value="v.name" placeholder="变量名" size="small"
|
|
|
- @update:value="nv => { v.name = nv; onFieldChange('variables', [...selectedData.variables]) }" />
|
|
|
+ @update:value="nv => { updateFieldName(v, i, 'variables', nv) }" />
|
|
|
<n-input :value="v.label" placeholder="中文名" size="small"
|
|
|
@update:value="nv => { v.label = nv; onFieldChange('variables', [...selectedData.variables]) }" />
|
|
|
</div>
|
|
|
@@ -1385,7 +1537,7 @@ function applyGraphData(graphData) {
|
|
|
<div v-for="(v, i) in selectedData?.inputs || []" :key="'in-' + i" class="var-form">
|
|
|
<div class="var-form-row">
|
|
|
<n-input :value="v.name" placeholder="变量名" size="small"
|
|
|
- @update:value="nv => { v.name = nv; onFieldChange('inputs', [...selectedData.inputs]) }" />
|
|
|
+ @update:value="nv => { updateFieldName(v, i, 'inputs', nv) }" />
|
|
|
<n-input :value="v.label" placeholder="中文名" size="small"
|
|
|
@update:value="nv => { v.label = nv; onFieldChange('inputs', [...selectedData.inputs]) }" />
|
|
|
</div>
|
|
|
@@ -1417,7 +1569,7 @@ function applyGraphData(graphData) {
|
|
|
<div v-for="(v, i) in selectedData?.outputs || []" :key="'out-' + i" class="var-form">
|
|
|
<div class="var-form-row">
|
|
|
<n-input :value="v.name" placeholder="变量名" size="small"
|
|
|
- @update:value="nv => { v.name = nv; onFieldChange('outputs', [...selectedData.outputs]) }" />
|
|
|
+ @update:value="nv => { updateFieldName(v, i, 'outputs', nv) }" />
|
|
|
<n-input :value="v.label" placeholder="中文名" size="small"
|
|
|
@update:value="nv => { v.label = nv; onFieldChange('outputs', [...selectedData.outputs]) }" />
|
|
|
</div>
|
|
|
@@ -1466,7 +1618,7 @@ function applyGraphData(graphData) {
|
|
|
<div v-for="(v, i) in selectedData?.inputs || []" :key="'in-' + i" class="var-form">
|
|
|
<div class="var-form-row">
|
|
|
<n-input :value="v.name" placeholder="变量名" size="small"
|
|
|
- @update:value="nv => { v.name = nv; onFieldChange('inputs', [...selectedData.inputs]) }" />
|
|
|
+ @update:value="nv => { updateFieldName(v, i, 'inputs', nv) }" />
|
|
|
<n-input :value="v.label" placeholder="中文名" size="small"
|
|
|
@update:value="nv => { v.label = nv; onFieldChange('inputs', [...selectedData.inputs]) }" />
|
|
|
</div>
|
|
|
@@ -1498,7 +1650,7 @@ function applyGraphData(graphData) {
|
|
|
<div v-for="(v, i) in selectedData?.outputs || []" :key="'out-' + i" class="var-form">
|
|
|
<div class="var-form-row">
|
|
|
<n-input :value="v.name" placeholder="变量名" size="small"
|
|
|
- @update:value="nv => { v.name = nv; onFieldChange('outputs', [...selectedData.outputs]) }" />
|
|
|
+ @update:value="nv => { updateFieldName(v, i, 'outputs', nv) }" />
|
|
|
<n-input :value="v.label" placeholder="中文名" size="small"
|
|
|
@update:value="nv => { v.label = nv; onFieldChange('outputs', [...selectedData.outputs]) }" />
|
|
|
</div>
|
|
|
@@ -1547,7 +1699,7 @@ function applyGraphData(graphData) {
|
|
|
<div v-for="(v, i) in selectedData?.inputs || []" :key="'in-' + i" class="var-form">
|
|
|
<div class="var-form-row">
|
|
|
<n-input :value="v.name" placeholder="变量名" size="small"
|
|
|
- @update:value="nv => { v.name = nv; onFieldChange('inputs', [...selectedData.inputs]) }" />
|
|
|
+ @update:value="nv => { updateFieldName(v, i, 'inputs', nv) }" />
|
|
|
<n-input :value="v.label" placeholder="中文名" size="small"
|
|
|
@update:value="nv => { v.label = nv; onFieldChange('inputs', [...selectedData.inputs]) }" />
|
|
|
</div>
|
|
|
@@ -1579,7 +1731,7 @@ function applyGraphData(graphData) {
|
|
|
<div v-for="(v, i) in selectedData?.outputs || []" :key="'out-' + i" class="var-form">
|
|
|
<div class="var-form-row">
|
|
|
<n-input :value="v.name" placeholder="变量名" size="small"
|
|
|
- @update:value="nv => { v.name = nv; onFieldChange('outputs', [...selectedData.outputs]) }" />
|
|
|
+ @update:value="nv => { updateFieldName(v, i, 'outputs', nv) }" />
|
|
|
<n-input :value="v.label" placeholder="中文名" size="small"
|
|
|
@update:value="nv => { v.label = nv; onFieldChange('outputs', [...selectedData.outputs]) }" />
|
|
|
</div>
|
|
|
@@ -1635,7 +1787,7 @@ function applyGraphData(graphData) {
|
|
|
<div v-for="(v, i) in selectedData?.inputs || []" :key="'in-' + i" class="var-form">
|
|
|
<div class="var-form-row">
|
|
|
<n-input :value="v.name" placeholder="变量名" size="small"
|
|
|
- @update:value="nv => { v.name = nv; onFieldChange('inputs', [...selectedData.inputs]) }" />
|
|
|
+ @update:value="nv => { updateFieldName(v, i, 'inputs', nv) }" />
|
|
|
<n-input :value="v.label" placeholder="中文名" size="small"
|
|
|
@update:value="nv => { v.label = nv; onFieldChange('inputs', [...selectedData.inputs]) }" />
|
|
|
</div>
|
|
|
@@ -1667,7 +1819,7 @@ function applyGraphData(graphData) {
|
|
|
<div v-for="(v, i) in selectedData?.outputs || []" :key="'out-' + i" class="var-form">
|
|
|
<div class="var-form-row">
|
|
|
<n-input :value="v.name" placeholder="变量名" size="small"
|
|
|
- @update:value="nv => { v.name = nv; onFieldChange('outputs', [...selectedData.outputs]) }" />
|
|
|
+ @update:value="nv => { updateFieldName(v, i, 'outputs', nv) }" />
|
|
|
<n-input :value="v.label" placeholder="中文名" size="small"
|
|
|
@update:value="nv => { v.label = nv; onFieldChange('outputs', [...selectedData.outputs]) }" />
|
|
|
</div>
|
|
|
@@ -1792,7 +1944,7 @@ function applyGraphData(graphData) {
|
|
|
<div v-for="(v, i) in selectedData?.inputs || []" :key="'in-' + i" class="var-form">
|
|
|
<div class="var-form-row">
|
|
|
<n-input :value="v.name" placeholder="变量名" size="small"
|
|
|
- @update:value="nv => { v.name = nv; onFieldChange('inputs', [...selectedData.inputs]) }" />
|
|
|
+ @update:value="nv => { updateFieldName(v, i, 'inputs', nv) }" />
|
|
|
<n-input :value="v.label" placeholder="中文名" size="small"
|
|
|
@update:value="nv => { v.label = nv; onFieldChange('inputs', [...selectedData.inputs]) }" />
|
|
|
</div>
|
|
|
@@ -1824,7 +1976,7 @@ function applyGraphData(graphData) {
|
|
|
<div v-for="(v, i) in selectedData?.outputs || []" :key="'out-' + i" class="var-form">
|
|
|
<div class="var-form-row">
|
|
|
<n-input :value="v.name" placeholder="变量名" size="small"
|
|
|
- @update:value="nv => { v.name = nv; onFieldChange('outputs', [...selectedData.outputs]) }" />
|
|
|
+ @update:value="nv => { updateFieldName(v, i, 'outputs', nv) }" />
|
|
|
<n-input :value="v.label" placeholder="中文名" size="small"
|
|
|
@update:value="nv => { v.label = nv; onFieldChange('outputs', [...selectedData.outputs]) }" />
|
|
|
</div>
|
|
|
@@ -1869,7 +2021,7 @@ function applyGraphData(graphData) {
|
|
|
<div v-for="(v, i) in selectedData?.inputs || []" :key="i" class="var-form">
|
|
|
<div class="var-form-row">
|
|
|
<n-input :value="v.name" placeholder="变量名" size="small"
|
|
|
- @update:value="nv => { v.name = nv; onFieldChange('inputs', [...selectedData.inputs]) }" />
|
|
|
+ @update:value="nv => { updateFieldName(v, i, 'inputs', nv) }" />
|
|
|
<n-input :value="v.label" placeholder="中文名" size="small"
|
|
|
@update:value="nv => { v.label = nv; onFieldChange('inputs', [...selectedData.inputs]) }" />
|
|
|
</div>
|
|
|
@@ -1905,7 +2057,7 @@ function applyGraphData(graphData) {
|
|
|
<div v-for="(f, i) in selectedData?.fields || []" :key="i" class="var-form">
|
|
|
<div class="var-form-row">
|
|
|
<n-input :value="f.name" placeholder="字段名" size="small"
|
|
|
- @update:value="nv => { f.name = nv; onFieldChange('fields', [...selectedData.fields]) }" />
|
|
|
+ @update:value="nv => { updateFieldName(f, i, 'fields', nv) }" />
|
|
|
<n-input :value="f.label" placeholder="中文名" size="small"
|
|
|
@update:value="nv => { f.label = nv; onFieldChange('fields', [...selectedData.fields]) }" />
|
|
|
</div>
|
|
|
@@ -1915,7 +2067,11 @@ function applyGraphData(graphData) {
|
|
|
<n-input :value="f.description" placeholder="描述" size="small"
|
|
|
@update:value="nv => { f.description = nv; onFieldChange('fields', [...selectedData.fields]) }" />
|
|
|
</div>
|
|
|
- <div class="var-form-row" style="justify-content:flex-end">
|
|
|
+ <div class="var-form-row mapping-row">
|
|
|
+ <n-select :value="currentMappingValue(f)"
|
|
|
+ :options="[{ label: '自动推断', value: null }, ...inputMappingOptions]" size="small"
|
|
|
+ clearable placeholder="关联方式:自动推断" style="flex:1"
|
|
|
+ @update:value="nv => updateInputMapping(f, nv)" />
|
|
|
<button class="text-btn danger" @click="removeOutputField(i)">删除</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -2053,7 +2209,7 @@ function applyGraphData(graphData) {
|
|
|
</div>
|
|
|
|
|
|
<!-- 运行对话框 -->
|
|
|
- <div class="dialog-overlay" v-if="showRunDialog" @click.self="cancelRun">
|
|
|
+ <div class="dialog-overlay" v-if="showRunDialog">
|
|
|
<div class="run-dialog">
|
|
|
<div class="dialog-header">
|
|
|
<span class="dialog-title">运行工作流</span>
|
|
|
@@ -2063,6 +2219,14 @@ function applyGraphData(graphData) {
|
|
|
</div>
|
|
|
<div class="dialog-body">
|
|
|
<div v-if="inputFields.length === 0" class="empty-hint">此工作流无需输入参数</div>
|
|
|
+ <div class="run-actions" v-if="inputFields.length">
|
|
|
+ <button class="text-btn" @click="exportRunVariables">
|
|
|
+ <n-icon size="12"><DownloadOutline /></n-icon> 变量导出
|
|
|
+ </button>
|
|
|
+ <button class="text-btn" @click="onImportVariablesClick">
|
|
|
+ <n-icon size="12"><CloudUploadOutline /></n-icon> 变量导入
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
<div v-for="f in inputFields" :key="f.name" class="run-field">
|
|
|
<label class="run-field-label">{{ f.label || f.name }}</label>
|
|
|
<n-input v-model:value="formData[f.name]" :placeholder="f.description || f.name"
|
|
|
@@ -3376,6 +3540,19 @@ function applyGraphData(graphData) {
|
|
|
margin-bottom: 4px;
|
|
|
}
|
|
|
|
|
|
+/* 运行对话框变量导出/导入按钮 */
|
|
|
+.run-actions {
|
|
|
+ display: flex;
|
|
|
+ gap: 10px;
|
|
|
+ margin-bottom: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.run-actions .text-btn {
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 4px;
|
|
|
+}
|
|
|
+
|
|
|
/* 文件上传区域 */
|
|
|
.run-upload {
|
|
|
margin-top: 16px;
|