forked from SmartBench/CMVR-IOT-UI
feat(flow): 添加工作流版本管理和草稿功能
- 新增 flowSaveDraft、flowVersionList、flowVersionDetail 等API接口 - 添加工作流草稿保存和版本历史记录功能 - 实现版本预览、恢复为草稿和回退版本功能 - 在界面添加版本标签显示当前发布版本信息 - 集成版本历史抽屉组件展示发布记录列表 - 添加发布说明输入框和确认对话框 - 实现版本比较和状态跟踪功能
This commit is contained in:
parent
ad64917603
commit
160527a3ac
@ -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',
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user