feat: 工作流新增任务表单抽屉

This commit is contained in:
xlsea 2024-05-25 14:15:59 +08:00
commit 9b79d41ecb
23 changed files with 406 additions and 95 deletions

View File

@ -40,7 +40,7 @@ defineExpose({
</script>
<template>
<NPopover trigger="click" placement="bottom-start">
<NPopover class="cron-popover" trigger="click" placement="bottom-start">
<template #trigger>
<NInput v-bind="attrs" v-model:value="cron" />
</template>
@ -49,10 +49,7 @@ defineExpose({
</template>
<style>
.n-popover {
.cron-popover {
padding: 0 !important;
}
.n-popover .n-popover-shared .n-popover-arrow-wrapper .n-popover-arrow {
}
</style>

View File

@ -39,6 +39,14 @@ export function fetchAddWorkflow(data: Flow.NodeDataType) {
});
}
export function fetchUpdateWorkflow(data: Flow.NodeDataType) {
return request<null>({
url: `/workflow`,
method: 'put',
data
});
}
export function fetchWorkflowInfo(id: string) {
return request<Flow.NodeDataType>({
url: `/workflow/${id}`,

View File

@ -0,0 +1,60 @@
<script setup lang="ts">
import type { InputInst } from 'naive-ui';
import { nextTick, ref, watch } from 'vue';
defineOptions({
name: 'EditableInput'
});
interface Props {
modelValue?: string;
}
const props = defineProps<Props>();
interface Emits {
(e: 'update:modelValue', modelValue: string): void;
}
const emit = defineEmits<Emits>();
const inputRef = ref<InputInst>();
const value = ref();
const idEdit = ref<boolean>(false);
watch(
() => props.modelValue,
val => {
value.value = val;
},
{ immediate: true }
);
const edit = () => {
idEdit.value = true;
nextTick(() => {
inputRef.value?.focus();
});
};
const save = () => {
emit('update:modelValue', value.value!);
idEdit.value = false;
};
</script>
<template>
<NInput v-if="idEdit" ref="inputRef" v-model:value="value" type="text" @blur="save" />
<NEllipsis v-else>
<span class="flex items-center">
{{ value }}
<NButton text type="info" class="m-l-6px" @click="edit">
<template #icon>
<icon-ant-design:edit-outlined class="text-icon" />
</template>
</NButton>
</span>
</NEllipsis>
</template>
<style scoped lang="scss"></style>

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="max-w-116px">{{ item.callback.webhook }}</NEllipsis>
</div>
<div>
<span class="content_label">{{ $t('node.callback.conditionNodes.contentType') }}:</span>

View File

@ -88,18 +88,18 @@ const show = () => {
</div>
<div v-if="nodeData.groupName" class="content">
<div>
<span class="content_label">{{ $t('snail.groupName') }}:</span>
<NEllipsis class="w-135px">
<span class="content_label">{{ $t('snail.groupName') }}:&nbsp;</span>
<NEllipsis class="max-w-132px">
{{ nodeData.groupName }}
</NEllipsis>
</div>
<div>
<span class="content_label">{{ $t('snail.blockStrategy') }}:</span>
{{ blockStrategyRecord[nodeData.blockStrategy!] }}
<span class="content_label">{{ $t('snail.blockStrategy') }}:&nbsp;</span>
{{ $t(blockStrategyRecord[nodeData.blockStrategy!]) }}
</div>
<div>.........</div>
</div>
<div v-else class="content">
<div v-else class="content min-h-85px">
<span class="placeholder">{{ $t('snail.form.workflowTip') }}</span>
</div>
<NTooltip v-if="store.type === 2" trigger="hover">

View File

@ -5,6 +5,7 @@ import { fetchNodeRetry, fetchNodeStop } from '../api';
import { useFlowStore } from '../stores';
import { $t } from '../locales';
import { failStrategyRecord, taskBatchStatusEnum } from '../constants/business';
import TaskDrawer from '../drawer/task-drawer.vue';
import AddNode from './add-node.vue';
defineOptions({
@ -93,15 +94,15 @@ const index = ref<number>(0);
const drawer = ref<boolean>(false);
const form = ref<Flow.ConditionNodeType>({});
// const save = (val: Flow.ConditionNodeType) => {
// const oldLevel = nodeConfig.value.conditionNodes![index.value].priorityLevel;
// const newLevel = val.priorityLevel;
// nodeConfig.value.conditionNodes![index.value] = val;
// if (oldLevel !== newLevel) {
// arrTransfer(index.value, newLevel! - oldLevel!);
// }
// emit('update:modelValue', nodeConfig.value);
// };
const save = (val: Flow.ConditionNodeType) => {
const oldLevel = nodeConfig.value.conditionNodes![index.value].priorityLevel;
const newLevel = val.priorityLevel;
nodeConfig.value.conditionNodes![index.value] = val;
if (oldLevel !== newLevel) {
arrTransfer(index.value, newLevel! - oldLevel!);
}
emit('update:modelValue', nodeConfig.value);
};
const show = (currentIndex: number) => {
if (!props.disabled) {
@ -214,16 +215,18 @@ const isStop = (taskBatchStatus: number) => {
<span class="priority-title">{{ $t('node.priority') }}{{ item.priorityLevel }}</span>
<icon-ant-design:close-outlined v-if="!disabled" class="close" @click.stop="delTerm(i)" />
</div>
<div class="content min-h-81px">
<div class="content min-h-72px">
<div v-if="!item.jobTask?.jobId" class="placeholder">{{ $t('snail.form.taskTip') }}</div>
<template v-if="item.jobTask?.jobId">
<div>
<span class="content_label">{{ $t('snail.taskName') }}:</span>
<NEllipsis class="w-126px">{{ `${item.jobTask?.jobName}(${item.jobTask?.jobId})` }}</NEllipsis>
<span class="content_label">{{ $t('snail.taskName') }}:&nbsp;</span>
<NEllipsis class="max-w-123px">
{{ `${item.jobTask?.jobName}(${item.jobTask?.jobId})` }}
</NEllipsis>
</div>
<div>
<span class="content_label">{{ $t('snail.failStrategy') }} :</span>
{{ failStrategyRecord[item.failStrategy!] }}
<span class="content_label">{{ $t('snail.failStrategy') }}:&nbsp;</span>
{{ $t(failStrategyRecord[item.failStrategy!]) }}
</div>
<div>.........</div>
</template>
@ -266,16 +269,15 @@ const isStop = (taskBatchStatus: number) => {
</div>
</div>
<AddNode v-if="nodeConfig.conditionNodes!.length > 1" v-model="nodeConfig.childNode!" :disabled="disabled" />
<!--
<TaskDrawer
v-if="store.type === 0 && drawer"
<TaskDrawer
v-if="store.type === 0"
v-model:open="drawer"
v-model="form"
v-model:len="nodeConfig.conditionNodes!.length"
@save="save"
/>
<DetailCard v-if="store.type !== 0 && cardDrawer" :id="detailId" v-model:open="cardDrawer" :ids="detailIds" />
-->
<!-- <DetailCard v-if="store.type !== 0 && cardDrawer" :id="detailId" v-model:open="cardDrawer" :ids="detailIds" /> -->
</div>
</template>

View File

@ -44,8 +44,8 @@ export const triggerTypeRecord: Record<Flow.TriggerType, string> = {
export const triggerTypeOptions = transformRecordToOption(triggerTypeRecord);
export const workFlowNodeStatusRecord: Record<Flow.WorkFlowNodeStatus, string> = {
0: '关闭',
1: '开启'
0: 'snail.enum.workFlowNodeStatus.close',
1: 'snail.enum.workFlowNodeStatus.open'
};
export const workFlowNodeStatusOptions = transformRecordToOption(workFlowNodeStatusRecord);

View File

@ -7,9 +7,10 @@ import { $t } from '../locales';
import { fetchGroupNameList } from '../api';
import { isNotNull } from '../utils/common';
import { useFlowStore } from '../stores';
import EditableInput from '../common/editable-input.vue';
defineOptions({
name: 'StartDetail'
name: 'StartDrawer'
});
interface Props {
@ -49,6 +50,9 @@ watch(
() => props.modelValue,
val => {
form.value = val;
if (val.triggerType === 2) {
form.value.triggerInterval = Number(val.triggerInterval);
}
if (val.workflowName) {
title = val.workflowName;
} else if (val.groupName) {
@ -92,7 +96,7 @@ const typeChange = (value: number) => {
if (value === 1) {
form.value.triggerInterval = '* * * * * ?';
} else if (value === 2) {
form.value.triggerInterval = '60';
form.value.triggerInterval = 60;
}
};
@ -116,10 +120,10 @@ const rules: Record<RuleKey, FormItemRule> = {
<template>
<NDrawer v-model:show="drawer" display-directive="if" :width="610" @after-leave="close">
<NDrawerContent :title="title">
<template #header>
<EditableInput v-model="form.workflowName" class="max-w-570px min-w-570px" />
</template>
<NForm ref="formRef" :model="form" :rules="rules" label-align="left" label-width="100px">
<NFormItem path="workflowName" label="工作流名称">
<NInput v-model:value="form.workflowName" placeholder="请输入工作流名称" />
</NFormItem>
<NFormItem path="groupName" label="组名称">
<NSelect
v-model:value="form.groupName"
@ -177,8 +181,8 @@ const rules: Record<RuleKey, FormItemRule> = {
<NRadioGroup v-model:value="form.blockStrategy">
<NSpace>
<NRadio
v-for="options in blockStrategyOptions"
:key="options.value"
v-for="(options, index) in blockStrategyOptions"
:key="index"
:label="$t(options.label)"
:value="options.value"
/>
@ -191,8 +195,8 @@ const rules: Record<RuleKey, FormItemRule> = {
<NRadioGroup v-model:value="form.workflowStatus">
<NSpace>
<NRadio
v-for="options in workFlowNodeStatusOptions"
:key="options.value"
v-for="(options, index) in workFlowNodeStatusOptions"
:key="index"
:label="$t(options.label)"
:value="options.value"
/>

View File

@ -0,0 +1,164 @@
<script setup lang="ts">
import { ref, watch } from 'vue';
import { type FormInst, useMessage } from 'naive-ui';
import { useFlowStore } from '../stores';
import { $t } from '../locales';
import { failStrategyOptions, workFlowNodeStatusOptions } from '../constants/business';
import EditableInput from '../common/editable-input.vue';
defineOptions({
name: 'TaskDrawer'
});
interface Props {
modelValue?: Flow.ConditionNodeType;
open?: boolean;
len?: number;
}
const props = withDefaults(defineProps<Props>(), {
open: false,
len: 0,
modelValue: () => ({})
});
interface Emits {
(e: 'update:open', open: boolean): void;
(e: 'save', form: Flow.ConditionNodeType): void;
}
const emit = defineEmits<Emits>();
const store = useFlowStore();
const message = useMessage();
const drawer = ref<boolean>(false);
const form = ref<Flow.ConditionNodeType>({});
const jobList = ref<{ id: string; jobName: string }[]>([]);
watch(
() => store.jobList,
val => {
jobList.value = val;
},
{ immediate: true, deep: true }
);
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);
} else {
message.warning('请检查表单信息');
}
});
};
const rules = {
failStrategy: [{ required: true, message: '请选择失败策略' }],
workflowNodeStatus: [{ required: true, message: '请选择工作流状态' }],
jobTask: {
jobId: [{ required: true, message: '请选择任务' }]
}
};
const jobTaskChange = (_: string, option: { label: string; value: number }) => {
form.value.jobTask!.jobName = option.label;
};
</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" :model="form" :rules="rules" label-align="left" label-width="100px">
<NFormItem path="jobTask.jobId" label="所属任务" placeholder="请选择任务">
<NSelect
v-model:value="form.jobTask!.jobId"
:options="
jobList.map(job => {
return {
label: job.jobName,
value: job.id
};
})
"
@update:value="jobTaskChange"
/>
</NFormItem>
<NFormItem path="failStrategy" label="失败策略">
<NRadioGroup v-model:value="form.failStrategy">
<NSpace>
<NRadio
v-for="(options, index) in failStrategyOptions"
:key="index"
:label="$t(options.label)"
:value="options.value"
/>
</NSpace>
</NRadioGroup>
</NFormItem>
<NFormItem path="workflowNodeStatus" label="节点状态">
<NRadioGroup v-model:value="form.workflowNodeStatus">
<NSpace>
<NRadio
v-for="(options, index) in workFlowNodeStatusOptions"
:key="index"
:label="$t(options.label)"
:value="options.value"
/>
</NSpace>
</NRadioGroup>
</NFormItem>
</NForm>
<template #footer>
<NButton type="primary" @click="save">保存</NButton>
<NButton class="ml-12px" @click="close">取消</NButton>
</template>
</NDrawerContent>
</NDrawer>
</template>

View File

@ -33,6 +33,14 @@ const local: FlowI18n.Schema = {
cancel: 'Cancel',
decisionFailed: 'Decision Failed',
skip: 'Skip'
},
workFlowNodeStatus: {
open: 'Open',
close: 'Close'
},
triggerType: {
time: 'Fixed Time',
cron: 'CRON Expressions'
}
}
},

View File

@ -33,6 +33,14 @@ const local: FlowI18n.Schema = {
cancel: '取消',
decisionFailed: '判定未通过',
skip: '跳过'
},
workFlowNodeStatus: {
open: '开启',
close: '关闭'
},
triggerType: {
time: '固定时间',
cron: 'CRON 表达式'
}
}
},

View File

@ -595,7 +595,7 @@
}
.end-node .end-node-text {
color: #ccc;
color: #d6d6d6;
}
.auto-judge .sort-left:hover,
@ -608,4 +608,8 @@
.add-branch {
background-color: #3e5a2d;
}
.content {
color: #d6d6d6;
}
}

View File

@ -37,6 +37,14 @@ declare namespace FlowI18n {
decisionFailed: string;
skip: string;
};
workFlowNodeStatus: {
open: string;
close: string;
};
triggerType: {
time: string;
cron: string;
};
};
};
node: {

View File

@ -1,7 +1,7 @@
import { BACKEND_ERROR_CODE, createFlatRequest } from '@sa/axios';
import { localStg } from './storage';
const baseURL = '/proxy-default';
const baseURL = '/snail-job';
export const request = createFlatRequest<Service.Response>(
{

View File

@ -66,7 +66,7 @@ export function getServiceBaseURL(env: Env.ImportMeta, isProxy: boolean) {
*/
function createProxyPattern(key?: App.Service.OtherBaseURLKey) {
if (!key) {
return '/proxy-default';
return '/snail-job';
}
return `/proxy-${key}`;

View File

@ -9,6 +9,8 @@ defineOptions({
name: 'PwdLogin'
});
const devMode = import.meta.env.DEV;
const authStore = useAuthStore();
const { formRef, validate } = useNaiveForm();
const { defaultRequiredRule } = useFormRules();
@ -19,8 +21,8 @@ interface FormModel {
}
const model: FormModel = reactive({
userName: 'admin',
password: 'admin'
userName: devMode ? 'admin' : '',
password: devMode ? 'admin' : ''
});
type RuleKey = Extract<keyof FormModel, 'userName' | 'password'>;

View File

@ -52,7 +52,7 @@ const model: Model = reactive(createDefaultModel());
function createDefaultModel(): Model {
return {
groupName: '',
token: generateToken(32),
token: import.meta.env.VITE_APP_DEFAULT_TOKEN || '',
groupStatus: 1,
description: '',
idGeneratorMode: 2,

View File

@ -351,11 +351,7 @@ watch(visible, () => {
<NGrid cols="2 s:1 m:2" responsive="screen" x-gap="20">
<NGi>
<NFormItem :label="$t('page.jobTask.triggerType')" path="triggerType">
<TriggerType
v-model:value="model.triggerType"
:placeholder="$t('page.jobTask.form.triggerType')"
@update:value="model.triggerInterval = ''"
/>
<TriggerType v-model:value="model.triggerType" :placeholder="$t('page.jobTask.form.triggerType')" />
</NFormItem>
</NGi>
<NGi>

View File

@ -28,13 +28,8 @@ const { columns, columnChecks, data, getData, loading, mobilePagination, searchP
taskBatchStatus: null
},
columns: () => [
// {
// type: 'selection',
// align: 'center',
// width: 48
// },
{
key: 'index',
key: 'id',
title: $t('common.index'),
align: 'center',
width: 120

View File

@ -2,6 +2,7 @@
import { onMounted, ref } from 'vue';
import Workflow, { flowFetch, flowStores } from '@sa/workflow';
import { useRouter } from 'vue-router';
import { $t } from '@/locales';
const store = flowStores.useFlowStore();
const router = useRouter();
@ -16,6 +17,7 @@ onMounted(() => {
});
const node = ref<Flow.NodeDataType>({
workflowName: `Workflow ${new Date().getTime()}`,
workflowStatus: 1,
blockStrategy: 1,
description: undefined,
@ -25,7 +27,7 @@ const node = ref<Flow.NodeDataType>({
const save = async () => {
const { error } = await flowFetch.fetchAddWorkflow(node.value);
if (!error) {
window.$message?.success('工作流新增成功');
window.$message?.info($t('common.addSuccess'));
router.push('/workflow/task');
}
};

View File

@ -1,17 +1,52 @@
<script setup lang="ts">
import WorkFlowIframe from '../modules/workflow-iframe.vue';
import { onMounted, ref } from 'vue';
import Workflow, { flowFetch, flowStores } from '@sa/workflow';
import { useRoute, useRouter } from 'vue-router';
import { $t } from '@/locales';
defineOptions({
name: 'WorkFlowEdit'
const store = flowStores.useFlowStore();
const route = useRoute();
const router = useRouter();
const spinning = ref(false);
const disabled = ref(false);
const id: string = String(route.query.id);
const node = ref<Flow.NodeDataType>({});
const getDetail = async () => {
spinning.value = true;
const { data, error } = await flowFetch.fetchWorkflowInfo(id);
if (!error) {
node.value = data;
}
spinning.value = false;
};
onMounted(() => {
store.clear();
store.setType(0);
store.setId(id);
getDetail();
disabled.value = false;
});
const update = async () => {
const { error } = await flowFetch.fetchUpdateWorkflow(node.value);
if (!error) {
window.$message?.info($t('common.updateSuccess'));
router.push({ path: '/workflow/task' });
}
};
const cancel = () => {
router.push('/workflow/task');
};
</script>
<template>
<div class="iframe"><WorkFlowIframe value="D7Rzd7Oe" /></div>
<Workflow v-model="node" :spinning="spinning" :disabled="disabled" @save="update" @cancel="cancel" />
</template>
<style scoped>
.iframe {
padding: 0 !important;
}
</style>
<style scoped></style>

View File

@ -81,7 +81,7 @@ onActivated(() => {
<iframe
ref="iframeRef"
class="size-full"
:src="`${mode === 'prod' ? baseUrl : ''}/lib/index.html?id=${id}&mode=${mode}&x1c2Hdd6=${value}`"
:src="`${mode === 'prod' ? baseUrl + '/' : '/'}lib/index.html?id=${id}&mode=${mode}&x1c2Hdd6=${value}`"
/>
</NSpin>
</template>

View File

@ -1,5 +1,5 @@
<script setup lang="tsx">
import { NButton, NButtonGroup, NPopconfirm, NPopover, NTag } from 'naive-ui';
import { NButton, NDropdown, NPopconfirm, NTag } from 'naive-ui';
import { useRouter } from 'vue-router';
import {
fetchDelWorkflow,
@ -38,7 +38,7 @@ const { columns, columnChecks, data, getData, loading, mobilePagination, searchP
// width: 48
// },
{
key: 'index',
key: 'id',
title: $t('common.index'),
align: 'center',
width: 120
@ -129,17 +129,50 @@ const { columns, columnChecks, data, getData, loading, mobilePagination, searchP
fixed: 'right',
width: 200,
render: row => {
const options = [
{
label: $t('common.execute'),
key: 'execute',
click: () => execute(row.id!)
},
{
type: 'divider',
key: 'd1'
},
{
label: $t('common.copy'),
key: 'copy',
click: () => copy(row.id!)
},
{
type: 'divider',
key: 'd2'
},
{
label: $t('common.batchList'),
key: 'batchList',
click: () => batch(row.id!)
}
];
function onSelect(key: string) {
options.filter(o => o.key === key)[0].click!();
}
return (
<div class="flex-center gap-8px">
<NButton type="warning" ghost size="small" onClick={() => edit(row.id!)}>
<NButton text type="warning" ghost size="small" onClick={() => edit(row.id!)}>
{$t('common.edit')}
</NButton>
<n-divider vertical />
{hasAuth('R_ADMIN') ? (
<NPopconfirm onPositiveClick={() => handleDelete(row.id!)}>
{{
default: () => $t('common.confirmDelete'),
trigger: () => (
<NButton type="error" ghost size="small">
<NButton text type="error" ghost size="small">
{$t('common.delete')}
</NButton>
)
@ -149,30 +182,17 @@ const { columns, columnChecks, data, getData, loading, mobilePagination, searchP
''
)}
<NPopover trigger="click" placement="bottom" raw show-arrow={false} class="b-rd-6px bg-#fff dark:bg-#000">
<n-divider vertical />
<NDropdown trigger="click" show-arrow={false} options={options} size="small" on-select={onSelect}>
{{
trigger: () => (
<NButton type="primary" ghost size="small">
default: () => (
<NButton text type="primary" ghost size="small">
更多
</NButton>
),
default: () => (
<div>
<NButtonGroup vertical>
<NButton type="primary" ghost size="small" onClick={() => execute(row.id!)}>
{$t('common.execute')}
</NButton>
<NButton type="primary" ghost size="small" onClick={() => copy(row.id!)}>
{$t('common.copy')}
</NButton>
<NButton type="success" ghost size="small" onClick={() => batch(row.id!)}>
{$t('common.batchList')}
</NButton>
</NButtonGroup>
</div>
)
}}
</NPopover>
</NDropdown>
</div>
);
}
@ -188,8 +208,6 @@ const {
async function handleBatchDelete() {
// request
console.log(checkedRowKeys.value);
onBatchDeleted();
}
@ -207,7 +225,7 @@ function edit(id: string) {
}
function handleAdd() {
router.push({ path: '/workflow/form/edit' });
router.push({ path: '/workflow/form/add' });
}
function detail(id: string) {