feat(flow): 添加工作流版本管理和草稿功能

- 新增 flowSaveDraft、flowVersionList、flowVersionDetail 等API接口
- 添加工作流草稿保存和版本历史记录功能
- 实现版本预览、恢复为草稿和回退版本功能
- 在界面添加版本标签显示当前发布版本信息
- 集成版本历史抽屉组件展示发布记录列表
- 添加发布说明输入框和确认对话框
- 实现版本比较和状态跟踪功能
This commit is contained in:
lixiaolong 2026-08-20 13:36:24 +08:00
parent ad64917603
commit 160527a3ac
2 changed files with 295 additions and 3 deletions

View File

@ -9,6 +9,26 @@ export function flowDeploy(data) {
})
}
export function flowSaveDraft(data) {
return request({ url: '/flow/draft', method: 'put', data })
}
export function flowVersionList(itemId) {
return request({ url: `/flow/versions/${itemId}`, method: 'get' })
}
export function flowVersionDetail(versionId) {
return request({ url: `/flow/version/${versionId}`, method: 'get' })
}
export function flowRestoreDraft(versionId, data) {
return request({ url: `/flow/version/${versionId}/restore-draft`, method: 'post', data })
}
export function flowRollbackVersion(versionId, data) {
return request({ url: `/flow/version/${versionId}/rollback`, method: 'post', data })
}
export function flowExecute(data) {
return request({
url: '/flow/execute',

View File

@ -12,8 +12,14 @@
<span class="flow-status__dot"></span>
<span>{{ stateMap[flowState] }}</span>
</div>
<el-tag v-if="publishedVersionNo" size="small" type="success" effect="plain">
当前发布 V{{ publishedVersionNo }}
</el-tag>
<el-tag v-if="versionPreviewMode" size="small" type="warning">
正在查看 V{{ previewingVersionNo }}
</el-tag>
</div>
<div class="btn__container">
<div v-if="!versionPreviewMode" class="btn__container">
<div class="btn flow-actions">
<el-tag v-if="hasRunResult && ['testRunError', 'testRunFinish'].includes(flowState)" :type="flowState === 'testRunFinish' ? 'success' : 'danger'">
{{ totalRunningTime }}ms
@ -47,6 +53,16 @@
</el-button>
</div>
<div class="btn publish-actions">
<el-button
:loading="draftSaving"
:disabled="!draftDirty || ['testRunning', 'pause'].includes(flowState)"
@click="saveDraft"
>
<el-icon><DocumentChecked /></el-icon>保存
</el-button>
<el-button :disabled="['testRunning', 'pause'].includes(flowState)" @click="openVersionHistory">
<el-icon><Clock /></el-icon>发布记录
</el-button>
<el-tooltip
class="box-item"
effect="dark"
@ -91,6 +107,9 @@
</el-popover>
</div>
</div>
<div v-else class="btn__container">
<el-button type="primary" @click="exitVersionPreview">退出预览</el-button>
</div>
</el-header>
<el-container class="container">
<el-aside v-if="!flowStore.disableForm">
@ -310,6 +329,33 @@
</div>
</template>
</el-dialog>
<el-drawer v-model="versionDrawerVisible" title="发布记录" size="560px">
<div v-loading="versionLoading" class="version-history">
<el-empty v-if="!versionLoading && !versionList.length" description="暂无发布记录" />
<div v-for="version in versionList" :key="version.id" class="version-history__item">
<div class="version-history__main">
<div class="version-history__title">
<strong>V{{ version.versionNo }}</strong>
<el-tag v-if="version.id === publishedVersionId" size="small" type="success">当前发布</el-tag>
<el-tag v-if="version.sourceVersionId" size="small" type="warning">回退版本</el-tag>
</div>
<div class="version-history__meta">
{{ version.createBy || '未知用户' }} · {{ version.createTime || '-' }}
</div>
<div v-if="version.publishNote" class="version-history__note">{{ version.publishNote }}</div>
</div>
<div class="version-history__actions">
<el-button link type="primary" @click="previewVersion(version)">查看</el-button>
<el-button link type="primary" @click="restoreVersionAsDraft(version)">恢复为草稿</el-button>
<el-button link type="danger" :disabled="version.id === publishedVersionId" @click="rollbackVersion(version)">
回退
</el-button>
</div>
</div>
</div>
</el-drawer>
</template>
<script setup>
import { computed, reactive, ref, watch, onMounted, onUnmounted, nextTick } from "vue";
@ -348,6 +394,11 @@ import {
} from "@/utils/actionQueue";
import {
flowDeploy,
flowSaveDraft,
flowVersionList,
flowVersionDetail,
flowRestoreDraft,
flowRollbackVersion,
flowView,
flowPause,
flowResume,
@ -359,10 +410,13 @@ import { getDetect } from "@/api/test/detect";
import { getRobotList, getRobotDevicesByRobotId } from '@/api/inspection/robot'
import { restoreOperationalState } from '@/api/device/system'
import { getDeviceKindForAction, toDeviceOptions } from '@/utils/robotDevice'
import { sanitizeFlowResourceDefinition } from '@/utils/flowResources'
import {
CircleClose,
Clock,
CollectionTag,
Delete,
DocumentChecked,
FullScreen,
Location,
RefreshLeft,
@ -761,10 +815,24 @@ const stateMap = {
testRunError: "试运行失败",
release: "已发布",
updateData: "修改中",
draftSaved: "草稿已保存",
pause: "已暂停",
stop: "已终止",
};
const flowState = ref("unpublished");
const draftRevision = ref(0);
const draftDirty = ref(false);
const draftSaving = ref(false);
const publishedVersionId = ref("");
const publishedVersionNo = ref(null);
const versionDrawerVisible = ref(false);
const versionLoading = ref(false);
const versionList = ref([]);
const versionPreviewMode = ref(false);
const previewingVersionNo = ref(null);
let draftBeforeVersionPreview = null;
let oldFlowDataBeforeVersionPreview = null;
let disableFormBeforeVersionPreview = false;
const flowInfoData = ref({
itemId: "",
instId: "",
@ -1044,6 +1112,137 @@ const loopFlowView = async (instId, taskId = null) => {
};
let oldFlowData = {};
const getSavableFlowData = () => {
const graphData = lf.getGraphData();
graphData.nodes.forEach((node) => emitter.emit("contentChange", { id: node.id }));
const validation = validateFlowResources(lf.getGraphData());
if (validation.errors.length) {
throw new Error(validation.errors[0].message || "工作流资源配置不完整");
}
return sanitizeFlowResourceDefinition(validation.graph);
};
const saveDraft = async () => {
if (!flowInfoData.value.itemId) return false;
draftSaving.value = true;
try {
const flowData = getSavableFlowData();
const res = await flowSaveDraft({
id: flowInfoData.value.itemId,
flowData: JSON.stringify(flowData),
draftRevision: draftRevision.value,
});
if (res.code !== 200) throw new Error(res.msg || "保存失败");
draftRevision.value = res.data?.draftRevision ?? draftRevision.value + 1;
draftDirty.value = false;
oldFlowData = JSON.parse(JSON.stringify(lf.getGraphData()));
if (flowState.value === "updateData") flowState.value = "draftSaved";
ElMessage.success("草稿已保存");
return true;
} catch (error) {
ElMessage.error(error?.message || "保存草稿失败");
return false;
} finally {
draftSaving.value = false;
}
};
const refreshVersionList = async () => {
versionLoading.value = true;
try {
const res = await flowVersionList(flowInfoData.value.itemId);
versionList.value = res.data || [];
} catch (error) {
ElMessage.error(error?.message || "加载发布记录失败");
} finally {
versionLoading.value = false;
}
};
const openVersionHistory = async () => {
versionDrawerVisible.value = true;
await refreshVersionList();
};
const previewVersion = async (version) => {
try {
const res = await flowVersionDetail(version.id);
const detail = res.data || version;
draftBeforeVersionPreview = JSON.parse(JSON.stringify(lf.getGraphData()));
oldFlowDataBeforeVersionPreview = JSON.parse(JSON.stringify(oldFlowData));
disableFormBeforeVersionPreview = flowStore.disableForm;
flowStore.updateDisableForm(true);
lf.render(normalizeFlowResourceSchema(JSON.parse(detail.flowData)));
previewingVersionNo.value = detail.versionNo;
versionPreviewMode.value = true;
versionDrawerVisible.value = false;
nextTick(() => lf.fitView());
} catch (error) {
ElMessage.error(error?.message || "加载发布版本失败");
}
};
const exitVersionPreview = () => {
if (draftBeforeVersionPreview) {
lf.render(draftBeforeVersionPreview);
oldFlowData = oldFlowDataBeforeVersionPreview || {};
}
flowStore.updateDisableForm(disableFormBeforeVersionPreview);
versionPreviewMode.value = false;
previewingVersionNo.value = null;
draftBeforeVersionPreview = null;
oldFlowDataBeforeVersionPreview = null;
nextTick(() => lf.fitView());
};
const restoreVersionAsDraft = async (version) => {
try {
await ElMessageBox.confirm(
`将 V${version.versionNo} 恢复为当前草稿?当前未保存修改会被覆盖,正式运行版本不会改变。`,
"恢复为草稿",
{ type: "warning" }
);
const res = await flowRestoreDraft(version.id, { draftRevision: draftRevision.value });
const item = res.data;
const graph = normalizeFlowResourceSchema(JSON.parse(item.flowData));
lf.render(graph);
draftRevision.value = item.draftRevision ?? draftRevision.value + 1;
draftDirty.value = false;
oldFlowData = JSON.parse(JSON.stringify(lf.getGraphData()));
flowState.value = "draftSaved";
versionDrawerVisible.value = false;
nextTick(() => lf.fitView());
ElMessage.success(`已将 V${version.versionNo} 恢复为草稿`);
} catch (error) {
if (error !== "cancel" && error !== "close") {
ElMessage.error(error?.message || "恢复草稿失败");
}
}
};
const rollbackVersion = async (version) => {
try {
await ElMessageBox.confirm(
`立即回退到 V${version.versionNo}?系统会创建一个新的发布版本,正在运行的任务不受影响。`,
"回退发布版本",
{ type: "warning", confirmButtonText: "确认回退" }
);
const res = await flowRollbackVersion(version.id, {
publishNote: `回退到 V${version.versionNo}`,
});
publishedVersionId.value = res.data?.id || "";
publishedVersionNo.value = res.data?.versionNo || null;
flowState.value = "release";
await refreshVersionList();
ElMessage.success(`已发布回退版本 V${publishedVersionNo.value}`);
} catch (error) {
if (error !== "cancel" && error !== "close") {
ElMessage.error(error?.message || "回退版本失败");
}
}
};
/**
* 发布
*/
@ -1053,17 +1252,35 @@ const deployFlow = async () => {
return;
}
if (draftDirty.value) {
ElMessage.warning("请先保存当前草稿,再发布试运行通过的内容");
return;
}
try {
const res = await flowDeploy({ id: flowInfoData.value.itemId });
const prompt = await ElMessageBox.prompt("可填写本次发布说明", "发布工作流", {
inputPlaceholder: "例如:调整录像和视频分析参数",
confirmButtonText: "发布",
cancelButtonText: "取消",
});
const res = await flowDeploy({
id: flowInfoData.value.itemId,
draftRevision: draftRevision.value,
publishNote: prompt.value,
});
if (res.code === 200) {
ElMessage.success(res.msg);
publishedVersionId.value = res.data?.id || publishedVersionId.value;
publishedVersionNo.value = res.data?.versionNo || publishedVersionNo.value;
flowState.value = "release";
} else {
ElMessage.error(res.msg);
}
} catch (error) {
ElMessage.error("部署操作产生异常");
if (error !== "cancel" && error !== "close") {
ElMessage.error(error?.message || "发布操作产生异常");
}
}
};
@ -1071,17 +1288,31 @@ const loadFlowData = async (id, lf) => {
try {
const res = await getDetect(id);
if (res.data && res.data.flowData) {
draftRevision.value = res.data.draftRevision ?? 0;
publishedVersionId.value = res.data.publishedVersionId || "";
publishedVersionNo.value = res.data.publishedVersionNo || null;
draftDirty.value = false;
const flowData = normalizeFlowResourceSchema(JSON.parse(res.data.flowData));
lf.render(flowData);
nextTick(() => {
oldFlowData = JSON.parse(JSON.stringify(lf.getGraphData()));
if (
publishedVersionId.value &&
res.data.draftHash &&
res.data.publishedDraftHash &&
res.data.draftHash !== res.data.publishedDraftHash
) {
flowState.value = "draftSaved";
}
lf.fitView();
});
} else {
// 渲染数据 data
lf.render(initFlowData);
oldFlowData = initFlowData;
draftDirty.value = true;
flowState.value = "updateData";
nextTick(() => lf.zoom(0.8, [850, 450]));
}
} catch (err) {
@ -1133,6 +1364,7 @@ const initFlow = () => {
if (flowStore.disableForm) return;
const flowData = lf.getGraphData();
if (JSON.stringify(oldFlowData) !== JSON.stringify(flowData)) {
draftDirty.value = true;
flowState.value = "updateData";
}
});
@ -2367,5 +2599,45 @@ onUnmounted(() => {
}
}
.version-history {
display: flex;
flex-direction: column;
gap: 10px;
}
.version-history__item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
min-height: 82px;
padding: 14px 0;
border-bottom: 1px solid #ebeef5;
}
.version-history__main {
min-width: 0;
flex: 1;
}
.version-history__title,
.version-history__actions {
display: flex;
align-items: center;
gap: 8px;
}
.version-history__meta,
.version-history__note {
margin-top: 6px;
color: #606266;
font-size: 13px;
line-height: 1.4;
}
.version-history__note {
overflow-wrap: anywhere;
}
</style>