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) {
|
export function flowExecute(data) {
|
||||||
return request({
|
return request({
|
||||||
url: '/flow/execute',
|
url: '/flow/execute',
|
||||||
|
|||||||
@ -12,8 +12,14 @@
|
|||||||
<span class="flow-status__dot"></span>
|
<span class="flow-status__dot"></span>
|
||||||
<span>{{ stateMap[flowState] }}</span>
|
<span>{{ stateMap[flowState] }}</span>
|
||||||
</div>
|
</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>
|
||||||
<div class="btn__container">
|
<div v-if="!versionPreviewMode" class="btn__container">
|
||||||
<div class="btn flow-actions">
|
<div class="btn flow-actions">
|
||||||
<el-tag v-if="hasRunResult && ['testRunError', 'testRunFinish'].includes(flowState)" :type="flowState === 'testRunFinish' ? 'success' : 'danger'">
|
<el-tag v-if="hasRunResult && ['testRunError', 'testRunFinish'].includes(flowState)" :type="flowState === 'testRunFinish' ? 'success' : 'danger'">
|
||||||
{{ totalRunningTime }}ms
|
{{ totalRunningTime }}ms
|
||||||
@ -47,6 +53,16 @@
|
|||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="btn publish-actions">
|
<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
|
<el-tooltip
|
||||||
class="box-item"
|
class="box-item"
|
||||||
effect="dark"
|
effect="dark"
|
||||||
@ -91,6 +107,9 @@
|
|||||||
</el-popover>
|
</el-popover>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else class="btn__container">
|
||||||
|
<el-button type="primary" @click="exitVersionPreview">退出预览</el-button>
|
||||||
|
</div>
|
||||||
</el-header>
|
</el-header>
|
||||||
<el-container class="container">
|
<el-container class="container">
|
||||||
<el-aside v-if="!flowStore.disableForm">
|
<el-aside v-if="!flowStore.disableForm">
|
||||||
@ -310,6 +329,33 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</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>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, reactive, ref, watch, onMounted, onUnmounted, nextTick } from "vue";
|
import { computed, reactive, ref, watch, onMounted, onUnmounted, nextTick } from "vue";
|
||||||
@ -348,6 +394,11 @@ import {
|
|||||||
} from "@/utils/actionQueue";
|
} from "@/utils/actionQueue";
|
||||||
import {
|
import {
|
||||||
flowDeploy,
|
flowDeploy,
|
||||||
|
flowSaveDraft,
|
||||||
|
flowVersionList,
|
||||||
|
flowVersionDetail,
|
||||||
|
flowRestoreDraft,
|
||||||
|
flowRollbackVersion,
|
||||||
flowView,
|
flowView,
|
||||||
flowPause,
|
flowPause,
|
||||||
flowResume,
|
flowResume,
|
||||||
@ -359,10 +410,13 @@ import { getDetect } from "@/api/test/detect";
|
|||||||
import { getRobotList, getRobotDevicesByRobotId } from '@/api/inspection/robot'
|
import { getRobotList, getRobotDevicesByRobotId } from '@/api/inspection/robot'
|
||||||
import { restoreOperationalState } from '@/api/device/system'
|
import { restoreOperationalState } from '@/api/device/system'
|
||||||
import { getDeviceKindForAction, toDeviceOptions } from '@/utils/robotDevice'
|
import { getDeviceKindForAction, toDeviceOptions } from '@/utils/robotDevice'
|
||||||
|
import { sanitizeFlowResourceDefinition } from '@/utils/flowResources'
|
||||||
import {
|
import {
|
||||||
CircleClose,
|
CircleClose,
|
||||||
|
Clock,
|
||||||
CollectionTag,
|
CollectionTag,
|
||||||
Delete,
|
Delete,
|
||||||
|
DocumentChecked,
|
||||||
FullScreen,
|
FullScreen,
|
||||||
Location,
|
Location,
|
||||||
RefreshLeft,
|
RefreshLeft,
|
||||||
@ -761,10 +815,24 @@ const stateMap = {
|
|||||||
testRunError: "试运行失败",
|
testRunError: "试运行失败",
|
||||||
release: "已发布",
|
release: "已发布",
|
||||||
updateData: "修改中",
|
updateData: "修改中",
|
||||||
|
draftSaved: "草稿已保存",
|
||||||
pause: "已暂停",
|
pause: "已暂停",
|
||||||
stop: "已终止",
|
stop: "已终止",
|
||||||
};
|
};
|
||||||
const flowState = ref("unpublished");
|
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({
|
const flowInfoData = ref({
|
||||||
itemId: "",
|
itemId: "",
|
||||||
instId: "",
|
instId: "",
|
||||||
@ -1044,6 +1112,137 @@ const loopFlowView = async (instId, taskId = null) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let oldFlowData = {};
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (draftDirty.value) {
|
||||||
|
ElMessage.warning("请先保存当前草稿,再发布试运行通过的内容");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
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) {
|
if (res.code === 200) {
|
||||||
ElMessage.success(res.msg);
|
ElMessage.success(res.msg);
|
||||||
|
publishedVersionId.value = res.data?.id || publishedVersionId.value;
|
||||||
|
publishedVersionNo.value = res.data?.versionNo || publishedVersionNo.value;
|
||||||
flowState.value = "release";
|
flowState.value = "release";
|
||||||
} else {
|
} else {
|
||||||
ElMessage.error(res.msg);
|
ElMessage.error(res.msg);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error("部署操作产生异常");
|
if (error !== "cancel" && error !== "close") {
|
||||||
|
ElMessage.error(error?.message || "发布操作产生异常");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1071,17 +1288,31 @@ const loadFlowData = async (id, lf) => {
|
|||||||
try {
|
try {
|
||||||
const res = await getDetect(id);
|
const res = await getDetect(id);
|
||||||
if (res.data && res.data.flowData) {
|
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));
|
const flowData = normalizeFlowResourceSchema(JSON.parse(res.data.flowData));
|
||||||
lf.render(flowData);
|
lf.render(flowData);
|
||||||
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
oldFlowData = JSON.parse(JSON.stringify(lf.getGraphData()));
|
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();
|
lf.fitView();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// 渲染数据 data
|
// 渲染数据 data
|
||||||
lf.render(initFlowData);
|
lf.render(initFlowData);
|
||||||
oldFlowData = initFlowData;
|
oldFlowData = initFlowData;
|
||||||
|
draftDirty.value = true;
|
||||||
|
flowState.value = "updateData";
|
||||||
nextTick(() => lf.zoom(0.8, [850, 450]));
|
nextTick(() => lf.zoom(0.8, [850, 450]));
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -1133,6 +1364,7 @@ const initFlow = () => {
|
|||||||
if (flowStore.disableForm) return;
|
if (flowStore.disableForm) return;
|
||||||
const flowData = lf.getGraphData();
|
const flowData = lf.getGraphData();
|
||||||
if (JSON.stringify(oldFlowData) !== JSON.stringify(flowData)) {
|
if (JSON.stringify(oldFlowData) !== JSON.stringify(flowData)) {
|
||||||
|
draftDirty.value = true;
|
||||||
flowState.value = "updateData";
|
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>
|
</style>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user