feat: 新增工作流开始节点编辑抽屉

This commit is contained in:
xlsea 2024-05-20 17:10:58 +08:00
parent 60d0c218d4
commit 5fcd6ed214
22 changed files with 268 additions and 70 deletions

View File

@ -11,7 +11,7 @@
"i18n-ally.enabledFrameworks": ["vue"],
"i18n-ally.editor.preferEditor": true,
"i18n-ally.keystyle": "nested",
"i18n-ally.localesPaths": ["src/locales/langs"],
"i18n-ally.localesPaths": ["src/locales/langs", "packages/work-flow/src/locales/langs"],
"prettier.enable": false,
"typescript.tsdk": "node_modules/typescript/lib",
"unocss.root": ["./"]

View File

@ -30,3 +30,18 @@ export function fetchGroupNameList() {
method: 'get'
});
}
export function fetchAddWorkflow(data: Flow.NodeDataType) {
return request<null>({
url: `/workflow`,
method: 'post',
data
});
}
export function fetchWorkflowInfo(id: string) {
return request<Flow.NodeDataType>({
url: `/workflow/${id}`,
method: 'get'
});
}

View File

@ -45,7 +45,7 @@ const addType = (type: number) => {
nodeType: 2,
conditionNodes: [
{
nodeName: `${$t('node.condition.conditionNodes.nodeName')}1`,
nodeName: `${$t('node.condition.conditionNodes.nodeName')} 1`,
priorityLevel: 1,
decision: {
expressionType: 1,

View File

@ -40,7 +40,7 @@ watch(
const addTerm = () => {
const len = nodeConfig.value.conditionNodes!.length;
nodeConfig.value.conditionNodes!.splice(-1, 0, {
nodeName: `$t('node.condition.nodeName')${len}`,
nodeName: `${$t('node.condition.nodeName')} ${len}`,
priorityLevel: len,
decision: {
expressionType: 1,
@ -187,7 +187,7 @@ const getClass = (item: Flow.ConditionNodeType) => {
<div class="branch-wrap">
<div class="branch-box-wrap">
<div class="branch-box">
<NButton v-if="!disabled" type="success" class="add-branch" @click="addTerm">
<NButton v-if="!disabled" strong type="success" class="add-branch" @click="addTerm">
{{ $t('node.condition.addBranch') }}
</NButton>
<div v-for="(item, index) in nodeConfig.conditionNodes" :key="index" class="col-box">

View File

@ -156,7 +156,7 @@ const getClass = (item: Flow.ConditionNodeType) => {
<template v-if="item.callback?.webhook">
<div class="flex justify-between">
<span class="content_label">Webhook:</span>
<Nellipsis class="w-116px">{{ item.callback.webhook }}</Nellipsis>
<NEllipsis class="w-116px">{{ item.callback.webhook }}</NEllipsis>
</div>
<div>
<span class="content_label">{{ $t('node.callback.conditionNodes.contentType') }}:</span>

View File

@ -80,10 +80,10 @@ const show = () => {
class="node-wrap-box node-error-success start-node"
@click="show"
>
<div class="title bg-#ffffff">
<span class="text" calss="color-#ff943e">
<div class="title">
<span class="text text-#ff943e">
<NBadge dot :color="nodeData.workflowStatus === 1 ? '#52c41a' : '#ff000d'" />
{{ nodeData.workflowName ? nodeData.workflowName : $t('snail.form.groupName') }}
&nbsp;{{ nodeData.workflowName ? nodeData.workflowName : $t('snail.form.groupName') }}
</span>
</div>
<div v-if="nodeData.groupName" class="content">

View File

@ -43,7 +43,7 @@ watch(
const addTerm = () => {
const len = nodeConfig.value.conditionNodes!.length + 1;
nodeConfig.value.conditionNodes?.push({
nodeName: `${$t('node.task.name')}${len}`,
nodeName: `${$t('node.task.name')} ${len}`,
priorityLevel: len,
failStrategy: 1,
workflowNodeStatus: 1,
@ -179,7 +179,9 @@ const isStop = (taskBatchStatus: number) => {
<template>
<div class="node-wrap">
<div class="branch-box">
<NButton v-if="!disabled" class="add-branch" type="success" @click="addTerm">{{ $t('node.task.add') }}</NButton>
<NButton v-if="!disabled" class="add-branch" strong type="success" @click="addTerm">
{{ $t('node.task.add') }}
</NButton>
<div v-for="(item, i) in nodeConfig.conditionNodes" :key="i" class="col-box">
<div class="condition-node">
<div class="condition-node-box">
@ -207,7 +209,7 @@ const isStop = (taskBatchStatus: number) => {
<div class="title">
<span class="text color-#3296fa">
<NBadge processing dot :color="item.workflowNodeStatus === 1 ? '#52c41a' : '#ff4d4f'" />
{{ item.nodeName }}
&nbsp;{{ item.nodeName }}
</span>
<span class="priority-title">{{ $t('node.priority') }}{{ item.priorityLevel }}</span>
<icon-ant-design:close-outlined v-if="!disabled" class="close" @click.stop="delTerm(i)" />

View File

@ -3,6 +3,7 @@ import { ref, watch } from 'vue';
import CronInput from '@sa/cron-input';
import { type FormInst, type FormItemRule, useMessage } from 'naive-ui';
import { blockStrategyOptions, triggerTypeOptions, workFlowNodeStatusOptions } from '../constants/business';
import { $t } from '../locales';
import { fetchGroupNameList } from '../api';
import { isNotNull } from '../utils/common';
import { useFlowStore } from '../stores';
@ -132,9 +133,9 @@ const rules: Record<RuleKey, FormItemRule> = {
};
})
"
></NSelect>
/>
</NFormItem>
<NGrid :cols="24">
<NGrid :cols="24" x-gap="20">
<NGi :span="8">
<NFormItem path="triggerType" label="触发类型">
<NSelect
@ -152,13 +153,18 @@ const rules: Record<RuleKey, FormItemRule> = {
v-model:value="form.triggerInterval"
placeholder="请输入Cron表达式"
/>
<NInputNumber v-else v-model:value="form.triggerInterval as number" placeholder="请输入触发间隔">
<NInputNumber
v-else
v-model:value="form.triggerInterval as number"
class="w-full"
placeholder="请输入触发间隔"
>
<template #suffix></template>
</NInputNumber>
</NFormItem>
</NGi>
</NGrid>
<NGrid :cols="24">
<NGrid :cols="24" x-gap="20">
<NGi :span="8">
<NFormItem path="executorTimeout" label="执行超时时间">
<NInputNumber v-model:value="form.executorTimeout" placeholder="请输入超时时间">
@ -168,12 +174,30 @@ const rules: Record<RuleKey, FormItemRule> = {
</NGi>
<NGi :span="16">
<NFormItem path="blockStrategy" label="阻塞策略">
<NRadioGroup v-model:value="form.blockStrategy" :option="blockStrategyOptions" />
<NRadioGroup v-model:value="form.blockStrategy">
<NSpace>
<NRadio
v-for="options in blockStrategyOptions"
:key="options.value"
:label="$t(options.label)"
:value="options.value"
/>
</NSpace>
</NRadioGroup>
</NFormItem>
</NGi>
</NGrid>
<NFormItem path="workflowStatus" label="节点状态">
<NRadioGroup v-model:value="form.workflowStatus" :option="workFlowNodeStatusOptions" />
<NRadioGroup v-model:value="form.workflowStatus">
<NSpace>
<NRadio
v-for="options in workFlowNodeStatusOptions"
:key="options.value"
:label="$t(options.label)"
:value="options.value"
/>
</NSpace>
</NRadioGroup>
</NFormItem>
<NFormItem path="description" label="描述">
<NInput

View File

@ -1,6 +1,7 @@
import Workflow from './workflow.vue';
import * as flowLocales from './locales';
import * as flowStores from './stores';
import * as flowFetch from './api';
export { flowLocales, flowStores };
export { flowLocales, flowStores, flowFetch };
export default Workflow;

View File

@ -40,6 +40,14 @@ export const useFlowStore = defineStore('workflow', () => {
});
}
function clear() {
id.value = undefined;
type.value = undefined;
groupName.value = undefined;
jobList.value = [];
clearFLowStorage();
}
return {
id,
type,
@ -49,6 +57,7 @@ export const useFlowStore = defineStore('workflow', () => {
setType,
setId,
clearFLowStorage,
getJobList
getJobList,
clear
};
});

View File

@ -149,7 +149,7 @@
left: 50%;
transform: translateX(-50%);
transform-origin: center center;
z-index: 1;
z-index: 10;
display: inline-flex;
align-items: center;
line-height: 1;
@ -171,13 +171,12 @@
padding: 8px 15px !important;
color: #67c23a;
background-color: #f0f9eb;
border: 1px solid #dcdfe6;
border-color: #b3e19d;
// border: 1px solid #b3e19d;
}
.add-branch:hover {
color: #ffffff;
border-color: #b3e19d;
// border-color: #b3e19d;
background-color: #67c23a;
}
@ -282,7 +281,7 @@
border-style: solid;
border-width: 8px 6px 4px;
border-color: rgb(202, 202, 202) transparent transparent;
background: rgb(239, 239, 239);
background: rgb(255 255 255 / 0%);
}
.auto-judge .title {
@ -590,22 +589,11 @@
background: var(--el-bg-color);
}
.top-left-cover-line,
.top-right-cover-line,
.bottom-left-cover-line,
.bottom-right-cover-line {
background-color: var(--el-bg-color);
}
.node-wrap-box::before,
.auto-judge::before {
background-color: var(--el-bg-color);
}
.branch-box .add-branch {
background: var(--el-bg-color);
}
.end-node .end-node-text {
color: #ccc;
}
@ -615,3 +603,9 @@
background: var(--el-bg-color);
}
}
.dark {
.add-branch {
background-color: #3e5a2d;
}
}

View File

@ -9,7 +9,7 @@ declare namespace Flow {
type WorkFlowNodeStatus = 0 | 1;
/** 组 */
type NodeDataType = {
export type NodeDataType = {
/** 流程ID */
id?: string;
/** 工作流名称 */

View File

@ -0,0 +1,27 @@
interface Window {
/** NProgress instance */
NProgress?: import('nprogress').NProgress;
/** Loading bar instance */
$loadingBar?: import('naive-ui').LoadingBarProviderInst;
/** Dialog instance */
$dialog?: import('naive-ui').DialogProviderInst;
/** Message instance */
$message?: import('naive-ui').MessageProviderInst;
/** Notification instance */
$notification?: import('naive-ui').NotificationProviderInst;
}
interface ViewTransition {
ready: Promise<void>;
}
interface Document {
startViewTransition?: (callback: () => Promise<void> | void) => ViewTransition;
}
interface ImportMeta {
readonly env: Env.ImportMeta;
}
/** Build time of the project */
declare const BUILD_TIME: string;

View File

@ -1,5 +1,4 @@
import { BACKEND_ERROR_CODE, createFlatRequest } from '@sa/axios';
import { useMessage } from 'naive-ui';
import { localStg } from './storage';
const baseURL = '/proxy-default';
@ -36,12 +35,12 @@ export const request = createFlatRequest<Service.Response>(
onError(error) {
// when the request is fail, you can show error message
let message = error.message;
let msg = error.message;
let backendErrorCode = '';
// get backend error message and code
if (error.code === BACKEND_ERROR_CODE) {
message = error.response?.data?.message || message;
msg = error.response?.data?.message || msg;
backendErrorCode = error.response?.data?.status.toString() || '';
}
@ -57,7 +56,7 @@ export const request = createFlatRequest<Service.Response>(
return;
}
useMessage().error(message);
window.$message?.error(msg);
}
}
);

View File

@ -10,22 +10,35 @@ defineOptions({
interface Props {
modelValue?: Flow.NodeDataType;
spinning?: boolean;
disabled?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
disabled: false,
spinning: false,
modelValue: () => ({})
});
interface Emits {
(e: 'update:modelValue', modelValue: Flow.NodeDataType): void;
(e: 'save'): void;
(e: 'cancel'): void;
}
const emit = defineEmits<Emits>();
const zoom = ref<number>(100);
const nodeData = ref<Flow.NodeDataType>({});
const save = async () => {
emit('save');
};
const cancel = () => {
emit('cancel');
};
watch(
() => props.modelValue,
val => {
@ -40,16 +53,59 @@ watch(
emit('update:modelValue', val);
}
);
const onZoom = (n: number) => {
zoom.value += 10 * n;
if (zoom.value <= 10) {
zoom.value = 10;
} else if (zoom.value >= 300) {
zoom.value = 300;
}
};
</script>
<template>
<div class="workflow-design">
<div class="box-scale">
<StartNode v-model="nodeData" :disabled="disabled" />
<NodeWrap v-if="nodeData.nodeConfig" v-model="nodeData.nodeConfig" :disabled="disabled" />
<div class="end-node">
<div class="end-node-circle"></div>
<div class="end-node-text">{{ $t('node.endNode') }}</div>
<div class="workflow">
<div class="workflow-affix">
<NAffix :trigger-top="0">
<div class="header">
<div>
<NTooltip title="缩小">
<template #trigger>
<NButton type="info" strong @click="onZoom(-1)">
<icon-ant-design:minus-outlined />
</NButton>
</template>
</NTooltip>
<span class="ml-8px mr-8px text-#333639 dark:text-#d6d6d6">{{ zoom }}%</span>
<NTooltip title="放大">
<template #trigger>
<NButton type="info" strong @click="onZoom(1)">
<icon-ant-design:plus-outlined />
</NButton>
</template>
</NTooltip>
</div>
<div v-if="!disabled" class="buttons">
<NButton type="info" siz="large" @click="save">保存</NButton>
<NButton siz="large" class="ml-16px" @click="cancel">取消</NButton>
</div>
</div>
</NAffix>
<div class="workflow-body">
<NSpin :show="spinning">
<div class="workflow-design" :style="`transform: scale(${zoom / 100})`">
<div class="box-scale">
<StartNode v-model="nodeData" :disabled="disabled" />
<NodeWrap v-if="nodeData.nodeConfig" v-model="nodeData.nodeConfig" :disabled="disabled" />
<div class="end-node">
<div class="end-node-circle"></div>
<div class="end-node-text">{{ $t('node.endNode') }}</div>
</div>
</div>
</div>
</NSpin>
</div>
</div>
</div>
@ -57,4 +113,42 @@ watch(
<style lang="scss">
@import './styles/index';
.workflow {
padding: 0 !important;
height: calc(100% - 50px);
&-affix {
.header {
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
background-color: #fff;
box-sizing: border-box;
padding: 8px 16px;
}
}
&-body {
overflow: auto;
height: 100%;
}
&-design {
margin-top: 16px;
transform-origin: 0 0 !important;
}
}
.dark {
.workflow {
&-affix {
.header {
box-shadow: 0 1px 4px rgba(255, 255, 255, 0.08);
background-color: #121212;
}
}
}
}
</style>

View File

@ -299,6 +299,7 @@ const local: App.I18n.Schema = {
workflow_form_batch: 'Workflow Batch Detail',
workflow_form_detail: 'Workflow Detail',
workflow_form_edit: 'Edit Workflow',
workflow_form_add: 'Add Workflow',
job: 'Schedule Task Management',
job_task: 'Schedule Task List',
job_batch: 'Schedule Task Batch List',

View File

@ -300,6 +300,7 @@ const local: App.I18n.Schema = {
workflow_form_batch: '工作流批次详情',
workflow_form_detail: '工作流详情',
workflow_form_edit: '编辑工作流',
workflow_form_add: '新增工作流',
job: '定时任务',
job_task: '任务管理',
job_batch: '执行批次',

View File

@ -50,6 +50,7 @@ export const views: Record<LastLevelRouteKey, RouteComponent | (() => Promise<Ro
"user-center": () => import("@/views/user-center/index.vue"),
user_manager: () => import("@/views/user/manager/index.vue"),
workflow_batch: () => import("@/views/workflow/batch/index.vue"),
workflow_form_add: () => import("@/views/workflow/form/add/index.vue"),
workflow_form_batch: () => import("@/views/workflow/form/batch/index.vue"),
workflow_form_copy: () => import("@/views/workflow/form/copy/index.vue"),
workflow_form_detail: () => import("@/views/workflow/form/detail/index.vue"),

View File

@ -556,6 +556,16 @@ export const generatedRoutes: GeneratedRoute[] = [
i18nKey: 'route.workflow_form'
},
children: [
{
name: 'workflow_form_add',
path: '/workflow/form/add',
component: 'view.workflow_form_add',
meta: {
hideInMenu: true,
title: 'workflow_form_add',
i18nKey: 'route.workflow_form_add'
}
},
{
name: 'workflow_form_batch',
path: '/workflow/form/batch',

View File

@ -203,6 +203,7 @@ const routeMap: RouteMap = {
"workflow": "/workflow",
"workflow_batch": "/workflow/batch",
"workflow_form": "/workflow/form",
"workflow_form_add": "/workflow/form/add",
"workflow_form_batch": "/workflow/form/batch",
"workflow_form_copy": "/workflow/form/copy",
"workflow_form_detail": "/workflow/form/detail",

View File

@ -1,27 +1,4 @@
interface Window {
/** NProgress instance */
NProgress?: import('nprogress').NProgress;
/** Loading bar instance */
$loadingBar?: import('naive-ui').LoadingBarProviderInst;
/** Dialog instance */
$dialog?: import('naive-ui').DialogProviderInst;
/** Message instance */
$message?: import('naive-ui').MessageProviderInst;
/** Notification instance */
$notification?: import('naive-ui').NotificationProviderInst;
}
interface ViewTransition {
ready: Promise<void>;
}
interface Document {
startViewTransition?: (callback: () => Promise<void> | void) => ViewTransition;
}
interface ImportMeta {
readonly env: Env.ImportMeta;
}
/** Build time of the project */
declare const BUILD_TIME: string;

View File

@ -0,0 +1,42 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import Workflow, { flowFetch, flowStores } from '@sa/workflow';
import { useRouter } from 'vue-router';
const store = flowStores.useFlowStore();
const router = useRouter();
const spinning = ref(false);
const disabled = ref(false);
onMounted(() => {
store.clear();
store.setType(0);
disabled.value = false;
});
const node = ref<Flow.NodeDataType>({
workflowStatus: 1,
blockStrategy: 1,
description: undefined,
executorTimeout: 60
});
const save = async () => {
const { error } = await flowFetch.fetchAddWorkflow(node.value);
if (!error) {
window.$message?.success('工作流新增成功');
router.push('/workflow/task');
}
};
const cancel = () => {
router.push('/workflow/task');
};
</script>
<template>
<Workflow v-model="node" :spinning="spinning" :disabled="disabled" @save="save" @cancel="cancel" />
</template>
<style scoped></style>