feat: 增加工作流合法性校验功能

This commit is contained in:
2026-03-04 19:56:42 +08:00
parent a79718b03b
commit ae9bb2c53f
12 changed files with 1755 additions and 38 deletions

View File

@@ -47,6 +47,19 @@
"subProcess": "SubProcess",
"workflowSelect": "WorkflowSelect",
"bochaSearch": "BochaSearch",
"check": "Check",
"checkPassed": "Workflow check passed",
"checkFailed": "Workflow check failed. Please fix the issues first",
"checkContentEmpty": "Canvas content is empty, unable to check",
"checkIssuesTitle": "Workflow Check Issues",
"checkStageLabel": "Stage",
"issueCount": "Issue Count",
"checkLevel": "Level",
"checkCode": "Rule Code",
"checkLocation": "Location",
"checkMessage": "Description",
"stageSave": "Save Check",
"stagePreExecute": "Pre-execute Check",
"descriptions": {
"fileContentExtraction": "Extract text content from PDF or Word documents, etc",
"documentAddress": "Document URL address",

View File

@@ -47,6 +47,19 @@
"subProcess": "子流程",
"workflowSelect": "工作流选择",
"bochaSearch": "博查搜索",
"check": "检查",
"checkPassed": "工作流检查通过",
"checkFailed": "工作流检查未通过,请先修复问题",
"checkContentEmpty": "当前画布内容为空,无法检查",
"checkIssuesTitle": "工作流检查问题",
"checkStageLabel": "校验阶段",
"issueCount": "问题数量",
"checkLevel": "级别",
"checkCode": "规则码",
"checkLocation": "位置",
"checkMessage": "问题描述",
"stageSave": "保存校验",
"stagePreExecute": "预执行校验",
"descriptions": {
"fileContentExtraction": "提取 PDF 或者 Word 等文件中的文字内容",
"documentAddress": "文档的url地址",

View File

@@ -2,10 +2,11 @@
import {computed, onMounted, onUnmounted, ref} from 'vue';
import {useRoute} from 'vue-router';
import {usePreferences} from '@easyflow/preferences';
import {getOptions, sortNodes} from '@easyflow/utils';
import {getIconByValue} from '#/views/ai/model/modelUtils/defaultIcon';
import {ArrowLeft, Position} from '@element-plus/icons-vue';
import {ArrowLeft, CircleCheck, Close, Position} from '@element-plus/icons-vue';
import {Tinyflow} from '@tinyflow-ai/vue';
import {ElButton, ElDrawer, ElMessage, ElSkeleton} from 'element-plus';
@@ -24,6 +25,7 @@ import nodeNames from './customNode/nodeNames';
import '@tinyflow-ai/vue/dist/index.css';
const route = useRoute();
const {isDark} = usePreferences();
// vue
onMounted(async () => {
document.addEventListener('keydown', handleKeydown);
@@ -38,6 +40,9 @@ onMounted(async () => {
});
onUnmounted(() => {
document.removeEventListener('keydown', handleKeydown);
if (focusPulseTimer) {
clearTimeout(focusPulseTimer);
}
});
// variables
const tinyflowRef = ref<InstanceType<typeof Tinyflow> | null>(null);
@@ -109,6 +114,75 @@ const provider = computed(() => ({
const customNode = ref();
const showTinyFlow = ref(false);
const saveLoading = ref(false);
const checkLoading = ref(false);
const checkIssuesVisible = ref(false);
const checkResult = ref<any>(null);
const checkContentSnapshot = ref<any>(null);
const checkIssues = computed(() => checkResult.value?.issues || []);
const issueFocusActive = ref(false);
const focusedIssueKey = ref('');
let focusPulseTimer: ReturnType<typeof setTimeout> | undefined;
type WorkflowCheckStage = 'SAVE' | 'PRE_EXECUTE';
const builtInNodeIconMap: Record<string, string> = {
startNode: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22ZM12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20ZM12 15C10.3431 15 9 13.6569 9 12C9 10.3431 10.3431 9 12 9C13.6569 9 15 10.3431 15 12C15 13.6569 13.6569 15 12 15Z"></path></svg>',
loopNode: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M5.46257 4.43262C7.21556 2.91688 9.5007 2 12 2C17.5228 2 22 6.47715 22 12C22 14.1361 21.3302 16.1158 20.1892 17.7406L17 12H20C20 7.58172 16.4183 4 12 4C9.84982 4 7.89777 4.84827 6.46023 6.22842L5.46257 4.43262ZM18.5374 19.5674C16.7844 21.0831 14.4993 22 12 22C6.47715 22 2 17.5228 2 12C2 9.86386 2.66979 7.88416 3.8108 6.25944L7 12H4C4 16.4183 7.58172 20 12 20C14.1502 20 16.1022 19.1517 17.5398 17.7716L18.5374 19.5674Z"></path></svg>',
conditionNode: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M4 4H10V10H4V4ZM14 4H20V10H14V4ZM4 14H10V20H4V14ZM14 14H20V20H14V14ZM10 7H14V9H10V7ZM7 10H9V14H7V10ZM15 10H17V14H15V10Z"></path></svg>',
llmNode: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M20.7134 7.12811L20.4668 7.69379C20.2864 8.10792 19.7136 8.10792 19.5331 7.69379L19.2866 7.12811C18.8471 6.11947 18.0555 5.31641 17.0677 4.87708L16.308 4.53922C15.8973 4.35653 15.8973 3.75881 16.308 3.57612L17.0252 3.25714C18.0384 2.80651 18.8442 1.97373 19.2761 0.930828L19.5293 0.319534C19.7058 -0.106511 20.2942 -0.106511 20.4706 0.319534L20.7238 0.930828C21.1558 1.97373 21.9616 2.80651 22.9748 3.25714L23.6919 3.57612C24.1027 3.75881 24.1027 4.35653 23.6919 4.53922L22.9323 4.87708C21.9445 5.31641 21.1529 6.11947 20.7134 7.12811ZM9 2C13.0675 2 16.426 5.03562 16.9337 8.96494L19.1842 12.5037C19.3324 12.7367 19.3025 13.0847 18.9593 13.2317L17 14.071V17C17 18.1046 16.1046 19 15 19H13.001L13 22H4L4.00025 18.3061C4.00033 17.1252 3.56351 16.0087 2.7555 15.0011C1.65707 13.6313 1 11.8924 1 10C1 5.58172 4.58172 2 9 2ZM9 4C5.68629 4 3 6.68629 3 10C3 11.3849 3.46818 12.6929 4.31578 13.7499C5.40965 15.114 6.00036 16.6672 6.00025 18.3063L6.00013 20H11.0007L11.0017 17H15V12.7519L16.5497 12.0881L15.0072 9.66262L14.9501 9.22118C14.5665 6.25141 12.0243 4 9 4ZM19.4893 16.9929L21.1535 18.1024C22.32 16.3562 23 14.2576 23 12.0001C23 11.317 22.9378 10.6486 22.8186 10L20.8756 10.5C20.9574 10.9878 21 11.489 21 12.0001C21 13.8471 20.4436 15.5642 19.4893 16.9929Z"></path></svg>',
knowledgeNode: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M15.5 5C13.567 5 12 6.567 12 8.5C12 10.433 13.567 12 15.5 12C17.433 12 19 10.433 19 8.5C19 6.567 17.433 5 15.5 5ZM10 8.5C10 5.46243 12.4624 3 15.5 3C18.5376 3 21 5.46243 21 8.5C21 9.6575 20.6424 10.7315 20.0317 11.6175L22.7071 14.2929L21.2929 15.7071L18.6175 13.0317C17.7315 13.6424 16.6575 14 15.5 14C12.4624 14 10 11.5376 10 8.5ZM3 4H8V6H3V4ZM3 11H8V13H3V11ZM21 18V20H3V18H21Z"></path></svg>',
searchEngineNode: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M18.031 16.6168L22.3137 20.8995L20.8995 22.3137L16.6168 18.031C15.0769 19.263 13.124 20 11 20C6.032 20 2 15.968 2 11C2 6.032 6.032 2 11 2C15.968 2 20 6.032 20 11C20 13.124 19.263 15.0769 18.031 16.6168ZM16.0247 15.8748C17.2475 14.6146 18 12.8956 18 11C18 7.1325 14.8675 4 11 4C7.1325 4 4 7.1325 4 11C4 14.8675 7.1325 18 11 18C12.8956 18 14.6146 17.2475 15.8748 16.0247L16.0247 15.8748Z"></path></svg>',
httpNode: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M6.23509 6.45329C4.85101 7.89148 4 9.84636 4 12C4 16.4183 7.58172 20 12 20C13.0808 20 14.1116 19.7857 15.0521 19.3972C15.1671 18.6467 14.9148 17.9266 14.8116 17.6746C14.582 17.115 13.8241 16.1582 12.5589 14.8308C12.2212 14.4758 12.2429 14.2035 12.3636 13.3943L12.3775 13.3029C12.4595 12.7486 12.5971 12.4209 14.4622 12.1248C15.4097 11.9746 15.6589 12.3533 16.0043 12.8777C16.0425 12.9358 16.0807 12.9928 16.1198 13.0499C16.4479 13.5297 16.691 13.6394 17.0582 13.8064C17.2227 13.881 17.428 13.9751 17.7031 14.1314C18.3551 14.504 18.3551 14.9247 18.3551 15.8472V15.9518C18.3551 16.3434 18.3168 16.6872 18.2566 16.9859C19.3478 15.6185 20 13.8854 20 12C20 8.70089 18.003 5.8682 15.1519 4.64482C14.5987 5.01813 13.8398 5.54726 13.575 5.91C13.4396 6.09538 13.2482 7.04166 12.6257 7.11976C12.4626 7.14023 12.2438 7.12589 12.012 7.11097C11.3905 7.07058 10.5402 7.01606 10.268 7.75495C10.0952 8.2232 10.0648 9.49445 10.6239 10.1543C10.7134 10.2597 10.7307 10.4547 10.6699 10.6735C10.59 10.9608 10.4286 11.1356 10.3783 11.1717C10.2819 11.1163 10.0896 10.8931 9.95938 10.7412C9.64554 10.3765 9.25405 9.92233 8.74797 9.78176C8.56395 9.73083 8.36166 9.68867 8.16548 9.64736C7.6164 9.53227 6.99443 9.40134 6.84992 9.09302C6.74442 8.8672 6.74488 8.55621 6.74529 8.22764C6.74529 7.8112 6.74529 7.34029 6.54129 6.88256C6.46246 6.70541 6.35689 6.56446 6.23509 6.45329ZM12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22Z"></path></svg>',
codeNode: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M23 12L15.9289 19.0711L14.5147 17.6569L20.1716 12L14.5147 6.34317L15.9289 4.92896L23 12ZM3.82843 12L9.48528 17.6569L8.07107 19.0711L1 12L8.07107 4.92896L9.48528 6.34317L3.82843 12Z"></path></svg>',
templateNode: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M2 4C2 3.44772 2.44772 3 3 3H21C21.5523 3 22 3.44772 22 4V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V4ZM4 5V19H20V5H4ZM7 8H17V11H15V10H13V14H14.5V16H9.5V14H11V10H9V11H7V8Z"></path></svg>',
confirmNode: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M15.3873 13.4975L17.9403 20.5117L13.2418 22.2218L10.6889 15.2076L6.79004 17.6529L8.4086 1.63318L19.9457 12.8646L15.3873 13.4975ZM15.3768 19.3163L12.6618 11.8568L15.6212 11.4459L9.98201 5.9561L9.19088 13.7863L11.7221 12.1988L14.4371 19.6583L15.3768 19.3163Z"></path></svg>',
endNode: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M6 5.1438V16.0002H18.3391L6 5.1438ZM4 2.932C4 2.07155 5.01456 1.61285 5.66056 2.18123L21.6501 16.2494C22.3423 16.8584 21.9116 18.0002 20.9896 18.0002H6V22H4V2.932Z"></path></svg>',
};
const builtInNodeTitleMap: Record<string, string> = {
startNode: '开始节点',
loopNode: '循环',
conditionNode: '条件判断',
llmNode: '大模型',
knowledgeNode: '知识库',
searchEngineNode: '搜索引擎',
httpNode: 'Http 请求',
codeNode: '动态代码',
templateNode: '内容模板',
confirmNode: '用户确认',
endNode: '结束节点',
};
const checkIssueDisplayList = computed(() =>
checkIssues.value.map((issue: any, index: number) => {
const node = findNodeByIssue(issue);
const nodeType = node?.type;
const icon =
node?.data?.icon ||
(nodeType ? customNode.value?.[nodeType]?.icon : undefined) ||
(nodeType ? builtInNodeIconMap[nodeType] : undefined);
const title =
issue?.nodeName ||
node?.data?.title ||
(nodeType ? builtInNodeTitleMap[nodeType] : undefined) ||
'节点';
return {
issue,
index,
key: issueKey(issue, index),
nodeDisplay: {
icon,
title,
},
};
}),
);
function findNodeByIssue(issue: any) {
if (!issue?.nodeId) {
return null;
}
const nodes = checkContentSnapshot.value?.nodes || [];
return nodes.find((node: any) => node.id === issue.nodeId) || null;
}
const handleKeydown = (event: KeyboardEvent) => {
// 检查是否是 Ctrl+S
if ((event.ctrlKey || event.metaKey) && event.key === 's') {
@@ -149,29 +223,43 @@ async function loadCustomNode() {
});
}
async function runWorkflow() {
if (!saveLoading.value) {
await handleSave().then(() => {
getWorkflowInfo(workflowId.value);
getRunningParams();
});
if (saveLoading.value || checkLoading.value) {
return;
}
const passed = await runCheck('PRE_EXECUTE');
if (!passed) {
return;
}
const saved = await handleSave();
if (!saved) {
return;
}
await getWorkflowInfo(workflowId.value);
await getRunningParams();
}
async function handleSave(showMsg: boolean = false) {
async function handleSave(showMsg: boolean = false): Promise<boolean> {
const passed = await runCheck('SAVE', true);
if (!passed) {
return false;
}
saveLoading.value = true;
await api
.post('/api/v1/workflow/update', {
try {
const res = await api.post('/api/v1/workflow/update', {
id: workflowId.value,
content: tinyflowRef.value?.getData(),
})
.then((res) => {
saveLoading.value = false;
if (res.errorCode === 0 && showMsg) {
ElMessage.success(res.message);
}
});
if (res.errorCode === 0 && showMsg) {
ElMessage.success(res.message);
}
return res.errorCode === 0;
} catch {
return false;
} finally {
saveLoading.value = false;
}
}
async function getWorkflowInfo(workflowId: any) {
api.get(`/api/v1/workflow/detail?id=${workflowId}`).then((res) => {
return api.get(`/api/v1/workflow/detail?id=${workflowId}`).then((res) => {
workflowInfo.value = res.data;
tinyFlowData.value = workflowInfo.value.content
? JSON.parse(workflowInfo.value.content)
@@ -179,24 +267,24 @@ async function getWorkflowInfo(workflowId: any) {
});
}
async function getLlmList() {
api.get('/api/v1/model/list').then((res) => {
return api.get('/api/v1/model/list').then((res) => {
llmList.value = res.data;
});
}
async function getKnowledgeList() {
api.get('/api/v1/documentCollection/list').then((res) => {
return api.get('/api/v1/documentCollection/list').then((res) => {
knowledgeList.value = res.data;
});
}
async function getCodeEngineList() {
api.get('/api/v1/workflow/supportedCodeEngines').then((res) => {
return api.get('/api/v1/workflow/supportedCodeEngines').then((res) => {
if (res?.errorCode === 0 && Array.isArray(res.data) && res.data.length > 0) {
codeEngineList.value = res.data;
}
});
}
function getRunningParams() {
api
return api
.get(`/api/v1/workflow/getRunningParameters?id=${workflowId.value}`)
.then((res) => {
if (res.errorCode === 0) {
@@ -205,6 +293,102 @@ function getRunningParams() {
}
});
}
async function runCheck(stage: WorkflowCheckStage, silentPass: boolean = false) {
const content = tinyflowRef.value?.getData();
if (!content) {
ElMessage.error($t('aiWorkflow.checkContentEmpty'));
return false;
}
checkContentSnapshot.value = content;
checkLoading.value = true;
try {
const res = await api.post('/api/v1/workflow/check', {
id: workflowId.value,
content,
stage,
});
checkResult.value = res.data;
if (!res.data?.passed) {
checkIssuesVisible.value = true;
ElMessage.error($t('aiWorkflow.checkFailed'));
return false;
}
checkIssuesVisible.value = false;
focusedIssueKey.value = '';
issueFocusActive.value = false;
if (!silentPass) {
ElMessage.success($t('aiWorkflow.checkPassed'));
}
return true;
} catch {
return false;
} finally {
checkLoading.value = false;
}
}
function checkStageText(stage?: string) {
if (stage === 'SAVE') {
return $t('aiWorkflow.stageSave');
}
if (stage === 'PRE_EXECUTE') {
return $t('aiWorkflow.stagePreExecute');
}
return '-';
}
function issueKey(issue: any, index: number) {
return `${issue.code || '-'}-${issue.nodeId || '-'}-${issue.edgeId || '-'}-${index}`;
}
function canFocusIssue(issue: any) {
return Boolean(issue?.nodeId);
}
function issueLevelClass(level?: string) {
if (!level) {
return 'is-default';
}
const normalized = String(level).toLowerCase();
if (normalized === 'error') {
return 'is-error';
}
if (normalized === 'warn' || normalized === 'warning') {
return 'is-warning';
}
if (normalized === 'info') {
return 'is-info';
}
return 'is-default';
}
async function focusIssue(issue: any, index: number) {
if (!canFocusIssue(issue)) {
return;
}
focusedIssueKey.value = issueKey(issue, index);
const tinyflowInstance = tinyflowRef.value?.getInstance?.();
if (!tinyflowInstance?.focusNode) {
return;
}
const focused = await tinyflowInstance.focusNode(issue.nodeId, {
duration: 280,
zoom: 1,
});
if (!focused) {
return;
}
issueFocusActive.value = true;
if (focusPulseTimer) {
clearTimeout(focusPulseTimer);
}
focusPulseTimer = setTimeout(() => {
issueFocusActive.value = false;
}, 1800);
}
function closeCheckIssues() {
checkIssuesVisible.value = false;
focusedIssueKey.value = '';
issueFocusActive.value = false;
}
async function handleCheck() {
await runCheck('PRE_EXECUTE');
}
function onSubmit() {
initState.value = !initState.value;
}
@@ -261,7 +445,11 @@ function onAsyncExecute(info: any) {
</script>
<template>
<div class="head-div h-full w-full" v-loading="pageLoading">
<div
class="head-div h-full w-full"
:class="{ 'workflow-issue-focus': issueFocusActive }"
v-loading="pageLoading"
>
<CommonSelectDataModal
ref="workflowSelectRef"
page-url="/api/v1/workflow/page"
@@ -325,12 +513,24 @@ function onAsyncExecute(info: any) {
</ElButton>
</div>
<div>
<ElButton :disabled="saveLoading" :icon="Position" @click="runWorkflow">
<ElButton
:loading="checkLoading"
:disabled="saveLoading"
:icon="CircleCheck"
@click="handleCheck"
>
{{ $t('aiWorkflow.check') }}
</ElButton>
<ElButton
:disabled="saveLoading || checkLoading"
:icon="Position"
@click="runWorkflow"
>
{{ $t('button.runTest') }}
</ElButton>
<ElButton
type="primary"
:disabled="saveLoading"
:disabled="saveLoading || checkLoading"
@click="handleSave(true)"
>
{{ $t('button.save') }}(ctrl+s)
@@ -342,11 +542,58 @@ function onAsyncExecute(info: any) {
v-if="showTinyFlow"
class="tiny-flow-container"
:data="JSON.parse(JSON.stringify(tinyFlowData))"
:theme="isDark ? 'dark' : 'light'"
:provider="provider"
:custom-nodes="customNode"
:on-node-execute="runIndependently"
/>
<ElSkeleton class="load-div" v-else :rows="5" animated />
<transition name="checklist-slide">
<div v-if="checkIssuesVisible" class="checklist-panel">
<div class="checklist-header">
<div class="checklist-title">
{{ $t('aiWorkflow.checkIssuesTitle') }}
</div>
<ElButton
:icon="Close"
text
circle
:aria-label="$t('common.close')"
@click="closeCheckIssues"
/>
</div>
<div class="check-summary">
<span>{{ $t('aiWorkflow.checkStageLabel') }}{{ checkStageText(checkResult?.stage) }}</span>
<span>{{ $t('aiWorkflow.issueCount') }}{{ checkResult?.issueCount || 0 }}</span>
</div>
<div class="checklist-body">
<button
v-for="item in checkIssueDisplayList"
:key="item.key"
class="check-item"
:class="[
{ 'is-clickable': canFocusIssue(item.issue), 'is-active': focusedIssueKey === item.key },
]"
:disabled="!canFocusIssue(item.issue)"
@click="focusIssue(item.issue, item.index)"
>
<div class="check-item-meta">
<span class="check-level" :class="issueLevelClass(item.issue.level)">{{ item.issue.level || '-' }}</span>
<span class="check-node-display">
<span
v-if="item.nodeDisplay.icon"
class="check-node-icon"
v-html="item.nodeDisplay.icon"
/>
<span v-else class="check-node-icon-fallback">N</span>
<span class="check-node-title">{{ item.nodeDisplay.title }}</span>
</span>
</div>
<div class="check-item-message">{{ item.issue.message || '-' }}</div>
</button>
</div>
</div>
</transition>
<ElSkeleton v-if="!showTinyFlow" class="load-div" :rows="5" animated />
</div>
</template>
@@ -361,6 +608,7 @@ function onAsyncExecute(info: any) {
}
.head-div {
position: relative;
background-color: var(--el-bg-color);
}
@@ -372,4 +620,186 @@ function onAsyncExecute(info: any) {
.load-div {
margin: 20px;
}
.checklist-panel {
position: absolute;
left: 20px;
right: 20px;
bottom: 16px;
z-index: 40;
max-height: min(320px, 42vh);
display: flex;
flex-direction: column;
padding: 12px;
border: 1px solid var(--el-border-color);
border-radius: 12px;
background: var(--el-bg-color-overlay);
box-shadow: 0 10px 28px rgba(0, 0, 0, 0.12);
}
.checklist-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.checklist-title {
font-size: 16px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.check-summary {
display: flex;
gap: 20px;
margin: 0 2px 10px;
font-size: 13px;
color: var(--el-text-color-secondary);
}
.checklist-body {
overflow: auto;
display: flex;
flex-direction: column;
gap: 8px;
}
.check-item {
text-align: left;
border: 1px solid var(--el-border-color-lighter);
border-radius: 10px;
background: var(--el-fill-color-lighter);
padding: 10px 12px;
transition: border-color 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease;
}
.check-item.is-clickable {
cursor: pointer;
}
.check-item.is-clickable:hover {
border-color: var(--el-color-primary-light-5);
background: var(--el-fill-color-light);
}
.check-item.is-active {
border-color: var(--el-color-primary);
box-shadow: 0 0 0 1px var(--el-color-primary-light-7) inset;
}
.check-item:disabled {
cursor: default;
opacity: 0.85;
}
.check-item-meta {
display: flex;
align-items: center;
gap: 10px;
font-size: 12px;
line-height: 1.2;
margin-bottom: 6px;
}
.check-level {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 56px;
height: 20px;
padding: 0 8px;
border-radius: 999px;
font-weight: 600;
color: var(--el-text-color-primary);
background: var(--el-fill-color-dark);
}
.check-level.is-error {
color: var(--el-color-danger);
background: var(--el-color-danger-light-9);
}
.check-level.is-warning {
color: var(--el-color-warning);
background: var(--el-color-warning-light-9);
}
.check-level.is-info {
color: var(--el-color-primary);
background: var(--el-color-primary-light-9);
}
.check-node-display {
display: inline-flex;
align-items: center;
gap: 8px;
min-width: 0;
}
.check-node-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
color: var(--el-text-color-secondary);
}
.check-node-icon :deep(svg) {
width: 16px;
height: 16px;
}
.check-node-icon-fallback {
display: inline-flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
border-radius: 50%;
font-size: 10px;
font-weight: 700;
color: var(--el-color-primary);
background: var(--el-color-primary-light-9);
}
.check-node-title {
font-size: 14px;
color: var(--el-text-color-primary);
font-weight: 500;
}
.check-item-message {
font-size: 13px;
line-height: 1.45;
color: var(--el-color-danger);
word-break: break-word;
}
.checklist-slide-enter-active,
.checklist-slide-leave-active {
transition: all 0.24s ease;
}
.checklist-slide-enter-from,
.checklist-slide-leave-to {
opacity: 0;
transform: translateY(16px);
}
.workflow-issue-focus :deep(.svelte-flow__node.selected) {
border-color: var(--el-color-danger) !important;
box-shadow: 0 0 0 3px rgba(245, 108, 108, 0.26);
animation: issue-node-pulse 1.2s ease-out 1;
}
@keyframes issue-node-pulse {
0% {
box-shadow: 0 0 0 0 rgba(245, 108, 108, 0.42);
}
100% {
box-shadow: 0 0 0 10px transparent;
}
}
</style>