feat: 新增条件分支编辑与详情抽屉

This commit is contained in:
xlsea 2024-05-30 15:37:56 +08:00
parent 11e8188559
commit 63285b755f
9 changed files with 376 additions and 42 deletions

View File

@ -97,3 +97,11 @@ export function fetchWorkflowNodeRetry(id: string, workflowNodeId: number) {
method: 'get' method: 'get'
}); });
} }
export function fetchCheckNodeExpression(expression: Flow.BrachNodeType) {
return request<{ key: number; value: string }>({
url: '/workflow/check-node-expression',
method: 'post',
data: expression
});
}

View File

@ -6,6 +6,7 @@ import { useFlowStore } from '../stores';
import { fetchBatchDetail, fetchJobDetail, fetchTaskList, fetchWorkflowNodeRetry } from '../api'; import { fetchBatchDetail, fetchJobDetail, fetchTaskList, fetchWorkflowNodeRetry } from '../api';
import { executorTypeRecord, operationReasonRecord, taskBatchStatusRecord } from '../constants/business'; import { executorTypeRecord, operationReasonRecord, taskBatchStatusRecord } from '../constants/business';
import { $t } from '../locales'; import { $t } from '../locales';
import { isNotNull } from '../utils/common';
import LogDrawer from './log-drawer.vue'; import LogDrawer from './log-drawer.vue';
defineOptions({ defineOptions({
@ -19,6 +20,7 @@ interface Props {
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
id: undefined,
show: false, show: false,
ids: () => [] ids: () => []
}); });
@ -267,8 +269,8 @@ const onUpdatePage = (page: number) => {
<NDescriptionsItem :label="$t('snail.jobBatch.jobName')">{{ jobData?.jobName }}</NDescriptionsItem> <NDescriptionsItem :label="$t('snail.jobBatch.jobName')">{{ jobData?.jobName }}</NDescriptionsItem>
<NDescriptionsItem :label="$t('snail.jobBatch.taskBatchStatus')"> <NDescriptionsItem :label="$t('snail.jobBatch.taskBatchStatus')">
<NTag v-if="jobData.taskBatchStatus" :type="tagColor(jobData?.taskBatchStatus!)"> <NTag v-if="isNotNull(jobData.taskBatchStatus)" :type="tagColor(jobData.taskBatchStatus!)">
{{ $t(taskBatchStatusRecord[jobData?.taskBatchStatus]) }} {{ $t(taskBatchStatusRecord[jobData.taskBatchStatus!]) }}
</NTag> </NTag>
</NDescriptionsItem> </NDescriptionsItem>
@ -277,14 +279,14 @@ const onUpdatePage = (page: number) => {
</NDescriptionsItem> </NDescriptionsItem>
<NDescriptionsItem :label="$t('snail.jobBatch.operationReason')"> <NDescriptionsItem :label="$t('snail.jobBatch.operationReason')">
<NTag v-if="jobData.operationReason" :type="tagColor(jobData?.operationReason!)"> <NTag v-if="isNotNull(jobData.operationReason)" :type="tagColor(jobData.operationReason!)">
{{ $t(operationReasonRecord[jobData.operationReason]) }} {{ $t(operationReasonRecord[jobData.operationReason!]) }}
</NTag> </NTag>
</NDescriptionsItem> </NDescriptionsItem>
<NDescriptionsItem v-if="!slots.default" :label="$t('snail.jobBatch.executorType')"> <NDescriptionsItem v-if="!slots.default" :label="$t('snail.jobBatch.executorType')">
<NTag v-if="jobData.executorType" :type="tagColor(jobData?.executorType!)"> <NTag v-if="isNotNull(jobData.executorType)" :type="tagColor(jobData.executorType!)">
{{ $t(executorTypeRecord[jobData?.executorType!]) }} {{ $t(executorTypeRecord[jobData.executorType!]) }}
</NTag> </NTag>
</NDescriptionsItem> </NDescriptionsItem>

View File

@ -0,0 +1,111 @@
<script setup lang="ts">
import { nextTick, ref, watch } from 'vue';
import CodeMirror from 'vue-codemirror6';
import { oneDark } from '@codemirror/theme-one-dark';
import { javascript } from '@codemirror/lang-javascript';
import { expressionRecord, logicalConditionRecord } from '../constants/business';
defineOptions({
name: 'BranchDetail'
});
interface Props {
modelValue?: Flow.ConditionNodeType;
open?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
open: false,
modelValue: () => ({})
});
interface Emits {
(e: 'update:open', open: boolean): void;
}
const emit = defineEmits<Emits>();
const visible = ref(false);
const nodeExpression = ref('');
const theme = ref({
'.cm-line': {
fontSize: '18px'
},
'.cm-scroller': {
height: '520px',
overflowY: 'auto',
overflowX: 'hidden'
}
});
const onClose = () => {
emit('update:open', false);
};
const createDocument = () => {
const desc = document.getElementById('branch-desc');
const descriptions = desc?.querySelector('.n-descriptions-table');
const body = descriptions?.querySelector('tbody');
const tr = document.createElement('tr');
tr.className = 'n-descriptions-table-row';
const th = document.createElement('th');
th.className = 'n-descriptions-table-header';
th.innerHTML = '条件表达式';
th.setAttribute('colspan', '4');
tr.appendChild(th);
body?.insertBefore(tr, body?.childNodes[4]);
const rows: HTMLCollectionOf<Element> = body!.getElementsByClassName('n-descriptions-table-row')!;
const element = rows[3] as HTMLElement;
element.querySelector('.n-descriptions-table-header')?.remove();
const content = element.querySelector('.n-descriptions-table-content') as HTMLElement;
content.setAttribute('style', 'padding: 0');
content?.setAttribute('colspan', '4');
};
watch(
() => props.open,
val => {
visible.value = val;
if (val) {
nextTick(() => {
createDocument();
});
if (props.modelValue.decision?.nodeExpression) {
nodeExpression.value = props.modelValue.decision.nodeExpression;
}
}
},
{ immediate: true }
);
</script>
<template>
<NDrawer v-model:show="visible" placement="right" :width="500" display-directive="if" @after-leave="onClose">
<NDrawerContent title="决策详情">
<NDescriptions id="branch-desc" :column="2" label-placement="left" bordered :label-style="{ width: '120px' }">
<NDescriptionsItem label="节点名称" :span="2">{{ modelValue.nodeName }}</NDescriptionsItem>
<NDescriptionsItem label="判定逻辑">
{{ logicalConditionRecord[modelValue.decision?.logicalCondition!] }}
</NDescriptionsItem>
<NDescriptionsItem label="表达式类型">
{{ expressionRecord[modelValue.decision?.expressionType!] }}
</NDescriptionsItem>
<NDescriptionsItem label="条件表达式" :span="2" :content-style="{ padding: 0 }">
<CodeMirror
v-model="nodeExpression"
readonly
disabled
:theme="theme"
basic
:lang="javascript()"
:extensions="[oneDark]"
/>
</NDescriptionsItem>
</NDescriptions>
</NDrawerContent>
</NDrawer>
</template>

View File

@ -0,0 +1,204 @@
<script setup lang="ts">
import { ref, watch } from 'vue';
import type { FormInst, FormRules } from 'naive-ui';
import CodeMirror from 'vue-codemirror6';
import { oneDark } from '@codemirror/theme-one-dark';
import { javascript } from '@codemirror/lang-javascript';
import EditableInput from '../common/editable-input.vue';
import { fetchCheckNodeExpression } from '../api';
import { expressionOptions, logicalConditionOptions } from '../constants/business';
defineOptions({
name: 'BranchDrawer'
});
interface Props {
open?: boolean;
len?: number;
modelValue?: Flow.ConditionNodeType;
}
const props = withDefaults(defineProps<Props>(), {
open: false,
len: 0,
modelValue: () => ({})
});
interface Emits {
(e: 'update:open', open: boolean): void;
(e: 'save', form: Flow.NodeDataType): void;
}
const emit = defineEmits<Emits>();
const drawer = ref<boolean>(false);
const form = ref<Flow.ConditionNodeType>({
decision: {
logicalCondition: 1,
expressionType: 1
}
});
const theme = ref({
'.cm-line': {
fontSize: '18px'
},
'.cm-scroller': {
height: '500px',
overflowY: 'auto',
overflowX: 'hidden'
}
});
watch(
() => props.open,
val => {
drawer.value = val;
},
{ immediate: true }
);
watch(
() => props.modelValue,
val => {
form.value = val;
},
{ immediate: true, deep: true }
);
const formRef = ref<FormInst>();
const close = () => {
emit('update:open', false);
drawer.value = false;
};
const save = () => {
formRef.value
?.validate(errors => {
if (!errors) {
close();
emit('save', form.value);
}
})
.catch(() => window.$message?.warning('请检查表单信息'));
};
const checkNodeExpression = async () => {
if (!form.value.decision?.nodeExpression) {
return Promise.reject(new Error('请填写条件表达式'));
}
const { error, data } = await fetchCheckNodeExpression(form.value.decision!);
if (!error) {
if (data.key !== 1) {
return Promise.reject(data.value ?? '请检查条件表达式');
}
}
return Promise.resolve();
};
const rules: FormRules = {
decision: {
logicalCondition: [{ required: true, message: '请选择判定逻辑', trigger: 'change', type: 'number' }],
expressionType: [{ required: true, message: '请选择表达式类型', trigger: 'change', type: 'number' }],
nodeExpression: [{ required: true, validator: checkNodeExpression, trigger: 'blur' }]
}
};
</script>
<template>
<NDrawer v-model:show="drawer" display-directive="if" :width="500" @after-leave="close">
<NDrawerContent>
<template #header>
<div class="w-460px flex-center">
<EditableInput v-model="form.nodeName" class="mr-16px max-w-320px min-w-320px" />
<NSelect
v-model:value="form.priorityLevel"
class="max-w-110px"
:options="
Array(len)
.fill(0)
.map((_, index) => {
return {
label: '优先级 ' + (index + 1),
value: index + 1
};
})
"
/>
</div>
</template>
<NForm ref="formRef" :rules="rules" :model="form" label-align="left" label-width="100px">
<NFormItem path="decision.logicalCondition" label="判定逻辑">
<NRadioGroup v-model:value="form.decision!.logicalCondition">
<NSpace>
<NRadio
v-for="logical in logicalConditionOptions"
:key="logical.value"
:label="logical.label"
:value="logical.value"
/>
</NSpace>
</NRadioGroup>
</NFormItem>
<NFormItem path="decision.expressionType" label="表达式类型">
<NRadioGroup v-model:value="form.decision!.expressionType">
<NSpace>
<NRadio
v-for="strategy in expressionOptions"
:key="strategy.value"
:label="strategy.label"
:value="strategy.value"
/>
</NSpace>
</NRadioGroup>
</NFormItem>
<NFormItem path="decision.nodeExpression" label="条件表达式">
<CodeMirror
v-model="form.decision!.nodeExpression"
class="sj-code-mirror"
:theme="theme"
basic
:lang="javascript()"
:extensions="[oneDark]"
/>
</NFormItem>
</NForm>
<template #footer>
<NButton type="primary" @click="save">保存</NButton>
<NButton class="ml-12px" @click="close">取消</NButton>
</template>
</NDrawerContent>
</NDrawer>
</template>
<style scoped lang="scss">
.drawer-title {
display: flex;
align-items: center;
justify-content: space-between;
}
</style>
<style>
.sj-code-mirror {
width: 100%;
.cm-scroller::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.cm-scroller::-webkit-scrollbar-thumb {
background: #9c9c9c9c;
-webkit-box-shadow: inset 0 0 3px rgba(0, 0, 0, 0.3);
}
.cm-scroller::-webkit-scrollbar-track {
background: #282c34;
}
}
</style>

View File

@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch } from 'vue'; import { ref, watch } from 'vue';
import CronInput from '@sa/cron-input'; import CronInput from '@sa/cron-input';
import { type FormInst, type FormItemRule, useMessage } from 'naive-ui'; import { type FormInst, type FormItemRule } from 'naive-ui';
import { blockStrategyOptions, triggerTypeOptions, workFlowNodeStatusOptions } from '../constants/business'; import { blockStrategyOptions, triggerTypeOptions, workFlowNodeStatusOptions } from '../constants/business';
import { $t } from '../locales'; import { $t } from '../locales';
import { fetchGroupNameList } from '../api'; import { fetchGroupNameList } from '../api';
@ -30,7 +30,6 @@ interface Emits {
const emit = defineEmits<Emits>(); const emit = defineEmits<Emits>();
const message = useMessage();
const store = useFlowStore(); const store = useFlowStore();
let title: string = ''; let title: string = '';
@ -73,14 +72,13 @@ const close = () => {
const save = () => { const save = () => {
formRef.value formRef.value
?.validate() ?.validate(errors => {
.then(() => { if (!errors) {
close(); close();
emit('save', form.value); emit('save', form.value);
}
}) })
.catch(() => { .catch(() => window.$message?.warning('请检查表单信息'));
message.warning('请检查表单信息');
});
}; };
const getGroupNameList = async () => { const getGroupNameList = async () => {
@ -145,7 +143,14 @@ const rules: Record<RuleKey, FormItemRule> = {
<NSelect <NSelect
v-model:value="form.triggerType" v-model:value="form.triggerType"
placeholder="请选择触发类型" placeholder="请选择触发类型"
:options="triggerTypeOptions" :options="
triggerTypeOptions.map(option => {
return {
label: $t(option.label),
value: option.value
};
})
"
@update:value="typeChange" @update:value="typeChange"
/> />
</NFormItem> </NFormItem>

View File

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch } from 'vue'; import { ref, watch } from 'vue';
import { type FormInst, useMessage } from 'naive-ui'; import { type FormInst } from 'naive-ui';
import { useFlowStore } from '../stores'; import { useFlowStore } from '../stores';
import { $t } from '../locales'; import { $t } from '../locales';
import { failStrategyOptions, workFlowNodeStatusOptions } from '../constants/business'; import { failStrategyOptions, workFlowNodeStatusOptions } from '../constants/business';
@ -30,7 +30,6 @@ interface Emits {
const emit = defineEmits<Emits>(); const emit = defineEmits<Emits>();
const store = useFlowStore(); const store = useFlowStore();
const message = useMessage();
const drawer = ref<boolean>(false); const drawer = ref<boolean>(false);
const form = ref<Flow.ConditionNodeType>({}); const form = ref<Flow.ConditionNodeType>({});
const jobList = ref<{ id: string; jobName: string }[]>([]); const jobList = ref<{ id: string; jobName: string }[]>([]);
@ -67,14 +66,14 @@ const close = () => {
}; };
const save = () => { const save = () => {
formRef.value?.validate(errors => { formRef.value
if (!errors) { ?.validate(errors => {
close(); if (!errors) {
emit('save', form.value); close();
} else { emit('save', form.value);
message.warning('请检查表单信息'); }
} })
}); .catch(() => window.$message?.warning('请检查表单信息'));
}; };
const rules = { const rules = {

View File

@ -3,6 +3,8 @@ import { nextTick, ref, watch } from 'vue';
import { $t } from '../locales'; import { $t } from '../locales';
import { useFlowStore } from '../stores'; import { useFlowStore } from '../stores';
import { expressionRecord, logicalConditionRecord, taskBatchStatusEnum } from '../constants/business'; import { expressionRecord, logicalConditionRecord, taskBatchStatusEnum } from '../constants/business';
import BranchDrawer from '../drawer/branch-drawer.vue';
import BranchDetail from '../detail/branch-detail.vue';
import AddNode from './add-node.vue'; import AddNode from './add-node.vue';
defineOptions({ defineOptions({
@ -111,15 +113,15 @@ const drawer = ref<boolean>(false);
const detailDrawer = ref<boolean[]>([]); const detailDrawer = ref<boolean[]>([]);
const form = ref<Flow.ConditionNodeType>({}); const form = ref<Flow.ConditionNodeType>({});
// const save = (val: Flow.ConditionNodeType) => { const save = (val: Flow.ConditionNodeType) => {
// const oldLevel = nodeConfig.value.conditionNodes![currentIndex.value].priorityLevel; const oldLevel = nodeConfig.value.conditionNodes![currentIndex.value].priorityLevel;
// const newLevel = val.priorityLevel; const newLevel = val.priorityLevel;
// nodeConfig.value.conditionNodes![currentIndex.value] = val; nodeConfig.value.conditionNodes![currentIndex.value] = val;
// if (oldLevel !== newLevel) { if (oldLevel !== newLevel) {
// arrTransfer(currentIndex.value, newLevel! - oldLevel!); arrTransfer(currentIndex.value, newLevel! - oldLevel!);
// } }
// emit('update:modelValue', nodeConfig.value); emit('update:modelValue', nodeConfig.value);
// }; };
const show = (index: number) => { const show = (index: number) => {
if (!props.disabled && index !== nodeConfig.value.conditionNodes!.length - 1) { if (!props.disabled && index !== nodeConfig.value.conditionNodes!.length - 1) {
@ -247,13 +249,12 @@ const getClass = (item: Flow.ConditionNodeType) => {
<div v-if="index == 0" class="bottom-left-cover-line"></div> <div v-if="index == 0" class="bottom-left-cover-line"></div>
<div v-if="index == nodeConfig.conditionNodes!.length - 1" class="top-right-cover-line"></div> <div v-if="index == nodeConfig.conditionNodes!.length - 1" class="top-right-cover-line"></div>
<div v-if="index == nodeConfig.conditionNodes!.length - 1" class="bottom-right-cover-line"></div> <div v-if="index == nodeConfig.conditionNodes!.length - 1" class="bottom-right-cover-line"></div>
<!-- <BranchDetail
<BranchDetail v-if="store.type !== 0"
v-if="store.type !== 0 && detailDrawer[index]"
v-model:open="detailDrawer[index]" v-model:open="detailDrawer[index]"
v-model="nodeConfig.conditionNodes![index]" v-model="nodeConfig.conditionNodes![index]"
/> />
-->
<!-- <!--
<DetailCard <DetailCard
v-if="store.type !== 0 && cardDrawer[index]" v-if="store.type !== 0 && cardDrawer[index]"
@ -271,7 +272,7 @@ const getClass = (item: Flow.ConditionNodeType) => {
</div> </div>
<AddNode v-model="nodeConfig.childNode!" :disabled="disabled"></AddNode> <AddNode v-model="nodeConfig.childNode!" :disabled="disabled"></AddNode>
</div> </div>
<!-- <BranchDrawer v-model:open="drawer" v-model="form" v-model:len="nodeConfig.conditionNodes!.length" @save="save" /> --> <BranchDrawer v-model:open="drawer" v-model="form" v-model:len="nodeConfig.conditionNodes!.length" @save="save" />
</div> </div>
</template> </template>

View File

@ -176,6 +176,10 @@ const isRetry = (taskBatchStatus: number) => {
const isStop = (taskBatchStatus: number) => { const isStop = (taskBatchStatus: number) => {
return taskBatchStatus === 1 || taskBatchStatus === 2; return taskBatchStatus === 1 || taskBatchStatus === 2;
}; };
const isShow = (taskBatchStatus: number) => {
return isRetry(taskBatchStatus!) || isStop(taskBatchStatus!);
};
</script> </script>
<template> <template>
@ -187,7 +191,7 @@ const isStop = (taskBatchStatus: number) => {
<div v-for="(item, i) in nodeConfig.conditionNodes" :key="i" class="col-box"> <div v-for="(item, i) in nodeConfig.conditionNodes" :key="i" class="col-box">
<div class="condition-node"> <div class="condition-node">
<div class="condition-node-box"> <div class="condition-node-box">
<NPopover :disabled="store.type !== 2"> <NPopover :disabled="store.type !== 2 || !isShow(item.taskBatchStatus!)">
<div class="popover"> <div class="popover">
<NButton v-if="isRetry(item.taskBatchStatus!)" text @click="retry(item!)"> <NButton v-if="isRetry(item.taskBatchStatus!)" text @click="retry(item!)">
<span class="popover-item"> <span class="popover-item">

View File

@ -325,7 +325,7 @@
} }
.auto-judge .content { .auto-judge .content {
line-height: 136%; line-height: 23px;
position: relative; position: relative;
padding-top: 15px; padding-top: 15px;
min-height: 59px; min-height: 59px;